diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e08c0122..6cbb35d72 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,8 @@ jobs: fail-fast: false matrix: include: - - {os: macos-12, archs: "x86_64 arm64"} + - {os: macos-13, archs: "x86_64"} + - {os: macos-14, archs: "arm64"} - {os: ubuntu-latest, archs: "x86_64 i686"} - {os: ubuntu-latest, archs: "aarch64"} - {os: windows-2019, archs: "AMD64 x86"} @@ -59,7 +60,7 @@ jobs: if: matrix.archs == 'aarch64' - name: Create wheels + run tests - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 with: config-file: "./cibuildwheel.toml" env: @@ -161,7 +162,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/CREDITS b/CREDITS index a3206bb9a..baff0c089 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,8 +822,12 @@ I: 2222 N: Ryan Carsten Schmidt W: https://github.com/ryandesign -I: 2361 +I: 2361, 2365 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 ed4223374..89207a63a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,11 +6,19 @@ **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) +- 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 + 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) 5.9.8 ===== diff --git a/Makefile b/Makefile index 1ab660119..f09d83ec5 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 @@ -19,7 +20,7 @@ PY3_DEPS = \ pypinfo \ requests \ rstcheck \ - ruff \ + ruff==0.3.4 \ setuptools \ sphinx_rtd_theme \ teyit \ @@ -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 @@ -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 @@ -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/README.rst b/README.rst index e739c75a2..5f0f31635 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 diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index a53235dab..744ddadf4 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/_static/css/custom.css b/docs/_static/css/custom.css index e88f53077..49da69dfb 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; +} diff --git a/docs/index.rst b/docs/index.rst index fc4cddc24..4959801f6 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 @@ -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 8138db41e..71ae4533a 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) @@ -2037,7 +2037,7 @@ def swap_memory(): # ===================================================================== -# --- disks/paritions related functions +# --- disks/partitions related functions # ===================================================================== diff --git a/psutil/_common.py b/psutil/_common.py index 6989feafd..f33f3e035 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/_psbsd.py b/psutil/_psbsd.py index da68f5efd..4e67a9d85 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/_pslinux.py b/psutil/_pslinux.py index 2a59bfe13..7c1a4f972 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 dddbece1f..20987ecc8 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/_pswindows.py b/psutil/_pswindows.py index 8db66f0a0..3c60a949a 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/arch/osx/net.c b/psutil/arch/osx/net.c index 24ce1b834..d365676ce 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" diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c index a2faa157c..53626c2dc 100644 --- a/psutil/arch/osx/sensors.c +++ b/psutil/arch/osx/sensors.c @@ -12,6 +12,7 @@ #include +#include #include #include @@ -59,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 4b4e041a4..a2c793855 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/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c index dac1129c0..77b6dbf1e 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 ea91d870c..36dbc627e 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -1557,7 +1557,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_bsd.py b/psutil/tests/test_bsd.py index 7b502bcb8..a714632dc 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 221aef57c..81c627d85 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -240,7 +240,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 360ba25b0..c5e9d8fec 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -279,7 +279,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( @@ -343,7 +343,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( @@ -1362,9 +1362,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( diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index 700c054f8..93204fa06 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -15,11 +15,11 @@ import pickle import socket import stat +import sys import unittest import psutil import psutil.tests -from psutil import LINUX from psutil import POSIX from psutil import WINDOWS from psutil._common import bcat @@ -34,7 +34,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 +46,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,33 +260,88 @@ 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): + if name in {"win_service_iter", "win_service_get"}: + continue + 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', msg='msg') + ) + ) + self.assertIsInstance(b, psutil.NoSuchProcess) + self.assertEqual(b.pid, 4567) + self.assertEqual(b.name, 'name') + self.assertEqual(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') + + 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') + + 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') # # 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") + # 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__) @@ -577,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) @@ -850,7 +907,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 +1016,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 +1045,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_osx.py b/psutil/tests/test_osx.py index 8fce8ae08..1fe02d3e4 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_posix.py b/psutil/tests/test_posix.py index d3eef768d..941f0fac1 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -347,7 +347,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] @@ -363,7 +363,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) @@ -449,7 +449,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 0dd433db7..e0f05f314 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -766,7 +766,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") elif QEMU_USER: self.assertEqual(p.cmdline()[2:], cmdline) else: @@ -1180,7 +1180,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_process_all.py b/psutil/tests/test_process_all.py index 8b915deb3..5d0fd0daf 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -105,12 +105,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() @@ -463,6 +465,84 @@ 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: + # 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: + 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 + with self.subTest(pid=pid): + check(pid) + + if __name__ == '__main__': from psutil.tests.runner import run_from_name diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 6707082f6..85ef9eb35 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -89,7 +89,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): @@ -356,7 +356,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: @@ -386,7 +386,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 @@ -425,7 +425,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]), @@ -580,7 +580,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 d09003d27..cb4bccf7f 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) diff --git a/pyproject.toml b/pyproject.toml index 0625af1fd..94266a7bf 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",