diff --git a/.ci/README b/.ci/README deleted file mode 100644 index 86b72afb83..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 OSX, 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/.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/.ci/appveyor/run_with_compiler.cmd b/.ci/appveyor/run_with_compiler.cmd deleted file mode 100644 index 5da547c499..0000000000 --- a/.ci/appveyor/run_with_compiler.cmd +++ /dev/null @@ -1,88 +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/.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 bb86700ea0..0000000000 --- a/.ci/travis/install.sh +++ /dev/null @@ -1,42 +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 - # py26) - # pyenv install 2.6.9 - # pyenv virtualenv 2.6.9 psutil - # ;; - py27) - pyenv install 2.7.10 - pyenv virtualenv 2.7.10 psutil - ;; - py34) - pyenv install 3.4.3 - pyenv virtualenv 3.4.3 psutil - ;; - esac - pyenv rehash - 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 - pip install -U ipaddress mock -fi - -pip install -U coverage coveralls flake8 pep8 setuptools diff --git a/.ci/travis/run.sh b/.ci/travis/run.sh deleted file mode 100755 index 1501387a53..0000000000 --- a/.ci/travis/run.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -set -e -set -x - -PYVER=`python -c 'import sys; print(".".join(map(str, sys.version_info[:2])))'` - -# setup OSX -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/__main__.py -else - PSUTIL_TESTING=1 python -Wa psutil/tests/__main__.py -fi - -if [ "$PYVER" == "2.7" ] || [ "$PYVER" == "3.6" ]; then - # run mem leaks test - PSUTIL_TESTING=1 python -Wa psutil/tests/test_memory_leaks.py - # run linter (on Linux only) - if [[ "$(uname -s)" != 'Darwin' ]]; then - python -m flake8 - fi -fi diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000..3867ab07fa --- /dev/null +++ b/.clang-format @@ -0,0 +1,68 @@ +# Re-adapted from: https://gist.github.com/JPHutchins/6ef33a52cc92fc4a71996b32b11724b4 +# clang-format doc: https://clang.llvm.org/docs/ClangFormatStyleOptions.html + +BasedOnStyle: Google +AlignAfterOpenBracket: BlockIndent +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/.coveragerc b/.coveragerc deleted file mode 100644 index 7d3f185f55..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 OSX - 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/.dprint.jsonc b/.dprint.jsonc new file mode 100644 index 0000000000..be59266815 --- /dev/null +++ b/.dprint.jsonc @@ -0,0 +1,31 @@ +{ + "markdown": { + "lineWidth": 79, + "textWrap": "always", + }, + "json": { + "indentWidth": 4, + "associations": [ + "**/*.json", + "**/*.jsonc", + ], + }, + "yaml": { + "associations": [ + "**/*.yml", + "**/*.yaml", + "**/.clang-format", + ], + }, + "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.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/.git-pre-commit b/.git-pre-commit deleted file mode 100755 index c3c605e054..0000000000 --- a/.git-pre-commit +++ /dev/null @@ -1,118 +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. - -""" -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)] - - lineno = 0 - for path in py_files: - with open(path) 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") - - # flake8 - 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 scape 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") - - -main() diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..03c7c77c17 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,9 @@ +# These are supported funding model platforms + +tidelift: "pypi/psutil" +github: giampaolo +patreon: # Replace with a single Patreon 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.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8 diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000000..7db9f45e83 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,21 @@ +--- +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 } + +## Description + +{{{ + 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/.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/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 0000000000..32b20a93fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,18 @@ +--- +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 } + +## 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 new file mode 100644 index 0000000000..dd506cb9fb --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +## 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 } + +## 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 +}}} diff --git a/.github/no-response.yml b/.github/no-response.yml new file mode 100644 index 0000000000..56457a28d0 --- /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: need-more-info +# 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/.github/placeholder b/.github/placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml new file mode 100644 index 0000000000..62f58c4e75 --- /dev/null +++ b/.github/workflows/bsd.yml @@ -0,0 +1,66 @@ +# 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: + # 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/**" + - "setup.py" + - "tests/**" + 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 || '' }} + cancel-in-progress: true +jobs: + freebsd: + # if: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Run tests + uses: vmactions/freebsd-vm@v1 + with: + usesh: true + run: | + make ci-test + + openbsd: + # if: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Run tests + uses: vmactions/openbsd-vm@v1 + with: + usesh: true + run: | + make ci-test + + netbsd: + # if: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Run tests + uses: vmactions/netbsd-vm@v1 + with: + usesh: true + run: | + make ci-test diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..7547a4a4ae --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,93 @@ +# Runs CI tests and generates wheels on the following platforms: +# * Linux +# * macOS +# * Windows +# +# Useful URLs: +# * https://github.com/pypa/cibuildwheel +# * https://github.com/actions/checkout +# * https://github.com/actions/setup-python +# * https://github.com/actions/upload-artifact + +on: [push, pull_request] +name: build +concurrency: + # 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: + + # Run tests on Linux, macOS, Windows + tests: + name: "${{ matrix.os }}, ${{ matrix.arch }}" + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-latest, arch: x86_64 } + - { os: ubuntu-24.04-arm, arch: aarch64 } + - { os: macos-15, arch: x86_64 } + - { os: macos-15, arch: arm64 } + - { os: windows-2025, arch: AMD64 } + - { os: windows-11-arm, arch: ARM64 } + steps: + - uses: actions/checkout@v5 + + # Install Python 3.8 on macOS ARM64 for universal2 support, else 3.11 + - name: Install Python + uses: actions/setup-python@v6 + with: + python-version: ${{ runner.os == 'macOS' && runner.arch == 'ARM64' && '3.8' || '3.11' }} + + - name: Build wheels + run tests + uses: pypa/cibuildwheel@v3.2.1 + env: + CIBW_ARCHS: "${{ matrix.arch }}" + CIBW_ENABLE: "cpython-freethreading ${{ startsWith(github.ref, 'refs/tags/') && '' || 'cpython-prerelease' }}" + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }}-${{ matrix.arch }} + path: wheelhouse + + # Run linters and build doc. + linters-and-doc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: 3.x + - name: "Run linters" + run: | + make ci-lint + - name: "Build doc" + run: | + pip install -r docs/requirements.txt + cd docs && make html + + # 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 + - uses: actions/download-artifact@v4 + with: + name: wheels + path: wheelhouse + - run: | + make ci-check-dist diff --git a/.github/workflows/changelog_bot.py b/.github/workflows/changelog_bot.py new file mode 100644 index 0000000000..c18b6cb95d --- /dev/null +++ b/.github/workflows/changelog_bot.py @@ -0,0 +1,541 @@ +# 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"] + # 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": author_name, + } + + +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], + ) + 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, + 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 + ) + + 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( + ( + 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: + # 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 + 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: + # 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") + + # 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..3af467eea3 --- /dev/null +++ b/.github/workflows/changelog_bot.yml @@ -0,0 +1,81 @@ +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 info + id: pr-info + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=${{ github.event.issue.number }} + PR_JSON=$(gh pr view $PR_NUMBER \ + --repo ${{ github.repository }} \ + --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: refs/pull/${{ github.event.issue.number }}/head + 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 + 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" + 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 }}" + 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 diff --git a/.github/workflows/issues.py b/.github/workflows/issues.py new file mode 100755 index 0000000000..b0af035392 --- /dev/null +++ b/.github/workflows/issues.py @@ -0,0 +1,349 @@ +#!/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. + +"""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 functools +import json +import os +import pathlib +import re +from pprint import pprint as pp + +from github import Github + +ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent +SCRIPTS_DIR = ROOT_DIR / 'scripts' + + +# --- constants + +# fmt: off +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", + "CloseHandle", "GetLastError", "NtQuery", "DLL", "MSVC", "TCHAR", + "WCHAR", ".bat", "OpenProcess", "TerminateProcess", + "windows error", "NtWow64", "NTSTATUS", "Visual Studio", + ], + "macos": [ + "macos", "mac ", "osx", "os x", "mojave", "sierra", "capitan", + "yosemite", "catalina", "mojave", "big sur", "xcode", "darwin", + "dylib", "m1", + ], + "aix": ["aix"], + "cygwin": ["cygwin"], + "freebsd": ["freebsd"], + "netbsd": ["netbsd"], + "openbsd": ["openbsd"], + "sunos": ["sunos", "solaris"], + "wsl": ["wsl"], + "unix": [ + "psposix", "waitpid", "statvfs", "/dev/tty", + "/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"], + "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", + "continuous integration", "unittest", "pytest", "unit test", + ], + # critical errors + "critical": [ + "WinError", "WindowsError", "RuntimeError", "ZeroDivisionError", + "SystemError", "MemoryError", "core dump", "segfault", + "segmentation fault", + ], +} + +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'), + ('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://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 \ +process, please add a comment to reopen this issue. +""" + +# REPLY_UPDATE_CHANGELOG = """\ +# """ + + +# --- github API utils + + +def is_pr(issue): + return issue.pull_request is not None + + +def has_label(issue, label): + assigned = [x.name for x in issue.labels] + return label in assigned + + +def get_repo(): + repo = os.environ['GITHUB_REPOSITORY'] + token = os.environ['GITHUB_TOKEN'] + return Github(token).get_repo(repo) + + +# --- event utils + + +@functools.lru_cache() +def _get_event_data(): + with open(os.environ["GITHUB_EVENT_PATH"]) as f: + ret = json.load(f) + 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 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 + + +def log(msg): + if '\n' in msg or "\r\n" in msg: + print(f">>>\n{msg}\n<<<", flush=True) + else: + print(f">>> {msg} <<<", flush=True) + + +def add_label(issue, label): + def should_add(issue, label): + if has_label(issue, 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(f"already has label f{label}") + return False + + return not has_label(issue, label) + + if not should_add(issue, label): + log(f"should not add label {label!r}") + return + + log(f"add label {label!r}") + issue.add_to_labels(label) + + +def _guess_labels_from_text(text): + assert isinstance(text, str), 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): + assert isinstance(text, str), text + for label, keyword in _guess_labels_from_text(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) + 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') + 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 'new-api' in s: + add_label(issue, 'new-api') + if 'new-platform' in s: + add_label(issue, 'new-platform') + else: + log("not found") + + +# --- events + + +def on_new_issue(issue): + def has_text(text): + 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 body_mentions_python_h() + ): + log("found mention of Python.h") + issue.create_comment(REPLY_MISSING_PYTHON_HEADERS) + issue.edit(state='closed') + return + + +def on_new_pr(issue): + pass + # pr = get_repo().get_pull(issue.number) + # files = [x.filename for x in list(pr.get_files())] + # if "changelog.rst" not in files: + # issue.create_comment(REPLY_UPDATE_CHANGELOG) + + +def main(): + issue = get_issue() + stype = "PR" if is_pr(issue) else "issue" + log(f"running issue bot for {stype} {issue!r}") + + if is_event_new_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(f"created new PR {issue}") + add_labels_from_text(issue, issue.title) + if issue.body: + add_labels_from_new_body(issue, issue.body) + on_new_pr(issue) + else: + log("unhandled event") + + +if __name__ == '__main__': + main() diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml new file mode 100644 index 0000000000..ee081e1aec --- /dev/null +++ b/.github/workflows/issues.yml @@ -0,0 +1,33 @@ +# Fired by Github Actions every time an issue, PR or comment is created. + +name: issues +on: + issues: + types: [opened] + pull_request_target: + types: [opened] + issue_comment: + types: [created] +permissions: + issues: write + pull-requests: write +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install Python + uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Install deps + run: python3 -m pip install PyGithub + + - name: Run + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PYTHONUNBUFFERED=1 PYTHONWARNINGS=always python3 .github/workflows/issues.py diff --git a/.github/workflows/sunos.yml b/.github/workflows/sunos.yml new file mode 100644 index 0000000000..0bc62f5a15 --- /dev/null +++ b/.github/workflows/sunos.yml @@ -0,0 +1,38 @@ +# Execute tests on SunOS +# https://github.com/vmactions/solaris-vm + +name: sunos +on: + push: + # only run this job if the following files are modified + paths: &sunos_paths + - ".github/workflows/sunos.yml" + - "psutil/_pssunos.py" + - "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 .[test] + PYTHONMALLOC=malloc PYTHONUNBUFFERED=1 python3 -m pytest tests/test_memleaks.py diff --git a/.gitignore b/.gitignore index 99d0d54571..3d8f05eaff 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,12 @@ syntax: glob *.rej *.so *.swp +.failed-tests.txt .cache/ .idea/ .tox/ build/ +docs/_build/ dist/ +wheelhouse/ +.tests/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9289eb6b7a..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -sudo: false -language: python -cache: pip -matrix: - include: - # Linux - - python: 2.6 - - python: 2.7 - - python: 3.4 - - python: 3.5 - - python: 3.6 - # OSX - - language: generic - os: osx - env: PYVER=py27 - - language: generic - os: osx - env: PYVER=py34 -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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..318a53f311 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# 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.). +- When reporting a malfunction, consider enabling + [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. + +## 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. +- 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/CREDITS b/CREDITS deleted file mode 100644 index 41061cdf78..0000000000 --- a/CREDITS +++ /dev/null @@ -1,537 +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 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. - -- Giampaolo - -Author -====== - -N: Giampaolo Rodola' -C: Italy -E: g.rodola@gmail.com -W: http://grodola.blogspot.com/ - -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) -- OSX: - - 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) - -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 -I: 517, 607, 610, 1131, 1123, 1130, 1154, 1164, 1174, 1177, 1210, 1214 - -N: Jeff Tang -W: https://github.com/mrjefftang -I: 340, 529, 616, 653, 654, 648, 641 - -N: Jeremy Whitlock -E: jcscoobyrs@gmail.com -D: great help with OSX C development. -I: 125, 150, 174, 206 - -N: Landry Breuil -W: https://github.com/landryb -D: OpenBSD implementation. -I: 615 - -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: Justin Venus -E: justin.venus@gmail.com -D: Solaris support -I: 18 - -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: 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: 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 - -N: Riccardo Murri -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 -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 -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 -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 -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 -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 - -N: Anders Chrigström -W: https://github.com/anders-chrigstrom -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 -C: Israel -W: https://github.com/mindw -I: 578, 581, 587 - -N: spacewanderlzx -C: Guangzhou,China -E: spacewanderlzx@gmail.com -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 -E: Reykjavik, Iceland -E: https://github.com/arnimarj -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: Frank Benkstein -W: https://github.com/fbenkstein -I: 732, 733 - -N: Visa Hankala -E: visa@openbsd.org -I: 741 - -N: Sebastian-Gabriel Brestin -C: Romania -E: sebastianbrestin@gmail.com -I: 704 - -N: Timmy Konick -W: https://github.com/tijko -I: 751 - -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 - -N: Farhan Khan -E: khanzf@gmail.com -I: 823 - -N: Jake Omann -E: https://github.com/jomann09 -I: 816, 775 - -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 -E: andre.l.caron@gmail.com -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 -E: pierre.fersing@bleemeo.com -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 -I: 1042, 1079 - -N: Oleksii Shevchuk -W: https://github.com/alxchk -I: 1077, 1093, 1091, 1220 - -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 diff --git a/DEVGUIDE.rst b/DEVGUIDE.rst deleted file mode 100644 index 2d48ced27c..0000000000 --- a/DEVGUIDE.rst +++ /dev/null @@ -1,209 +0,0 @@ -======================= -Setup and running tests -======================= - -If you plan on hacking on psutil this is what you're supposed to do first: - -- clone the GIT repository: - -.. code-block:: bash - - $ git clone git@github.com:giampaolo/psutil.git - -- install test deps and GIT hooks: - -.. code-block:: bash - - make setup-dev-env - -- run tests: - -.. code-block:: bash - - 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 `_). -- 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 ``make install-git-hooks``. -- C code strictly follows `PEP 7 `_ - styling guides. - -======== -Makefile -======== - -Some useful make commands: - -.. code-block:: bash - - make install # install - make setup-dev-env # install useful dev libs (pyflakes, unittest2, etc.) - make test # run unit tests - make test-memleaks # run memory leak tests - make test-coverage # run test coverage - make flake8 # run PEP8 linter - -There are some differences between ``make`` on UNIX and Windows. -For instance, to run a specific Python version. On UNIX: - -.. code-block:: bash - - make test PYTHON=python3.5 - -On Windows: - -.. code-block:: bat - - set PYTHON=C:\python35\python.exe && make test - -...or: - -.. code-block:: bat - - make -p 35 test - -If you want to modify psutil and run a script on the fly which uses it do -(on UNIX): - -.. code-block:: bash - - make test TSCRIPT=foo.py - -On Windows: - -.. code-block:: bat - - set TSCRIPT=foo.py && make test - -==================== -Adding a new feature -==================== - -Usually the files involved when adding a new functionality are: - -.. 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/tests/test_process|system.py # main test suite - psutil/tests/test_{platform}.py # platform specific test suite - -Typical process occurring when adding a new functionality (API): - -- define the new function 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 - ``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``). - 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 - -====================== -Continuous integration -====================== - -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 -**Windows** by using: - -- `Travis `_ (Linux, OSX) -- `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: - -.. 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 - :target: https://ci.appveyor.com/project/giampaolo/psutil - :alt: Windows tests (Appveyor) - -OSX, BSD, AIX and Solaris are currently tested manually (sigh!). - -Test coverage -------------- - -Test coverage is provided by `coveralls.io `_, -it is controlled via `.travis.yml `_ -and it is updated on every git push. -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 - :alt: Test coverage (coverall.io) - -============= -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/ - -======================= -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. - -============= -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/HISTORY.rst b/HISTORY.rst index 65cad58e01..54808c578f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3258 +1,3 @@ -*Bug tracker at https://github.com/giampaolo/psutil/issues* +History has moved to: -5.4.4 -===== - -XXXX-XX-XX - -**Enhancements** - -- 1239_: [Linux] expose kernel "slab" memory for psutil.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 - address space first. (patch by Georg Sauthoff) -- 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 - 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) - -5.4.3 -===== - -*2018-01-01* - -**Enhancements** - -- 775_: disk_partitions() on Windows return mount points. - -**Bug fixes** - -- 1193_: pids() may return False on OSX. - -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_: added support for sensors_battery() on OSX. (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. - -**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 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. - -5.4.1 -===== - -*2017-11-08* - -**Enhancements** - -- 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 - 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 - 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()'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 - 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_: 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. -- 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()'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) - -**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 - 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] 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 - properly handle unicode paths and may raise UnicodeDecodeError. -- 1033_: [OSX, 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 - 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 - 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] 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 - 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] 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 - 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: - - 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 -- 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; 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. - -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 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*. - -**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_: 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()'s 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 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. - -5.1.0 -===== - -*2017-02-01* - -**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. - -**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_: 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. - -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] 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. - -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(). - -4.4.2 -===== - -*2016-10-26* - -**Bug fixes** - -- 931_: psutil no longer compiles on Solaris. - -4.4.1 -===== - -*2016-10-25* - -**Bug fixes** - -- 927_: ``Popen.__del__`` may cause maximum recursion depth error. - -4.4.0 -===== - -*2016-10-23* - -**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. - -**Bug fixes** - -- 514_: [OSX] possibly fix Process.memory_maps() segfault (critical!). -- 783_: [OSX] 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() - 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_: [OSX, 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 - OSError with no exception set if process is gone. -- 916_: [OSX] 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(), 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 - 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() is 20% faster - Process.status() is 28% faster - Process.name() is 25% faster - Process.num_threads is 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 - into account. -- 816_: [Windows] fixed net_io_counter() 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] 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 - -**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 OSX 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. - -**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_: [OSX] 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. - -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, OSX, 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 scripts/procsmem.py script. - -**Bug fixes** - -- 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 - broken on 2.4 kernels. -- 770_: [NetBSD] disk_io_counters() metrics didn't update. - -3.4.2 -===== - -*2016-01-20* - -**Enhancements** - -- 728_: [Solaris] exposed psutil.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. - -3.4.1 -===== - -*2016-01-15* - -**Enhancements** - -- 557_: [NetBSD] added NetBSD support. (contributed by Ryo Onodera and - Thomas Klausner) -- 708_: [Linux] psutil.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. - -**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. - -3.3.0 -===== - -*2015-11-25* - -**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) - -**Bug fixes** - -- 692_: [UNIX] 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] 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] 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 OSX. (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 - 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() - -**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] 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. - -3.1.0 -===== - -*2015-07-15* - -**Enhancements** - -- 534_: [Linux] disk_partitions() added support for ZFS filesystems. -- 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 -- 651_: continuous code quality test integration with scrutinizer-ci.com - -**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 - 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. - -3.0.1 -===== - -*2015-06-18* - -**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. - -3.0.0 -===== - -*2015-06-13* - -**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. -- 469_: on Python >= 3.4 ``IOPRIO_CLASS_*`` and ``*_PRIORITY_CLASS`` constants - returned by psutil.Process' ionice() and nice() methods 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, ...), - 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 py.test and nose test discovery and tests run. -- 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 physical CPUs - instead of physical 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. - -2.2.1 -===== - -*2015-02-02* - -**Bug fixes** - -- 496_: [Linux] fix "ValueError: ambiguos inode with multiple PIDs references" - (patch by Bruno Binet) - -2.2.0 -===== - -*2015-01-06* - -**Enhancements** - -- 521_: drop support for Python 2.4 and 2.5. -- 553_: new examples/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. - -**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. - (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. - -2.1.3 -===== - -*2014-09-26* - -- 536_: [Linux]: 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_: new examples/ps.py sample code. - -**Bug fixes** - -- 340_: [Windows] Process.get_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 - Daeschler) -- 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 - (< 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] psutil.net_connections() exceptions. (patch by Alexander Grothe) - -2.1.0 -===== - -*2014-04-08* - -**Enhancements** - -- 387_: system-wide open connections a-la netstat. - -**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. - -2.0.0 -===== - -*2014-03-10* - -**Enhancements** - -- 424_: [Windows] installer for Python 3.X 64 bit. -- 427_: number of logical and physical CPUs (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 - 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 get_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. -- 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() 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. - -**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()": - - +--------------------------+----------------------+ - | 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 psutil.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 psutil.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 psutil.wait_procs() has - been renamed to "returncode" for consistency with subprocess.Popen. - -1.2.1 -===== - -*2013-11-25* - -**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. - -1.2.0 -===== - -*2013-11-20* - -**Enhancements** - -- 439_: assume os.getpid() if no argument is passed to psutil.Process - constructor. -- 440_: new psutil.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. - -1.1.3 -===== - -*2013-11-07* - -**Bug fixes** - -- 442_: [Linux] 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] psutil won't compile on Debian 6.0 because of missing - prlimit(2) syscall. - -1.1.1 -===== - -*2013-10-08* - -**Bug fixes** - -- 442_: [Linux] 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. -- 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. - -**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. - -**API changes** - -- 408_: turn STATUS_* and CONN_* constants into plain Python strings. - -1.0.1 -===== - -*2013-07-12* - -**Bug fixes** - -- 405_: network_io_counters(pernic=True) no longer works as intended in 1.0.0. - -1.0.0 -===== - -*2013-07-10* - -**Enhancements** - -- 18_: Solaris support (yay!) (thanks Justin Venus) -- 367_: Process.get_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.get_memory_maps() may raise ValueError. -- 394_: [OSX] Mapped memory regions 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(). - -0.7.1 -===== - -*2013-05-03* - -**Bug fixes** - -- 325_: [BSD] psutil.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. - -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 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) - -**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 - 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_: [OSX] 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 - 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) -- 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_: [OSX] get_users() returns an empty list on OSX 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 - process. -- 366_: [FreeBSD] get_memory_maps(), get_num_fds(), get_open_files() and - getcwd() Process 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_: 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] 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. -- 261_: process extended memory info. -- 295_: [OSX] 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 - 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 - 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. -- 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, OSX) - - wired (OSX, 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. - -**Bug fixes** - -- 298_: [OSX 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. - -**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. - -0.5.1 -===== - -*2012-06-29* - -**Enhancements** - -- 293_: [Windows] process executable 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. - -0.5.0 -===== - -*2012-06-27* - -**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 - 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. -- 260_: process's mapped memory regions. (Windows patch by wj32.64, OSX 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 - with Python 3. -- 278_: new Process.as_dict() method. -- 281_: ppid, name, exe, cmdline and 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. -- 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. - -**Bug fixes** - -- 193_: psutil.Popen constructor can throw an exception if the spawned process - terminates quickly. -- 240_: [OSX] 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. -- 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. - (patch by Amoser) -- 267_: [OSX] 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 - 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 - reused. -- 314_: Process.get_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 - representation. - -0.4.1 -===== - -*2011-12-14* - -**Bug fixes** - -- 228_: some example scripts were not working with python 3. -- 230_: [Windows / OSX] 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. - -0.4.0 -===== - -*2011-10-29* - -**Enhancements** - -- 150_: network I/O counters. (OSX 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) -- 213_: examples/iotop.py script. -- 217_: Process.get_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. - -**Bug fixes** - -- 135_: [OSX] 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 - 100 on multicore processors. -- 197_: [Linux] Process.get_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 - 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. - -0.3.0 -===== - -*2011-07-08* - -**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. -- 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 - non-blocking mode. (patch by Philip Roberts) -- 178_: OSX - Process.get_threads() leaks memory -- 180_: [Windows] Process's get_num_threads() and get_threads() methods can - raise NoSuchProcess exception while process still exists. - -0.2.1 -===== - -*2011-03-20* - -**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 - 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_: [OSX] 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 - in C and no longer relies on lsof resulting in a 3x speedup. - -**Bug fixes** - -- 83_: process cmdline is empty on OSX 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. - -**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. -- 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 - 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. - -**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 OS X. -- 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. - -**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). - -0.1.3 -===== - -*2010-03-02* - -**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 - -**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 -- 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 OS X includes shared library size -- 77_: NoSuchProcess wasn't raised on Process.create_time if kill() was - used first. - -0.1.2 -===== - -*2009-05-06* - -**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 - -**Bug fixes** - -- 36_: [Windows] NoSuchProcess not raised when accessing timing methods. -- 40_: test_get_cpu_times() failing on FreeBSD and OS X. -- 42_: [Windows] get_memory_percent() raises AccessDenied. - -0.1.1 -===== - -*2009-03-06* - -**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 - 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 - (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(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 - -.. _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 +- https://psutil.readthedocs.io/latest/changelog.html diff --git a/IDEAS b/IDEAS deleted file mode 100644 index 9037269839..0000000000 --- a/IDEAS +++ /dev/null @@ -1,166 +0,0 @@ -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 - -PLATFORMS -========= - -- #355: Android (with patch) -- #82: Cygwin (PR at #998) -- #276: GNU/Hurd -- #693: Windows Nano -- DragonFlyBSD -- HP-UX - -FEATURES -======== - -- #371: sensors_temperatures() at least for OSX. - -- #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. - -- #900: wheels for OSX 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. - -- (Linux) locked files via /proc/locks: - https://www.centos.org/docs/5/html/5.2/Deployment_Guide/s2-proc-locks.html - -- #269: NIC rx/tx queue. This should probably go into net_if_stats(). - Figure out on what platforms this is supported: - Linux: yes - Others: ? - -- Process.threads(): thread names; patch for OSX 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. - -- #604: emulate os.getloadavg() on Windows - -- scripts/taskmgr-gui.py (using tk). - -- system-wide number of open file descriptors: - - https://jira.hyperic.com/browse/SIGAR-30 - -- 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.open_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. - PROS: on Windows we can take advantage of _psutil_windows.ppid_map() - which is faster than iterating over all pids and calling ppid(). - CONS: scripts/pstree.py shows this can be easily done in the user code - so maybe it's not worth the addition. - -- advanced cmdline interface exposing the whole API and providing different - kind of outputs (e.g. pprinted, colorized, json). - -- [Linux]: process cgroups (http://en.wikipedia.org/wiki/Cgroups). They look - similar to prlimit() in terms of functionality but uglier (they should allow - limiting per-process network IO resources though, which is great). Needs - further reading. - -- Python 3.3. exposed different sched.h functions: - http://docs.python.org/dev/whatsnew/3.3.html#os - http://bugs.python.org/issue12655 - 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). - -- ...also guest_time and cguest_time on Linux. - -- Enrich exception classes hierarchy on Python >= 3.3 / post PEP-3151 so that: - - NoSuchProcess inherits from ProcessLookupError - - AccessDenied inherits from PermissionError - - TimeoutExpired inherits from TimeoutError (debatable) - See: http://docs.python.org/3/library/exceptions.html#os-exceptions - -- Process.threads() might grow an extra "id" parameter so that it can be - used as such: - ``` - >>> p = psutil.Process(os.getpid()) - >>> p.threads(id=psutil.current_thread_id()) - thread(id=2539, user_time=0.03, system_time=0.02) - >>> - ``` - Note: this leads to questions such as "should we have a custom NoSuchThread - exception? Also see issue #418. - Note #2: this would work with os.getpid() only. - psutil.current_thread_id() might be desirable as per issue #418 though. - -- should psutil.TimeoutExpired exception have a 'msg' kwarg similar to - 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. - -BUGFIXES -======== - -- #600: windows / open_files(): support network file handles. - -REJECTED -======== - -- #550: threads per core - -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 diff --git a/INSTALL.rst b/INSTALL.rst index 9e5defb42b..62020237c1 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -1,163 +1,3 @@ -Install pip -=========== +Installation instructions have moved to: -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 - - wget https://bootstrap.pypa.io/get-pip.py -O - | python - -On OSX or via curl: - -.. code-block:: bash - - python < <(curl -s https://bootstrap.pypa.io/get-pip.py) - -On Windows, `download pip `__, open -cmd.exe and install it: - -.. code-block:: bat - - C:\Python27\python.exe get-pip.py - -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.: - -.. code-block:: bash - - sudo pip install psutil - -* install psutil for your user only (not at system level): - -.. code-block:: bash - - pip install --user psutil - -Linux -===== - -Ubuntu / Debian: - -.. code-block:: bash - - sudo apt-get install gcc python-dev python-pip - pip install psutil - -RedHat / CentOS: - - -.. code-block:: bash - - sudo yum install gcc python-devel python-pip - pip install psutil - -If you're on Python 3 use ``python3-dev`` and ``python3-pip`` instead. - -OSX -=== - -Install `Xcode `__ -first, then: - -.. code-block:: bash - - pip install psutil - -Windows -======= - -The easiest way to install psutil on Windows is to just use the pre-compiled -exe/wheel installers hosted on -`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. -This `blog post `__ -provides numerous info on how to properly set up VS (good luck with that). - -* 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, then you can finally compile (see -`here `__). -To compile / install psutil from sources on Windows run: - -.. code-block:: bat - - make.bat build - make.bat install - -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 - -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 - python -m pip install psutil - -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 `__. +- https://psutil.readthedocs.io/latest/install.html diff --git a/LICENSE b/LICENSE index e91b1359a2..cff5eb74e1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -psutil is distributed under BSD license reproduced below. +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, @@ -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. diff --git a/MANIFEST.in b/MANIFEST.in index 87d9bebb49..2491e05915 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,30 +1,71 @@ -include .coveragerc -include .git-pre-commit +include .clang-format +include .dprint.jsonc include .gitignore -include CREDITS -include DEVGUIDE.rst +include CONTRIBUTING.md include HISTORY.rst -include IDEAS include INSTALL.rst include LICENSE include MANIFEST.in include Makefile include README.rst +include SECURITY.md +include _bootstrap.py +include docs/.readthedocs.yaml +include docs/DEVNOTES.md include docs/Makefile include docs/README -include docs/_static/copybutton.js +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/_ext/genindex_filter.py +include docs/_globals.rst +include docs/_sponsors.html include docs/_static/css/custom.css -include docs/_static/favicon.ico -include docs/_static/sidebar.js +include docs/_static/images/favicon.svg +include docs/_static/images/icon-cpu.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-repl.js +include docs/_static/js/search-shortcuts.js +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/topbar.html +include docs/about.rst +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 +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/make.bat -include make.bat -include psutil/DEVNOTES +include docs/install.rst +include docs/migration.rst +include docs/performance.rst +include docs/platform.rst +include docs/recipes.rst +include docs/requirements.txt +include docs/shell-equivalents.rst +include docs/stdlib-equivalents.rst +include docs/timeline.rst include psutil/__init__.py include psutil/_common.py -include psutil/_compat.py -include psutil/_exceptions.py +include psutil/_enums.py +include psutil/_ntuples.py include psutil/_psaix.py include psutil/_psbsd.py include psutil/_pslinux.py @@ -33,12 +74,8 @@ 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 -include psutil/_psutil_posix.h include psutil/_psutil_sunos.c include psutil/_psutil_windows.c include psutil/_pswindows.py @@ -49,53 +86,99 @@ 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 +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 +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 +include psutil/arch/bsd/sys.c +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/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/sys_socks.c -include psutil/arch/freebsd/sys_socks.h +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 +include psutil/arch/linux/proc.c +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/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 -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/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 +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 +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 +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 +include psutil/arch/sunos/cpu.c +include psutil/arch/sunos/disk.c +include psutil/arch/sunos/environ.c +include psutil/arch/sunos/init.h +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/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 +include psutil/arch/windows/net.c 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/pids.c +include psutil/arch/windows/proc.c +include psutil/arch/windows/proc_handles.c +include psutil/arch/windows/proc_info.c +include psutil/arch/windows/proc_utils.c include psutil/arch/windows/security.c -include psutil/arch/windows/security.h +include psutil/arch/windows/sensors.c include psutil/arch/windows/services.c -include psutil/arch/windows/services.h -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_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 -include psutil/tests/test_system.py -include psutil/tests/test_unicode.py -include psutil/tests/test_windows.py +include psutil/arch/windows/socks.c +include psutil/arch/windows/sys.c +include psutil/arch/windows/wmi.c +include pyproject.toml include scripts/battery.py include scripts/cpu_distribution.py include scripts/disk_usage.py @@ -105,13 +188,23 @@ 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/download_exes.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 +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_timeline.py -include scripts/internal/purge.py -include scripts/internal/winmake.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_sysinfo.py +include scripts/internal/purge_installation.py +include scripts/internal/rst_unused_targets.py include scripts/iotop.py include scripts/killall.py include scripts/meminfo.py @@ -129,4 +222,26 @@ include scripts/top.py include scripts/who.py include scripts/winservices.py include setup.py -include tox.ini +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_heap.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_type_hints.py +include tests/test_unicode.py +include tests/test_windows.py +recursive-exclude docs/_static * diff --git a/Makefile b/Makefile index f7b4cd7f5b..38d0f891ae 100644 --- a/Makefile +++ b/Makefile @@ -1,41 +1,34 @@ -# 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. - -PYTHON = python -TSCRIPT = psutil/tests/__main__.py +# 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 ARGS = -# List of nice-to-have dev libs. -DEPS = \ - argparse \ - check-manifest \ - coverage \ - flake8 \ - futures \ - ipaddress \ - mock==1.0.1 \ - pep8 \ - perf \ - pyflakes \ - requests \ - setuptools \ - sphinx \ - twine \ - unittest2 \ - wheel - -# 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 - -all: test +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 +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 + +# 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 # =================================================================== 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 \ @@ -45,118 +38,128 @@ 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 \ - *\$testfn* \ + *\@psutil-* \ .coverage \ - .tox \ + .failed-tests.txt \ + .pytest_cache \ + .ruff_cache/ \ + .tests \ build/ \ dist/ \ docs/_build/ \ htmlcov/ \ - tmp/ + pytest-cache-files* \ + wheelhouse -_: - -build: _ ## Compile 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 - rm -rf tmp - $(PYTHON) -c "import psutil" # make sure it actually worked +.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 + @# within this directory. + $(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. - ${MAKE} build - PYTHONWARNINGS=all $(PYTHON) setup.py develop $(INSTALL_OPTS) - rm -rf tmp + $(MAKE) build + # 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) -m pip uninstall -y -v psutil || true - $(PYTHON) scripts/internal/purge.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 \ - "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'; \ - exec(pyexc); \ - ctx = ssl._create_unverified_context() if hasattr(ssl, '_create_unverified_context') else None; \ - kw = dict(context=ctx) if ctx else {}; \ - req = urlopen('https://bootstrap.pypa.io/get-pip.py', **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' % (sys.executable, f.name)); \ - f.close(); \ - sys.exit(code);" - -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) 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 + 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-lint: ## Install python deps necessary to run linters. + $(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) .[lint] + +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 # =================================================================== -test: ## Run all tests. - ${MAKE} install - $(TEST_PREFIX) $(PYTHON) $(TSCRIPT) +# 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). + # To run a specific test do `make test ARGS=tests/test_process.py::TestProcess::test_cmdline` + $(RUN_TEST) $(ARGS) + +test-parallel: ## Run all tests (except memleak tests) in parallel. + $(RUN_TEST) -p xdist -n auto --dist loadgroup $(ARGS) -test-process: ## Run process-related API tests. - ${MAKE} install - $(TEST_PREFIX) $(PYTHON) -m unittest -v psutil.tests.test_process +test-process: ## Run process-related tests. + $(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. - ${MAKE} install - $(TEST_PREFIX) $(PYTHON) -m unittest -v psutil.tests.test_system + $(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. - ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_misc.py + $(RUN_TEST) -k "test_misc.py or Misc" $(ARGS) + +test-scripts: ## Run scripts tests. + $(RUN_TEST) tests/test_scripts.py $(ARGS) + +test-testutils: ## Run test utils tests. + $(RUN_TEST) tests/test_testutils.py $(ARGS) test-unicode: ## Test APIs dealing with strings. - ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_unicode.py + $(RUN_TEST) tests/test_unicode.py $(ARGS) test-contracts: ## APIs sanity tests. - ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_contracts.py + $(RUN_TEST) tests/test_contracts.py $(ARGS) -test-connections: ## Test net_connections() and Process.connections(). - ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_connections.py +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) + +test-heap: ## Test psutil.heap_*() APIs. + $(RUN_TEST) -k "test_heap.py or heap_" $(ARGS) test-posix: ## POSIX specific tests. - ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_posix.py + $(RUN_TEST) -k "test_posix.py or posix_ or Posix" $(ARGS) 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 + $(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. - ${MAKE} install - $(TEST_PREFIX) $(PYTHON) psutil/tests/test_memory_leaks.py + PYTHONMALLOC=malloc $(RUN_TEST) -k test_memleaks.py $(ARGS) -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-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 tests.test_sudo -test-coverage: ## Run test coverage. - ${MAKE} install - # Note: coverage options are controlled by .coveragerc file +test-last-failed: ## Re-run tests which failed on last run + $(RUN_TEST) --last-failed $(ARGS) + +coverage: ## Run test coverage. rm -rf .coverage htmlcov - $(TEST_PREFIX) $(PYTHON) -m coverage run $(TSCRIPT) + $(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 @@ -166,27 +169,112 @@ test-coverage: ## Run test coverage. # Linters # =================================================================== -pep8: ## PEP8 linter. - @git ls-files | grep \\.py$ | xargs $(PYTHON) -m pep8 +# 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. + @$(call _ls,'*.py') | xargs $(PYTHON) -m ruff check --output-format=concise + +black: ## Run black formatter. + @$(call _ls,'*.py') | xargs $(PYTHON) -m black --check --safe + +lint-c: ## Run C linter. + @$(call _ls,'*.c' '*.h') | xargs -P0 -I{} clang-format --dry-run --Werror {} + +dprint: + @$(DPRINT) check -pyflakes: ## Pyflakes linter. - @export PYFLAKES_NODOCTEST=1 && \ - git ls-files | grep \\.py$ | xargs $(PYTHON) -m pyflakes +lint-rst: ## Run linter for .rst files. + @$(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 -flake8: ## flake8 linter. - @git ls-files | grep \\.py$ | xargs $(PYTHON) -m flake8 +lint-toml: ## Run linter for pyproject.toml. + @$(call _ls,'*.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 + +# --- 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) # =================================================================== -# GIT +# Fixers # =================================================================== -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 +fix-black: + @git ls-files '*.py' | xargs $(PYTHON) -m black + +fix-ruff: + @git ls-files '*.py' | xargs $(PYTHON) -m ruff check --fix --output-format=concise $(ARGS) -install-git-hooks: ## Install GIT pre-commit hook. - ln -sf ../../.git-pre-commit .git/hooks/pre-commit - chmod +x .git/hooks/pre-commit +fix-c: + @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 + +fix-rst: + @git ls-files '*.rst' | xargs rstwrap + +fix-dprint: + @$(DPRINT) fmt + +fix-all: ## Run all code fixers. + $(MAKE) fix-ruff + $(MAKE) fix-black + $(MAKE) fix-toml + $(MAKE) fix-dprint + +# =================================================================== +# CI jobs +# =================================================================== + +ci-lint: ## Run all linters on GitHub CI. + $(MAKE) install-pydeps-lint + 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. + $(MAKE) install-sysdeps + $(MAKE) install-pydeps-test + $(MAKE) build + $(MAKE) print-sysinfo + $(MAKE) test + $(MAKE) test-memleaks + +ci-test-cibuildwheel: ## Run CI tests for the built wheels. + $(MAKE) install-sysdeps + $(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 + cp -r tests .tests/ + 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 + $(MAKE) create-sdist + mv wheelhouse/* dist/ + $(MAKE) check-dist + $(MAKE) install + $(MAKE) print-dist # =================================================================== # Distribution @@ -194,59 +282,101 @@ install-git-hooks: ## Install GIT pre-commit hook. # --- create -sdist: ## Create tar.gz source distribution. - ${MAKE} generate-manifest - $(PYTHON) setup.py sdist +generate-manifest: ## Generates MANIFEST.in file. + $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in -wheel: ## Generate wheel. - $(PYTHON) setup.py bdist_wheel +create-sdist: ## Create tar.gz source distribution. + $(MAKE) generate-manifest + $(PYTHON_ENV_VARS) $(PYTHON) setup.py sdist -win-download-wheels: ## Download wheels hosted on appveyor. - $(TEST_PREFIX) $(PYTHON) scripts/internal/download_exes.py --user giampaolo --project psutil +create-wheels: ## Create .whl files + $(PYTHON_ENV_VARS) $(PYTHON) setup.py bdist_wheel -# --- upload +download-wheels: ## Download latest wheels hosted on github. + $(PYTHON) scripts/internal/download_wheels.py --tokenfile=~/.github.token + $(MAKE) print-dist -upload-src: ## Upload source tarball on https://pypi.python.org/pypi/psutil. - ${MAKE} sdist - $(PYTHON) setup.py sdist upload +create-dist: ## Create .tar.gz + .whl distribution. + $(MAKE) create-sdist + $(MAKE) download-wheels -upload-win-wheels: ## Upload wheels in dist/* directory on PYPI. - $(PYTHON) -m twine upload dist/*.whl +# --- check + +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 -# --- others +check-wheels: ## Check sanity of wheels. + $(PYTHON) -m abi3audit --verbose --strict dist/*-abi3-*.whl + $(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 + +# --- release pre-release: ## Check if we're ready to produce a new release. - rm -rf dist - ${MAKE} install - ${MAKE} generate-manifest - git diff MANIFEST.in > /dev/null # ...otherwise 'git diff-index HEAD' will complain - ${MAKE} win-download-wheels - ${MAKE} sdist - $(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';" - $(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) -m twine upload dist/* # upload tar.gz and Windows wheels on PYPI - ${MAKE} git-tag-release + $(MAKE) clean + $(MAKE) create-dist + $(MAKE) check-dist + $(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" + @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 + +release: ## Upload a new release. + $(PYTHON) -m twine upload dist/*.tar.gz + $(PYTHON) -m twine upload dist/*.whl + $(MAKE) git-tag-release + +git-tag-release: ## Git-tag a new release. + 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 + +# =================================================================== +# Printers +# =================================================================== 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 +print-access-denied: ## Print AD exceptions + $(PYTHON) scripts/internal/print_access_denied.py -check-manifest: ## Inspect MANIFEST.in file. - $(PYTHON) -m check_manifest -v $(ARGS) +print-api-speed: ## Benchmark all API calls + $(PYTHON) scripts/internal/print_api_speed.py $(ARGS) -generate-manifest: ## Generates MANIFEST.in file. - $(PYTHON) scripts/internal/generate_manifest.py > MANIFEST.in +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 + +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 @@ -256,15 +386,13 @@ 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 - $(TEST_PREFIX) $(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) - ${MAKE} install - $(TEST_PREFIX) $(PYTHON) scripts/internal/bench_oneshot_2.py + $(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 +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 diff --git a/README.rst b/README.rst index 73205270cb..9983411414 100644 --- a/README.rst +++ b/README.rst @@ -1,470 +1,265 @@ -.. 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 - :target: https://ci.appveyor.com/project/giampaolo/psutil - :alt: Windows tests (Appveyor) - -.. 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) - -.. 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 - :target: https://pypi.python.org/pypi/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/pypi/l/psutil.svg - :target: https://pypi.python.org/pypi/psutil/ - :alt: License - -=========== -Quick links -=========== - -- `Home page `_ -- `Install `_ -- `Documentation `_ -- `Download `_ -- `Forum `_ -- `StackOverflow `_ -- `Blog `_ -- `Development guide `_ -- `What's new `_ +.. -======= -Summary -======= +.. raw:: html + +
+ psutil +

Process and System Utilities for Python

+ Documentation    + Blog    + Who uses psutil    +
+ +
+ +
+ + Downloads + + + + Binary packages + + + + Latest version + -psutil (process and system utilities) is a cross-platform library for -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. -psutil currently supports the following platforms: + + Linux, macOS, Windows + + + + FreeBSD, NetBSD, OpenBSD + +
+ +..
+ +About +===== + +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 tool such as +*ps, top, free, iotop, netstat, ifconfig, lsof* and others (see +`shell equivalents`_). psutil supports the following platforms: - **Linux** - **Windows** -- **OSX**, +- **macOS** - **FreeBSD, OpenBSD**, **NetBSD** - **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. +Install +======= -==================== -Example applications -==================== +.. code-block:: -+------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------+ -| .. 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 | -+------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------+ + pip install psutil -Also see `scripts directory `__ -and `doc recipes `__. +For platform-specific details see +`installation `_. -===================== -Projects using psutil -===================== +Documentation +============= -At the time of writing psutil has roughly -`2.9 milion downloads `__ -per month and there are over -`7000 open source projects `__ -on github which depend from psutil. -Here's some I find particularly interesting: +psutil documentation is available at https://psutil.readthedocs.io/. -- 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 +Sponsors ======== -- 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 +.. raw:: html + + + + + + + + + + + +.. + +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 +`_ using it, and +14,000+ packages depending on it. Some notable projects using psutil: + +- `TensorFlow `_, + `PyTorch `_, +- `Home Assistant `_, + `Ansible `_, + `Apache Airflow `_, + `Sentry `_ +- `Celery `_, + `Dask `_ +- `Glances `_, + `bpytop `_, + `Ajenti `_, + `GRR `_ + +`Full list `_ -============== Example usages ============== -CPU -=== +For the full API with more examples, see the +`API overview `_ and +`API reference `_. + +**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) - >>> - >>> 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) - >>> + scpufreq(current=931.42, min=800.0, max=3500.0) -Memory -====== +**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) + 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 -===== +**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')] - >>> + 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 -======= +**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)} - >>> - >>> 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) + {'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=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': [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)]} - >>> - >>> psutil.net_if_stats() - {'eth0': snicstats(isup=True, duplex=, speed=100, mtu=1500), - 'lo': snicstats(isup=True, duplex=, speed=0, mtu=65536)} - >>> -Sensors -======= +**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), - 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)]} - >>> - >>> psutil.sensors_fans() - {'asus': [sfan(label='cpu_fan', current=3200)]} - >>> + {'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_battery() sbattery(percent=93, secsleft=16628, power_plugged=False) - >>> -Other system info -================= +**Processes** .. 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 -================== - -.. 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.name() - 'python' + 'python3' >>> p.exe() - '/usr/bin/python' - >>> p.cwd() - '/home/giampaolo' - >>> p.cmdline() - ['/usr/bin/python', 'main.py'] - >>> - >>> p.pid - 7055 - >>> p.ppid() - 7054 - >>> p.parent() - - >>> p.children() - [, - ] - >>> - >>> p.status() - 'running' - >>> p.username() - 'giampaolo' - >>> p.create_time() - 1267551141.5019531 - >>> p.terminal() - '/dev/pts/0' - >>> - >>> 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) + '/usr/bin/python3' >>> 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=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) - 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 - >>> 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.io_counters() - pio(read_count=478001, write_count=59371, read_bytes=700416, write_bytes=69632, read_chars=456232, write_chars=517543) - >>> + pmem(rss=3164160, vms=4410163, shared=897433, text=302694, data=2422374) + >>> p.net_connections(kind='tcp') + [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/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)] - >>> - >>> 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')] - >>> - >>> p.num_threads() - 4 - >>> p.num_fds() - 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() - 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) + [popenfile(path='/home/giampaolo/monit.py', fd=3, position=0, mode='r', flags=32768)] >>> - >>> 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', 'COLORTERM': 'gnome-terminal', - ...} - >>> - >>> p.as_dict() - {'status': 'running', 'num_ctx_switches': pctxsw(voluntary=63, involuntary=1), 'pid': 5457, ...} - >>> p.is_running() - True - >>> p.suspend() - >>> p.resume() - >>> - >>> p.terminate() - >>> p.wait(timeout=3) - 0 - >>> - >>> 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 - root 3 0.0 0.0 0 0 Jun17 00:05 ksoftirqd/0 + >>> for p in psutil.process_iter(['pid', 'name']): + ... print(p.pid, p.name()) ... - 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 -==================== - -.. code-block:: python - - >>> import psutil - >>> for proc in psutil.process_iter(attrs=['pid', 'name']): - ... print(proc.info) + 1 systemd + 2 kthreadd + 3 ksoftirqd/0 ... - {'pid': 1, 'name': 'systemd'} - {'pid': 2, 'name': 'kthreadd'} - {'pid': 3, 'name': '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) - >>> - -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 -================ -.. 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'} - -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 +.. _`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 +======= -Don't want to donate money? Then maybe you could `write me a recommendation on Linkedin `_. +BSD-3 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..fda8e36955 --- /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/_bootstrap.py b/_bootstrap.py new file mode 100644 index 0000000000..7a1da56fd4 --- /dev/null +++ b/_bootstrap.py @@ -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. + +"""Bootstrap utilities for loading psutil modules without psutil +being installed. +""" + +import ast +import importlib.util +import os +import pathlib + +ROOT_DIR = pathlib.Path(__file__).resolve().parent + + +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 = ROOT_DIR / "psutil" / "__init__.py" + with open(path, encoding="utf-8") 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/appveyor.yml b/appveyor.yml deleted file mode 100644 index 1bacc06517..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,124 +0,0 @@ -# Build: 1 (bump this up by 1 to force an appveyor run) - -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" - - - PYTHON: "C:\\Python36" - PYTHON_VERSION: "3.6.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" - 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" - -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 -m pip install --upgrade --user unittest2 ipaddress pypiwin32==219 wmi wheel" - - "%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 - -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" - -after_test: - - "%WITH_COMPILER% %PYTHON%/python setup.py bdist_wheel" - -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-ci - -# 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/__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 - - scripts/* - - setup.py diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml new file mode 100644 index 0000000000..763475d3fd --- /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 for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# RTD recommends specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/DEVNOTES.md b/docs/DEVNOTES.md new file mode 100644 index 0000000000..ca633c8f84 --- /dev/null +++ b/docs/DEVNOTES.md @@ -0,0 +1,202 @@ +A collection of ideas and notes about stuff to implement in future versions. + +## 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 + +- #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. + +- 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: + https://www.centos.org/docs/5/html/5.2/Deployment_Guide/s2-proc-locks.html + +- #269: NIC rx/tx queue. This should probably go into `net_if_stats()`. Figure + out on what platforms this is supported. Linux: yes. Others? + +- Asynchronous `psutil.Popen` (see http://bugs.python.org/issue1191964) + +- (Windows) fall back on using WMIC for Process methods returning + `AccessDenied`. + +- #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). + +- system-wide number of open file descriptors: + - https://jira.hyperic.com/browse/SIGAR-30 + +- Number of system threads. + - Windows: + http://msdn.microsoft.com/en-us/library/windows/desktop/ms684824(v=vs.85).aspx + +- `psutil.proc_tree()` something which obtains a `{pid:ppid, ...}` struct for + all running processes in one shot. This can be factored out from + `Process.children()` and exposed as a first class function. PROS: on Windows + we can take advantage of `ppid_map()`, which is faster than iterating over + all PIDs. CONS: `scripts/pstree.py` shows this can be easily done in the user + code, so maybe it's not worth the addition. + +- advanced cmdline interface exposing the whole API and providing different + kind of outputs (e.g. pprinted, colorized, json). + +- Linux: process cgroups (http://en.wikipedia.org/wiki/Cgroups). They look + similar to `prlimit()` in terms of functionality but, uglier (they should + allow limiting per-process network IO resources though, which is great). + Needs further reading. + +- Python 3.3. exposed different sched.h functions: + http://docs.python.org/dev/whatsnew/3.3.html#os + http://bugs.python.org/issue12655 + http://docs.python.org/dev/library/os.html#interface-to-the-scheduler It It + might be worth to take a look and figure out whether we can include some of + those in psutil. + +- `os.times()` provides `elapsed` times (`Process.cpu_times()` might as well?). + +- Enrich exception classes hierarchy on Python >= 3.3 / post PEP-3151 so that: + - `NoSuchProcess` inherits from `ProcessLookupError` + - `AccessDenied` inherits from `PermissionError` + - `TimeoutExpired inherits` from TimeoutError (debatable) See: + http://docs.python.org/3/library/exceptions.html#os-exceptions + +- `Process.threads()` might grow an extra "id" parameter so that it can be used + as: + + ```python + >>> p = psutil.Process(os.getpid()) + >>> p.threads(id=psutil.current_thread_id()) + thread(id=2539, user_time=0.03, system_time=0.02) + >>> + ``` + + Note: this leads to questions such as "should we have a custom `NoSuchThread` + exception? Also see issue #418. Also note: this would work with `os.getpid()` + only. `psutil.current_thread_id()` might be desirable as per issue #418 + though. + +- should `TimeoutExpired` exception have a 'msg' kwarg similar to + `NoSuchProcess` and `AccessDenied`? Not that we need it, but currently we + cannot raise a `TimeoutExpired` exception with a specific error string. + +- round `Process.memory_percent() result? + +## Resources + +- zabbix: https://zabbix.org/wiki/Get_Zabbix +- netdata: https://github.com/netdata/netdata + +## System tools source code + +Source code of system monitoring tools (ps, top, vmstat, etc.) on various +platforms. Useful as a reference when implementing or verifying psutil's +platform-specific code. + +### Linux + +- **procps-ng** (ps, top, free, vmstat, kill, uptime, w, who, pgrep, pmap, + pstree, pwdx, watch): https://gitlab.com/procps-ng/procps +- **sysstat** (iostat, mpstat, pidstat, sar): + https://github.com/sysstat/sysstat +- **iproute2** (ss, ip): https://github.com/iproute2/iproute2 +- **net-tools** (ifconfig, netstat, arp, route): + https://net-tools.sourceforge.io/ +- **util-linux** (mount, findmnt, taskset, ionice, renice, prlimit, lscpu, + lsblk): https://github.com/util-linux/util-linux +- **GNU coreutils** (df, nproc): https://github.com/coreutils/coreutils +- **lsof**: https://github.com/lsof-org/lsof +- **lm-sensors** (sensors): https://github.com/lm-sensors/lm-sensors +- **smem**: https://www.selenic.com/smem/ + +### macOS + +Apple open-source distributions: https://github.com/apple-oss-distributions/ + +- **ps**: https://github.com/apple-oss-distributions/adv_cmds +- **top**: https://github.com/apple-oss-distributions/top +- **sysctl**: https://github.com/apple-oss-distributions/system_cmds +- **netstat, ifconfig**: + https://github.com/apple-oss-distributions/network_cmds +- **lsof**: https://github.com/apple-oss-distributions/lsof + +### FreeBSD + +Repository: https://github.com/freebsd/freebsd-src + +- **ps**: https://github.com/freebsd/freebsd-src/tree/main/bin/ps +- **top**: https://github.com/freebsd/freebsd-src/tree/main/usr.bin/top +- **vmstat**: https://github.com/freebsd/freebsd-src/tree/main/usr.bin/vmstat +- **systat**: https://github.com/freebsd/freebsd-src/tree/main/usr.bin/systat +- **netstat**: https://github.com/freebsd/freebsd-src/tree/main/usr.bin/netstat +- **iostat**: https://github.com/freebsd/freebsd-src/tree/main/usr.sbin/iostat +- **procstat**: + https://github.com/freebsd/freebsd-src/tree/main/usr.bin/procstat +- **ifconfig**: https://github.com/freebsd/freebsd-src/tree/main/sbin/ifconfig +- **cpuset**: https://github.com/freebsd/freebsd-src/tree/main/bin/cpuset +- **pgrep**: https://github.com/freebsd/freebsd-src/tree/main/bin/pkill +- **swapinfo**: + https://github.com/freebsd/freebsd-src/tree/main/usr.sbin/swapinfo + +### NetBSD + +Repository: https://github.com/NetBSD/src + +- **ps**: https://github.com/NetBSD/src/tree/trunk/bin/ps +- **top**: https://github.com/NetBSD/src/tree/trunk/external/bsd/top +- **vmstat**: https://github.com/NetBSD/src/tree/trunk/usr.bin/vmstat +- **systat**: https://github.com/NetBSD/src/tree/trunk/usr.bin/systat +- **netstat**: https://github.com/NetBSD/src/tree/trunk/usr.bin/netstat +- **ifconfig**: https://github.com/NetBSD/src/tree/trunk/sbin/ifconfig +- **pgrep**: https://github.com/NetBSD/src/tree/trunk/bin/pgrep + +### OpenBSD + +Repository: https://github.com/openbsd/src + +- **ps**: https://github.com/openbsd/src/tree/master/bin/ps +- **top**: https://github.com/openbsd/src/tree/master/usr.bin/top +- **vmstat**: https://github.com/openbsd/src/tree/master/usr.bin/vmstat +- **systat**: https://github.com/openbsd/src/tree/master/usr.bin/systat +- **netstat**: https://github.com/openbsd/src/tree/master/usr.bin/netstat +- **ifconfig**: https://github.com/openbsd/src/tree/master/sbin/ifconfig +- **pgrep**: https://github.com/openbsd/src/tree/master/bin/pgrep +- **swapctl**: https://github.com/openbsd/src/tree/master/sbin/swapctl + +### Other tools + +- **zabbix**: https://github.com/zabbix/zabbix/ +- **htop**: https://github.com/htop-dev/htop +- **btop**: https://github.com/aristocratos/btop/ + +## Stats + +- https://pepy.tech/projects/psutil +- https://clickpy.clickhouse.com/dashboard/psutil +- https://pypistats.org/packages/psutil diff --git a/docs/Makefile b/docs/Makefile index 0c4bdf48a2..7de016cb11 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,226 +1,34 @@ # Makefile for Sphinx documentation -# -# You can set these variables from the command line. -PYTHON = python -SPHINXOPTS = -SPHINXBUILD = $(PYTHON) -m sphinx -PAPER = -BUILDDIR = _build +PYTHON = python3 +SPHINXBUILD = $(PYTHON) -m sphinx +BUILDDIR = _build +ALLSPHINXOPTS = --fail-on-warning --nitpicky --jobs=auto -d $(BUILDDIR)/doctrees . -# 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) . - -.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: +clean: ## Remove all build files 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." +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 -.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." +help: ## Display callable targets. + @awk -F':.*?## ' '/^[a-zA-Z0-9_.-]+:.*?## / {printf "\033[36m%-24s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort diff --git a/docs/_ext/availability.py b/docs/_ext/availability.py new file mode 100644 index 0000000000..7e1d5a0cd4 --- /dev/null +++ b/docs/_ext/availability.py @@ -0,0 +1,100 @@ +# 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 {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_ext/changelog_anchors.py b/docs/_ext/changelog_anchors.py new file mode 100644 index 0000000000..55a8637e57 --- /dev/null +++ b/docs/_ext/changelog_anchors.py @@ -0,0 +1,46 @@ +# 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 changelog.rst. + +This script gets called on `make html`, and adds numeric anchors for +version titles (e.g., `7.2.3 — 2026-02-08` -> #723). It also registers +them as Sphinx labels so that :ref:`722` cross-references resolve +correctly. +""" + +import re + +from docutils import nodes + +VERSION_RE = re.compile(r"^(\d+\.\d+\.\d+)") + + +def add_version_anchors(app, doctree): + docname = app.env.docname + if docname != "changelog": + return + + labels = app.env.domaindata.setdefault('std', {}).setdefault('labels', {}) + anonlabels = app.env.domaindata['std'].setdefault('anonlabels', {}) + + 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) + if anchor not in labels: + labels[anchor] = ("changelog", anchor, text) + anonlabels[anchor] = ("changelog", anchor) + + +def setup(app): + app.connect("doctree-read", 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 new file mode 100644 index 0000000000..5e5e05cfcb --- /dev/null +++ b/docs/_ext/check_python_syntax.py @@ -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. + +"""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) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_ext/field_role.py b/docs/_ext/field_role.py new file mode 100644 index 0000000000..c1b475a63b --- /dev/null +++ b/docs/_ext/field_role.py @@ -0,0 +1,22 @@ +# 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) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_ext/genindex_filter.py b/docs/_ext/genindex_filter.py new file mode 100644 index 0000000000..6692b2ffb8 --- /dev/null +++ b/docs/_ext/genindex_filter.py @@ -0,0 +1,55 @@ +# 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. + +"""Post-process genindex.html to clean up index entry labels. + +Transforms: + NAME (qualifier) + +Into: + NAME (qualifier) + +Also shortens verbose Sphinx qualifiers: + (in module psutil) → (function) if NAME ends with "()", else (constant) + (class in psutil) → (class) + (exception in psutil) → (exception) +""" + +import pathlib +import re + +# Matches NAME (qualifier) where NAME does not start +# with "(" — leaving pure-paren sub-entries like "(psutil.Foo method)" alone. +REGEX = re.compile(r'([^(<][^<]*?) (\([^)]+\))') + + +def replace(m): + href, name, qualifier = m.group(1), m.group(2), m.group(3) + if qualifier == "(in module psutil)": + qualifier = "(function)" if name.endswith("()") else "(constant)" + elif qualifier == "(class in psutil)": + qualifier = "(class)" + elif qualifier == "(exception in psutil)": + qualifier = "(exception)" + return ( + f'{name} {qualifier}' + ) + + +def on_build_finished(app, exception): + if exception or app.builder.name != "html": + return + genindex = pathlib.Path(app.outdir) / "genindex.html" + if not genindex.exists(): + return + original = genindex.read_text(encoding="utf-8") + processed = REGEX.sub(replace, original) + if processed != original: + genindex.write_text(processed, encoding="utf-8") + + +def setup(app): + app.connect("build-finished", on_build_finished) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_globals.rst b/docs/_globals.rst new file mode 100644 index 0000000000..f7c3a5276f --- /dev/null +++ b/docs/_globals.rst @@ -0,0 +1,25 @@ +.. 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/_sponsors.html b/docs/_sponsors.html new file mode 100644 index 0000000000..f6038a20b2 --- /dev/null +++ b/docs/_sponsors.html @@ -0,0 +1,63 @@ + + + + + + + + + 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 b76f442af8..a685446bea 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -1,518 +1,2190 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap'); + +/* ================================================================== */ +/* Sizes and effects */ +/* ================================================================== */ + +:root { + /* fonts & text */ + --font-body: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + --font-headings: "Roboto Slab", "ff-tisa-web-pro", "Georgia", Arial, sans-serif; + --font-sidebar: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + --font-hero: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + --font-hero-subtext: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + --font-search: "Inter", system-ui, -apple-system, sans-serif; + --code-font: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + --code-line-height: 1.5; + + --content-font-size: 16.5px; + --content-line-height: 1.65; + + --sidebar-font-size: 15px; + --sidebar-caption-font-size: 14px; + + /* others */ + --border-radius: 4px; + --theme-transition: 0.2s ease; + --adm-padding-y: 5px; +} + +/* ================================================================== */ +/* Colors — light mode (default) */ +/* ================================================================== */ + +:root { + /* page */ + --body-bg: #efefef; + --content-bg: #fcfcfc; + --surface-sunken: #f5f5f5; + --surface-raised: #f0f4f7; + --border: #d6d6d6; + + /* text */ + --content-text: #404040; + --text-muted: #555; + --headings: #404040; + --headings-underline: #ccc; + + /* links */ + --links: #2980b9; + --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.17); + --inline-code-text: inherit; + + /* top bar */ + --topbar-bg: #2a2828; + --topbar-fg: #d1d1d1; + --topbar-fg-active: #fff; + + /* sidebar */ + --sidebar-bg: #343131; + --sidebar-top-bg: #343131; + --sidebar-caption: #55a5d9; + --sidebar-item: #d9d9d9; + --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-note-accent-border: #2980b9; + --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-header-text: var(--content-text); + --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; + + /* primary CTA button */ + --cta-fill: color-mix(in srgb, var(--links) 78%, #666 22%); + --cta-fill-hover: var(--links); +} + +/* ================================================================== */ +/* Colors — dark mode */ +/* ================================================================== */ + +[data-theme="dark"] { + --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 */ + --content-text: #c8c8c8; + --text-muted: #d1d1d1; + --headings: #e0e0e0; + --headings-underline: #3a3a3a; + + /* inline code */ + --inline-code-bg: rgba(255, 255, 255, 0.08); + --inline-code-text: #e6e6e6; + + /* ntuple fields */ + --ntuple-color: #6db99a; + + /* links */ + --links: #6ab0de; + --links-visited: #6ab0de; /* same as --links: visited state disabled site-wide */ + --links-api: #7ecfff; + --links-api-underline: #7ecfff; + + /* admonitions */ + --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: #1d2e40; + --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: #b0873a; + --deprecated-color: #d06060; + + /* tables */ + --table-header-bg: #2d2d2d; + --table-header-text: var(--content-text); + --table-row-odd: #222222; + --table-row-even: #272727; + + /* cards */ + --card-bg: #252525; + --card-border: #2a4a6a; + + /* primary CTA button */ + --cta-fill: #2d6fa8; + --cta-fill-hover: #3a85c4; +} + +/* ================================================================== */ +/* Separators */ +/* ================================================================== */ + +[data-theme="dark"] hr { + border-color: var(--border); +} + +/* ================================================================== */ +/* 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 { + margin-top: -2px; + letter-spacing: 0.03em; + display: flex; + align-items: center; + justify-content: flex-start; + width: 300px; + padding-left: 1.15rem; + gap: 7px; + text-decoration: none !important; + font-weight: 700; + font-size: 1.15rem; + color: var(--topbar-fg-active) !important; +} + +.top-bar-logo span { + line-height: 1; + padding-top: 7px; +} + +.top-bar-logo img { + height: 26px; +} + +.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, +.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); +} + +/* ================================================================== */ +/* Home page */ +/* ================================================================== */ + +/* Hero ------------------------------------------------------------- */ + +.hero { + text-align: center; + padding: 1.5em 0 2em; + /*background: radial-gradient(ellipse, #2d2d2d 0%, var(--content-bg) 70%);*/ +} + +.hero-title { + font-family: var(--font-hero); + font-size: 3.5rem; + font-weight: 700; + color: var(--headings); + 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 { + font-family: var(--font-hero-subtext); + font-size: 1.15rem; + color: var(--text-muted); + margin-bottom: 0.9em; +} + +.hero-badges { + display: flex; + justify-content: center; + gap: 8px; + flex-wrap: wrap; +} + +.hero-badges img { + 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; + max-width: 100%; +} + +.sponsor-table svg { + max-width: 100%; + height: auto; +} + +/* Sponsors --------------------------------------------------------- */ + +.home-page #sponsors { + margin: 2rem -3rem 0; + padding: 1.5rem 3rem 2rem; + background: #f8f9fa; + border-top: 1px solid var(--border); + margin-bottom: 20px; + 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; +} + +.home-page #sponsors h1 { + text-align: center; +} + +.sponsor-logo { + filter: grayscale(100%) opacity(100%); + transition: filter 0.2s; +} + +.sponsor-logo:hover { + filter: grayscale(0%) opacity(100%); +} + +/* Platform pills ---------------------------------------------------- */ + +.home-platforms { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.6em; + margin: 1em 0 3em; +} + +.rst-content a.home-platforms-label, +.rst-content a.home-platforms-label:visited { + font-size: 0.82rem; + color: var(--text-muted); + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + text-decoration: none; + transition: color var(--theme-transition); +} + +.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); + border-radius: 999px; + 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 ------------------------------------------------ */ + +.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(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; + } +} + +.home-feature-card { + border: 1px solid var(--card-border); + border-radius: 8px; + padding: 20px 14px; + text-align: center; + background: var(--card-bg); + 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(--links); +} + +.home-icon-svg { + width: 2.2rem; + height: 2.2rem; + display: block; + 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(--links); + display: block; + margin-bottom: 10px; +} + +.home-feature-title { + font-weight: 700; + font-size: 0.95rem; + margin-bottom: 10px; + color: var(--headings); +} + +.home-feature-apis { + font-size: 0.78rem; + color: var(--text-muted); + font-family: var(--code-font); + line-height: 1.9; +} + +/* Description cards ------------------------------------------------- */ + +.home-desc-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + align-items: stretch; + gap: 1rem; + margin: 2rem 0; +} + +@media (max-width: 700px) { + .home-desc-cards { + grid-template-columns: 1fr; + } +} + +.home-desc-card { + border: 1px solid #e1e4e8; + border-radius: 8px; + padding: 1.1rem 1.3rem; + background: var(--card-bg); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06); + display: flex; + flex-direction: column; +} + +[data-theme="dark"] .home-desc-card { + border-color: #2e2e2e; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); +} + +.home-desc-card-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin: -1.1rem -1.3rem 0.75rem; + padding: 0.55rem 1.3rem; + background: var(--surface-raised); + border-bottom: 1px solid var(--card-border); + border-radius: 8px 8px 0 0; +} + +.home-desc-card-icon { + font-size: 1.1rem; + flex-shrink: 0; + padding-right: 0.35rem; + line-height: 1; + padding-top: 2px; +} + +.home-desc-card:nth-child(1) .home-desc-card-icon { color: #2a6a90; } +.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:nth-child(2), +.home-desc-card:nth-child(3) { + position: relative; + overflow: hidden; +} + +.home-desc-card:nth-child(1), +.home-desc-card:nth-child(3) { + background: #f0f6fb; +} + +.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; +} + +[data-theme="dark"] .home-desc-card:nth-child(2) { + background: #1f1c14; +} + +.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) .home-desc-card-header { + background: linear-gradient(to bottom, #d9c49a, #b8a078); + border-bottom-color: #8a7550; +} + +.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(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; + pointer-events: none; + line-height: 1; +} + +.home-desc-card:nth-child(1)::before { + content: '\f085'; + color: #2a6a90; +} + +.home-desc-card:nth-child(2)::before { + content: '\f080'; + color: #8a7550; +} + +.home-desc-card:nth-child(3)::before { + content: '\f0ac'; + color: #5d6d7e; +} + +.home-desc-card-header .home-desc-card-title { + color: #fff; + text-shadow: + 0 0 2px rgba(0, 0, 0, 0.4), + 0 1px 4px rgba(0, 0, 0, 0.5); + -webkit-text-stroke: 0.3px rgba(0, 0, 0, 0.25); +} + +.home-desc-card-header .home-desc-card-icon { + color: #fff; + text-shadow: none; + -webkit-text-stroke: 0; + font-size: 1.2rem; +} + +.home-desc-card-title { + font-size: 1.05rem; + font-weight: 700; + color: var(--headings); +} + +.home-desc-card p { + font-size: 0.88rem; + color: var(--content-text); + margin: 0 0 0.75rem; + line-height: 1.6; + flex-grow: 1; +} + +.home-desc-card-link { + display: inline-block; + font-size: 0.82rem; + font-weight: 600; + color: var(--links); + text-decoration: none; + margin-top: auto; + padding-top: 0.5rem; + border-top: 1px solid var(--border); + width: 100%; +} + +.home-desc-card-link:hover { + text-decoration: underline; + color: var(--links); +} + +/* TOC (hidden) ----------------------------------------------------- */ + +.home-page .toctree-wrapper { + display: none; +} + +/* ================================================================== */ +/* Layout */ +/* ================================================================== */ + +@media (min-width: 1200px) { + .wy-nav-content { + max-width: 1000px; + padding-left: 20px !important; + } +} + +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 { + font-family: var(--font-body); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.wy-nav-content-wrap { + background: var(--body-bg) !important; +} + .wy-nav-content { - max-width: 100% !important; - padding: 15px !important; + background: var(--content-bg) !important; + /* dark mode only: subtle separator between content and the strip */ + border-right: 1px solid var(--headings-underline); } -.rst-content dl:not(.docutils) { - margin: 0px 0px 0px 0px; +.rst-content { + color: var(--content-text); } -.data dd { - margin-bottom: 0px !important; +.rst-content p, .rst-content li, .rst-content dd { + font-size: var(--content-font-size) !important; + line-height: var(--content-line-height) !important; } -.data .descname { - border-right:10px !important; +@media (max-width: 768px) { + .wy-nav-content { + padding: 0 12px 1.618em 12px !important; + } } -.local-toc li ul li{ - padding-left: 20px !important; +.document { + padding-top: 20px !important; } -.function .descclassname { - font-weight: normal !important; +/* ================================================================== */ +/* Centered layout */ +/* ================================================================== */ + +/* +.wy-grid-for-nav { + background: var(--body-bg); } -.class .descclassname { - font-weight: normal !important; +.wy-body-for-nav { + max-width: 1400px; + margin: 0 auto; } -.admonition.warning { - padding-top: 2px !important; - padding-bottom: 2px !important; +.wy-nav-side { + left: max(0px, calc((100vw - 1400px) / 2)); } +*/ + +/* ================================================================== */ +/* Navigation */ +/* ================================================================== */ -.admonition.note { - padding-top: 2px !important; - padding-bottom: 2px !important; +/* 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; } -.rst-content dl:not(.docutils) dt { +/* ================================================================== */ +/* Sidebar */ +/* ================================================================== */ + +.wy-nav-side, +.wy-nav-top { + background-color: var(--sidebar-bg) !important; +} + + +.wy-side-nav-search { + background-color: var(--sidebar-top-bg) !important; + position: sticky; + top: 44px; + z-index: 10; +} + +/* hide version switch */ +.version-switch { + display: none !important; +} + +.wy-side-nav-search a.icon-home { + display: none; +} + +.wy-side-nav-search input[type="text"] { + font-family: var(--font-search); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.wy-side-nav-search input[type="text"]:focus { + border-color: var(--sidebar-search-focus-border); + box-shadow: 0 0 0 2px var(--sidebar-search-focus-shadow); + 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; + letter-spacing: 0.1em; +} + +.wy-menu-vertical li.toctree-l1 > a { + text-transform: capitalize; +} + +.wy-menu-vertical li.toctree-l1:not(.current) > a { + color: var(--sidebar-item) !important; +} + +.wy-menu-vertical li.toctree-l2 > a { + color: var(--sidebar-item-nested) !important; +} + +.wy-menu-vertical li.current > a { + border-left: 3px solid var(--sidebar-active-border) !important; +} + +.wy-menu-vertical a { + font-family: var(--font-sidebar) !important; + font-size: var(--sidebar-font-size) !important; +} + +/* ================================================================== */ +/* Theme transitions */ +/* ================================================================== */ + +/* 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; +} + +/* ================================================================== */ +/* Sidebar effects */ +/* ================================================================== */ + +@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; + } +} + +/* ================================================================== */ +/* Search Ctrl+K badge */ +/* ================================================================== */ + +.search-input-wrapper { + position: relative; + flex: 1; + display: flex; + align-items: center; +} + +.search-input-wrapper input[type="text"] { + width: 100% !important; + padding-right: 76px !important; + box-sizing: border-box; +} + +.search-kbd-hint { + position: absolute; + right: 12px; + display: flex; + align-items: center; + gap: 2px; + pointer-events: none; + transition: opacity 0.15s ease; +} + +.search-kbd-hint kbd { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 4px; + font-family: var(--font-search); + font-size: 10px; + font-weight: 600; + line-height: 1; + padding-top: 3px; + color: #888; + background: #f5f5f5; + border: 1px solid #d0d0d0; + border-radius: 3px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); +} + +.search-kbd-sep { + font-size: 10px; + color: #aaa; +} + +/* ================================================================== */ +/* Footer */ +/* ================================================================== */ + +footer div[role="navigation"][aria-label="Footer"] { + display: block; +} + +.footer-content { + display: flex; + align-items: center; + gap: 12px; +} + +.footer-text { + 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 { + 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; +} + +/* 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; } -.sig-paren { - padding-left: 2px; - padding-right: 2px; +[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 */ +.rst-footer-buttons .btn.btn-neutral { + padding-top: 6px !important; + padding-bottom: 4px !important; } -h1, h2, h3 { - background: #eee; +[data-theme="dark"] .rst-footer-buttons .btn.btn-neutral { + background-color: #2a2a2a !important; + color: #cccccc !important; + border-color: #444444 !important; +} + +[data-theme="dark"] .rst-footer-buttons .btn.btn-neutral:hover { + background-color: #353535 !important; + color: #ffffff !important; + border-color: #666666 !important; +} + +/* ================================================================== */ +/* Headings */ +/* ================================================================== */ + +h1, h2, h3, h4 { + margin-top: 20px !important; + margin-bottom: 10px !important; + color: var(--headings) !important; + font-family: var(--font-headings) !important; +} + +h1, h2 { padding: 5px; - border-bottom: 1px solid #ccc; +} + +h1, h1 a, +h2, h2 a, +h3, h3 a, +h4, h4 a { + color: var(--headings) !important; } h1 { - font-size: 35px; + font-size: 2.2rem; } -.admonition.warning { - padding-top: 5px !important; - padding-bottom: 5px !important; +h2 { + font-size: 1.7rem; } -.admonition.warning p { - margin-bottom: 5px !important; +h3 { + font-size: 1.3rem; + padding: 5px 0 5px 0; } -.admonition.note { - padding-top: 5px !important; - padding-bottom: 5px !important; +h4 { + font-size: 1.05rem; + padding: 5px 0 5px 0; } -.admonition.note p { - margin-bottom: 5px !important; - backround-color: rgb(238, 255, 204) !important; +.rst-content p { + margin-bottom: 16px !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; +/* ================================================================== */ +/* Tables */ +/* ================================================================== */ + +.wy-table-responsive table thead { + background-color: var(--table-header-bg); } -.highlight .hll { - background-color: #ffffcc +.wy-table-responsive table thead th { + color: var(--table-header-text); } -.highlight { - background: #eeffcc; +.wy-table-responsive table tbody tr.row-odd td { + background-color: var(--table-row-odd) !important; } -.highlight-default, .highlight-python { - border-radius: 3px !important; - border: 1px solid #ac9 !important; +.wy-table-responsive table tbody tr.row-even td { + background-color: var(--table-row-even) !important; } -.highlight .c { - color: #408090; - font-style: italic +.wy-table-responsive table, +.wy-table-responsive table th, +.wy-table-responsive table td { + border-color: var(--border) !important; } -.wy-side-nav-search { - background-color: grey !important +.document th { + padding: 6px 10px !important; + font-weight: 600; } -.highlight { - border-radius: 3px !important; +.document th p { + margin-bottom: 0px !important; +} +.document td { + padding: 6px 10px !important; } -div.highlight-default { - margin-bottom: 10px !important; +.document td p { + 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 { + table-layout: fixed; + width: 100%; } -pre { - padding: 5px !important; +.wy-table-responsive table.longtable th, +.wy-table-responsive table.longtable td { + padding: 10px !important; + white-space: normal !important; + word-wrap: break-word; + 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%; +} + +.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 */ +/* ================================================================== */ + +.rst-content ul { + margin-top: 0px !important; +} + +.rst-content ul p { + margin-bottom: 0px !important; +} + +.rst-content li { + list-style: outside; + margin-left: 15px; } /* ================================================================== */ -/* Warnings and info boxes like python doc */ +/* Links */ /* ================================================================== */ +.rst-content a { + color: var(--links); +} + +.rst-content a:visited { + color: var(--links-visited); +} + +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 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; +} + +/* adjust cpython API links (intersphinx) icon position */ +a.reference.external[href*="docs.python.org"]:not(:has(img))::after { + margin-left: 0.2em; +} + +/* 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; +} + +/* ================================================================== */ +/* Inline code / API refs */ +/* ================================================================== */ + +/* inline literals: ``like this`` */ +.rst-content code, .rst-content tt, code { + color: var(--inline-code-text) !important; + background: var(--inline-code-bg) !important; + 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; +} + +/* undo inline-code styling inside the sidebar nav */ +.wy-nav-side code, +.wy-nav-side tt { + background: transparent !important; + color: inherit !important; + padding: 0 !important; + border-radius: 0 !important; + font-size: inherit !important; +} + +/* 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; + /*background: rgba(175, 184, 193, 0.2) !important;*/ + background: none !important; + /*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; +} + +/* ================================================================== */ +/* 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: 4px solid var(--adm-note-accent-border) !important; + border-radius: var(--border-radius) !important; +} + +div.admonition p { + margin-bottom: var(--adm-padding-y) !important; } +/* per-type color overrides */ div.warning { - background-color: #ffe4e4 !important; - border: 1px solid #f66 !important; - border-radius: 3px !important; + 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: 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 a { + text-decoration: none !important; } -div.note { - background-color: #eee !important; - border: 1px solid #ccc !important; - border-radius: 3px !important; +div.seealso a.reference { + color: var(--adm-seealso-link) !important; + text-decoration: underline; + text-underline-offset: 2px; } -div.admonition p.admonition-title + p { +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 { content: ":" !important; } -div.body div.admonition, div.body div.impl-detail { -} - -.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 { +/* 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; } -.note code { - background: #d6d6d6 !important; -} /* ================================================================== */ -/* Syntax highlight like Python doc. +/* Version directives (versionadded / versionchanged / deprecated) */ /* ================================================================== */ -/* Comment */ -.highlight .err { - border: 1px solid #FF0000 +div.versionadded, +div.versionchanged, +div.deprecated { + border-left: 3px solid; + padding: 0 1rem; } -/* Error */ -.highlight .k { - color: #007020; - font-weight: bold +div.versionadded { + border-left-color: var(--versionadded-border); } -/* Keyword */ -.highlight .o { - color: #666666 +div.versionchanged { + border-left-color: var(--versionchanged-border); } -/* Operator */ -.highlight .ch { - color: #408090; - font-style: italic +div.deprecated { + border-left-color: var(--deprecated-border); } -/* Comment.Hashbang */ -.highlight .cm { - color: #408090; - font-style: italic +div.versionadded .versionmodified { + color: var(--versionadded-color); } -/* Comment.Multiline */ -.highlight .cp { - color: #007020 +div.versionchanged .versionmodified { + color: var(--versionchanged-color); } -/* Comment.Preproc */ -.highlight .cpf { - color: #408090; - font-style: italic +div.deprecated .versionmodified { + color: var(--deprecated-color); } -/* Comment.PreprocFile */ -.highlight .c1 { - color: #408090; - font-style: italic -} +/* ================================================================== */ +/* Search results page */ +/* ================================================================== */ -/* Comment.Single */ -.highlight .cs { - color: #408090; - background-color: #fff0f0 +/* ---- 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; + flex-direction: column; + gap: 8px; +} + +#search-results li { + list-style: none !important; + margin: 0 !important; + padding: 12px 16px !important; + display: flex !important; + flex-wrap: wrap; + align-items: flex-start; + column-gap: 6px; + line-height: normal !important; + border: 1px solid rgba(0, 0, 0, 0.08) !important; + border-radius: 6px; + background-color: var(--card-bg); + box-shadow: 0 2px 3px rgba(0, 0, 0, 0.04); } -/* Comment.Special */ -.highlight .gd { - color: #A00000 +[data-theme="dark"] #search-results li { + border-color: var(--border) !important; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35); } -/* Generic.Deleted */ -.highlight .ge { - font-style: italic +/* compact card: link-only result with no description or context */ +#search-results li:not(:has(> span)):not(:has(> p.context)) { + padding: 8px 16px !important; } -/* Generic.Emph */ -.highlight .gr { - color: #FF0000 +/* keyboard-active result */ +#search-results li.search-result-active { + border-color: var(--links) !important; + box-shadow: 0 0 0 3px rgba(41, 128, 185, 0.15); + outline: none; } -/* Generic.Error */ -.highlight .gh { - color: #000080; - font-weight: bold +[data-theme="dark"] #search-results li.search-result-active { + border-color: var(--links) !important; + box-shadow: 0 0 0 3px rgba(106, 176, 222, 0.2); } -/* Generic.Heading */ -.highlight .gi { - color: #00A000 -} +/* ---- Card content ---- */ -/* Generic.Inserted */ -.highlight .go { - color: #333333 +/* 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; } -/* Generic.Output */ -.highlight .gp { - color: #c65d09; - font-weight: bold +/* 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; } -/* Generic.Prompt */ -.highlight .gs { - font-weight: bold +/* 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; } -/* Generic.Strong */ -.highlight .gu { - color: #800080; - font-weight: bold -} +/* ---- Icons: generic (Sphinx SearchResultKind) ---- */ -/* Generic.Subheading */ -.highlight .gt { - color: #0044DD +#search-results li::before { + font-family: FontAwesome; + font-size: 18px; + flex-shrink: 0; + margin-top: 1px; + width: 1.2em; + text-align: center; } -/* Generic.Traceback */ -.highlight .kc { - color: #007020; - font-weight: bold +/* page title match */ +#search-results li.kind-title::before { + content: "\f0c1"; } -/* Keyword.Constant */ -.highlight .kd { - color: #007020; - font-weight: bold +/* API object (function, class, constant, …) */ +#search-results li.kind-object::before { + content: "âš™ï¸"; } -/* Keyword.Declaration */ -.highlight .kn { - color: #007020; - font-weight: bold +/* body text match */ +#search-results li.kind-text::before { + content: "\f0f6"; } -/* Keyword.Namespace */ -.highlight .kp { - color: #007020 +/* index entry */ +#search-results li.kind-index::before { + content: "\f02e"; + color: #e5a825; } -/* Keyword.Pseudo */ -.highlight .kr { - color: #007020; - font-weight: bold +/* ---- 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; } -/* Keyword.Reserved */ -.highlight .kt { - color: #902000 +/* 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; } -/* Keyword.Type */ -.highlight .m { - color: #208050 +[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; } -/* Literal.Number */ -.highlight .s { - color: #4070a0 +/* 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; } -/* Literal.String */ -.highlight .na { - color: #4070a0 +/* 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; } -/* Name.Attribute */ -.highlight .nb { - color: #007020 +/* recipes: lightbulb */ +#search-results li:has(> a[href^="recipes.html"])::before, +#search-results li:has(> a[href*="/recipes.html"])::before { + content: "\f0eb"; + color: #eab308; } -/* Name.Builtin */ -.highlight .nc { - color: #0e84b5; - font-weight: bold +/* 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; } -/* Name.Class */ -.highlight .no { - color: #60add5 +/* 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; } -/* Name.Constant */ -.highlight .nd { - color: #555555; - font-weight: bold +/* 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); } -/* Name.Decorator */ -.highlight .ni { - color: #d55537; - font-weight: bold +/* ---- Search-term highlighting ---- */ + +span.highlighted { + background-color: #fff3b0 !important; + color: inherit !important; + border: none !important; + outline: none !important; + box-shadow: none !important; + border-radius: 2px; } -/* Name.Entity */ -.highlight .ne { - color: #007020 +[data-theme="dark"] span.highlighted { + background-color: rgba(255, 193, 7, 0.3) !important; + border: 1px solid rgba(255, 193, 7, 0.5) !important; } -/* Name.Exception */ -.highlight .nf { - color: #06287e +/* ================================================================== */ +/* API function / class signatures */ +/* ================================================================== */ + +.rst-content dl:not(.docutils) { + margin: 0px 0px 0px 0px !important; } -/* Name.Function */ -.highlight .nl { - color: #002070; - font-weight: bold +.rst-content dl.py + dl.py > dt { + margin-top: 28px !important; } -/* Name.Label */ -.highlight .nn { - color: #0e84b5; - font-weight: bold +.data dd { + margin-bottom: 0px !important; } -/* Name.Namespace */ -.highlight .nt { - color: #062873; - font-weight: bold +.sig-paren { + padding-left: 2px; + padding-right: 2px; } -/* Name.Tag */ -.highlight .nv { - color: #bb60d5 +/* 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: var(--content-font-size) !important; } -/* Name.Variable */ -.highlight .ow { - color: #007020; - font-weight: bold +/* The function name itself */ +.rst-content dl:not(.docutils) dt .descname { + color: var(--func-sig-name) !important; + font-weight: bold !important; } -/* Operator.Word */ -.highlight .w { - color: #bbbbbb +/* 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; } -/* Text.Whitespace */ -.highlight .mb { - color: #208050 +/* Keep the [source] link subtle */ +.viewcode-link { + color: var(--func-sig-border) !important; + font-size: 0.8em; + font-style: italic; } -/* Literal.Number.Bin */ -.highlight .mf { - color: #208050 +/* ================================================================== */ +/* 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; } -/* Literal.Number.Float */ -.highlight .mh { - color: #208050 +.rst-content dl.glossary > dd { + margin: 0 0 1em 1.5em !important; + background: transparent !important; + border: none !important; + padding: 0 !important; } -/* Literal.Number.Hex */ -.highlight .mi { - color: #208050 +.rst-content dl.glossary > dd p { + margin: 0 0 0.5em 0 !important; } -/* Literal.Number.Integer */ -.highlight .mo { - color: #208050 +/* ================================================================== */ +/* 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; + } } -/* Literal.Number.Oct */ -.highlight .sa { - color: #4070a0 +.index-qualifier { + font-size: 0.90em !important; + color: var(--text-muted); + opacity: 0.8; } -/* Literal.String.Affix */ -.highlight .sb { - color: #4070a0 +.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; } -/* Literal.String.Backtick */ -.highlight .sc { - color: #4070a0 +.rst-content table.genindextable { + border-collapse: separate; + border-spacing: 3em 0; + margin-left: -3em; + margin-right: -3em; } -/* Literal.String.Char */ -.highlight .dl { - color: #4070a0 +.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; } -/* Literal.String.Delimiter */ -.highlight .sd { - color: #4070a0; - font-style: italic +/* ================================================================== */ +/* Funding page */ +/* ================================================================== */ + +.rst-content #how-to-fund dl.simple { + display: flex; + flex-direction: column; + gap: 0.7em; + margin: 1em 0 1.5em 0; } -/* Literal.String.Doc */ -.highlight .s2 { - color: #4070a0 +.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; } -/* Literal.String.Double */ -.highlight .se { - color: #4070a0; - font-weight: bold +.rst-content #how-to-fund dl.simple dd { + margin: 0.2em 0 0 1.1em !important; + color: var(--text-muted); } -/* Literal.String.Escape */ -.highlight .sh { - color: #4070a0 +.rst-content #how-to-fund dl.simple dd p { + margin: 0 !important; } -/* Literal.String.Heredoc */ -.highlight .si { - color: #70a0d0; - font-style: italic +.rst-content a.reference.external[href^="mailto:"]:visited, +.rst-content a.reference.external[href^="mailto:"]:visited * { + color: var(--links) !important; } -/* Literal.String.Interpol */ -.highlight .sx { - color: #c65d09 +/* 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; } -/* Literal.String.Other */ -.highlight .sr { - color: #235388 +.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; } -/* Literal.String.Regex */ -.highlight .s1 { - color: #4070a0 +.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; } -/* Literal.String.Single */ -.highlight .ss { - color: #517918 +/* 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; } -/* Literal.String.Symbol */ -.highlight .bp { - color: #007020 +.rst-content table.sponsor-table, +.rst-content table.sponsor-table tr, +.rst-content table.sponsor-table td { + background: transparent !important; + border: none !important; } -/* Name.Builtin.Pseudo */ -.highlight .fm { - color: #06287e +.sponsor-cta-wrap { + text-align: center; + margin-top: 1em; } -/* Name.Function.Magic */ -.highlight .vc { - color: #bb60d5 +.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); } -/* Name.Variable.Class */ -.highlight .vg { - color: #bb60d5 +.rst-content a.sponsor-cta:hover { + color: var(--links); + opacity: 1; + text-decoration: underline; + text-decoration-color: var(--links); } -/* Name.Variable.Global */ -.highlight .vi { - color: #bb60d5 +/* ================================================================== */ +/* Syntax highlight — imported Monokai rules */ +/* ================================================================== */ + +[data-theme="dark"] .highlight { color: #F8F8F2; } +[data-theme="dark"] .highlight .hll { background-color: #49483e; } +[data-theme="dark"] .highlight .c { color: #959077; } /* Comment */ +[data-theme="dark"] .highlight .err { color: #ED007E; background-color: #1E0010; } /* Error */ +[data-theme="dark"] .highlight .esc { color: #F8F8F2; } /* Escape */ +[data-theme="dark"] .highlight .g { color: #F8F8F2; } /* Generic */ +[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: #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 */ +[data-theme="dark"] .highlight .cm { color: #959077; } /* Comment.Multiline */ +[data-theme="dark"] .highlight .cp { color: #959077; } /* Comment.Preproc */ +[data-theme="dark"] .highlight .cpf { color: #959077; } /* Comment.PreprocFile */ +[data-theme="dark"] .highlight .c1 { color: #959077; } /* Comment.Single */ +[data-theme="dark"] .highlight .cs { color: #959077; } /* Comment.Special */ +[data-theme="dark"] .highlight .gd { color: #FF4689; } /* Generic.Deleted */ +[data-theme="dark"] .highlight .ge { color: #F8F8F2; font-style: italic; } /* Generic.Emph */ +[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: #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 */ +[data-theme="dark"] .highlight .gt { color: #F8F8F2; } /* Generic.Traceback */ +[data-theme="dark"] .highlight .kc { color: #66D9EF; } /* Keyword.Constant */ +[data-theme="dark"] .highlight .kd { color: #66D9EF; } /* Keyword.Declaration */ +[data-theme="dark"] .highlight .kn { color: #FF4689; } /* Keyword.Namespace */ +[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: #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: #9ccfa5; } /* Name.Class */ +[data-theme="dark"] .highlight .no { color: #66D9EF; } /* Name.Constant */ +[data-theme="dark"] .highlight .nd { color: #9ccfa5; } /* Name.Decorator */ +[data-theme="dark"] .highlight .ni { color: #F8F8F2; } /* Name.Entity */ +[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: #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: #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: #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: #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: #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: #b9a5e0; } /* Literal.Number.Integer.Long */ + +/* ================================================================== */ +/* Code blocks */ +/* ================================================================== */ + +/* layout */ +div[class^="highlight-"] { + border-radius: var(--border-radius); + background: transparent; } -/* Name.Variable.Instance */ -.highlight .vm { - color: #bb60d5 +/* font */ +.highlight, +.highlight pre { + font-size: 13px !important; + font-family: var(--code-font) !important; } -/* Name.Variable.Magic */ -.highlight .il { - color: #208050 +/* REPL >>> prompt */ +.highlight .gp { + font-weight: normal; + font-style: normal; } + + /* REPL output */ +.highlight .go { + font-weight: normal; + font-style: normal; +} + +.highlight pre { + line-height: var(--code-line-height) !important; +} + +/* border */ +.rst-content div[class^=highlight], +.rst-content pre.literal-block { + border-color: var(--border) !important; +} + +/* reduce bottom margin inside list items */ +.rst-content li > div[class^='highlight'] { + margin-bottom: 6px !important; +} + +/* background */ +[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; } /* (, [, ], )*/ +.highlight .mi { font-weight:normal; } /* numbers */ +.highlight .mf { font-weight:normal; } /* floats */ + +/* ----------------- REPL light theme ---------------- */ + +[data-theme="light"] .highlight-pycon .pycon-number { color: #204A87; } +[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: #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: #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: #6c7a88; } /* output dimmer than code */ + +/* ----------------- syntax highlight ---------------- */ + +[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/_static/favicon.ico b/docs/_static/favicon.ico deleted file mode 100644 index c9efc5844a..0000000000 Binary files a/docs/_static/favicon.ico and /dev/null differ diff --git a/docs/_static/images/favicon.svg b/docs/_static/images/favicon.svg new file mode 100644 index 0000000000..65a8a36c53 --- /dev/null +++ b/docs/_static/images/favicon.svg @@ -0,0 +1,5 @@ + + + + + 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-memory.svg b/docs/_static/images/icon-memory.svg new file mode 100644 index 0000000000..e499ac5aa1 --- /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-apivoid.svg b/docs/_static/images/logo-apivoid.svg new file mode 100644 index 0000000000..49ebb25cd8 --- /dev/null +++ b/docs/_static/images/logo-apivoid.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + 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/_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..9fa30a4e0e --- /dev/null +++ b/docs/_static/images/logo-sansec.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/_static/images/logo-tidelift.svg b/docs/_static/images/logo-tidelift.svg new file mode 100644 index 0000000000..448dd7a081 --- /dev/null +++ b/docs/_static/images/logo-tidelift.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + 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/_static/js/highlight-repl.js b/docs/_static/js/highlight-repl.js new file mode 100644 index 0000000000..a866ea9306 --- /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 after '=' or at the start of a line. + html = html.replace(/(?<==)\d+\.?\d*|^\d+\.?\d*/gm, + '$&'); + span.innerHTML = html; + }); +}); diff --git a/docs/_static/js/search-shortcuts.js b/docs/_static/js/search-shortcuts.js new file mode 100644 index 0000000000..8d5d32c199 --- /dev/null +++ b/docs/_static/js/search-shortcuts.js @@ -0,0 +1,151 @@ +// Implement keyboard shortcuts re. to search. +// - Ctrl+K: un/focus the search input (was /) +// - esc: unfocus the search input +// Search results page: +// - up/down: move selection +// - enter: open entry + +document.addEventListener("DOMContentLoaded", function () { + // No-op on touch/mobile devices. + if (window.matchMedia("(pointer: coarse)").matches) + return; + + var searchInput = document.querySelector( + "#rtd-search-form input[name='q']" + ); + + // Disable Sphinx's built-in "/" search shortcut. + function disableSphinxShortcut() { + if (typeof DOCUMENTATION_OPTIONS !== "undefined") + DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS = false; + } + + // Focus the search input on Ctrl+K; blur it on Escape. + function initCtrlK(input) { + if (!input) return; + document.addEventListener("keydown", function (e) { + if (e.ctrlKey && e.key === "k") { + e.preventDefault(); + if (document.activeElement === input) + input.blur(); + else { + input.focus(); + input.select(); + } + } + else if (e.key === "Escape" && document.activeElement === input) + input.blur(); + }); + } + + // Inject the Ctrl+K visual badge inside the search input. + function initCtrlKBadge(input) { + if (!input) return; + var wrapper = document.createElement("div"); + wrapper.className = "search-input-wrapper"; + input.parentNode.insertBefore(wrapper, input); + wrapper.appendChild(input); + + var badge = document.createElement("span"); + badge.className = "search-kbd-hint"; + badge.innerHTML = + "Ctrl+K"; + wrapper.appendChild(badge); + + input.addEventListener("focus", function () { badge.style.opacity = "0"; }); + input.addEventListener("blur", function () { badge.style.opacity = ""; }); + } + + // Enable Up/Down arrow navigation and Enter to open on the + // search results page. The first result is auto-selected once + // results finish loading. + function initResultsNavigation(searchInput) { + var container = document.getElementById("search-results"); + if (!container) return; + + var activeIndex = -1; + + // Return the current list of result
  • elements. + function getResults() { + return Array.from(container.querySelectorAll("ul.search li")); + } + + // Highlight the result at `index` and scroll it into view. + function setActive(index) { + var results = getResults(); + if (!results.length) return; + index = Math.max(0, Math.min(index, results.length - 1)); + results.forEach(function (li) { + li.classList.remove("search-result-active"); + }); + activeIndex = index; + results[index].classList.add("search-result-active"); + results[index].scrollIntoView({ block: "nearest" }); + } + + // Remove highlight from all results. + function clearActive() { + activeIndex = -1; + getResults().forEach(function (li) { + li.classList.remove("search-result-active"); + }); + } + + initResultsObserver(container, getResults, clearActive, setActive); + initNavigationKeys(searchInput, getResults, setActive, clearActive); + + // Watch for new
  • elements and auto-select the first result + // once the batch finishes loading. + function initResultsObserver(container, getResults, clearActive, setActive) { + var debounceTimer; + new MutationObserver(function (mutations) { + var hasNewLi = mutations.some(function (m) { + return Array.from(m.addedNodes).some(function (n) { + return n.tagName === "LI"; + }); + }); + if (!hasNewLi) return; + clearActive(); + clearTimeout(debounceTimer); + debounceTimer = setTimeout(function () { + if (getResults().length) setActive(0); + }, 80); + }).observe(container, { childList: true, subtree: true }); + } + + // Handle arrow-key navigation and Enter to open the active + // result. Down from the search input moves to the first result. + function initNavigationKeys(searchInput, getResults, setActive, clearActive) { + document.addEventListener("keydown", function (e) { + if (!getResults().length) return; + if (document.activeElement === searchInput) { + if (e.key !== "ArrowDown") return; + e.preventDefault(); + searchInput.blur(); + setActive(0); + return; + } + if (e.key === "ArrowDown") { + e.preventDefault(); + setActive(activeIndex === -1 ? 0 : activeIndex + 1); + } + else if (e.key === "ArrowUp") { + e.preventDefault(); + if (activeIndex > 0) + setActive(activeIndex - 1); + } + else if (e.key === "Enter" && activeIndex >= 0) { + e.preventDefault(); + var link = getResults()[activeIndex].querySelector("a"); + if (link) + link.click(); + } + }); + } + } + + disableSphinxShortcut(); + initCtrlK(searchInput); + initCtrlKBadge(searchInput); + initResultsNavigation(searchInput); +}); diff --git a/docs/_static/js/sidebar-close.js b/docs/_static/js/sidebar-close.js new file mode 100644 index 0000000000..1fdf7eccaf --- /dev/null +++ b/docs/_static/js/sidebar-close.js @@ -0,0 +1,60 @@ +// On mobile, close the sidebar when tapping on the content +// area or swiping left. + +document.addEventListener('DOMContentLoaded', function () { + var content = document.querySelector( + 'section[data-toggle="wy-nav-shift"]' + ); + + if (!content) + return; + + function closeSidebar() { + var shifted = document.querySelectorAll( + '[data-toggle="wy-nav-shift"].shift' + ); + + if (shifted.length) { + shifted.forEach(function (el) { + el.classList.remove('shift'); + }); + var rstVersions = document.querySelector( + '[data-toggle="rst-versions"]' + ); + if (rstVersions) + rstVersions.classList.remove('shift'); + } + } + + // Close on tap. + content.addEventListener('click', function (e) { + if (e.target.closest('.wy-nav-top')) + return; + closeSidebar(); + }); + + // Close on left swipe (anywhere on the page). + var touchStartX = null; + + document.addEventListener( + 'touchstart', + function (e) { + touchStartX = e.touches[0].clientX; + }, + { passive: true } + ); + + document.addEventListener( + 'touchend', + function (e) { + if (touchStartX === null) + return; + + var dx = e.changedTouches[0].clientX - touchStartX; + touchStartX = null; + if (dx < -40) + closeSidebar(); + }, + { passive: true } + ); +}); diff --git a/docs/_static/js/theme-toggle.js b/docs/_static/js/theme-toggle.js new file mode 100644 index 0000000000..72462f269d --- /dev/null +++ b/docs/_static/js/theme-toggle.js @@ -0,0 +1,39 @@ +(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 icon = document.getElementById('theme-icon'); + if (icon) icon.className = dark ? 'fa fa-sun-o' : 'fa fa-moon-o'; + } + + // restore saved preference + setTheme(localStorage.getItem(KEY) === 'dark'); + + // wire up the toggle button (DOM is ready because script is deferred) + var btn = document.getElementById('theme-toggle'); + if (btn) { + btn.addEventListener('click', function () { + setTheme(html.getAttribute('data-theme') !== 'dark'); + }); + } + + // Shift+D: toggle dark mode. + document.addEventListener('keydown', function (e) { + var tag = document.activeElement && document.activeElement.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA') + return; + if (e.shiftKey && e.key === 'D') + setTheme(html.getAttribute('data-theme') !== 'dark'); + }); +})(); diff --git a/docs/_static/logo.png b/docs/_static/logo.png deleted file mode 100644 index 7d975ec9d2..0000000000 Binary files a/docs/_static/logo.png and /dev/null differ diff --git a/docs/_static/pmap-small.png b/docs/_static/pmap-small.png deleted file mode 100644 index 70ed13731d..0000000000 Binary files a/docs/_static/pmap-small.png and /dev/null differ diff --git a/docs/_static/pmap.png b/docs/_static/pmap.png deleted file mode 100644 index a92694cda0..0000000000 Binary files a/docs/_static/pmap.png and /dev/null differ diff --git a/docs/_static/procinfo-small.png b/docs/_static/procinfo-small.png deleted file mode 100644 index 39be8cc7d5..0000000000 Binary files a/docs/_static/procinfo-small.png and /dev/null differ diff --git a/docs/_static/procinfo.png b/docs/_static/procinfo.png deleted file mode 100644 index 44d37972c5..0000000000 Binary files a/docs/_static/procinfo.png and /dev/null differ diff --git a/docs/_static/procsmem-small.png b/docs/_static/procsmem-small.png deleted file mode 100644 index c4f569e5a3..0000000000 Binary files a/docs/_static/procsmem-small.png and /dev/null differ diff --git a/docs/_static/procsmem.png b/docs/_static/procsmem.png deleted file mode 100644 index d7dc8c9185..0000000000 Binary files a/docs/_static/procsmem.png and /dev/null differ 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; k + {%- if (theme_prev_next_buttons_location == 'bottom' or theme_prev_next_buttons_location == 'both') and (next or prev) %} + + {%- endif %} + +
    + {%- block contentinfo %} + + {%- endblock %} +
    + + diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 0000000000..f9f562b762 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,36 @@ +{% extends "!layout.html" %} + +{% block htmltitle %} + + +{{ super() }} +{% endblock %} + +{% block extrabody %} +{% include "topbar.html" %} +{% endblock %} + +{% block extrahead %} +{{ super() }} + + + + + +{% endblock %} diff --git a/docs/_templates/topbar.html b/docs/_templates/topbar.html new file mode 100644 index 0000000000..c79d7395d6 --- /dev/null +++ b/docs/_templates/topbar.html @@ -0,0 +1,17 @@ +
    + +
    diff --git a/docs/about.rst b/docs/about.rst new file mode 100644 index 0000000000..450df1ee3d --- /dev/null +++ b/docs/about.rst @@ -0,0 +1,44 @@ +:orphan: + +.. _tips: + +About this site +=============== + +This site is built with `Sphinx `__ and the +`Read the Docs theme `__, with a +number of custom additions: a dark mode, keyboard shortcuts, a top bar, an +improved search navigation. + +Keyboard shortcuts +------------------ + +.. list-table:: + :widths: 30 70 + :header-rows: 1 + + * - Key + - Action + * - Ctrl+K + - Open search + * - Shift+D + - Toggle dark / light mode + * - ↓ / ↑ + - Navigate search results + * - Enter + - Open highlighted search result + * - Escape + - Close search + +Dark mode +--------- + +Click the moon icon in the top-right corner, or press **Shift+D** anywhere on +the page. Your preference is saved and restored on your next visit. + +Search +------ + +Press **Ctrl+K** to jump straight to the search box. Once results appear, use +the **arrow keys** to move through them and **Enter** to open one — no mouse +needed. diff --git a/docs/adoption.rst b/docs/adoption.rst new file mode 100644 index 0000000000..ce017ec1c0 --- /dev/null +++ b/docs/adoption.rst @@ -0,0 +1,211 @@ +Who uses psutil +=============== + +psutil is among 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. The projects below are a small +sample of notable software that depends on psutil. + +See also :doc:`alternatives` for related Python libraries and equivalents in +other languages. + +Infrastructure / automation +--------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 28 32 14 26 + + * - Project + - Description + - Stars + - psutil usage + * - `Home Assistant `__ + - Open source home automation platform + - |homeassistant-stars| + - system monitor integration + * - `Ansible `__ + - IT automation platform + - |ansible-stars| + - system fact gathering + * - `Apache Airflow `__ + - Workflow orchestration platform + - |airflow-stars| + - process supervisor, unit testing + * - `Celery `__ + - Distributed task queue + - |celery-stars| + - worker process monitoring, memleak detection + * - `Salt `__ + - Infrastructure automation at scale + - |salt-stars| + - deep system data collection (grains) + * - `Dask `__ + - Parallel computing with task scheduling + - |dask-stars| + - `metrics dashboard `__, profiling + * - `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 `__ + - Open source machine learning framework by Google + - |tensorflow-stars| + - unit tests + * - `PyTorch `__ + - Tensors and dynamic neural networks with GPU acceleration + - |pytorch-stars| + - benchmark scripts + * - `Ray `__ + - AI compute engine with distributed runtime + - |ray-stars| + - metrics dashboard + * - `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 `__ + - Error tracking and performance monitoring + - |sentry-stars| + - send telemetry metrics + * - `Locust `__ + - Scalable load testing in Python + - |locust-stars| + - monitoring of the Locust process + * - `Spyder `__ + - Scientific Python IDE + - |spyder-stars| + - deep integration, UI stats, process management + * - `psleak `__ + - Test framework to detect memory leaks 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 `__ + - System monitoring tool (top/htop alternative) + - |glances-stars| + - core dependency for all metrics + * - `bpytop `__ + - Terminal resource monitor + - |bpytop-stars| + - core dependency for all metrics + * - `auto-cpufreq `__ + - Automatic CPU speed and power optimizer for Linux + - |auto-cpufreq-stars| + - core dependency for CPU monitoring + * - `GRR `__ + - Remote live forensics by Google + - |grr-stars| + - core dependency for system data collection + * - `s-tui `__ + - Terminal CPU stress and monitoring utility + - |stui-stars| + - core dependency for metrics + * - `asitop `__ + - Apple Silicon performance monitoring CLI + - |asitop-stars| + - core dependency for system metrics + * - `psdash `__ + - Web dashboard using psutil and Flask + - |psdash-stars| + - core dependency for all metrics + * - `dd-agent `__ + - Original monitoring agent by Datadog + - |dd-agent-stars| + - system metrics collection + * - `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 +.. |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 + +.. --- Notes +.. Stars shield: +.. https://shields.io/badges/git-hub-repo-stars diff --git a/docs/alternatives.rst b/docs/alternatives.rst new file mode 100644 index 0000000000..604ba0df0c --- /dev/null +++ b/docs/alternatives.rst @@ -0,0 +1,183 @@ +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 +----------------------- + +.. seealso:: + :doc:`stdlib-equivalents` for a detailed function-by-function comparison. + +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 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 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 values. + +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 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. + +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 (invalid format, compatibility with old kernels, +graceful fallbacks, etc.). + +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, micro architecture, 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. + + * - `heim `_ + - Rust + - Async-first library covering CPU, memory, disk, network, + processes and sensors. + + * - `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. + + * - `rust-psutil `_ + - Rust + - Directly inspired by psutil and follows a similar API. + + * - `sysinfo `_ + - Rust + - CPU, memory, disk, network, processes, components. + + * - `systeminformation `_ + - Node.js + - CPU, memory, disk, network, processes, battery, Docker. diff --git a/docs/api-overview.rst b/docs/api-overview.rst new file mode 100644 index 0000000000..a7c067b12e --- /dev/null +++ b/docs/api-overview.rst @@ -0,0 +1,566 @@ +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 +--------- + +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 +^^^^^^^^ + +.. 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) # set IO priority + >>> p.ionice() + pionice(ioclass=, value=0) + >>> + >>> p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) # set resource limits + >>> 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_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), + ...] + >>> + >>> 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.send_signal(signal.SIGTERM) + >>> 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) + >>> + +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/api.rst b/docs/api.rst new file mode 100644 index 0000000000..29a9861f2a --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,2932 @@ +.. note:: + 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``). + +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 + +System related functions +------------------------ + +CPU +^^^ + +.. function:: cpu_times(percpu=False) + + 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. Cross-platform fields: + + - :field:`user`: time spent by processes executing in user mode; on Linux + this also includes :field:`guest` time. + + - :field:`system`: time spent by processes executing in kernel mode. + + - :field:`idle`: time spent doing nothing. + + Platform-specific fields: + + - :field:`nice` *(Linux, macOS, BSD)*: time spent by :term:`niced ` + (lower-priority) processes executing in user mode; on Linux this also + includes :field:`guest_nice` time. + + - :field:`iowait` *(Linux, SunOS, AIX)*: time spent waiting for I/O to + complete (:term:`iowait`). This is *not* accounted in :field:`idle` time + counter. + + - :field:`irq` *(Linux, Windows, BSD)*: time spent for servicing + :term:`hardware interrupts `. + + - :field:`softirq` *(Linux)*: time spent for servicing + :term:`soft interrupts `. + + - :field:`steal` *(Linux)*: CPU time the virtual machine wanted to run, but + was used by other virtual machines or the host. + + - :field:`guest` *(Linux)*: time the host CPU spent running a guest operating + system (virtual machine). Already included in :field:`user` 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. + + - :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 for each :term:`logical CPU` on the + system. The list is ordered by CPU index. The order of the list is consistent + across calls. + + .. 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) + + .. 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 `_. + + .. versionchanged:: 4.1.0 + Windows: added :field:`irq` and :field:`dpc` fields (:field:`irq` was + called :field:`interrupt` before 8.0.0). + + .. versionchanged:: 8.0.0 + 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: :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) + + Return the current system-wide CPU utilization as a percentage. + + If *interval* is > ``0.0``, measures CPU times before and after the interval + (blocking). If ``0.0`` or ``None``, returns the utilization since the 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. + + If *percpu* is ``True``, returns a list of floats representing each + :term:`logical CPU`. The list is ordered by CPU index and consistent across + calls. + + This function is thread-safe. It maintains an internal map of thread IDs + (:func:`threading.get_ident`) so that independent results are returned when + called from different threads at different intervals. + + .. 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) + [5.6, 1.0] + >>> + + .. seealso:: :ref:`faq_cpu_percent` + + .. versionchanged:: 5.9.6 + the function is now thread safe. + +.. function:: cpu_times_percent(interval=None, percpu=False) + + Similar to :func:`cpu_percent`, but provides utilization percentages for each + specific CPU time. *interval* and *percpu* arguments have the same meaning as + in :func:`cpu_percent`. On Linux, :field:`guest` and :field:`guest_nice` + percentages are not accounted in :field:`user` and :field:`user_nice`. + + .. seealso:: :ref:`faq_cpu_percent` + + .. versionchanged:: 4.1.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. + +.. function:: cpu_count(logical=True) + + Return the number of :term:`logical CPUs ` in the system (same + as :func:`os.cpu_count`), or ``None`` if undetermined. Unlike + :func:`os.cpu_count`, this is not influenced by the + :envvar:`PYTHON_CPU_COUNT` environment variable (Python 3.13+). + + If *logical* is ``False`` return the number of + :term:`physical CPUs ` only, or ``None`` if undetermined + (always ``None`` on OpenBSD and NetBSD). + + Example on a system with 2 cores + Hyper Threading: + + .. code-block:: pycon + + >>> import psutil + >>> psutil.cpu_count() + 4 + >>> psutil.cpu_count(logical=False) + 2 + + Note that this may differ from the number of CPUs the current process can + actually use (e.g. due to :term:`CPU affinity`, cgroups, or Windows processor + groups). The number of usable CPUs can be obtained with: + + .. code-block:: pycon + + >>> len(psutil.Process().cpu_affinity()) + 1 + + .. seealso:: :ref:`faq_cpu_count` + +.. function:: cpu_stats() + + Return various CPU statistics. All fields are + :term:`cumulative counters ` since boot. + + - :field:`ctx_switches`: number of :term:`context switches ` + (voluntary + involuntary). + - :field:`interrupts`: number of + :term:`hardware interrupts `. + - :field:`soft_interrupts`: number of + :term:`soft interrupts `; always set to ``0`` on Windows + and SunOS. + - :field:`syscalls`: number of system calls; always set to ``0`` on Linux. + + .. code-block:: pycon + + >>> 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 :field:`current`, :field:`min` and :field:`max` CPU frequencies + expressed in MHz. On Linux, :field:`current` is the real-time frequency value + (changing), 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 :field:`min` and :field:`max` cannot be determined they are set to + ``0.0``. + + .. code-block:: pycon + + >>> 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. + + .. 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. + On UNIX, this relies on :func:`os.getloadavg`. On Windows, this is emulated + via a background thread that updates every 5 seconds; the first call (and for + the following 5 seconds) returns ``(0.0, 0.0, 0.0)``. The values only make + sense relative to the number of installed :term:`logical CPUs ` + (e.g. ``3.14`` on a 10-CPU system means 31.4% load). + + .. code-block:: pycon + + >>> 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. All values are expressed in + bytes. + + - :field:`total`: total physical RAM. + - :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`. + - :field:`percent`: the percentage usage calculated as + ``(total - available) / total * 100``. + - :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``. + - :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 :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. + - :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 :term:`swapping `. On BSD systems + it is counted in :field:`available`. + - :field:`buffers` *(Linux, BSD)*: see :term:`buffers`. On OpenBSD + :field:`buffers` and :field:`cached` are aliases. + - :field:`cached` *(Linux, BSD, Windows)*: RAM used by the kernel to cache + file contents (data read from or written to disk). On OpenBSD + :field:`buffers` and :field:`cached` are aliases. See :term:`page cache`. + - :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 + :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 :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 + 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 + - macOS + - Windows + - FreeBSD + * - total + - ``MemTotal`` + - ``sysctl() hw.memsize`` + - ``PhysicalTotal`` + - ``sysctl() hw.physmem`` + * - available + - ``MemAvailable`` + - ``inactive + free`` + - ``PhysicalAvailable`` + - ``inactive + cached + free`` + * - used + - ``total - available`` + - ``active + wired`` + - ``total - available`` + - ``active + wired + cached`` + * - free + - ``MemFree`` + - ``free - speculative`` + - same as ``available`` + - ``sysctl() vm.stats.vm.v_free_count`` + * - active + - ``Active`` + - ``active`` + - + - ``sysctl() vm.stats.vm.v_active_count`` + * - inactive + - ``Inactive`` + - ``inactive`` + - + - ``sysctl() vm.stats.vm.v_inactive_count`` + * - buffers + - ``Buffers`` + - + - + - ``sysctl() vfs.bufspace`` + * - cached + - ``Cached + SReclaimable`` + - + - ``SystemCache`` + - ``sysctl() vm.stats.vm.v_cache_count`` + * - shared + - ``Shmem`` + - + - + - ``sysctl(CTL_VM/VM_METER) t_vmshr + t_rmshr`` + * - slab + - ``Slab`` + - + - + - + * - wired + - + - ``wired`` + - ``KernelNonpaged`` + - ``sysctl() vm.stats.vm.v_wire_count`` + + Example on Linux: + + .. 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:: + - 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`` command. + - On BSD, :field:`free`, :field:`active`, :field:`inactive`, + :field:`cached`, and :field:`wired` match ``vmstat -s`` command. + - On Windows, :field:`total`, :field:`used` ("In use"), and + :field:`available` match the Task Manager (Performance > Memory tab). + + .. note:: + if you just want to know how much physical memory is left in a + cross-platform manner, rely on :field:`available` and :field:`percent` + fields. + + .. seealso:: + - `scripts/meminfo.py`_ + - :ref:`faq_virtual_memory_available` + - :ref:`faq_used_plus_free` + + .. versionchanged:: 4.2.0 + Linux: added :field:`shared` field. + + .. versionchanged:: 5.4.4 + Linux: added :field:`slab` field. + + .. versionchanged:: 8.0.0 + Windows: added :field:`cached` and :field:`wired` fields. + +.. function:: swap_memory() + + Return system :term:`swap memory` statistics: + + * :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. + * :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``. + * :field:`sin`: number of bytes the system has moved from disk + (:term:`swap `) back into RAM. See :term:`swap-in`. + * :field:`sout`: number of bytes the system has moved from RAM to disk + (:term:`swap `). A continuously increasing :field:`sout` rate + is a sign of memory pressure. See :term:`swap-out`. + + :field:`sin` and :field:`sout` are + :term:`cumulative counters ` since boot. Monitor their + rate of change rather than the absolute value to detect active + :term:`swapping `. On Windows both are always ``0``. + + .. code-block:: pycon + + >>> import psutil + >>> psutil.swap_memory() + sswap(total=2097147904, used=886620160, free=1210527744, percent=42.3, sin=1050411008, sout=1906720768) + + .. seealso:: + - `scripts/meminfo.py`_ + - :ref:`Swap activity recipe ` + + .. versionchanged:: 5.2.3 + Linux: use /proc instead of ``sysinfo()`` syscall to support + :data:`PROCFS_PATH` usage (e.g. useful for Docker containers ...). + + .. versionchanged:: 8.0.0 + OpenBSD: :field:`sin` / :field:`sout` are no longer set to ``0``. + +Disks +^^^^^ + +.. function:: disk_partitions(all=False) + + Return mounted disk partitions as a list. This is similar to the ``df`` + command on UNIX. When *all* is ``False``, virtual/pseudo filesystems (tmpfs, + sysfs, devtmpfs, cgroup, etc.) are excluded, keeping only physical devices + (e.g., hard disks, CD-ROM drives, USB keys). The filtering logic varies by + platform: on Linux, it checks ``/proc/filesystems`` for ``nodev``-flagged + types (ZFS is always included); on macOS, it checks whether the device path + exists; on SunOS and AIX, it excludes filesystems with zero total size. On + BSD, *all* is ignored and all partitions are always returned. + + * :field:`device`: the device path (e.g. "/dev/hda1"). On Windows this is the + drive letter (e.g. "C:\\"). + * :field:`mountpoint`: the mount point path (e.g. "/"). On Windows this is + the drive letter (e.g. "C:\\"). + * :field:`fstype`: the partition filesystem (e.g. "ext3" on UNIX or "NTFS" on + Windows). + * :field:`opts`: a comma-separated string indicating different mount options + for the drive/partition. Platform-dependent. + + .. 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')] + + .. seealso:: `scripts/disk_usage.py`_. + + .. versionchanged:: 5.7.4 + added :field:`maxfile` and :field:`maxpath` fields. + + .. versionchanged:: 6.0.0 + removed :field:`maxfile` and :field:`maxpath` fields. + +.. function:: disk_usage(path) + + Return disk usage statistics for the partition containing *path*. Values are + expressed in bytes and include :field:`total`, :field:`used` and + :field:`free` space, plus the :field:`percentage` usage. On UNIX, *path* must + point to a path within a **mounted** filesystem partition. This function was + later incorporated in Python 3.3 as :func:`shutil.disk_usage` (see + `BPO-12442`_). + + .. code-block:: pycon + + >>> import psutil + >>> psutil.disk_usage('/') + sdiskusage(total=21378641920, used=4809781248, free=15482871808, percent=22.5) + + .. note:: + UNIX typically reserves 5% of disk space for root. :field:`total` and + :field:`used` refer to overall space, while :field:`free` and + :field:`percent` reflect unprivileged user usage. As a result, + :field:`percent` may appear ~5% higher than expected. All values match the + ``df`` command line utility. + + .. seealso:: `scripts/disk_usage.py`_. + + .. versionchanged:: 4.3.0 + :field:`percent` value takes root reserved space into account. + +.. function:: disk_io_counters(perdisk=False, nowrap=True) + + Return system-wide disk I/O statistics: + + - :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: + + - :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`. + - :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. + + 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``. + + .. 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, you may need to run ``diskperf -y`` command first, otherwise + this function might not detect any disks. + + .. 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. + + .. versionchanged:: 4.0.0 + added :field:`busy_time` (Linux, FreeBSD), :field:`read_merged_count` and + :field:`write_merged_count` (Linux) fields. + + .. versionchanged:: 4.0.0 + NetBSD: removed :field:`read_time` and :field:`write_time` fields. + +Network +^^^^^^^ + +.. function:: net_io_counters(pernic=False, nowrap=True) + + Return system-wide network I/O statistics: + + - :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. + - :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 + 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 :term:`NICs ` installed this function will return + ``None`` or ``{}`` if *pernic* is ``True``. + + .. 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)} + + .. seealso:: `scripts/nettop.py`_ and `scripts/ifconfig.py`_. + + .. 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. Each entry provides 7 + fields: + + - :field:`fd`: the socket :term:`file descriptor`; set to ``-1`` on Windows + and SunOS. + - :field:`family`: the address family, either :data:`socket.AF_INET`, + :data:`socket.AF_INET6` or :data:`socket.AF_UNIX`. + - :field:`type`: the address type, either :data:`socket.SOCK_STREAM`, + :data:`socket.SOCK_DGRAM` or :data:`socket.SOCK_SEQPACKET`. + - :field:`laddr`: the local address as a ``(ip, port)`` named tuple, or a + ``path`` for :data:`socket.AF_UNIX` sockets. + - :field:`raddr`: the remote address. When the socket is not connected, this + is either an empty tuple (``AF_INET*``) or an empty string (``""``) for + ``AF_UNIX`` sockets (see note below). + - :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). + + 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 | + +----------------+-----------------------------------------------------+ + + .. 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 not run as root, those connections are silently skipped instead of + raising :exc:`PermissionError`. That means the returned list may be + incomplete. + + .. note:: + - Linux, FreeBSD, OpenBSD: :field:`raddr` field for UNIX sockets is always + set to ``""``; 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:: + + - :meth:`Process.net_connections` to get per-process connections + - `scripts/netstat.py`_ + + .. versionadded:: 2.1.0 + + .. versionchanged:: 5.3.0 + socket :field:`fd` is now set for real instead of being ``-1``. + + .. versionchanged:: 5.3.0 + :field:`laddr` and :field:`raddr` are named tuples. + + .. versionchanged:: 5.9.5 + OpenBSD: retrieve :field:`laddr` path for :data:`socket.AF_UNIX` sockets + (before it was an empty string). + + .. versionchanged:: 8.0.0 + :field:`status` field is now a :class:`ConnectionStatus` enum member + instead of a plain ``str``. See :ref:`migration guide `. + +.. function:: net_if_addrs() + + Return a dict mapping each :term:`NIC` to its addresses. Interfaces may have + multiple addresses per family. Each entry includes 5 fields (addresses may be + ``None``): + + - :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. + - :field:`ptp`: a "point to point" address (typically a VPN); always ``None`` + on Windows. + + .. 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)]} + >>> + + .. seealso:: `scripts/nettop.py`_ and `scripts/ifconfig.py`_. + + .. versionadded:: 3.0.0 + + .. versionchanged:: 3.2.0 + added :field:`ptp` field. + + .. versionchanged:: 4.4.0 + Windows: added support for :field:`netmask` field, which is no longer + ``None``. + + .. versionchanged:: 7.0.0 + Windows: added support for :field:`broadcast` field, which is no longer + ``None``. + +.. function:: net_if_stats() + + Return a dictionary mapping each :term:`NIC` to its stats: + + - :field:`isup`: whether the NIC is up and running (bool). + - :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. + ``"up,broadcast,running,multicast"``); may be an empty string. + + .. 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')} + + .. seealso:: `scripts/nettop.py`_ and `scripts/ifconfig.py`_. + + .. versionadded:: 3.0.0 + + .. versionchanged:: 5.7.3 + UNIX: :field:`isup` also reflects whether the :term:`NIC` is running. + + .. versionchanged:: 5.9.3 + added :field:`flags` field. + +Sensors +^^^^^^^ + +.. function:: sensors_temperatures(fahrenheit=False) + + Return hardware temperatures. Each entry represents a sensor (CPU, disk, + etc.). Values are in Celsius unless *fahrenheit* is ``True``. If unsupported, + an empty dict is returned. Each entry includes: + + - :field:`label`: string label for the sensor, if available, else ``""``. + - :field:`current`: current temperature reading (changing), or ``None`` if + unavailable. + - :field:`high`: sensor-specified high temperature threshold (fixed), or + ``None`` if unavailable. Typically indicates when hardware may start + throttling to reduce heat. + - :field:`critical`: sensor-specified critical temperature threshold (fixed), + or ``None`` if unavailable. Typically indicates when hardware considers + itself at risk; behavior may include throttling, fan ramp-up, or shutdown. + + .. 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), + 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)]} + + .. seealso:: `scripts/temperatures.py`_ and `scripts/sensors.py`_. + + .. availability:: Linux, FreeBSD + + .. versionadded:: 5.1.0 + + .. versionchanged:: 5.5.0 + added FreeBSD support. + +.. function:: sensors_fans() + + Return hardware fan speeds in RPM (revolutions per minute). If unsupported, + return an empty dict. + + .. code-block:: pycon + + >>> import psutil + >>> psutil.sensors_fans() + {'asus': [sfan(label='cpu_fan', current=3200)]} + + .. seealso:: `scripts/fans.py`_ and `scripts/sensors.py`_. + + .. availability:: Linux + + .. versionadded:: 5.2.0 + +.. function:: sensors_battery() + + Return battery status information. If no battery is installed or metrics + can't be determined ``None`` is returned. + + - :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:`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. + + .. 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 + + .. seealso:: `scripts/battery.py`_ and `scripts/sensors.py`_. + + .. availability:: Linux, Windows, macOS, 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 return value is based on the + system clock, which means it can be affected by changes such as manual + adjustments or time synchronization (e.g. NTP). + + .. code-block:: pycon + + >>> 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' + +.. function:: users() + + Return users currently connected on the system as a list. Each entry + includes: + + - :field:`name`: the name of the user. + - :field:`terminal`: the tty or pseudo-tty associated with the user, if any, + else ``None``. + - :field:`host`: the host name associated with the entry, if any (for + example, the remote host in an SSH session), else ``None``. + - :field:`started`: the creation time as a floating point number expressed in + seconds since the epoch. + - :field:`pid`: the PID of the login process (like sshd for remote logins, + tmux, etc.). On Windows and OpenBSD this is always ``None``. + + .. 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)] + + .. versionchanged:: 5.3.0 + added :field:`pid` field. + +------------------------------------------------------------------------------- + +Processes +--------- + +Functions +^^^^^^^^^ + +.. function:: pids() + + Return a sorted list of currently running PIDs. To iterate over all processes + and avoid race conditions :func:`process_iter` is preferred, see + :ref:`perf-process-iter`. + + .. 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. + +.. function:: process_iter(attrs=None, ad_value=None) + + Return an iterator yielding a :class:`Process` instance for all running + processes. 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` is called internally, and + the results are cached so that subsequent method calls (e.g. ``p.name()``, + ``p.status()``) return the cached values 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. + + Processes are returned sorted by PID. + + .. code-block:: pycon + + >>> import psutil + >>> for proc in psutil.process_iter(['pid', 'name', 'username']): + ... print(proc.pid, proc.name(), proc.username()) # return cached values, never raise + ... + 1 systemd root + 2 kthreadd root + 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 + + >>> 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* arguments. + + .. versionchanged:: 6.0.0 + + - No longer checks whether each yielded process PID has been reused. + - 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. + - 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 + 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) + + Bulk operation that waits for a list of :class:`Process` instances to + terminate. Return a ``(gone, alive)`` tuple. The ``gone`` processes will have + a new ``returncode`` attribute set by :meth:`Process.wait`. + + *callback* is called with a :class:`Process` instance whenever a process + terminates. + + Returns as soon as all processes terminate or *timeout* (seconds) expires. + Unlike :meth:`Process.wait`, it does not raise :exc:`TimeoutExpired` on + timeout. + + Typical usage: + + - send SIGTERM to a list of processes + - wait a short time + - send SIGKILL to any still alive + + .. code-block:: python + + import psutil + + def on_terminate(proc): + print(f"{proc} terminated with exit code {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: + print(f"{p} is still alive, send SIGKILL") + p.kill() + +Exceptions +^^^^^^^^^^ + +.. exception:: Error() + + Base exception class. All other exceptions inherit from this one. + +.. exception:: NoSuchProcess(pid, name=None, msg=None) + + Raised by :class:`Process` class or its methods when a process with the given + *pid* is not found, no longer exists, or its PID has been reused. *name* + attribute is set only if :meth:`Process.name` was called before the process + disappeared. + + .. seealso:: :ref:`faq_no_such_process` + +.. exception:: ZombieProcess(pid, name=None, ppid=None, msg=None) + + Subclass of :exc:`NoSuchProcess`. Raised by :class:`Process` methods when + encountering a :term:`zombie process` on UNIX (Windows does not have + zombies). *name* and *ppid* attributes are set if :meth:`Process.name` or + :meth:`Process.ppid` were called before the process became a zombie. + + If you do not need to detect zombies, 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) + + Raised by :class:`Process` methods when an action is denied due to + insufficient privileges. *name* is set if :meth:`Process.name` was called + before the exception was raised. + + .. seealso:: :ref:`faq_access_denied` + +.. exception:: 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 set 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, the + current process *pid* (:func:`os.getpid`) is used. Raises + :exc:`NoSuchProcess` if *pid* does not exist. + + On Linux, *pid* can also refer to a thread ID (the :field:`id` field returned + by :meth:`threads`). + + When calling methods of this class, always be prepared to catch + :exc:`NoSuchProcess` and :exc:`AccessDenied` exceptions. The builtin + :func:`hash` can be used on instances to uniquely identify a process over + time (the hash combines PID and creation time), so instances can also be used + in a :class:`set`. + + .. note:: + + This class is bound to a process via its **PID**. If the process terminates + and the OS reuses its PID, you may accidentally interact with another + process. To prevent this, use :meth:`is_running` first. Some methods (e.g., + setters and signal-related methods) perform an additional check using PID + + creation time, and will raise :exc:`NoSuchProcess` if the PID has been + reused. See :ref:`faq_pid_reuse` for details. + + .. note:: + + To fetch multiple attributes efficiently, use the :meth:`oneshot` context + manager or the :meth:`as_dict` utility method. + + .. attribute:: pid + + The process PID as a read-only property. + + .. attribute:: attrs + + 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. + + .. 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 :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 + + .. method:: oneshot() + + Context manager that speeds up retrieval of multiple process attributes. + Internally, many attributes (e.g. :meth:`name`, :meth:`ppid`, :meth:`uids`, + :meth:`create_time`, ...) share the same underlying system call; within + this context, those calls are executed once and results are cached, + avoiding redundant syscalls. + + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> with p.oneshot(): + ... p.name() # actual syscall + ... p.cpu_times() # from cache + ... p.create_time() # from cache + ... p.ppid() # from cache + ... p.status() # from cache + ... + >>> + + The table below lists methods that benefit from the speedup, grouped by + platform. Methods separated by an empty row share the same underlying + system call. The *speedup* row estimates the gain when all listed methods + are called together (best case), as measured by + `bench_oneshot.py `_ + script. + + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | 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:`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_threads` | :meth:`create_time` | :meth:`memory_info` | :meth:`ppid` | :meth:`ppid` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`terminal` | | :meth:`gids` | :meth:`memory_percent` | :meth:`status` | :meth:`status` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | | :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:`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:`uids` | :meth:`uids` | :meth:`username` | :meth:`username` | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`username` | | :meth:`username` | :meth:`username` | | | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | | | | | | | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`memory_footprint` | | | | | | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | :meth:`memory_maps` | | | | | | + +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ + | *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 + + .. method:: name() + + 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 :ref:`find a 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 is an empty string. The return value is cached after + first call. + + .. 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. + + .. code-block:: pycon + + >>> import psutil + >>> psutil.Process().cmdline() + ['python3', '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. + + .. 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 + 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 is affected by changes such as manual adjustments or + time synchronization (e.g. NTP). + + .. 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) + + Utility method returning multiple process information as a dictionary. + + 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 + 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 + >>> 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', ...} + >>> + + .. versionchanged:: 3.0.0 + *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` context + manager. + + .. method:: ppid() + + The process parent PID. On Windows the return value is cached after the + first call. On POSIX it is not cached because it may change if the process + becomes a :term:`zombie `. See also :meth:`parent` and + :meth:`parents` methods. + + .. 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 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. + + .. versionadded:: 5.6.0 + + .. method:: status() + + 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:`ProcessStatus` enum member instead of a + plain ``str``. See :ref:`migration guide `. + + .. method:: cwd() + + The process current working directory as an absolute path. If it cannot be + determined (e.g. a system process or directory no longer exists) it returns + 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 the :field:`real` process UID from :meth:`uids`. + + .. method:: uids() + + The :field:`real`, :field:`effective` and :field:`saved` user ID of this + process as a named tuple. This is the same as :func:`os.getresuid`, but can + be used for any process PID. + + .. availability:: UNIX + + .. method:: gids() + + The :field:`real`, :field:`effective` and :field:`saved` group ID of this + process as a named tuple. This is the same as :func:`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 :term:`niceness ` (priority). On UNIX this is a + number which goes from ``-20`` to ``20``. The higher the nice value, the + lower the priority of the process. + + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> p.nice(10) # set lower priority + >>> p.nice() # get + 10 + >>> + + On Windows *value* is one of the :ref:`*_PRIORITY_CLASS ` + constants: + + .. code-block:: pycon + + >>> p.nice(psutil.HIGH_PRIORITY_CLASS) # set higher priority + >>> p.nice() # get + + + This method was later incorporated in Python 3.3 as :func:`os.getpriority` + and :func:`os.setpriority` (see `BPO-10784`_). + + .. versionchanged:: 8.0.0 + on Windows, the return value is now a :class:`ProcessPriority` enum + member. See :ref:`migration guide `. + + .. method:: ionice(ioclass=None, value=None) + + Get or set process :term:`I/O niceness ` (priority). Called with no + arguments (get), returns a ``(ioclass, value)`` named tuple on Linux or an + *ioclass* integer on Windows. Called with *ioclass* (one of the + :ref:`IOPRIO_* ` constants), sets the I/O priority. On + Linux, an additional *value* ranging from ``0`` to ``7`` can be specified + to further adjust the priority level specified by *ioclass*. + + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> if psutil.LINUX: + ... p.ionice(psutil.IOPRIO_CLASS_RT, value=7) # highest + ... else: + ... p.ionice(psutil.IOPRIO_HIGH) + ... + >>> p.ionice() # get + pionice(ioclass=, value=7) + + .. availability:: Linux, Windows + + .. versionchanged:: 5.6.2 + Windows: accept new :data:`IOPRIO_* ` constants. + + .. versionchanged:: 8.0.0 + *ioclass* is now a :class:`ProcessIOPriority` enum member. See + :ref:`migration guide `. + + .. method:: rlimit(resource, limits=None) + + Get or set process :term:`resource limits `. *resource* + must be one of the :ref:`RLIMIT_* ` constants. *limits* + is an optional ``(soft, hard)`` tuple. If provided, the method sets the + limits; if omitted, it returns the current ``(soft, hard)`` tuple. This is + the same as stdlib :func:`resource.getrlimit` and + :func:`resource.setrlimit`, but can be used for any process PID. + + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> p.rlimit(psutil.RLIMIT_NOFILE, (128, 128)) # max 128 file descriptors + >>> p.rlimit(psutil.RLIMIT_FSIZE, (1024, 1024)) # max file size 1024 bytes + >>> p.rlimit(psutil.RLIMIT_FSIZE) # get current limits of ... + (1024, 1024) + + .. seealso:: `scripts/procinfo.py`_. + + .. availability:: Linux, FreeBSD + + .. versionchanged:: 5.7.3 + added FreeBSD support. + + .. method:: io_counters() + + Return process I/O statistics. All fields are + :term:`cumulative counters ` since process creation. + + - :field:`read_count`: number of read syscalls (e.g., ``read()``, + ``pread()``). + - :field:`write_count`: number of write syscalls (e.g., ``write()``, + ``pwrite()``). + - :field:`read_bytes`: bytes read (``-1`` on BSD). + - :field:`write_bytes`: bytes written (``-1`` on BSD). + + Linux specific: + + - :field:`read_chars` *(Linux)*: bytes read via ``read()`` and ``pread()`` + syscalls. Unlike :field:`read_bytes`, this includes tty I/O and counts + bytes regardless of whether actual disk I/O occurred (e.g. reads served + from :term:`page cache` are included). + - :field:`write_chars` *(Linux)*: bytes written via ``write()`` and + ``pwrite()`` syscalls. Same caveats as :field:`read_chars`. + + Windows specific: + + - :field:`other_count` *(Windows)*: the number of I/O operations performed + other than read and write operations. + - :field:`other_bytes` *(Windows)*: the number of bytes transferred during + operations other than read and write operations. + + .. 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, Windows, BSD, AIX + + .. versionchanged:: 5.2.0 + Linux: added :field:`read_chars` and :field:`write_chars` fields. + + .. versionchanged:: 5.2.0 + Windows: added :field:`other_count` and :field:`other_bytes` fields. + + .. method:: num_ctx_switches() + + The number of :term:`context switches ` performed by this + process as a ``(voluntary, involuntary)`` named tuple + (:term:`cumulative counter`). + + .. note:: + (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 + added AIX support. + + .. method:: num_fds() + + 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). + + .. availability:: Windows + + .. method:: num_threads() + + The number of threads currently used by this process (non cumulative). + + .. method:: threads() + + Return a list of threads spawned by this process. On OpenBSD, root + privileges are required. Each entry includes: + + - :field:`id`: 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 in your app. + - :field:`user_time`: time spent in user mode. + - :field:`system_time`: time spent in kernel mode. + + .. method:: cpu_times() + + Return accumulated process CPU times as + :term:`cumulative counters ` (seconds) (see + `explanation `_). Same as + :func:`os.times`, but works for any process PID. + + - :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). + - :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`). Excluded from :field:`user` and :field:`system` times + count (because the CPU is idle). + + .. 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 + added :field:`children_user` and :field:`children_system` fields. + + .. versionchanged:: 5.6.4 + Linux: added :field:`iowait` field. + + .. method:: cpu_percent(interval=None) + + Return process CPU utilization as a percentage. Values can exceed ``100.0`` + if the process runs multiple threads on different CPUs. + + If *interval* is > ``0.0``, measures CPU times before and after the + interval (blocking). If ``0.0`` or ``None``, returns the utilization since + the 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 method be called with at least ``0.1`` seconds between calls. + + .. 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 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` + - :ref:`faq_cpu_percent_gt_100` + + .. method:: cpu_affinity(cpus=None) + + 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. + + .. 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 + + .. 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. + + .. seealso:: `scripts/cpu_distribution.py`_. + + .. availability:: Linux, FreeBSD, SunOS + + .. versionadded:: 5.1.0 + + .. method:: memory_info() + + Return memory information about the process. Fields vary by platform (all + values in bytes). The portable fields available on all platforms are + :field:`rss` and :field:`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 | + +---------+---------+----------+---------+-----+-----------------+ + + - :field:`rss`: aka :term:`RSS`. On UNIX matches the ``top`` RES column. On + Windows maps to ``WorkingSetSize``. + + - :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 :field:`virtual` from + :meth:`memory_info_ex` for the true virtual address space size. + + - :field:`shared` *(Linux)*: :term:`shared memory` that *could* be shared + with other processes (shared libraries, + :term:`memory-mapped files `). Counted even if no other + process is currently mapping it. Matches ``top``'s SHR column. + + - :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. + + - :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 + :field:`stack`). Matches ``top``'s DATA column. + + - :field:`stack` *(BSD)*: size of the process stack segment. Reported + separately from :field:`data` (unlike Linux where both are combined). + + - :field:`peak_rss` *(BSD, Windows)*: see :term:`peak_rss`. On BSD may be + ``0`` for kernel PIDs. On Windows maps to ``PeakWorkingSetSize``. + + - :field:`peak_vms` *(Windows)*: see :term:`peak_vms`. Maps to + ``PeakPagefileUsage``. + + For the full definitions of Windows fields see + `PROCESS_MEMORY_COUNTERS_EX`_. + + Example on Linux: + + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> 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 :field:`rss` and :field:`vms`. + + .. versionchanged:: 8.0.0 (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. + - *macOS*: removed :field:`pfaults` and :field:`pageins` fields with no + backward-compatible aliases. Use :meth:`page_faults` instead. + - *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`. + - *BSD*: added :field:`peak_rss` field. + + .. method:: memory_info_ex() + + Extends :meth:`memory_info` with additional platform-specific memory + metrics (all values in bytes). On platforms where extra fields are not + implemented this returns the same result as :meth:`memory_info`. + + +-------------+----------------+--------------------+ + | 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 | | + +-------------+----------------+--------------------+ + + - :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` + (:term:`heap`, stack, private mappings) not backed by any file. Set to 0 + on Linux < 4.5. + - :field:`rss_file` *(Linux, macOS)*: resident file-backed memory mapped + from files (:term:`shared libraries `, + :term:`memory-mapped files `). Set to 0 on Linux < 4.5. + - :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. + - :field:`wired` *(macOS)*: memory pinned in RAM by the kernel on behalf of + this process; cannot be compressed or paged out. + - :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 :term:`swapping `. + - :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 :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`). + - :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. + - :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`_. + + .. versionadded:: 8.0.0 + + .. method:: memory_footprint() + + Return :field:`uss`, :field:`pss` and :field:`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. + + - :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. + + - :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. + + - :field:`swap` *(Linux)*: process memory currently in + :term:`swap `, counted per-mapping. + + Example on Linux: + + .. code-block:: pycon + + >>> import psutil + >>> p = psutil.Process() + >>> p.memory_footprint() + pfootprint(uss=6545408, pss=6872064, swap=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` + plus :meth:`memory_footprint` in a single named tuple. + + .. versionadded:: 4.0.0 + + .. deprecated:: 8.0.0 + use :meth:`memory_footprint` instead. See + :ref:`migration guide `. + + .. method:: memory_percent(memtype="rss") + + Return process memory usage as a percentage of total physical memory. Same + as: + + .. code-block:: python + + Process().memory_info().rss / virtual_memory().total * 100 + + *memtype* selects which memory field to use and can be any attribute from + :meth:`memory_info`, :meth:`memory_info_ex`, or :meth:`memory_footprint` + (default is ``"rss"``). + + .. versionchanged:: 4.0.0 + added *memtype* parameter. + + .. method:: memory_maps(grouped=True) + + Return the process's :term:`memory-mapped ` file regions as + a list. 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; the tuple also includes *addr* (address range) and *perms* + (permission string, e.g., ``"r-xp"``). + + +---------------+---------+--------------+-----------+ + | 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``): + + - :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. + - :field:`shared_clean`: :term:`shared memory` not written to since loaded + (clean); can be discarded and reloaded from disk for free. + - :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. + - :field:`referenced`: bytes recently accessed. + - :field:`anonymous`: :term:`anonymous memory` in this mapping + (:term:`heap`, stack). + - :field:`swap`: bytes from this mapping currently in + :term:`swap `. + + FreeBSD fields: + + - :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 + + >>> 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), + ...] + + .. seealso:: `scripts/pmap.py`_. + + .. availability:: Linux, Windows, FreeBSD, SunOS + + .. versionchanged:: 5.6.0 + removed macOS support because inherently broken (see issue :gh:`1291`) + + .. method:: children(recursive=False) + + Return the children of this process as a list of :class:`Process` + instances. If *recursive* is ``True``, return all descendants. Pseudo-code + example (assuming A is this process): + + .. code-block:: none + + A ─┠+ │ + ├─ B (child) ─┠+ │ └─ X (grandchild) ─┠+ │ └─ Y (great-grandchild) + ├─ C (child) + └─ D (child) + + .. code-block:: pycon + + >>> p.children() + B, C, D + >>> p.children(recursive=True) + B, X, Y, C, D + + Note: if a process in the tree disappears (e.g., X), its descendants (Y) + won’t be returned since the reference to the parent is lost. This concept + is well illustrated by this + `unit test `_. + + .. seealso:: how to :ref:`kill a process tree `. + + .. method:: page_faults() + + Return the number of :term:`page faults ` for this process as a + ``(minor, major)`` named tuple. Both are + :term:`cumulative counters ` since process creation. + + .. code-block:: pycon + + >>> 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. Each entry includes: + + - :field:`path`: the absolute file name. + - :field:`fd`: the :term:`file descriptor` number; on Windows this is + always ``-1``. + + Linux only: + + - :field:`position` (*Linux*): the file position (offset). + - :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"``). + - :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). + + .. 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:: + - 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. + + .. versionchanged:: 4.1.0 + 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 :field:`pid` field). The *kind* parameter and + the same limitations apply (root may be needed on some platforms). + + .. 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=)] + + .. method:: connections() + + Same as :meth:`net_connections` (deprecated). + + .. deprecated:: 6.0.0 + use :meth:`net_connections` instead. + + .. method:: is_running() + + Return whether the current process is running. 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. + + 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. + + .. method:: send_signal(sig) + + 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`. + + .. seealso:: how to :ref:`kill a process tree ` + + .. versionchanged:: 3.2.0 + Windows: add support for CTRL_C_EVENT and CTRL_BREAK_EVENT signals. + + .. 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. + + .. 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. + + .. 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`. + + .. seealso:: how to :ref:`kill a process tree `. + + .. 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`_. + + .. seealso:: how to :ref:`kill a process tree `. + + .. 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 an + integer >= 0 indicating the exit code. If the process was terminated by a + signal, returns the negated value of the signal which caused the + termination (e.g. ``-SIGTERM``). If PID is not a child of :func:`os.getpid` + (current process), it just waits until the process disappears and return + ``None``. If PID does not exist return ``None`` immediately. + + *On Windows*: always return the exit code via `GetExitCodeProcess`_. + + *timeout* is expressed in seconds. If specified, and the process is still + alive, raise :exc:`TimeoutExpired`. ``timeout=0`` can be used in + non-blocking apps: it will either return immediately or raise + :exc:`TimeoutExpired`. + + The return value is cached. To wait for multiple processes use + :func:`psutil.wait_procs`. + + .. code-block:: pycon + + >>> 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 :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`_ + + 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. + + .. versionchanged:: 5.7.1 + return value is cached (instead of returning ``None``). + + .. versionchanged:: 5.7.1 + 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. + +------------------------------------------------------------------------------- + +Popen class +^^^^^^^^^^^ + +.. class:: Popen(*args, **kwargs) + + 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: + :meth:`send_signal() `, + :meth:`terminate() `, + :meth:`kill() `. This is done to avoid killing another + process if its PID has been reused, fixing `BPO-6973`_. + + .. code-block:: pycon + + >>> import psutil + >>> from subprocess import PIPE + >>> + >>> p = psutil.Popen(["/usr/bin/python3", "-c", "print('hello')"], stdout=PIPE) + >>> p.name() + 'python3' + >>> 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 +: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 :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 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. + +.. 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() + + Return low-level heap statistics from the system's C allocator. On Linux, + this exposes ``uordblks`` and ``hblkhd`` fields from glibc `mallinfo2`_. + + - ``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()``. + + .. 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: + + +---------------+------------------------------------------------------------------------------------+-----------------+ + | 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`` | + +---------------+------------------------------------------------------------------------------------+-----------------+ + + .. availability:: Linux with glibc, Windows, macOS, FreeBSD, NetBSD + + .. versionadded:: 7.2.0 + +.. function:: heap_trim() + + Request that the underlying allocator free any unused memory it's holding in + 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**. + + 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. + + .. availability:: Linux with glibc, Windows, macOS, FreeBSD, NetBSD + + .. versionadded:: 7.2.0 + +------------------------------------------------------------------------------- + +Windows services +---------------- + +.. function:: win_service_iter() + + Return an iterator yielding :class:`WindowsService` instances for all + installed Windows services. + + .. versionadded:: 4.2.0 + + .. availability:: Windows + +.. function:: win_service_get(name) + + Get a Windows service by name, returning a :class:`WindowsService` instance. + Raise :exc:`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's 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 either ``'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 can 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. + + .. 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 + + .. versionadded:: 4.2.0 + +------------------------------------------------------------------------------- + +Constants +--------- + +The following enum classes group related constants, and are useful for type +annotations and introspection. The individual constants (e.g. +: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:: ProcessStatus + + :class:`enum.StrEnum` collection of :data:`STATUS_* ` + constants. Returned by :meth:`Process.status`. + + .. versionadded:: 8.0.0 + +.. class:: ProcessPriority + + :class:`enum.IntEnum` collection of + :data:`*_PRIORITY_CLASS ` constants for + :meth:`Process.nice` on Windows. + + .. availability:: Windows + + .. versionadded:: 8.0.0 + +.. class:: ProcessIOPriority + + :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 + + .. versionadded:: 8.0.0 + +.. class:: ProcessRlimit + + :class:`enum.IntEnum` collection of :data:`RLIMIT_* ` + constants for :meth:`Process.rlimit`. + + .. availability:: Linux, FreeBSD + + .. versionadded:: 8.0.0 + +.. class:: ConnectionStatus + + :class:`enum.StrEnum` collection of :data:`CONN_* ` + constants. Returned in the :field:`status` field of + :func:`psutil.net_connections` and :meth:`Process.net_connections`. + + .. versionadded:: 8.0.0 + +.. 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:: BatteryTime + + :class:`enum.IntEnum` collection of + :data:`POWER_TIME_* ` constants. May appear in the + *secsleft* field of :func:`psutil.sensors_battery`. + + .. versionadded:: 5.1.0 + +.. _const-oses: + +Operating system constants +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``bool`` constants which define what platform you're on. ``True`` if on the +platform, ``False`` otherwise. + +.. data:: POSIX +.. data:: LINUX +.. data:: WINDOWS +.. data:: MACOS +.. data:: FREEBSD +.. data:: NETBSD +.. data:: OPENBSD +.. data:: BSD +.. data:: SUNOS +.. data:: AIX +.. data:: OSX + + Alias for :data:`MACOS`. + + .. deprecated:: 5.4.7 + use :data:`MACOS` instead. + +.. _const-pstatus: + +Process status constants +^^^^^^^^^^^^^^^^^^^^^^^^ + +Represent the current status of a process. Returned by :meth:`Process.status`. + +.. versionchanged:: 8.0.0 + constants are now :class:`ProcessStatus` enum members (were plain strings). + 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 is stopped (e.g., by ``SIGSTOP`` or ``SIGTSTP``, which is sent + on Ctrl+Z) and will not run until resumed (e.g., via ``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()``. See + also :ref:`faq_zombie_process`. + +.. data:: STATUS_DEAD + + The process is about to disappear (final state before it is gone). + +.. 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 + + (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 + + (NetBSD only) The process has been explicitly paused, similar to the stopped + state but managed by the NetBSD scheduler. + + .. versionadded:: 3.4.1 + +.. _const-proc-prio: + +Process priority constants +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Represent the priority of a process on Windows (see `SetPriorityClass`_ doc). +They can be used in conjunction with :meth:`Process.nice` to get or set process +priority. + +.. availability:: Windows + +.. versionchanged:: 8.0.0 + constants are now :class:`ProcessPriority` enum members (were plain + integers). See :ref:`migration guide `. + +.. _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 + +------------------------------------------------------------------------------- + +.. _const-proc-ioprio: + +Process I/O priority constants +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Represent the I/O priority class of a process (Linux and Windows only). They +can be used in conjunction with :meth:`Process.ionice` (*ioclass* argument). + +Linux (see `ioprio_get()`_ manual): + + .. data:: IOPRIO_CLASS_RT + + Highest priority. + + .. data:: IOPRIO_CLASS_BE + + Normal priority. + + .. data:: IOPRIO_CLASS_IDLE + + Lowest priority. + + .. data:: IOPRIO_CLASS_NONE + + No priority set (default; treated as :data:`IOPRIO_CLASS_BE`). + +Windows: + + .. data:: IOPRIO_VERYLOW + .. data:: IOPRIO_LOW + .. data:: IOPRIO_NORMAL + .. data:: IOPRIO_HIGH + +.. versionadded:: 5.6.2 + +.. versionchanged:: 8.0.0 + constants are now :class:`ProcessIOPriority` enum members. See + :ref:`migration guide `. + +.. _const-proc-rlimit: + +Process resource constants +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Constants for getting or setting process resource limits, to be used in in +conjunction with :meth:`Process.rlimit`. The meaning of each constant is +explained in :func:`resource.getrlimit` documentation. + +.. availability:: Linux, FreeBSD + +.. versionchanged:: 8.0.0 + these constants are now :class:`ProcessRlimit` enum members (were plain + integers). See :ref:`migration guide `. + +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 + + .. versionadded:: 5.7.3 + + + .. data:: RLIMIT_SBSIZE + + .. versionadded:: 5.7.3 + + .. data:: RLIMIT_NPTS + + .. versionadded:: 5.7.3 + +.. _const-conn: + +Connections constants +^^^^^^^^^^^^^^^^^^^^^ + +:class:`enum.StrEnum` constants representing the status of a TCP connection. +Returned by :meth:`Process.net_connections` and :func:`psutil.net_connections` +(:field:`status` field). + +.. versionchanged:: 8.0.0 + constants are now :class:`ConnectionStatus` enum members (were plain + strings). See :ref:`migration guide `. + +.. 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) + +Hardware constants +^^^^^^^^^^^^^^^^^^ + +.. _const-aflink: + +.. data:: AF_LINK + + Identifies a MAC address associated with a network interface. Returned by + :func:`psutil.net_if_addrs` (:field:`family` field). + + .. versionadded:: 3.0.0 + +.. _const-duplex: + +.. data:: NIC_DUPLEX_FULL +.. data:: NIC_DUPLEX_HALF +.. data:: NIC_DUPLEX_UNKNOWN + + 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` (:field:`duplex` field). + + .. versionadded:: 3.0.0 + +.. _const-power: + +.. data:: POWER_TIME_UNKNOWN +.. 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 :field:`secsleft` field. + + .. 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 ``/proc`` 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-related APIs and many (but not all) :class:`Process` + class methods). + + .. availability:: Linux, SunOS, 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 + + A tuple to check psutil installed version. + + .. code-block:: pycon + + >>> import psutil + >>> 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 +.. _`psleak`: https://github.com/giampaolo/psleak +.. _`/proc/meminfo`: https://man7.org/linux/man-pages/man5/proc_meminfo.5.html + +.. === 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 +.. _`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 new file mode 100644 index 0000000000..351a2963ce --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,3013 @@ +Changelog +========= + +8.0.0 (IN DEVELOPMENT) +^^^^^^^^^^^^^^^^^^^^^^ + +.. 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:`2787`, :gh:`2739`, :gh:`2790`, :gh:`2797`, +:gh:`2801`, :gh:`2803`, :gh:`2808`, :gh:`2819`, :gh:`2820`, :gh:`2823`) + +- Split docs from a single HTML file into multiple new sections: + + - :doc:`/about ` (linked from footer) list all keyboard shortcuts + - :doc:`/adoption `: notable software using 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:`/faq `: extended FAQ section + - :doc:`/funding `: list funding methods and current sponsors + - :ref:`/genindex `: a general index + - :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 + +- Theming: + + - Renewed, modern, custom theme. + - Top bar + - Toggable dark theme (``Shift+D`` keyboard shortcut). + - Show "last updated" and external icons in the footer. + - Show icon for external URLs. + - Use Monokai theme for code snippets. + +- Usability: + + - Improved overall doc clarity and shortened long sentences. + - Show a clickable COPY button to copy code snippets. + - Show ``psutil.`` prefix for all APIs. + - Search: ``Ctrl+K`` to focus the search box (replaces ``/``). + - Search: ``Up``/``Down`` arrow keys navigate results; ``Enter`` opens the + selected result. + - Search results are styled as cards with subtle borders and shadows (no + longer rely on RTD). + +- Testing: + + - Replace ``rstcheck`` with ``sphinx-lint`` for RST linting. + - Add custom script to detect dead reference links in ``.rst`` files. + - Use sphinx extension to validate Python code snippets syntax at build-time. + +- RTD: + + - Configured RTD to automatically public doc from Git tags, instead of from + main on every push. + - Removed /en language from RTD URLs. Turn that into a redirect. + - Before: https://psutil.readthedocs.io/en/stable/ + - Now: https://psutil.readthedocs.io/stable/ + +- Misc: + + - Build doc as part of CI process (fails on error). + - All ``.rst`` files are now wrapped to 79 characters via + https://github.com/giampaolo/rstwrap. + +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 :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. :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 :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())``. 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`, + :gh:`2733`). + + - 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: :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 :field:`peak_rss`. + + - Linux: :field:`lib` and :field:`dirty` removed (always 0 since Linux + 2.6). Deprecated aliases returning 0 and emitting + :exc:`DeprecationWarning` are kept. + + - macOS: :field:`pfaults` and :field:`pageins` removed with + **no backward-compatible aliases**. Use :meth:`Process.page_faults` + instead. + + - 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 + :meth:`Process.memory_footprint` instead. See + :ref:`migration guide `. + +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 + :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` :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 :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 + 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 `. +- :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. +- :gh:`2805`, [BSD]: remove ``procfs`` dependency on NetBSD for + :func:`cpu_stats` and :func:`virtual_memory`; values are now retrieved via + the ``sysctl(9)`` and ``uvm(9)`` kernel APIs instead. (patch by Santhosh + Raju) +- :gh:`2816`, [OpenBSD]: :func:`swap_memory` :field:`sin` and :field:`sout` are + no longer set to ``0``. + +**Bug fixes** + +- :gh:`1007`, [Windows]: :func:`boot_time` no longer fluctuates by ~1 second + across calls or across processes. It is now read atomically from the kernel + via ``NtQuerySystemInformation(SystemTimeOfDayInformation)``, replacing the + old ``time.time() - uptime()`` computation that sampled two counters from + Python and produced sub-second differences. +- :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:`2715`, [Linux]: ``wait_pid_pidfd_open()`` (from :meth:`Process.wait`) + crashes with ``EINVAL`` due to kernel race condition. +- :gh:`2726`, [macOS]: :meth:`Process.num_ctx_switches` return an unusual high + number due to a C type precision issue. +- :gh:`2732`, [Linux]: :func:`net_if_stats`: handle ``EBUSY`` from + ``ioctl(SIOCETHTOOL)``. +- :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:`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 :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 + systems without hyper threading. +- :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. +- :gh:`2811`, [OpenBSD]: :func:`virtual_memory` :field:`shared` field returned + pages instead of bytes, plus it was overvalued (summed shared ``virtual`` + + ``real``, now we only return ``real``). +- :gh:`2813`, [OpenBSD]: :func:`virtual_memory` :field:`buffers` was always 0. + Now it returns a meaningful value, which is the same as :field:`cached`. + That's because OpenBSD does not distinguish between the 2. +- :gh:`2814`, [NetBSD]: :func:`virtual_memory` :field:`cached` is overvalued, + since it includes anonymous pages. +- :gh:`2815`, [OpenBSD]: :func:`virtual_memory` :field:`shared` was overvalued + (summed shared ``virtual`` + ``real``, now we only return ``real``). +- :gh:`2822`, [BSD]: :meth:`Process.cmdline` on NetBSD could raise + ``OSError: [Errno 14] Bad address`` if the process about to exit. It now + raises :exc:`NoSuchProcess` instead. + +7.2.2 — 2026-01-28 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`2705`: [Linux]: :meth:`Process.wait` now uses ``pidfd_open()`` + + ``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** + +- :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 + :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. + +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`` with ``str_format``, and ``strlcat`` / ``strlcpy`` with + ``str_copy`` / ``str_append``. Unifies string handling across platforms. + +**Bug fixes** + +- :gh:`2674`, [Windows]: :func:`disk_usage` could truncate values on 32-bit + 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` (buffer overflow / misformat risk). +- :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]: :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` 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 + 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 + :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` :field:`used` field 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]: :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. + +**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 + :field:`broadcast` address instead of ``None``. +- :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 uses ``host_statistics64`` + (same as ``vm_stat``), more accurate. + +**Compatibility notes** + +- :gh:`2480`: Python 2.7 is no longer supported. +- :gh:`2490`: removed long deprecated :meth:`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 :exc:`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` 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. +- :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 running tests via ``python3 -m psutil.tests`` even if + ``pytest`` is not installed. + +**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]: :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). + +6.0.0 — 2024-06-18 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :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 preemptively 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`: 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`` + 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. +- :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 named tuple returned by :func:`disk_partitions`' no longer + has :field:`maxfile` and :field:`maxpath` fields. +- :gh:`2396`: :func:`process_iter` no longer preemptively 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` 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. + +**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 + :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 + 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. +- :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 preemptively checked for :meth:`Process.ppid` + and :meth:`Process.parents`. +- :gh:`2312`: use ``ruff`` linter instead of ``flake8 + isort``. + +**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 + :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 + :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 + preemptively 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 + :exc:`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 :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` 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). + +**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 :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. +- :gh:`2216`, [Windows]: fix tests when running in a virtual environment (patch + by Matthieu Darbois) +- :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 :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 + :exc:`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. (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 :func:`swap_memory` :field:`percent` 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 + :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 :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 :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) + +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 :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. +- :gh:`2057`, [OpenBSD]: add support for :func:`cpu_freq`. +- :gh:`2107`, [Linux]: :meth:`Process.memory_full_info` now reads + ``/proc/pid/smaps_rollup`` instead of ``/proc/pid/smaps`` (5x faster). + +**Bug fixes** + +- :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 :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 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :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. 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. +- :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` :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 mount points 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]: :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 + :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. +- :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. (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 + 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: :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`: switch CI from Travis/Cirrus to GitHub Actions (Linux, macOS, + FreeBSD). AppVeyor still used for Windows. + +**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 :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`, :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` :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 :field:`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 :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 a + segfault due to a NULL pointer. +- :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) + +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 :exc:`KeyError`. +- :gh:`1642`, [SunOS]: querying basic info for PID 0 results in + :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. +- :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` 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). +- :gh:`1673`, [OpenBSD]: :meth:`Process.connections`, :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``. +- :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 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 + 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` :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) + +**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`: :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. +- :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 + :exc:`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`` 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]: :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** + +- :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 + :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. +- :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 :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`` + 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]: :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) +- :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`` / ``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]: 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 + 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 + :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`` 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 + :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 :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. +- :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, :meth:`Process.cmdline` falls back + to ``NtQueryInformationProcess`` on ``ERROR_ACCESS_DENIED``. (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`` where possible, reducing + :exc:`AccessDenied` 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` 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) + +**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 :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 + 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]: :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]: :func:`disk_io_counters` falls back to ``/sys/block`` if + ``/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 + :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 + :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 + :exc:`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` named tuple'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 :field:`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 + :exc:`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`: 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`: :meth:`Process.memory_info_ex` now warns with + :exc:`FutureWarning` instead of :exc:`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` 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. + +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 + :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 :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 + 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 :exc:`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` 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 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 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 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 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 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`` + exceptions as :exc:`NoSuchProcess`. +- :gh:`1016`: :func:`disk_io_counters` raises :exc:`RuntimeError` on a system + with no disks. +- :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 :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 + :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 + :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`'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 (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. +- :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 + :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 :exc:`IOError` on old RedHat + distros. +- :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. +- :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]: 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 + :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` :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 + 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 +^^^^^^^^^^^^^^^^^^ + +**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 :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 :exc:`OSError`. +- :gh:`1010`, [Linux]: :func:`virtual_memory` may raise :exc:`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 + :exc:`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: + :field:`other_count` and :field:`other_bytes`. +- :gh:`976`, [Linux]: :meth:`Process.io_counters` has 2 new fields: + :field:`read_chars` and :field:`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 + :exc:`ZeroDivisionError`. + +5.1.2 — 2017-02-03 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`966`, [Linux]: :func:`sensors_battery` :field:`power_plugged` may + erroneously return ``None`` on Python 3. +- :gh:`968`, [Linux]: :func:`disk_io_counters` raises :exc:`TypeError` on + Python 3. +- :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 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`966`, [Linux]: :func:`sensors_battery` :field:`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 + :exc:`FileNotFoundError`. +- :gh:`966`, [Linux]: :func:`sensors_battery` :field:`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]: :meth:`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 :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 +^^^^^^^^^^^^^^^^^^ + +.. note:: + psutil 5.0 introduces breaking API changes. See the + :ref:`migration guide ` if upgrading from 4.x. + +**Enhncements** + +- :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** + +- :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 + :meth:`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 + :field:`netmask`. +- :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. + +**Bug fixes** + +- :gh:`514`, [macOS], **[critical]**: :meth:`Process.memory_maps` can segfault. +- :gh:`783`, [macOS]: :meth:`Process.status` may erroneously return + :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`: :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 :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 :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 :exc:`OSError` and :exc:`RuntimeError`. +- :gh:`909`, [macOS]: :meth:`Process.open_files` and + :meth:`Process.connections` methods may raise :exc:`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 :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 + :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. +- :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 :exc:`TypeError` on Python + 3. +- :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 + 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 :field:`shared` + 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 :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. + +4.1.0 — 2016-03-12 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`777`, [Linux]: :meth:`Process.open_files` on Linux return 3 new fields: + :field:`position`, :field:`mode` and :field:`flags`. +- :gh:`779`: :meth:`Process.cpu_times` returns two new fields, + :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: + :field:`interrupt` and :field:`dpc`. Same for :func:`cpu_times_percent`. +- :gh:`792`: new :func:`cpu_stats` function returning number of CPU + :field:`ctx_switches`, :field:`interrupts`, :field:`soft_interrupts` and + :field:`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` :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 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`523`, [Linux], [FreeBSD]: :func:`disk_io_counters` return a new + :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) +- :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 (: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`_. + +**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 + :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 + 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 :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. + +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` :field:`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` :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 :field:`sin` / + :field:`sout` can't be determined due to missing ``/proc/vmstat``. +- :gh:`724`, [FreeBSD]: :func:`virtual_memory` :field:`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 :exc:`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]: fix various unicode handling issues. On Python 2, + string APIs now return encoded strings using ``sys.getfilesystemencoding()``. + +**Bug fixes** + +- :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` :exc:`UnicodeDecodeError` in case + of non-ASCII NIC names. +- :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 + 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`: :exc:`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 (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 :exc:`AccessDenied`. +- :gh:`637`, [POSIX]: raise exception if trying to send signal to PID 0 as it + 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. +- :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 (: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`` + 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 :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` 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`: add `scripts/pstree.py`_. +- :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. + +**Bug fixes** + +- :gh:`496`, [SunOS], **[critical]**: can't import psutil. +- :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. +- :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 `scripts/ps.py`_. + +**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 + :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 :exc:`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`: 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. +- :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`: 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) +- :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 +:exc:`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 + :class:`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 :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. + +**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]: 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. +- :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]: `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``. +- :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` :field:`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 :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 + Arfrever) + +**API changes** + +- :meth:`Process.connections` :field:`status` field is no longer a string but a + constant object (``psutil.CONN_*``). +- :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 +^^^^^^^^^^^^^^^^^^ + +**Bug fixes** + +- :gh:`325`, [BSD], **[critical]**: :func:`virtual_memory` can raise + :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` + 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 :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). + +**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 :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 :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. +- :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 + :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 + 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 :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 :exc:`NotImplementedError` instead of + :exc:`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]: 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`: 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]: :meth:`Process.name`, :meth:`Process.memory_info`, + :meth:`Process.memory_percent`, :meth:`Process.cpu_times`, + :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]: :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`: 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** + +- :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]: add :meth:`Process.num_handles`. +- :gh:`209`: :func:`disk_partitions` now provides also mount options. +- :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`: 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`: add :meth:`Process.as_dict`. +- :gh:`281`: :meth:`Process.ppid`, :meth:`Process.name`, :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 in + case the process is terminated. +- :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 + ``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]: :meth:`Process.io_counters` erroneously raise + :exc:`NoSuchProcess` on old Linux versions. Where not available it now raises + :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. + +**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`: 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`: 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. +- :gh:`221`, [FreeBSD]: :meth:`Process.open_files` has been rewritten in C and + no longer relies on ``lsof``. +- :gh:`223`: add `scripts/top.py`_. +- :gh:`227`: add `scripts/nettop.py`_. + +**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 multi core 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`: add :func:`cpu_times` and :func:`cpu_percent` per-cpu support. +- :gh:`163`: add :meth:`Process.terminal`. +- :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 + +**Bug fixes** + +- :gh:`159`, [Windows]: ``SetSeDebug()`` does not close handles or unset + impersonation on return. +- :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. +- :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`: 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`: 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 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** + +- :gh:`83`, [macOS]: :meth:`Process.cmdline` is empty on macOS 64-bit. +- :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]**: :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 :exc:`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`: 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`: 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`: add :meth:`Process.connections`. +- :gh:`107`, [Windows]: add support for Windows 64 bit. (patch by cjgohlke) +- :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` 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. +- :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 :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 + ``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 named tuple instead of a tuple. +- :func:`cpu_times` returns a named tuple instead of a tuple. +- 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 :gh:`123`). +- :meth:`Process.cpu_percent` and :func:`cpu_percent` no longer returns + immediately by default (see :gh:`123`). + +0.1.3 — 2010-03-02 +^^^^^^^^^^^^^^^^^^ + +**Enhancements** + +- :gh:`14`: :meth:`Process.username`. +- :gh:`51`, [Linux], [Windows]: add :meth:`Process.cwd`. +- :gh:`59`: :meth:`Process.is_running` is now 10 times faster. +- :gh:`61`, [FreeBSD]: added support for FreeBSD 64 bit. +- :gh:`71`: add :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`: 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. + +**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 :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 + 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 :exc:`WindowsError`. +- :gh:`30`: psutil.get_pid_list() was returning two 0 PIDs. diff --git a/docs/conf.py b/docs/conf.py index df825cbd55..0a62529d98 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,376 +1,119 @@ -# -*- coding: utf-8 -*- -# -# 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. +# 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 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('.')) +"""Sphinx configuration file. -# -- General configuration ------------------------------------------------ +Sphinx doc: +https://www.sphinx-doc.org/en/master/usage/configuration.html + +RTD theme doc: +https://sphinx-rtd-theme.readthedocs.io/en/stable/ +""" import datetime -import os +import pathlib +import sys + +_HERE = pathlib.Path(__file__).resolve().parent +_ROOT_DIR = _HERE.parent +sys.path.insert(0, str(_ROOT_DIR)) +sys.path.insert(0, str(_HERE / '_ext')) +from _bootstrap import get_version # noqa: E402 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: - for line in f: - if line.startswith('__version__'): - ret = eval(line.strip().split(' = ')[1]) - assert ret.count('.') == 2, ret - for num in ret.split('.'): - assert num.isdigit(), ret - return ret - else: - raise ValueError("couldn't find version string") - - 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. -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'] - -# 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' +# ===================================================================== +# Extensions +# ===================================================================== + +extensions = [ + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx_copybutton", + # custom extensions in _ext/ dir + "availability", + "changelog_anchors", + "check_python_syntax", + "field_role", + "genindex_filter", +] -# The master toctree document. -master_doc = 'index' +# ===================================================================== +# Project metadata +# ===================================================================== -# General information about the project. project = PROJECT_NAME -copyright = '2009-%s, %s' % (THIS_YEAR, AUTHOR) author = AUTHOR +version = release = VERSION +copyright = f"2009-{THIS_YEAR}, {AUTHOR}" # shown in the footer -# 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 = None - -# 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'] - -# 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 - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -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 +# ===================================================================== +# Cross-references and external links +# ===================================================================== -# 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 = '%s-doc' % PROJECT_NAME - -# -- 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', +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), } - -# 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', u'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', u'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', u'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_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', - ], +extlinks = { + "gh": ("https://github.com/giampaolo/psutil/issues/%s", "#%s"), } + +# ===================================================================== +# Paths +# ===================================================================== + +html_static_path = ["_static"] +exclude_patterns = ["_build", "_globals.rst"] + +# ===================================================================== +# HTML +# ===================================================================== + +html_title = PROJECT_NAME +html_favicon = "_static/images/favicon.svg" +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" + +# ===================================================================== +# Plugins +# ===================================================================== + +copybutton_exclude = ".linenos, .gp" + +# ===================================================================== +# Theming +# ===================================================================== + +html_theme = "sphinx_rtd_theme" + +if html_theme == "sphinx_rtd_theme": + html_theme_options = { + "collapse_navigation": False, + "navigation_depth": 5, + "flyout_display": "attached", + } + templates_path = ["_templates", "_static/images"] + pygments_style = "tango" # https://pygments.org/styles/ + html_css_files = [ + "css/custom.css", + ] + html_js_files = [ + "js/highlight-repl.js", + "js/external-urls.js", + ("js/theme-toggle.js", {"defer": "defer"}), + ("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 new file mode 100644 index 0000000000..3e0bab3726 --- /dev/null +++ b/docs/credits.rst @@ -0,0 +1,584 @@ +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 Rodola + +Top contributors +---------------- + +* `Giampaolo Rodola`_: creator, primary author and long-time maintainer +* `Jay Loden`_: original co-author, initial design and project bootstrap, + initial macOS / Windows / FreeBSD implementations +* `Arnon Yaari`_: AIX implementation +* `Landry Breuil`_: initial OpenBSD implementation +* `Ryo Onodera`_ and `Thomas Klausner`_: initial 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 +~~~~ + +* `Amaan Qureshi`_ - :gh:`2770` +* `Felix Yan`_ - :gh:`2732` +* `Santhosh Raju`_ - :gh:`2805` +* `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` +* `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` +* `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 +.. _`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 +.. _`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 +.. ============================================================================ + +.. _`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 +.. _`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 +.. _`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 +.. _`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-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 +.. _`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 +.. _`EccoTheFlintstone`: https://github.com/EccoTheFlintstone +.. _`Eli Wenig`: https://github.com/elisw93 +.. _`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 +.. _`Ghislain Le Meur`: https://github.com/gigi206 +.. _`Giampaolo Rodola`: https://github.com/giampaolo +.. _`Gleb Smirnoff`: https://github.com/glebius +.. _`Gregory Szorc`: https://github.com/indygreg +.. _`Grzegorz Bokota`: https://github.com/Czaki +.. _`Guido Imperiale`: https://github.com/crusaderky +.. _`Guillermo`: https://github.com/guille +.. _`Himanshu Shekhar`: https://github.com/himanshub16 +.. _`Hiroyuki Tanaka`: https://github.com/myheroyuki +.. _`Hugo van Kemenade`: https://github.com/hugovk +.. _`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/1saac-k +.. _`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 +.. _`Jonathan Kohler`: https://github.com/kohlerjl +.. _`Josiah Carlson`: https://github.com/josiahcarlson +.. _`Julien Lebot`: https://github.com/julien-lebot +.. _`Kamil Rytarowski`: https://github.com/krytarowski +.. _`karthik`: https://github.com/karthikrev +.. _`Koen Kooi`: https://github.com/koenkooi +.. _`Landry Breuil`: https://github.com/landryb +.. _`Lawrence D'Anna`: https://github.com/smoofra +.. _`Lawrence Ye`: https://github.com/LEAFERx +.. _`Lysandros Nikolaou`: https://github.com/lysnikolaou +.. _`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 +.. _`Mike Hommey`: https://github.com/glandium +.. _`Mike Sarahan`: https://github.com/msarahan +.. _`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 +.. _`PetrPospisil`: https://github.com/PetrPospisil +.. _`Pierre Fersing`: https://github.com/PierreF +.. _`Po-Chuan Hsieh`: https://github.com/sunpoet +.. _`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 +.. _`Santhosh Raju`: https://github.com/fraggerfox +.. _`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 +.. _`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 Schlueter`: https://github.com/modelrockettier +.. _`Timmy Konick`: https://github.com/tijko +.. _`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 +.. _`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 diff --git a/docs/devguide.rst b/docs/devguide.rst new file mode 100644 index 0000000000..41f39e54c9 --- /dev/null +++ b/docs/devguide.rst @@ -0,0 +1,192 @@ +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 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 test dependencies + make build + make install + make test + +- ``make`` (via the `Makefile`_) is used for building, testing and general + development tasks, including on Windows (see below): + + .. code-block:: bash + + 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 test: + + .. code-block:: none + + make test ARGS=tests/test_system.py + +- Do not use ``sudo``. ``make install`` installs psutil in editable mode, so + you can modify the code while developing. + +- 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``. +- 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 +---------- + +If you need to debug unusual situations or report a bug, you can enable 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: + +.. 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(...) -> 998 (Unknown error) (ignored) + +Coding style +------------ + +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``). + +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 + 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 # OS-specific tests + +Adding a new API +---------------- + +- 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 on GitHub. +- Clone your fork: ``git clone git@github.com:YOUR-USERNAME/psutil.git`` +- Create a branch: ``git checkout -b new-feature`` +- 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 run automatically on every ``git push`` on all platforms except AIX. +See +`.github/workflows `_. + +Documentation +------------- + +- Source is in the `docs/`_ directory. +- To build HTML: + + .. code-block:: bash + + cd docs/ + python3 -m pip install -r requirements.txt + make html + +- 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): + + - `/stable`_: latest release published on `PyPI`_ + - `/latest`_: ``master`` development branch + +.. note:: + + ``/latest`` may contain unreleased changes. Use ``/stable`` for production + docs. + +Releases +-------- + +- 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 +.. _`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/ +.. _`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 +.. _`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 new file mode 100644 index 0000000000..3946de10d8 --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,400 @@ +FAQ +=== + +This section answers common questions and pitfalls when using psutil. + +.. contents:: + :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 + rss, vms = p.memory_info() + + # good + 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 +---------- + +.. _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. 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.name(), p.username()) # no try/except needed + +.. _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 two 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.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 + +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 a 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:** + +- 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`. +- Sending signals (:meth:`Process.terminate`, :meth:`Process.kill`, etc.) has + no effect. +- 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. +- :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 + + import psutil + + for p in psutil.process_iter(["status"]): + 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. + +.. _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: + +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. + +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 :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 :term:`logical CPUs ` + (including hyperthreads). It is the same as + ``psutil.cpu_count(logical=True)``, but psutil does not honour + :envvar:`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 :term:`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 + :term:`physical cores `, which has no stdlib equivalent. + +Memory +------ + +.. _faq_virtual_memory_available: + +What is the difference between virtual_memory() available and free? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:func:`virtual_memory` returns both :field:`free` and :field:`available`, but +they measure different things: + +- :field:`free`: memory that is not being used at all. +- :field:`available`: how much memory can be given to processes without + :term:`swapping `. This includes reclaimable + :term:`caches ` and :term:`buffers` that the OS can reclaim under + pressure. + +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? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- :field:`rss` (:term:`Resident Set Size `) is the amount of physical + memory (RAM) currently mapped into the process. +- :field:`vms` (:term:`Virtual Memory Size `) is the total virtual address + space of the process, including memory that has been + :term:`swapped out `, shared libraries, and + :term:`memory-mapped files `. + +:field:`rss` is the go-to metric for answering "how much RAM is this process +using?". Note that it includes :term:`shared memory`, so it may overestimate +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. 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 :field:`rss` +(:term:`Resident Set Size `), which includes +:term:`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 :field:`rss`. + +:meth:`Process.memory_footprint` returns :field:`uss` +(:term:`Unique Set Size `), i.e. :term:`private memory` of the process. It +represents the amount of memory that would be freed if the process were +terminated right now. It is more accurate than :term:`RSS`, but substantially +slower and requires higher privileges. On Linux it also returns :field:`pss` +(:term:`Proportional Set Size `) and :term:`swap `. + +.. _faq_used_plus_free: + +Why does virtual_memory() used + free != total? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Because some memory (like :term:`page cache` and :term:`buffers`) is +reclaimable and accounted separately: + +.. code-block:: pycon + + >>> import psutil + >>> m = psutil.virtual_memory() + >>> m.used + m.free == m.total + False + +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/funding.rst b/docs/funding.rst new file mode 100644 index 0000000000..b63abc8cd0 --- /dev/null +++ b/docs/funding.rst @@ -0,0 +1,50 @@ +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/glossary.rst b/docs/glossary.rst new file mode 100644 index 0000000000..7e5ade8162 --- /dev/null +++ b/docs/glossary.rst @@ -0,0 +1,355 @@ +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 :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 :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 :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`. + + buffers + + Kernel memory used to cache filesystem metadata such as + superblocks, inodes, and directory entries. Distinct from the + :term:`page cache`, which caches file *contents*. + Like the page cache, buffer memory is reclaimable: the OS can + free it under memory pressure. + Reported as the :field:`buffers` field of :func:`virtual_memory` + (Linux, BSD). + + busy_time + + A :term:`cumulative counter` (milliseconds) tracking the time a disk + 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 + 100% means the disk is saturated. + + .. seealso:: :ref:`Real-time disk I/O percent recipe ` + + CPU affinity + + 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 when the CPU stops executing one process or thread for another. + Frequent switching can indicate high system + load or thread contention. See :meth:`Process.num_ctx_switches` + and :func:`cpu_stats` (:field:`ctx_switches` field). + A :field:`voluntary` context switch occurs when a process gives up the + CPU, usually because it's waiting for something (I/O, a sleep, a mutex + lock). High rates are normal for I/O-bound workloads (e.g. a web server) + and usually point to I/O or locking as the bottleneck. + An :field:`involuntary` context switch occurs when the OS forcibly takes + the CPU from the process. High rates mean the process has more work to do + but is being kicked off the core. This usually indicates too many active + threads/processes competing for too few CPU cores. + + 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). + + 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 :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 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`. + + hardware interrupt + + 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 :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`. + + 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 + + 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. + 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 + + An I/O scheduling priority that controls how much disk bandwidth a + process receives. On Linux three scheduling classes are supported: + :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 + + 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`. + + mapped memory + + A region of a process's virtual address space typically created via + ``mmap()``. Mappings can be file-backed (e.g. shared libraries, + memory-mapped files) or :term:`anonymous `. + Each mapping has its own permissions and memory accounting fields + (:term:`RSS`, :term:`PSS`, private / shared pages). + See :meth:`Process.memory_maps`. + + 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 + + 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 :ref:`priority constants `. See + :meth:`Process.nice`. + + page cache + + 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 :field:`cached` field + of :func:`virtual_memory` on Linux/BSD. + + page fault + + An event that occurs when a process accesses a virtual memory page that + is not currently mapped in physical RAM. A :field:`minor` fault occurs + when a page is already in physical RAM (e.g., in the :term:`page cache` + or other :term:`shared memory`), but it's not yet mapped into the + process's virtual address space, so no disk I/O is required (fast). A + :field:`major` fault requires reading the page from disk, and is + significantly more expensive. Many major faults may indicate memory + pressure or excessive swapping. See :meth:`Process.page_faults`. + + 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 :field:`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 :term:`private ` committed + memory), which is not the same as UNIX VMS. See also :term:`peak_rss`. + + 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)``. + + 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 + :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 + + *Proportional Set Size*, the amount of RAM used by a process, + 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`. + + 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`. + + RSS + + *Resident Set Size*, the amount of physical RAM currently used by a + process. This includes :term:`shared memory` pages. It is the most + commonly reported memory metric (shown as ``RES`` in ``top``), but can be + misleading because :term:`shared memory` is counted in full for each + process that maps it. + See :meth:`Process.memory_info`. + + 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 :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. + + 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 :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 :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 :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`. + + .. seealso:: + - :ref:`swap activity recipe ` + - :term:`thrashing` + + swap memory + + 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 `. + + 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 :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 + + *Unique Set Size*, the :term:`private memory` of a process, that belongs + exclusively to it, and which 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 + + See :term:`context switch`. + + VMS + + *Virtual Memory Size*, the total virtual address space reserved by a + 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 + + 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. + + .. seealso:: :ref:`faq_zombie_process` diff --git a/docs/index.rst b/docs/index.rst index 395fd688f0..c7b264a53c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,2873 +1,176 @@ .. module:: psutil :synopsis: psutil module -.. moduleauthor:: Giampaolo Rodola' - -psutil documentation -==================== - -Quick links ------------ - -- `Home page `__ -- `Install `_ -- `Blog `__ -- `Forum `__ -- `Download `__ -- `Development guide `_ -- `What's new `__ - -About ------ - -psutil (python system and process utilities) is a cross-platform library for -retrieving information on 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**. -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: - -- **Linux** -- **Windows** -- **OSX**, -- **FreeBSD, OpenBSD**, **NetBSD** -- **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. - -The psutil documentation you're reading is distributed as a single HTML page. - -Install -------- - -The easiest way to install psutil is via ``pip``:: - - pip 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 -`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: - - - **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** *(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 - - **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 - in a virtualized environment - - **guest** *(Linux 2.6.24+)*: 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 - (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, 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) - - .. versionchanged:: 4.1.0 added *interrupt* and *dpc* fields on Windows. - -.. 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. - - >>> 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. - -.. 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. - -.. 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. - 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). - On OpenBSD and NetBSD ``psutil.cpu_count(logical=False)`` always return - ``None``. Example on a system having 2 physical hyper-thread CPU cores: - - >>> import psutil - >>> psutil.cpu_count() - 4 - >>> psutil.cpu_count(logical=False) - 2 - - Example returning the number of CPUs usable by the current process: - - >>> 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 nameduple 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. - 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``. - - 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, OSX, Windows - - .. versionadded:: 5.1.0 - - -Memory ------- - -.. function:: virtual_memory() - - Return statistics about system memory usage as a named tuple including the - following fields, expressed in bytes. Main metrics: - - - **total**: total physical memory. - - **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. - - Other metrics: - - - **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** *(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** *(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, OSX)*: 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 the **available** field. - - >>> 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 = 100 * 1024 * 1024 # 100MB - >>> if mem.available <= THRESHOLD: - ... print("warning") - ... - >>> - - .. 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 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``. - 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. 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 OSX and BSD it is retrieved via - `getfsstat(2) `__. See - `disk_usage.py `__ - script providing an example usage. - - >>> 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')] - -.. 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 `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 `iostat 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 ncessary to issue ``diskperf -y`` command from cmd.exe - first in order to enable IO counters. - - >>> 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) - - 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 OSX 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. - - >>> 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 - `__ 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 - 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). - - 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 OSX and AIX this function requires root privileges. - To get per-process connections use :meth:`Process.connections`. - Also, see - `netstat.py sample 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='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) - ...] - - .. note:: - (OSX 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) "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. - - .. 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. - -.. 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 `__, - `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': [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)]} - >>> - - 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``. - -.. 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. - - **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. - - 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)} - - Also see `nettop.py `__ - and `ifconfig.py `__ - for an example application. - - .. versionadded:: 3.0.0 - - -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. - 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 - - .. versionadded:: 5.1.0 - - .. warning:: - - this API is experimental. Backward incompatible changes may occur if - deemed necessary. - -.. 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 (rounds 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 - - .. 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 - 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 OSX support - - .. warning:: - - this API is experimental. Backward incompatible changes may occur if - deemed necessary. - -Other system info ------------------ - -.. function:: boot_time() - - Return the system boot time expressed in seconds since the epoch. - Example: - - .. 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: - - - **user**: 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. - - **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 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] - -.. 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. - 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 should be 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 - returned :class:`Process` instances. - If *attrs* is an empty list it will retrieve all process info (slow). - 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) - ... - {'name': 'systemd', 'pid': 1, 'username': 'root'} - {'name': 'kthreadd', 'pid': 2, 'username': 'root'} - {'name': 'ksoftirqd/0', 'pid': 3, 'username': 'root'} - ... - - Example of 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 - {1: {'name': 'systemd', 'username': 'root'}, - 2: {'name': 'kthreadd', 'username': 'root'}, - 3: {'name': 'ksoftirqd/0', 'username': 'root'}, - ...} - - Example showing how to filter processes by name:: - - >>> 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. - - .. versionchanged:: - 5.3.0 added "attrs" and "ad_value" parameters. - -.. 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 (will be ``None`` for - processes which are not our children). - ``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). - 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). 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`. - - .. 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``). - -.. class:: TimeoutExpired(seconds, pid=None, name=None, msg=None) - - Raised by :meth:`Process.wait` if timeout expires and process is still - alive. - -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`, :class:`ZombieProcess` 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 - `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. - - .. note:: - - the way this class is bound to a process is uniquely 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: - :meth:`nice` (set), - :meth:`ionice` (set), - :meth:`cpu_affinity` (set), - :meth:`rlimit` (set), - :meth:`children`, - :meth:`parent`, - :meth:`suspend` - :meth:`resume`, - :meth:`send_signal`, - :meth:`terminate` - :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() - - 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 emtpy 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 | OSX | BSD | SunOS | AIX | - +==============================+===============================+==============================+==============================+==========================+==========================+ - | :meth:`cpu_num` | :meth:`cpu_percent` | :meth:`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:`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_maps` | :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:`username` | :meth:`gids` | :meth:`memory_percent` | :meth:`status` | :meth:`status` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`gids` | | :meth:`name` | :meth:`num_ctx_switches` | :meth:`terminal` | :meth:`terminal` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`num_ctx_switches` | | :meth:`ppid` | :meth:`ppid` | | | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :meth:`num_threads` | | :meth:`status` | :meth:`status` | :meth:`gids` | :meth:`gids` | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :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_full_info` | | | | | | - +------------------------------+-------------------------------+------------------------------+------------------------------+--------------------------+--------------------------+ - | :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` method. - - .. 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 this may also be an empty string. - The return value is cached after first call. - - >>> import psutil - >>> psutil.Process().exe() - '/usr/bin/python2.7' - - .. 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', '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 - - .. versionadded:: 4.0.0 - .. versionchanged:: 5.3.0 added SunOS support - - .. 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. - - >>> 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 (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. - 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'} - - .. 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` method. - - .. method:: status() - - The current process status as a string. The returned string is one of the - :data:`psutil.STATUS_*` constants. - - .. method:: cwd() - - The process current working directory as an absolute path. - - .. 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() `__ - (UNIX only). - 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: - - >>> p.nice(psutil.HIGH_PRIORITY_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 ``2`` (normal), - ``1`` (low) or ``0`` (very low). Also it returns an integer instead of a - named tuple. - - Availability: Linux and Windows > Vista - - .. versionchanged:: - 3.0.0 on Python >= 3.4 the returned ``ioclass`` constant is an - `enum `__ - instead of a plain integer. - - .. method:: rlimit(resource, limits=None) - - 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() `__. - 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() - >>> # 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) - (1024, 1024) - >>> - - Availability: Linux - - .. method:: io_counters() - - Return process I/O statistics as a named tuple. - For Linux you can refer to - `/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 - ``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). - - .. 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 including thread - id and thread CPU times (user/system). On OpenBSD this method requires - root privileges. - - .. 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 OSX 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. - - .. versionchanged:: - 4.1.0 return two extra fields: *children_user* and *children_system*. - - .. 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 plaforms are `rss` and `vms`. - All numbers are expressed in bytes. - - +---------+---------+-------+---------+-----+------------------------------+ - | Linux | OSX | BSD | Solaris | AIX | Windows | - +=========+=========+=======+=========+=====+==============================+ - | rss | rss | rss | rss | rss | rss (alias for ``wset``) | - +---------+---------+-------+---------+-----+------------------------------+ - | vms | vms | vms | vms | vms | vms (alias for ``pagefile``) | - +---------+---------+-------+---------+-----+------------------------------+ - | shared | pfaults | text | | | num_page_faults | - +---------+---------+-------+---------+-----+------------------------------+ - | text | pageins | data | | | peak_wset | - +---------+---------+-------+---------+-----+------------------------------+ - | lib | | stack | | | wset | - +---------+---------+-------+---------+-----+------------------------------+ - | data | | | | | peak_paged_pool | - +---------+---------+-------+---------+-----+------------------------------+ - | dirty | | | | | paged_pool | - +---------+---------+-------+---------+-----+------------------------------+ - | | | | | | peak_nonpaged_pool | - +---------+---------+-------+---------+-----+------------------------------+ - | | | | | | nonpaged_pool | - +---------+---------+-------+---------+-----+------------------------------+ - | | | | | | pagefile | - +---------+---------+-------+---------+-----+------------------------------+ - | | | | | | peak_pagefile | - +---------+---------+-------+---------+-----+------------------------------+ - | | | | | | 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 - (see `doc `__). - 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 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 `__). - - - **text** *(Linux, BSD)*: - aka TRS (text resident set) the amount of memory devoted to - executable code. This matches "top"'s CODE column - (see `doc `__). - - - **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 `__). - - - **lib** *(Linux)*: the memory used by shared libraries. - - - **dirty** *(Linux)*: the number of dirty pages. - - - **pfaults** *(OSX)*: number of page faults. - - - **pageins** *(OSX)*: number of actual pageins. - - For on explanation of Windows fields rely on - `PROCESS_MEMORY_COUNTERS_EX `__ structure doc. - Example on Linux: - - >>> import psutil - >>> p = psutil.Process() - >>> p.memory_info() - 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`. - - .. 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 - some platform (Linux, OSX, 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 - `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`. - - - **uss** *(Linux, OSX, 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. - - - **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)*: 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. - - Example on Linux: - - >>> 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) - >>> - - See also `procsmem.py `__ - for an example application. - - .. versionadded:: 4.0.0 - - .. 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"``). - - .. 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 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*). - See `pmap.py `__ - for an example application. - - +---------------+--------------+---------+-----------+--------------+ - | Linux | OSX | 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 | | | | | - +---------------+--------------+---------+-----------+--------------+ - - >>> 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. - - .. 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:: 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 - `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"``). - - **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 for each handle 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 `_). - - .. 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:: 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``. - - **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 - 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.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')] - - .. 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. - - .. 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). - - .. 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)``. - - .. note:: - this will return ``True`` also if the process is a zombie - (``p.status() == psutil.STATUS_ZOMBIE``). - - .. 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 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`. - *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`. - To wait for multiple processes use :func:`psutil.wait_procs()`. - - >>> import psutil - >>> p = psutil.Process(9891) - >>> 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. - 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 `__. - - .. 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 - http://bugs.python.org/issue6973. - - >>> 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 - >>> - - :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. - - >>> 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 - -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 -========= - -.. _const-oses: -.. data:: POSIX -.. data:: WINDOWS -.. data:: LINUX -.. data:: OSX -.. 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 - -.. _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-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_IDLE (OSX, FreeBSD) -.. data:: STATUS_LOCKED (FreeBSD) -.. 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) - -.. _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). - -.. _const-prio: -.. data:: ABOVE_NORMAL_PRIORITY_CLASS -.. data:: BELOW_NORMAL_PRIORITY_CLASS -.. data:: HIGH_PRIORITY_CLASS -.. data:: IDLE_PRIORITY_CLASS -.. data:: NORMAL_PRIORITY_CLASS -.. 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 - :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 -.. 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. - *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:: - 3.0.0 on Python >= 3.4 these constants are - `enums `__ - instead of a plain integer. - -.. _const-rlimit: -.. 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. - - Availability: Linux - -.. _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 - ----- - -Unicode -======= - -Starting from version 5.3.0 psutil fully supports unicode, 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 OSX, "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 -======= - -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 --------------------- - -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(attrs=['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(attrs=["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 callabck function which is - called as soon as a child terminates. - """ - if pid == os.getpid(): - raise RuntimeError("I refuse to kill myself") - parent = psutil.Process(pid) - children = parent.children(recursive=True) - if include_parent: - children.append(parent) - for p in children: - p.send_signal(sig) - gone, alive = psutil.wait_procs(children, timeout=timeout, - 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: - p.terminate() - 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() - 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" % 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:: - - >>> 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 - >>> pp([(p.pid, p.info['name']) for p in psutil.process_iter(attrs=['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]) - [(1150, {'name': 'Xorg', 'status': 'running'}), - (1776, {'name': 'unity-panel-service', 'status': 'running'}), - (20492, {'name': 'python', 'status': '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': - ... 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(attrs=['name', 'memory_info']) if p.info['memory_info'].rss > 500 * 1024 * 1024]) - [(2650, 'chrome', 532324352), - (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:]) - [(2721, 'chrome', 10219.73), - (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 ----------------- - -:: - - 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 - - total = psutil.disk_usage('/').total - print(total) - print(bytes2human(total)) - -...prints:: - - 100399730688 - 93.5G - -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. +.. moduleauthor:: Giampaolo Rodola +.. title:: Home + +.. ============================================================================ +.. Hero +.. ============================================================================ + +.. raw:: html + + + +
    +
    psutil
    +
    Process and System Utilities for Python
    +
    + GitHub repo + Downloads + Latest version +
    +
    + +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. ---- -* 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 - 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 - (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 - 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 -============= - -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 - -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 `_. - -Timeline -======== - -- 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 `__ +.. ============================================================================ +.. Platform pills +.. ============================================================================ + +.. raw:: html + + + +.. ============================================================================ +.. Feature cards +.. ============================================================================ + +.. raw:: html + + + +.. ============================================================================ +.. 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.

    +
    +
    +
    + +
    Platform portability
    +
    +

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

    +
    +
    + + + +.. ============================================================================ +.. Sponsors +.. ============================================================================ + +Sponsors +-------- + +.. raw:: html + :file: _sponsors.html + +.. ============================================================================ +.. TOC: hidden via CSS, needed by Sphinx for sidebar nav +.. ============================================================================ + +.. toctree:: + :maxdepth: 2 + :caption: Documentation + + Install + API overview + API Reference + FAQ + Performance + Recipes + +.. toctree:: + :maxdepth: 2 + :caption: Reference + + Shell equivalents + Stdlib equivalents + Glossary + Platform support + Migration + +.. toctree:: + :maxdepth: 2 + :caption: About + + Who uses psutil + Alternatives + Funding + Credits + +.. toctree:: + :maxdepth: 2 + :titlesonly: + :caption: Project + + Changelog + Timeline + Development guide + General Index diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 0000000000..aac2e0209f --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,192 @@ +Install psutil +============== + +Linux, Windows, macOS (wheels) +------------------------------ + +Pre-compiled wheels are distributed for these platforms, so you usually won't +need a C compiler. Install psutil with: + +.. code-block:: none + + 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 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: + +.. code-block:: none + + make install-sysdeps + +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`_: + +.. code-block:: none + + pip install --no-binary :all: psutil + +Linux +^^^^^ + +Debian / Ubuntu: + +.. code-block:: none + + sudo apt install gcc python3-dev + pip install --no-binary :all: psutil + +RedHat / CentOS: + +.. code-block:: none + + sudo yum install gcc python3-devel + pip install --no-binary :all: psutil + +Arch: + +.. code-block:: none + + sudo pacman -S gcc python + pip install --no-binary :all: psutil + +Alpine: + +.. code-block:: none + + sudo apk add gcc python3-dev musl-dev linux-headers + pip install --no-binary :all: psutil + +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`_. +- MinGW is not supported for building psutil on Windows. +- To build directly from the source tarball (.tar.gz) on PYPI, run: + + .. 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: + + .. code-block:: none + + make build + make install + +macOS +^^^^^ + +Install Xcode first: + +.. code-block:: none + + xcode-select --install + pip install --no-binary :all: psutil + +FreeBSD +^^^^^^^ + +.. code-block:: none + + pkg install python3 gcc + python3 -m pip install psutil + +OpenBSD +^^^^^^^ + +.. code-block:: none + + export PKG_PATH=https://cdn.openbsd.org/pub/OpenBSD/`uname -r`/packages/`uname -m`/ + pkg_add -v python3 gcc + pip install psutil + +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 + pkgin install python311-* gcc12-* py311-setuptools-* py311-pip-* + python3.11 -m pip install psutil + +Sun Solaris +^^^^^^^^^^^ + +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: + +.. code-block:: none + + pkg install gcc + pip install psutil + +Troubleshooting +--------------- + +Install pip +^^^^^^^^^^^ + +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: + +.. code-block:: none + + python3 < <(curl -s https://bootstrap.pypa.io/get-pip.py) + +On Windows, `download pip`_, open cmd.exe and install it with: + +.. code-block:: none + + py get-pip.py + +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``: + +.. code-block:: none + + 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/ 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/migration.rst b/docs/migration.rst new file mode 100644 index 0000000000..eeb3d81fb9 --- /dev/null +++ b/docs/migration.rst @@ -0,0 +1,393 @@ +Migration guide +=============== + +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. + +.. contents:: + :local: + :depth: 2 + +.. _migration-8.0: + +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. +- :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). +- New :attr:`Process.attrs`: :class:`frozenset` of valid attribute names; + ``process_iter(attrs=[])`` is deprecated. +- 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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +: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 + + import psutil + + # 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()) # 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``). + +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)) # 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``. + +Named tuple field order changed +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- :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. :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``). + + .. 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. 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). + - macOS: :field:`pfaults` and :field:`pageins` removed with **no aliases**. + Use :meth:`Process.page_faults` instead. + - 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`. + :field:`paged_pool`, :field:`nonpaged_pool`, :field:`peak_paged_pool`, + :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 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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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`. + +Status and connection fields are now enums +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- :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:`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 :func:`repr` or :class:`type` may need updating. + +memory_full_info() is deprecated +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:meth:`Process.memory_full_info` is deprecated. Use +:meth:`Process.memory_footprint` instead (same fields). + +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. +: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`). + +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: + +.. 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. +``process_iter(attrs=[])`` (empty list meaning "all") is now deprecated; use +:attr:`Process.attrs` instead: + +.. code-block:: python + + # all attrs + psutil.process_iter(attrs=psutil.Process.attrs) + + # all except connections + psutil.process_iter(attrs=psutil.Process.attrs - {"net_connections"}) + +Python 3.6 dropped +^^^^^^^^^^^^^^^^^^^^ + +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`. + +------------------------------------------------------------------------------- + +.. _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. + +.. 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 + + # 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 :exc:`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 :field:`maxfile` and :field:`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 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 + + for p in psutil.process_iter(["name"]): + if p.is_running(): + print(p.pid, p.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 +:exc:`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/docs/performance.rst b/docs/performance.rst new file mode 100644 index 0000000000..e9cecf4f2a --- /dev/null +++ b/docs/performance.rst @@ -0,0 +1,204 @@ +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 one attribute from the same +process, use :meth:`Process.oneshot`. + +.. _perf-process-iter: + +Use process_iter() with an attrs list +-------------------------------------- + +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. + +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()) # 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): + +.. code-block:: python + + import psutil + + for p in psutil.process_iter(psutil.Process.attrs - {"memory_footprint", "memory_maps"}): + ... + +.. _perf-oneshot-bench: + +Measuring oneshot() speedup +--------------------------- + +`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 + + $ 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 + +.. _perf-api-speed: + +Measuring APIs speed +-------------------- + +`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: + +.. code-block:: none + + $ 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 + +.. _`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/platform.rst b/docs/platform.rst new file mode 100644 index 0000000000..fbd4b549b1 --- /dev/null +++ b/docs/platform.rst @@ -0,0 +1,81 @@ +Platform support +================ + +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: + +.. code-block:: none + + 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** +* 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 +* 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..db2fde4df1 --- /dev/null +++ b/docs/recipes.rst @@ -0,0 +1,545 @@ +Recipes +======= + +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. + +.. contents:: + :local: + :depth: 3 + +Processes +--------- + +Finding processes +^^^^^^^^^^^^^^^^^ + +.. _recipe_find_process_by_name: + +Find process by name: + +.. code-block:: python + + import psutil + + def find_procs_by_name(name): + ls = [] + for p in psutil.process_iter(["name"]): + if p.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, psutil + + def find_procs_by_name_ex(name): + ls = [] + for p in psutil.process_iter(["name", "exe", "cmdline"]): + if ( + 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 + +------------------------------------------------------------------------------- + +Find the process listening on a given TCP port: + +.. code-block:: python + + import psutil + + def find_proc_by_port(port): + for proc in psutil.process_iter(): + try: + cons = proc.net_connections(kind="tcp") + except psutil.Error: + pass + else: + for conn in cons: + if conn.laddr.port == port and conn.status == psutil.CONN_LISTEN: + return proc + return None + +------------------------------------------------------------------------------- + +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 + + def find_procs_using_file(path): + ls = [] + for p in psutil.process_iter(["open_files"]): + for f in p.open_files() or []: + if f.path == path: + ls.append(p) + break + return ls + +Filtering and sorting processes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Processes owned by user: + +.. code-block:: python + + 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 + ] + +------------------------------------------------------------------------------- + +Processes using log files: + +.. code-block:: pycon + + >>> for p in psutil.process_iter(["name", "open_files"]): + ... for file in p.open_files() or []: + ... if file.path.endswith(".log"): + ... 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 + 2650 chrome /home/giampaolo/.config/google-chrome/Default/data_reduction_proxy_leveldb/000003.log + +------------------------------------------------------------------------------- + +Processes consuming more than 500M of memory: + +.. code-block:: python + + import psutil + + 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 N processes by :term:`cumulative ` CPU time: + +.. code-block:: python + + 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:]] + +------------------------------------------------------------------------------- + +Top N processes by :term:`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"]): + 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] + +------------------------------------------------------------------------------- + +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(["num_fds"]): + procs.append((p.num_fds(), p)) + 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, 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 +^^^^^^^^^^^^^^^^^^^^^ + +.. _recipe_kill_proc_tree: + +Kill a process tree (including grandchildren): + +.. code-block:: python + + import os, signal, 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) + +------------------------------------------------------------------------------- + +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() + +------------------------------------------------------------------------------- + +Temporarily pause and resume a process using a context manager: + +.. code-block:: python + + import contextlib, 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, 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, time, 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): + """ + >>> 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}{}".format(value, s) + return "{}B".format(n) + +CPU +^^^ + +Print real-time CPU usage percentage: + +.. 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% + +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 + + 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 + +------------------------------------------------------------------------------- + +.. _recipe_disk_io_percent: + +Show real-time disk utilization percentage *(Linux, FreeBSD)*: + +.. code-block:: python + + import psutil, time + + def disk_io_percent(interval=1): + 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)) + +.. code-block:: none + + Disk: 3.2% + Disk: 78.5% + +Network +^^^^^^^ + +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 + + 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 new file mode 100644 index 0000000000..f52abbc989 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +sphinx +sphinx_rtd_theme +sphinx_copybutton +sphinxcontrib-googleanalytics diff --git a/docs/shell-equivalents.rst b/docs/shell-equivalents.rst new file mode 100644 index 0000000000..35cef170bb --- /dev/null +++ b/docs/shell-equivalents.rst @@ -0,0 +1,578 @@ +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. + +.. seealso:: + - :doc:`stdlib-equivalents` + - :doc:`alternatives` + +System-wide functions +--------------------- + +CPU +~~~ + +.. list-table:: + :header-rows: 1 + :class: wide-table + + * - 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` + - + - + - ``vmstat -s``, ``sysctl vm.stats.sys`` + - + * - :func:`getloadavg` + - ``uptime`` + - same + - same + - + +Memory +~~~~~~ + +.. list-table:: + :header-rows: 1 + :class: wide-table + + * - psutil function + - Linux + - macOS + - BSD + - Windows + * - :func:`virtual_memory` + - ``free``, ``vmstat``, ``cat /proc/meminfo`` + - ``vm_stat`` + - ``vmstat -s``, ``sysctl vm.stats`` + - ``systeminfo`` + * - :func:`swap_memory` + - ``free``, ``vmstat``, ``swapon`` + - ``sysctl vm.swapusage`` + - ``vmstat -s``, ``swapinfo`` + - + * - :func:`heap_info` + - + - + - + - + * - :func:`heap_trim` + - + - + - + - + +Disks +~~~~~ + +.. list-table:: + :header-rows: 1 + :class: wide-table + + * - psutil function + - Linux + - macOS + - BSD + - Windows + * - :func:`disk_usage` + - ``df`` + - same + - same + - ``fsutil volume diskfree C:\`` + * - :func:`disk_partitions` + - ``mount``, ``findmnt`` + - ``mount`` + - ``mount`` + - + * - :func:`disk_io_counters` + - ``iostat -dx`` + - ``iostat`` + - ``iostat -x`` + - + +Network +~~~~~~~ + +.. list-table:: + :header-rows: 1 + :class: wide-table + + * - 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 + :class: wide-table + + * - 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 + :class: wide-table + + * - 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 -A -eo pid`` + - same + - same + - ``tasklist`` + * - :func:`pid_exists` + - ``kill -0 PID`` + - same + - same + - + +Process methods +--------------- + +Identity +~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :class: wide-table + + * - 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 + :class: wide-table + + * - 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 + :class: wide-table + + * - 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 + :class: wide-table + + * - 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 + :class: wide-table + + * - 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 + :class: wide-table + + * - 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 + :class: wide-table + + * - 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``, ``fstat`` + - ``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 + :class: wide-table + + * - 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/docs/stdlib-equivalents.rst b/docs/stdlib-equivalents.rst new file mode 100644 index 0000000000..ab7bccd4e8 --- /dev/null +++ b/docs/stdlib-equivalents.rst @@ -0,0 +1,286 @@ +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). + +.. seealso:: + - :doc:`shell-equivalents` + - :doc:`alternatives` + +System-wide functions +--------------------- + +CPU +~~~ + +.. list-table:: + :class: longtable wide-table + :header-rows: 1 + :widths: 30 60 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 wide-table + :header-rows: 1 + :widths: 45 45 100 + + * - psutil + - stdlib + - notes + * - :func:`disk_usage` + - :func:`shutil.disk_usage` + - Same as :func:`shutil.disk_usage`; psutil also adds + :field:`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 wide-table + :header-rows: 1 + :widths: 45 45 100 + + * - psutil + - stdlib + - notes + * - :func:`net_if_addrs` + - :func:`socket.if_nameindex` + - Stdlib returns :term:`NIC` names only; psutil also returns + addresses, netmasks, broadcast, and PTP. + +Process +~~~~~~~ + +.. list-table:: + :class: longtable wide-table + :header-rows: 1 + :widths: 45 45 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 wide-table + :header-rows: 1 + :widths: 45 45 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 wide-table + :header-rows: 1 + :widths: 30 65 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 wide-table + :header-rows: 1 + :widths: 60 60 100 + + * - psutil + - stdlib + - notes + * - :meth:`Process.cpu_times` + - :func:`os.times` + - :func:`os.times` also has ``elapsed``; psutil adds + :field:`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 wide-table + :header-rows: 1 + :widths: 45 45 100 + + * - psutil + - stdlib + - notes + * - :meth:`Process.memory_info` + - :func:`resource.getrusage` + - Only :term:`peak_rss` (``ru_maxrss``); psutil returns :term:`RSS`, :term:`VMS`, and more. + * - :meth:`Process.page_faults` + - :func:`resource.getrusage` + - Current process only. + +I/O +~~~ + +.. list-table:: + :class: longtable wide-table + :header-rows: 1 + :widths: 45 45 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 wide-table + :header-rows: 1 + :widths: 50 55 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 wide-table + :header-rows: 1 + :widths: 45 55 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/docs/timeline.rst b/docs/timeline.rst new file mode 100644 index 0000000000..35c0eb0296 --- /dev/null +++ b/docs/timeline.rst @@ -0,0 +1,640 @@ +Timeline +======== + +2026 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `7.2.2 `_ + - 2026-01-28 + - :ref:`what's new <722>` + - `diff `__ + +2025 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `7.2.1 `_ + - 2025-12-29 + - :ref:`what's new <721>` + - `diff `__ + * - `7.2.0 `_ + - 2025-12-23 + - :ref:`what's new <720>` + - `diff `__ + * - `7.1.3 `_ + - 2025-11-02 + - :ref:`what's new <713>` + - `diff `__ + * - `7.1.2 `_ + - 2025-10-25 + - :ref:`what's new <712>` + - `diff `__ + * - `7.1.1 `_ + - 2025-10-19 + - :ref:`what's new <711>` + - `diff `__ + * - `7.1.0 `_ + - 2025-09-17 + - :ref:`what's new <710>` + - `diff `__ + * - `7.0.0 `_ + - 2025-02-13 + - :ref:`what's new <700>` + - `diff `__ + +2024 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `6.1.1 `_ + - 2024-12-19 + - :ref:`what's new <611>` + - `diff `__ + * - `6.1.0 `_ + - 2024-10-17 + - :ref:`what's new <610>` + - `diff `__ + * - `6.0.0 `_ + - 2024-06-18 + - :ref:`what's new <600>` + - `diff `__ + * - `5.9.8 `_ + - 2024-01-19 + - :ref:`what's new <598>` + - `diff `__ + +2023 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `5.9.7 `_ + - 2023-12-17 + - :ref:`what's new <597>` + - `diff `__ + * - `5.9.6 `_ + - 2023-10-15 + - :ref:`what's new <596>` + - `diff `__ + * - `5.9.5 `_ + - 2023-04-17 + - :ref:`what's new <595>` + - `diff `__ + +2022 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `5.9.4 `_ + - 2022-11-07 + - :ref:`what's new <594>` + - `diff `__ + * - `5.9.3 `_ + - 2022-10-18 + - :ref:`what's new <593>` + - `diff `__ + * - `5.9.2 `_ + - 2022-09-04 + - :ref:`what's new <592>` + - `diff `__ + * - `5.9.1 `_ + - 2022-05-20 + - :ref:`what's new <591>` + - `diff `__ + +2021 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `5.9.0 `_ + - 2021-12-29 + - :ref:`what's new <590>` + - `diff `__ + +2020 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `5.8.0 `_ + - 2020-12-19 + - :ref:`what's new <580>` + - `diff `__ + * - `5.7.3 `_ + - 2020-10-24 + - :ref:`what's new <573>` + - `diff `__ + * - `5.7.2 `_ + - 2020-07-15 + - :ref:`what's new <572>` + - `diff `__ + * - `5.7.1 `_ + - 2020-07-15 + - :ref:`what's new <571>` + - `diff `__ + * - `5.7.0 `_ + - 2020-02-18 + - :ref:`what's new <570>` + - `diff `__ + +2019 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `5.6.7 `_ + - 2019-11-26 + - :ref:`what's new <567>` + - `diff `__ + * - `5.6.6 `_ + - 2019-11-25 + - :ref:`what's new <566>` + - `diff `__ + * - `5.6.5 `_ + - 2019-11-06 + - :ref:`what's new <565>` + - `diff `__ + * - `5.6.4 `_ + - 2019-11-04 + - :ref:`what's new <564>` + - `diff `__ + * - `5.6.3 `_ + - 2019-06-11 + - :ref:`what's new <563>` + - `diff `__ + * - `5.6.2 `_ + - 2019-04-26 + - :ref:`what's new <562>` + - `diff `__ + * - `5.6.1 `_ + - 2019-03-11 + - :ref:`what's new <561>` + - `diff `__ + * - `5.6.0 `_ + - 2019-03-05 + - :ref:`what's new <560>` + - `diff `__ + * - `5.5.1 `_ + - 2019-02-15 + - :ref:`what's new <551>` + - `diff `__ + * - `5.5.0 `_ + - 2019-01-23 + - :ref:`what's new <550>` + - `diff `__ + +2018 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `5.4.8 `_ + - 2018-10-30 + - :ref:`what's new <548>` + - `diff `__ + * - `5.4.7 `_ + - 2018-08-14 + - :ref:`what's new <547>` + - `diff `__ + * - `5.4.6 `_ + - 2018-06-07 + - :ref:`what's new <546>` + - `diff `__ + * - `5.4.5 `_ + - 2018-04-13 + - :ref:`what's new <545>` + - `diff `__ + * - `5.4.4 `_ + - 2018-04-13 + - :ref:`what's new <544>` + - `diff `__ + * - `5.4.3 `_ + - 2018-01-01 + - :ref:`what's new <543>` + - `diff `__ + +2017 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `5.4.2 `_ + - 2017-12-07 + - :ref:`what's new <542>` + - `diff `__ + * - `5.4.1 `_ + - 2017-11-08 + - :ref:`what's new <541>` + - `diff `__ + * - `5.4.0 `_ + - 2017-10-12 + - :ref:`what's new <540>` + - `diff `__ + * - `5.3.1 `_ + - 2017-09-10 + - :ref:`what's new <531>` + - `diff `__ + * - `5.3.0 `_ + - 2017-09-01 + - :ref:`what's new <530>` + - `diff `__ + * - `5.2.2 `_ + - 2017-04-10 + - :ref:`what's new <522>` + - `diff `__ + * - `5.2.1 `_ + - 2017-03-24 + - :ref:`what's new <521>` + - `diff `__ + * - `5.2.0 `_ + - 2017-03-05 + - :ref:`what's new <520>` + - `diff `__ + * - `5.1.3 `_ + - 2017-02-07 + - :ref:`what's new <513>` + - `diff `__ + * - `5.1.2 `_ + - 2017-02-03 + - :ref:`what's new <512>` + - `diff `__ + * - `5.1.1 `_ + - 2017-02-03 + - :ref:`what's new <511>` + - `diff `__ + * - `5.1.0 `_ + - 2017-02-01 + - :ref:`what's new <510>` + - `diff `__ + +2016 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `5.0.1 `_ + - 2016-12-21 + - :ref:`what's new <501>` + - `diff `__ + * - `5.0.0 `_ + - 2016-11-06 + - :ref:`what's new <500>` + - `diff `__ + * - `4.4.2 `_ + - 2016-10-25 + - :ref:`what's new <442>` + - `diff `__ + * - `4.4.1 `_ + - 2016-10-23 + - :ref:`what's new <441>` + - `diff `__ + * - `4.4.0 `_ + - 2016-10-05 + - :ref:`what's new <440>` + - `diff `__ + * - `4.3.1 `_ + - 2016-09-01 + - :ref:`what's new <431>` + - `diff `__ + * - `4.3.0 `_ + - 2016-06-18 + - :ref:`what's new <430>` + - `diff `__ + * - `4.2.0 `_ + - 2016-05-14 + - :ref:`what's new <420>` + - `diff `__ + * - `4.1.0 `_ + - 2016-03-12 + - :ref:`what's new <410>` + - `diff `__ + * - `4.0.0 `_ + - 2016-02-17 + - :ref:`what's new <400>` + - `diff `__ + * - `3.4.2 `_ + - 2016-01-20 + - :ref:`what's new <342>` + - `diff `__ + * - `3.4.1 `_ + - 2016-01-15 + - :ref:`what's new <341>` + - `diff `__ + +2015 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `3.3.0 `_ + - 2015-11-25 + - :ref:`what's new <330>` + - `diff `__ + * - `3.2.2 `_ + - 2015-10-04 + - :ref:`what's new <322>` + - `diff `__ + * - `3.2.1 `_ + - 2015-09-03 + - :ref:`what's new <321>` + - `diff `__ + * - `3.2.0 `_ + - 2015-09-02 + - :ref:`what's new <320>` + - `diff `__ + * - `3.1.1 `_ + - 2015-07-15 + - :ref:`what's new <311>` + - `diff `__ + * - `3.1.0 `_ + - 2015-07-15 + - :ref:`what's new <310>` + - `diff `__ + * - `3.0.1 `_ + - 2015-06-18 + - :ref:`what's new <301>` + - `diff `__ + * - `3.0.0 `_ + - 2015-06-13 + - :ref:`what's new <300>` + - `diff `__ + * - `2.2.1 `_ + - 2015-02-02 + - :ref:`what's new <221>` + - `diff `__ + * - `2.2.0 `_ + - 2015-01-06 + - :ref:`what's new <220>` + - `diff `__ + +2014 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `2.1.3 `_ + - 2014-09-26 + - :ref:`what's new <213>` + - `diff `__ + * - `2.1.2 `_ + - 2014-09-21 + - :ref:`what's new <212>` + - `diff `__ + * - `2.1.1 `_ + - 2014-04-30 + - :ref:`what's new <211>` + - `diff `__ + * - `2.1.0 `_ + - 2014-04-08 + - :ref:`what's new <210>` + - `diff `__ + * - `2.0.0 `_ + - 2014-03-10 + - :ref:`what's new <200>` + - `diff `__ + +2013 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `1.2.1 `_ + - 2013-11-25 + - :ref:`what's new <121>` + - `diff `__ + * - `1.2.0 `_ + - 2013-11-20 + - :ref:`what's new <120>` + - `diff `__ + * - `1.1.2 `_ + - 2013-10-22 + - :ref:`what's new <112>` + - `diff `__ + * - `1.1.1 `_ + - 2013-10-08 + - :ref:`what's new <111>` + - `diff `__ + * - `1.1.0 `_ + - 2013-09-28 + - :ref:`what's new <110>` + - `diff `__ + * - `1.0.1 `_ + - 2013-07-12 + - :ref:`what's new <101>` + - `diff `__ + * - `1.0.0 `_ + - 2013-07-10 + - :ref:`what's new <100>` + - `diff `__ + * - `0.7.1 `_ + - 2013-05-03 + - :ref:`what's new <071>` + - `diff `__ + * - `0.7.0 `_ + - 2013-04-12 + - :ref:`what's new <070>` + - `diff `__ + +2012 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `0.6.1 `_ + - 2012-08-16 + - :ref:`what's new <061>` + - `diff `__ + * - `0.6.0 `_ + - 2012-08-13 + - :ref:`what's new <060>` + - `diff `__ + * - `0.5.1 `_ + - 2012-06-29 + - :ref:`what's new <051>` + - `diff `__ + * - `0.5.0 `_ + - 2012-06-27 + - :ref:`what's new <050>` + - `diff `__ + +2011 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `0.4.1 `_ + - 2011-12-14 + - :ref:`what's new <041>` + - `diff `__ + * - `0.4.0 `_ + - 2011-10-29 + - :ref:`what's new <040>` + - `diff `__ + * - `0.3.0 `_ + - 2011-07-08 + - :ref:`what's new <030>` + - `diff `__ + * - `0.2.1 `_ + - 2011-03-20 + - :ref:`what's new <021>` + - `diff `__ + +2010 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `0.2.0 `_ + - 2010-11-13 + - :ref:`what's new <020>` + - `diff `__ + * - `0.1.3 `_ + - 2010-03-02 + - :ref:`what's new <013>` + - `diff `__ + +2009 +~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 50 40 20 + :class: modern-timeline-table + + * - Version + - Date + - Notes + - Diff + * - `0.1.2 `_ + - 2009-05-06 + - :ref:`what's new <012>` + - `diff `__ + * - `0.1.1 `_ + - 2009-03-06 + - :ref:`what's new <011>` + - `diff `__ diff --git a/make.bat b/make.bat deleted file mode 100644 index c7c319026f..0000000000 --- a/make.bat +++ /dev/null @@ -1,32 +0,0 @@ -@echo off - -rem ========================================================================== -rem Shortcuts for various tasks, emulating UNIX "make" on Windows. -rem It is primarly intended as a shortcut for compiling / installing -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 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 ========================================================================== - -if "%PYTHON%" == "" ( - set PYTHON=C:\Python27\python.exe -) -if "%TSCRIPT%" == "" ( - set TSCRIPT=psutil\tests\__main__.py -) - -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/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 1b0f2c045b..40181f0fc7 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. @@ -10,86 +8,105 @@ - Linux - Windows - - OSX + - macOS - FreeBSD - OpenBSD - NetBSD - Sun Solaris - AIX -Works with Python versions from 2.6 to 3.X. +Supported Python versions are cPython 3.7+ and PyPy. """ -from __future__ import division +from __future__ import annotations import collections import contextlib import datetime -import errno import functools import os import signal +import socket import subprocess import sys +import threading import time -import traceback +import warnings +from typing import TYPE_CHECKING as _TYPE_CHECKING + try: import pwd except ImportError: pwd = None from . import _common -from ._common import deprecated_method -from ._common import memoize -from ._common import memoize_when_activated -from ._common import wrap_numbers as _wrap_numbers -from ._compat import long -from ._compat import PY3 as _PY3 - -from ._common import STATUS_DEAD -from ._common import STATUS_DISK_SLEEP -from ._common import STATUS_IDLE # bsd -from ._common import STATUS_LOCKED -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_WAKING -from ._common import STATUS_ZOMBIE - -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 NIC_DUPLEX_FULL -from ._common import NIC_DUPLEX_HALF -from ._common import NIC_DUPLEX_UNKNOWN - +from . import _ntuples as _ntp from ._common import AIX from ._common import BSD -from ._common import FREEBSD # NOQA +from ._common import FREEBSD from ._common import LINUX -from ._common import NETBSD # NOQA -from ._common import OPENBSD # NOQA -from ._common import OSX -from ._common import POSIX # NOQA +from ._common import MACOS +from ._common import NETBSD +from ._common import OPENBSD +from ._common import OSX # deprecated alias +from ._common import POSIX 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 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 _TYPE_CHECKING: + from collections.abc import Collection + 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 -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 @@ -97,70 +114,25 @@ PROCFS_PATH = "/proc" from . import _pslinux as _psplatform - - from ._pslinux import IOPRIO_CLASS_BE # NOQA - from ._pslinux import IOPRIO_CLASS_IDLE # NOQA - from ._pslinux import IOPRIO_CLASS_NONE # NOQA - from ._pslinux import IOPRIO_CLASS_RT # NOQA - # Linux >= 2.6.36 - if _psplatform.HAS_PRLIMIT: - from ._psutil_linux import RLIM_INFINITY # NOQA - from ._psutil_linux import RLIMIT_AS # NOQA - from ._psutil_linux import RLIMIT_CORE # NOQA - from ._psutil_linux import RLIMIT_CPU # NOQA - from ._psutil_linux import RLIMIT_DATA # NOQA - from ._psutil_linux import RLIMIT_FSIZE # NOQA - from ._psutil_linux import RLIMIT_LOCKS # NOQA - from ._psutil_linux import RLIMIT_MEMLOCK # NOQA - from ._psutil_linux import RLIMIT_NOFILE # NOQA - from ._psutil_linux import RLIMIT_NPROC # NOQA - from ._psutil_linux import RLIMIT_RSS # NOQA - from ._psutil_linux import RLIMIT_STACK # NOQA - # Kinda ugly but considerably faster than using hasattr() and - # setattr() against the module object (we are at import time: - # speed matters). - from . import _psutil_linux - try: - RLIMIT_MSGQUEUE = _psutil_linux.RLIMIT_MSGQUEUE - except AttributeError: - pass - try: - RLIMIT_NICE = _psutil_linux.RLIMIT_NICE - except AttributeError: - pass - try: - RLIMIT_RTPRIO = _psutil_linux.RLIMIT_RTPRIO - except AttributeError: - pass - try: - RLIMIT_RTTIME = _psutil_linux.RLIMIT_RTTIME - except AttributeError: - pass - try: - RLIMIT_SIGPENDING = _psutil_linux.RLIMIT_SIGPENDING - except AttributeError: - pass + from ._enums import ProcessIOPriority + from ._enums import ProcessRlimit 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 - -elif OSX: + from ._enums import ProcessIOPriority + from ._enums import ProcessPriority + +elif MACOS: from . import _psosx as _psplatform 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 - from ._pssunos import CONN_IDLE # NOQA # This is public writable API which is read from _pslinux.py and # _pssunos.py via sys.modules. @@ -174,9 +146,11 @@ 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 __all__ = [ # exceptions "Error", "NoSuchProcess", "ZombieProcess", "AccessDenied", @@ -185,22 +159,10 @@ # 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_LOCKED", - - "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", - "AF_LINK", - "NIC_DUPLEX_FULL", "NIC_DUPLEX_HALF", "NIC_DUPLEX_UNKNOWN", - - "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", @@ -209,23 +171,53 @@ "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 # "sensors_temperatures", "sensors_battery", "sensors_fans" # sensors "users", "boot_time", # others ] +# fmt: on + __all__.extend(_psplatform.__extra__all__) -__author__ = "Giampaolo Rodola'" -__version__ = "5.4.4" -version_info = tuple([int(num) for num in __version__.split('.')]) +_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") + +del _globals, _export_enum + 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) +__author__ = "Giampaolo Rodola'" +__version__ = "8.0.0" +version_info = tuple(int(num) for num in __version__.split('.')) + +_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 @@ -233,18 +225,22 @@ # was compiled for a different version of psutil. # We want to prevent that by failing sooner rather than later. # See: https://github.com/giampaolo/psutil/issues/564 -if (int(__version__.replace('.', '')) != - getattr(_psplatform.cext, 'version', None)): - msg = "version conflict: %r C extension module was built for another " \ - "version of psutil" % getattr(_psplatform.cext, "__file__") +if int(__version__.replace('.', '')) != getattr( + _psplatform.cext, 'version', None +): + msg = 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(list(str(_psplatform.cext.version))) + msg += f" ({v} instead of {__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")) + msg += f" (different than {__version__})" + 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) @@ -257,104 +253,107 @@ 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(). + """Return a `{pid: ppid, ...}` dict for all running processes in + one shot. Used to speed up `Process.children()`. """ 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 -def _assert_pid_not_reused(fun): - """Decorator which raises NoSuchProcess in case a process is no - longer running or its PID has been reused. - """ - @functools.wraps(fun) - def wrapper(self, *args, **kwargs): - if not self.is_running(): - raise NoSuchProcess(self.pid, self._name) - return fun(self, *args, **kwargs) - return wrapper - - def _pprint_secs(secs): """Format seconds in a human readable form.""" now = time.time() secs_ago = int(now - secs) - if secs_ago < 60 * 60 * 24: - fmt = "%H:%M:%S" - else: - fmt = "%Y-%m-%d %H:%M:%S" + fmt = "%H:%M:%S" if secs_ago < 60 * 60 * 24 else "%Y-%m-%d %H:%M:%S" return datetime.datetime.fromtimestamp(secs).strftime(fmt) +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 # ===================================================================== -class Process(object): - """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 - 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. - - 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 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 +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 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. """ - def __init__(self, pid=None): + attrs: frozenset[str] = frozenset() # dynamically set later + + def __init__(self, pid: int | None = None) -> None: self._init(pid) def _init(self, pid, _ignore_nsp=False): if pid is None: pid = os.getpid() else: - if not _PY3 and not isinstance(pid, (int, long)): - raise TypeError('pid must be an integer (got %r)' % pid) if pid < 0: - raise ValueError('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 as err: + msg = "process PID out of range" + raise NoSuchProcess(pid, msg=msg) from err + self._pid = pid self._name = None self._exe = None self._create_time = None self._gone = False + self._pid_reused = False self._hash = None - self._oneshot_inctx = False + self._lock = threading.RLock() # used for caching on Windows only (on POSIX ppid may change) self._ppid = None # platform-specific modules define an _psplatform.Process @@ -362,13 +361,15 @@ def _init(self, pid, _ignore_nsp=False): self._proc = _psplatform.Process(pid) self._last_sys_cpu_times = None self._last_proc_cpu_times = None - # cache creation time for later use in is_running() method + self._exitcode = _SENTINEL + self._prefetch = {} + 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 @@ -376,36 +377,74 @@ 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) - else: - self._gone = True - # This pair is supposed to indentify 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) + msg = "process PID not found" + raise NoSuchProcess(pid, msg=msg) from None + 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 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. + + 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 + # 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) + 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. + return (self.pid, self._proc.create_time(monotonic=True)) + else: + return (self.pid, self.create_time()) def __str__(self): - try: - info = collections.OrderedDict() - except AttributeError: - info = {} # Python 2.6 + info = {} info["pid"] = self.pid - try: - info["name"] = self.name() - if self._create_time: + with self.oneshot(): + if self._pid_reused: + info["status"] = "terminated + PID reused" + else: + try: + info["name"] = self._name or self.name() + info["status"] = str(self.status()) + except ZombieProcess: + info["status"] = "zombie" + except NoSuchProcess: + info["status"] = "terminated" + except AccessDenied: + pass + + if self._exitcode not in {_SENTINEL, None}: + info["exitcode"] = self._exitcode + if self._create_time is not None: info['started'] = _pprint_secs(self._create_time) - except ZombieProcess: - info["status"] = "zombie" - except NoSuchProcess: - info["status"] = "terminated" - except AccessDenied: - pass - return "%s.%s(%s)" % ( - self.__class__.__module__, - self.__class__.__name__, - ", ".join(["%s=%r" % (k, v) for k, v in info.items()])) + + return "{}.{}({})".format( + self.__class__.__module__, + self.__class__.__name__, + ", ".join([f"{k}={v!r}" for k, v in info.items()]), + ) __repr__ = __str__ @@ -414,6 +453,20 @@ def __eq__(self, other): # on PID and creation time. if not isinstance(other, Process): return NotImplemented + 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: + if ident1 and not ident2: + try: + return self.status() == ProcessStatus.STATUS_ZOMBIE + except Error: + pass return self._ident == other._ident def __ne__(self, other): @@ -424,29 +477,52 @@ def __hash__(self): self._hash = hash(self._ident) return self._hash + def _raise_if_pid_reused(self): + """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 + # the actual Process API call. This way unit tests will tell + # us if the API is broken (aka don't raise NSP when it + # should). We also remain consistent with all other "get" + # APIs which don't use _raise_if_pid_reused(). + msg = "process no longer exists and its PID has been reused" + raise NoSuchProcess(self.pid, self._name, msg=msg) + @property - def pid(self): + 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 - def oneshot(self): - """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. + 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. >>> import psutil >>> p = psutil.Process() @@ -458,67 +534,83 @@ def oneshot(self): ... >>> """ - if self._oneshot_inctx: - # NOOP: this covers the use case where the user enters the - # context twice. Since as_dict() internally uses oneshot() - # I expect that the code below will be a pretty common - # "mistake" that the user will make, so let's guard - # against that: - # - # >>> with p.oneshot(): - # ... p.as_dict() - # ... - yield - else: - self._oneshot_inctx = True - try: - # cached in case cpu_percent() is used - self.cpu_times.cache_activate() - # cached in case memory_percent() is used - self.memory_info.cache_activate() - # cached in case parent() is used - self.ppid.cache_activate() - # cached in case username() is used - if POSIX: - self.uids.cache_activate() - # specific implementation cache - self._proc.oneshot_enter() + with self._lock: + if hasattr(self, "_cache"): + # NOOP: this covers the use case where the user enters the + # context twice: + # + # >>> with p.oneshot(): + # ... with p.oneshot(): + # ... + # + # Also, since as_dict() internally uses oneshot() + # I expect that the code below will be a pretty common + # "mistake" that the user will make, so let's guard + # against that: + # + # >>> with p.oneshot(): + # ... p.as_dict() + # ... yield - finally: - self.cpu_times.cache_deactivate() - self.memory_info.cache_deactivate() - self.ppid.cache_deactivate() - if POSIX: - self.uids.cache_deactivate() - self._proc.oneshot_exit() - self._oneshot_inctx = False - - def as_dict(self, attrs=None, ad_value=None): + 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: 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 + `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)): - raise TypeError("invalid attrs type %s" % type(attrs)) + msg = f"invalid attrs type {type(attrs)}" + raise TypeError(msg) attrs = set(attrs) - invalid_names = attrs - valid_names + invalid_names = attrs - valid_names - deprecated_names if invalid_names: - raise ValueError("invalid attr name%s %s" % ( + msg = "invalid attr name{} {}".format( "s" if len(invalid_names) > 1 else "", - ", ".join(map(repr, invalid_names)))) + ", ".join(map(repr, invalid_names)), + ) + raise ValueError(msg) - retdict = dict() - ls = attrs or valid_names + retdict = {} + names = attrs or sorted(valid_names) with self.oneshot(): - for name in ls: + for name in names: try: if name == 'pid': ret = self.pid @@ -537,36 +629,61 @@ def as_dict(self, attrs=None, ad_value=None): retdict[name] = ret return retdict - def parent(self): - """Return the parent process as a Process object pre-emptively + def parent(self) -> Process | None: + """Return the parent process as a `Process` object, preemptively 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() + # 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: pass - def is_running(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. + """ + parents = [] + proc = self.parent() + while proc is not None: + parents.append(proc) + proc = proc.parent() + return parents + + def is_running(self) -> bool: """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: + 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__. - return self == Process(self.pid) + # 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) + if self._pid_reused: + _pids_reused.add(self.pid) + 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. @@ -577,8 +694,9 @@ def is_running(self): # --- actual API + @_use_prefetch @memoize_when_activated - def ppid(self): + def ppid(self) -> int: """The process parent PID. On Windows the return value is cached after first call. """ @@ -589,13 +707,15 @@ 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 self._ppid = self._ppid or self._proc.ppid() return self._ppid - def name(self): + @_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: @@ -610,7 +730,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: @@ -621,11 +746,14 @@ def name(self): self._proc._name = name return name - def exe(self): + @_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): # try to guess exe from cmdline[0] in absence of a native # exe representation @@ -635,9 +763,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 @@ -660,26 +790,30 @@ def guess_it(fallback): self._exe = exe return self._exe - def cmdline(self): + @_use_prefetch + def cmdline(self) -> list[str]: """The command line this process has been called with.""" return self._proc.cmdline() - def status(self): - """The process current status as a STATUS_* constant.""" + @_use_prefetch + def status(self) -> ProcessStatus | str: + """The process current status as a `STATUS_` constant.""" try: return self._proc.status() except ZombieProcess: - return STATUS_ZOMBIE + return ProcessStatus.STATUS_ZOMBIE - def username(self): + @_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: # 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 @@ -689,124 +823,141 @@ def username(self): else: return self._proc.username() - def create_time(self): + @_use_prefetch + def create_time(self) -> float: """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. + 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() return self._create_time - def cwd(self): + @_use_prefetch + def cwd(self) -> str: """Process current working directory as an absolute path.""" return self._proc.cwd() - def nice(self, value=None): + @_use_prefetch + def nice(self, value: int | None = None) -> int | None: """Get or set process niceness (priority).""" 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: + @_use_prefetch @memoize_when_activated - def uids(self): - """Return process UIDs as a (real, effective, saved) - namedtuple. + def uids(self) -> puids: + """Return process UIDs as a `(real, effective, saved)` + named tuple. """ return self._proc.uids() - def gids(self): - """Return process GIDs as a (real, effective, saved) - namedtuple. + @_use_prefetch + def gids(self) -> pgids: + """Return process GIDs as a `(real, effective, saved)` + named tuple. """ return self._proc.gids() - def terminal(self): + @_use_prefetch + def terminal(self) -> str | None: """The terminal associated with this process, if any, else None. """ return self._proc.terminal() - def num_fds(self): + @_use_prefetch + def num_fds(self) -> int: """Return the number of file descriptors opened by this process (POSIX only). """ return self._proc.num_fds() - # Linux, BSD, AIX and Windows only if hasattr(_psplatform.Process, "io_counters"): - def io_counters(self): - """Return process I/O statistics as a - (read_count, write_count, read_bytes, write_bytes) - namedtuple. - Those are the number of read/write calls performed and the - amount of bytes read and written by the process. + @_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 >= Vista only if hasattr(_psplatform.Process, "ionice_get"): - def ionice(self, ioclass=None, value=None): + @_use_prefetch + 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. + 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. - Available on Linux and Windows > Vista only. + Availability: Linux, Windows """ if ioclass is None: if value is not None: - raise ValueError("'ioclass' argument must be specified") + msg = "'ioclass' argument must be specified" + raise ValueError(msg) return self._proc.ionice_get() else: + self._raise_if_pid_reused() return self._proc.ionice_set(ioclass, value) - # Linux only if hasattr(_psplatform.Process, "rlimit"): - def rlimit(self, resource, limits=None): - """Get or set process resource limits as a (soft, hard) + 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. - *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 only. + + Availability: Linux, FreeBSD """ - if limits is None: - return self._proc.rlimit(resource) - else: - return self._proc.rlimit(resource, limits) + if limits is not None: + self._raise_if_pid_reused() + return self._proc.rlimit(resource, limits) - # Windows, Linux and FreeBSD only if hasattr(_psplatform.Process, "cpu_affinity_get"): - def cpu_affinity(self, cpus=None): + @_use_prefetch + 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 """ - # Automatically remove duplicates both on get and - # set (for get it's not really necessary, it's - # just for extra safety). if cpus is None: - return list(set(self._proc.cpu_affinity_get())) + return sorted(set(self._proc.cpu_affinity_get())) else: + self._raise_if_pid_reused() if not cpus: if hasattr(self._proc, "_get_eligible_cpus"): cpus = self._proc._get_eligible_cpus() @@ -817,56 +968,63 @@ def cpu_affinity(self, cpus=None): # Linux, FreeBSD, SunOS if hasattr(_psplatform.Process, "cpu_num"): - def cpu_num(self): + @_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 - 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() - # Linux, OSX and Windows only + # All platforms has it, but maybe not in the future. if hasattr(_psplatform.Process, "environ"): - def environ(self): - """The environment variables of the process as a dict. Note: this - might not reflect changes made after the process started. """ + @_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. + """ return self._proc.environ() if WINDOWS: - def num_handles(self): + @_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() - def num_ctx_switches(self): + @_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() - def num_threads(self): + @_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"): - def threads(self): + @_use_prefetch + 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). + `(id, user_time, system_time)` named tuples. + On OpenBSD this method requires root access. """ return self._proc.threads() - @_assert_pid_not_reused - 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. + instances, preemptively checking whether PID has been reused. + If *recursive* is True return all the parent descendants. Example (A == this process): @@ -890,7 +1048,12 @@ 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() + # 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(): @@ -899,7 +1062,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 @@ -925,7 +1088,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) @@ -933,14 +1096,15 @@ def children(self, recursive=False): pass return ret - def cpu_percent(self, interval=None): + @_use_prefetch + def cpu_percent(self, interval: float | None = None) -> float: """Return a float representing the current process CPU utilization as a percentage. 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). @@ -970,7 +1134,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 = f"interval is not positive (got {interval!r})" + raise ValueError(msg) num_cpus = cpu_count() or 1 def timer(): @@ -1002,7 +1167,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 @@ -1025,51 +1190,88 @@ 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): - """Return a (user, system, children_user, children_system) - 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 - always set to 0. + def cpu_times(self) -> pcputimes: + """Return a `(user, system, children_user, children_system)` + 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() + @_use_prefetch @memoize_when_activated - def memory_info(self): - """Return a namedtuple with variable fields depending on the + 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 plaforms are `rss` and `vms`. + The portable fields available on all platforms are `rss` and `vms`. All numbers are expressed in bytes. """ return self._proc.memory_info() - @deprecated_method(replacement="memory_info") - def memory_info_ex(self): - return self.memory_info() + @_use_prefetch + @memoize_when_activated + def memory_info_ex(self) -> pmem_ex: + """Return a named tuple extending `memory_info()` with extra + metrics. - def memory_full_info(self): - """This method returns the same information as memory_info(), - plus, on some platform (Linux, OSX, Windows), also provides - additional metrics (USS, PSS and swap). - The additional metrics provide a better representation of actual - process memory usage. + 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"): + + @_use_prefetch + def memory_footprint(self) -> pfootprint: + """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 + 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() - Namely USS is the memory which is unique to a process and which - would be freed if the process was terminated right now. + # DEPRECATED + def memory_full_info(self) -> pfullmem: + """Return the same information as `memory_info()` plus + `memory_footprint()` in a single named tuple. - 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. + DEPRECATED in 8.0.0. Use `memory_footprint()` instead. """ - return self._proc.memory_full_info() - - def memory_percent(self, memtype="rss"): + 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: 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: @@ -1077,12 +1279,29 @@ 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.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: - raise ValueError("invalid memtype %r; valid types are %r" % ( - memtype, tuple(valid_types))) - fun = self.memory_info if memtype in _psplatform.pmem._fields else \ - self.memory_full_info + msg = ( + f"invalid memtype {memtype!r}; valid types are" + f" {tuple(valid_types)!r}" + ) + raise ValueError(msg) + 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) @@ -1090,25 +1309,30 @@ 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" + f" system memory is not positive ({total_phymem!r})" + ) + raise ValueError(msg) return (value / float(total_phymem)) * 100 if hasattr(_psplatform.Process, "memory_maps"): - # Available everywhere except OpenBSD and NetBSD. - def memory_maps(self, grouped=True): - """Return process' mapped memory regions as a list of namedtuples - whose fields are variable depending on the platform. + + @_use_prefetch + 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. 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() if grouped: d = {} @@ -1116,151 +1340,216 @@ 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 - return [nt(path, *d[path]) for path in d] # NOQA + 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] + + @_use_prefetch + def page_faults(self) -> ppagefaults: + """Return the number of page faults for this process as a + `(minor, major)` named tuple. - def open_files(self): - """Return files opened by process as a list of - (path, fd) namedtuples including the absolute file name - and file descriptor number. + - `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() + + @_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. + + On Linux the named tuple also includes `position`, `mode` and + `flags` fields. """ return self._proc.open_files() - def connections(self, kind='inet'): + @_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) namedtuples. + `(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 | +------------+----------------------------------------------------+ """ - return self._proc.connections(kind) + _check_conn_kind(kind) + return self._proc.net_connections(kind) + + @_common.deprecated_method(replacement="net_connections") + def connections(self, kind="inet") -> list[pconn]: + return self.net_connections(kind=kind) # --- signals if POSIX: + def _send_signal(self, sig): assert not self.pid < 0, self.pid - if self.pid == 0: + self._raise_if_pid_reused() + + pid, ppid, name = self.pid, self._ppid, self._name + if pid == 0: # see "man 2 kill" - raise ValueError( + msg = ( "preventing sending signal to process with PID 0 as it " "would affect every process in the process group of the " - "calling process (os.getpid()) instead of PID 0") + "calling process (os.getpid()) instead of PID 0" + ) + raise ValueError(msg) try: - os.kill(self.pid, sig) - except OSError as err: - if err.errno == errno.ESRCH: - if OPENBSD and pid_exists(self.pid): - # We do this because os.kill() lies in case of - # zombie processes. - raise ZombieProcess(self.pid, self._name, self._ppid) - else: - self._gone = True - raise NoSuchProcess(self.pid, self._name) - if err.errno in (errno.EPERM, errno.EACCES): - raise AccessDenied(self.pid, self._name) - raise - - @_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) . - On Windows only SIGTERM is valid and is treated as an alias - for kill(). + 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(pid, name, ppid) from err + self._gone = True + raise NoSuchProcess(pid, name) from err + except PermissionError as err: + raise AccessDenied(pid, name) from err + + def send_signal(self, sig: int) -> None: + """Send a signal *sig* to process, preemptively 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) 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") - - @_assert_pid_not_reused - def suspend(self): - """Suspend process execution with SIGSTOP pre-emptively checking + 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) -> None: + """Suspend process execution with SIGSTOP preemptively checking whether PID has been reused. - On Windows this has the effect ot suspending all process threads. + + On Windows this has the effect of suspending all process threads. """ if POSIX: self._send_signal(signal.SIGSTOP) else: # pragma: no cover + self._raise_if_pid_reused() self._proc.suspend() - @_assert_pid_not_reused - def resume(self): - """Resume process execution with SIGCONT pre-emptively checking + def resume(self) -> None: + """Resume process execution with SIGCONT preemptively checking whether PID has been reused. + On Windows this has the effect of resuming all process threads. """ 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 + def terminate(self) -> None: + """Terminate the process with SIGTERM preemptively 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) 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 + def kill(self) -> None: + """Kill the current process with SIGKILL preemptively checking whether PID has been reused. """ if POSIX: self._send_signal(signal.SIGKILL) else: # pragma: no cover + self._raise_if_pid_reused() self._proc.kill() - def wait(self, timeout=None): - """Wait for process to terminate and, if process is a children + def wait(self, timeout: float | None = None) -> int | None: + """Wait for process to terminate, and if process is a child of os.getpid(), also return its exit code, else None. - If the process is already terminated immediately return None - instead of raising NoSuchProcess. + 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`. 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: - raise ValueError("timeout must be a positive integer") - return self._proc.wait(timeout) + 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 + + 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 + + +# The valid attr names which can be processed by Process.as_dict(attrs=...) +# and process_iter(attrs=...). +# fmt: off +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', 'attrs'} +) +# fmt: on # ===================================================================== @@ -1269,38 +1558,34 @@ 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 + `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) >>> p.name() - 'python' + 'python3' >>> p.uids() user(real=1000, effective=1000, saved=1000) >>> p.username() 'giampaolo' >>> p.communicate() - ('hi\n', None) + ('hi', None) >>> p.terminate() >>> p.wait(timeout=2) 0 >>> - - For method names common to both classes such as kill(), terminate() - and wait(), psutil.Process implementation takes precedence. - - Unlike subprocess.Popen this class pre-emptively checks whether PID - has been reused on send_signal(), terminate() and kill() so that - you don't accidentally terminate another process, fixing - http://bugs.python.org/issue6973. - - For a complete documentation refer to: - http://docs.python.org/library/subprocess.html """ def __init__(self, *args, **kwargs): @@ -1313,7 +1598,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 @@ -1341,39 +1626,35 @@ 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 = 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(Popen, self).wait(timeout) + ret = super().wait(timeout) self.__subproc.returncode = ret return ret -# The valid attr names which can be processed by Process.as_dict(). -_as_dict_attrnames = set( - [x for x in dir(Process) if not x.startswith('_') and x not in - ['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', - 'is_running', 'as_dict', 'parent', 'children', 'rlimit', - 'memory_info_ex', 'oneshot']]) - - # ===================================================================== # --- system processes related functions # ===================================================================== -def pids(): +def pids() -> list[int]: """Return a list of current running PIDs.""" - return _psplatform.pids() + global _LOWEST_PID + ret = sorted(_psplatform.pids()) + _LOWEST_PID = ret[0] + return ret -def pid_exists(pid): - """Return True if given PID exists in the current process list. - This is faster than doing "pid in psutil.pids()" and - should be preferred. +def pid_exists(pid: int) -> bool: + """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 @@ -1389,94 +1670,114 @@ def pid_exists(pid): _pmap = {} +_pids_reused = set() -def process_iter(attrs=None, ad_value=None): - """Return a generator yielding a Process instance for all +def process_iter( + attrs: Collection[str] | None = None, ad_value: Any = None +) -> Iterator[Process]: + """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. - - 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.cache_clear()`. The sorting order in which processes are yielded is based on 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. - If *attrs* is an empty list it will retrieve all process info - (slow). + `Process.as_dict()`. + + 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 + def add(pid): proc = Process(pid) - if attrs is not None: - proc.info = proc.as_dict(attrs=attrs, ad_value=ad_value) - _pmap[proc.pid] = proc + pmap[proc.pid] = proc return proc def remove(pid): - _pmap.pop(pid, None) - + pmap.pop(pid, None) + + 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.keys()) + b = set(pmap) new_pids = a - b gone_pids = b - a - for pid in gone_pids: remove(pid) - for pid, proc in sorted(list(_pmap.items()) + - list(dict.fromkeys(new_pids).items())): - try: - if proc is None: # new process - yield add(pid) - else: - # use is_running() to check whether PID has been reused by - # another process in which case yield a new Process instance - if proc.is_running(): - if attrs is not None: - proc.info = proc.as_dict( - attrs=attrs, ad_value=ad_value) - yield proc - else: - 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 + while _pids_reused: + pid = _pids_reused.pop() + debug(f"refreshing Process instance for reused PID {pid}") + remove(pid) + try: + ls = sorted(list(pmap.items()) + list(dict.fromkeys(new_pids).items())) + for pid, proc in ls: + try: + if proc is None: # new process + proc = add(pid) + proc._prefetch = {} # clear cache + if attrs is not None: + proc._prefetch = proc.as_dict( + attrs=attrs, ad_value=ad_value + ) + yield proc + except NoSuchProcess: + remove(pid) + finally: + _pmap = pmap + +process_iter.cache_clear = lambda: _pmap.clear() # noqa: PLW0108 +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. - 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 + 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: @@ -1497,25 +1798,29 @@ def wait_procs(procs, timeout=None, callback=None): >>> for p in alive: ... p.kill() """ + def check_gone(proc, timeout): try: returncode = proc.wait(timeout=timeout) - except TimeoutExpired: + except (TimeoutExpired, subprocess.TimeoutExpired): 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: 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) + 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 callback is not None and not callable(callback): - raise TypeError("callback %r is not a callable" % callable) if timeout is not None: deadline = _timer() + timeout @@ -1537,14 +1842,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)) @@ -1554,9 +1859,9 @@ 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() 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). @@ -1571,33 +1876,34 @@ 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 -def cpu_times(percpu=False): - """Return system-wide CPU times as a namedtuple. +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 namedtuple's fields availability varies depending on the - platform: - - - user - - system - - idle - - nice (UNIX) - - iowait (Linux) - - irq (Linux, FreeBSD) - - softirq (Linux) - - steal (Linux >= 2.6.11) - - guest (Linux >= 2.6.24) - - guest_nice (Linux >= 3.2.0) - - When *percpu* is True return a list of namedtuples 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. + 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 + element to second CPU and so on. The order of the list is + consistent across calls. """ if not percpu: return _psplatform.cpu_times() @@ -1606,22 +1912,22 @@ def cpu_times(percpu=False): try: - _last_cpu_times = cpu_times() -except Exception: + _last_cpu_times = {threading.current_thread().ident: cpu_times()} +except Exception: # noqa: BLE001 # Don't want to crash at import time. - _last_cpu_times = None - traceback.print_exc() + _last_cpu_times = {} try: - _last_per_cpu_times = cpu_times(percpu=True) -except Exception: + _last_per_cpu_times = { + threading.current_thread().ident: cpu_times(percpu=True) + } +except Exception: # noqa: BLE001 # Don't want to crash at import time. - _last_per_cpu_times = None - traceback.print_exc() + _last_per_cpu_times = {} def _cpu_tot_time(times): - """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) @@ -1631,17 +1937,15 @@ 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 - tot -= getattr(times, "guest", 0) # Linux 2.6.24+ - tot -= getattr(times, "guest_nice", 0) # Linux 3.2.0+ + # https://github.com/torvalds/linux/blob/447976ef4/kernel/sched/cputime.c#L158 + tot -= times.guest + tot -= times.guest_nice return tot 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 @@ -1649,8 +1953,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 @@ -1658,7 +1961,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 @@ -1673,10 +1976,12 @@ 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): +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. @@ -1711,15 +2016,14 @@ def cpu_percent(interval=None, percpu=False): 2.9 >>> """ - global _last_cpu_times - global _last_per_cpu_times + tid = threading.current_thread().ident blocking = interval is not None and interval > 0.0 if interval is not None and interval < 0: - raise ValueError("interval is not positive (got %r)" % interval) + msg = f"interval is not positive (got {interval})" + raise ValueError(msg) def calculate(t1, t2): times_delta = _cpu_times_deltas(t1, t2) - all_delta = _cpu_tot_time(times_delta) busy_delta = _cpu_busy_time(times_delta) @@ -1736,14 +2040,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 = [] @@ -1751,28 +2050,25 @@ 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: 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()`. -def cpu_times_percent(interval=None, percpu=False): - """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() @@ -1781,13 +2077,13 @@ def cpu_times_percent(interval=None, percpu=False): >>> *interval* and *percpu* arguments have the same meaning as in - cpu_percent(). + `cpu_percent()`. """ - global _last_cpu_times_2 - global _last_per_cpu_times_2 + tid = threading.current_thread().ident blocking = interval is not None and interval > 0.0 if interval is not None and interval < 0: - raise ValueError("interval is not positive (got %r)" % interval) + msg = f"interval is not positive (got {interval!r})" + raise ValueError(msg) def calculate(t1, t2): nums = [] @@ -1804,7 +2100,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: @@ -1812,14 +2108,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 = [] @@ -1827,32 +2118,28 @@ 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 -def cpu_stats(): +def cpu_stats() -> scpustats: """Return CPU statistics.""" return _psplatform.cpu_stats() if hasattr(_psplatform, "cpu_freq"): - def cpu_freq(percpu=False): - """Return CPU frequency as a nameduple including current, + def cpu_freq(percpu: bool = False) -> scpufreq | list[scpufreq] | None: + """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 - 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: @@ -1865,29 +2152,57 @@ 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 - return _common.scpufreq(current, min_, max_) + + if set_none: + min_ = max_ = None + else: + min_ = mins / num_cpus + max_ = maxs / num_cpus + + return _ntp.scpufreq(current, min_, max_) __all__.append("cpu_freq") +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. + """ + if hasattr(os, "getloadavg"): + return os.getloadavg() + else: + return _psplatform.getloadavg() + + # ===================================================================== # --- system memory related functions # ===================================================================== -def virtual_memory(): - """Return statistics about system memory usage as a namedtuple - including the following fields, expressed in bytes: +def virtual_memory() -> svmem: + """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. + total physical memory available - available: the memory that can be given instantly to processes without the @@ -1897,42 +2212,20 @@ def virtual_memory(): 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: - OSX: active + inactive + 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; 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, OSX): - cache for various things. - - - wired (OSX, 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`. - - shared (BSD): - memory that may be simultaneously accessed by multiple processes. - - 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() @@ -1941,8 +2234,8 @@ def virtual_memory(): return ret -def swap_memory(): - """Return system swap memory statistics as a namedtuple including +def swap_memory() -> sswap: + """Return system swap memory statistics as a named tuple including the following fields: - total: total swap memory in bytes @@ -1952,28 +2245,29 @@ def swap_memory(): - 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() # ===================================================================== -# --- disks/paritions related functions +# --- disks/partitions related functions # ===================================================================== -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 + named tuple including total, used and free space expressed in bytes plus the percentage usage. """ 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 + (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. If *all* parameter is False return physical devices only and ignore @@ -1982,16 +2276,20 @@ def disk_partitions(all=False): return _psplatform.disk_partitions(all) -def disk_io_counters(perdisk=False, nowrap=True): - """Return system disk I/O statistics as a namedtuple including +def disk_io_counters( + perdisk: bool = False, nowrap: bool = True +) -> sdiskio | dict[str, sdiskio]: + """Return system 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 - - 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: @@ -2000,36 +2298,31 @@ def disk_io_counters(perdisk=False, nowrap=True): - 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 namedtuple - 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. """ - 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: 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( - _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" @@ -2038,8 +2331,10 @@ def disk_io_counters(perdisk=False, nowrap=True): # ===================================================================== -def net_io_counters(pernic=False, nowrap=True): - """Return network I/O statistics as a namedtuple including +def net_io_counters( + pernic: bool = False, nowrap: bool = True +) -> snetio | dict[str, snetio] | None: + """Return network I/O statistics as a named tuple including the following fields: - bytes_sent: number of bytes sent @@ -2050,19 +2345,17 @@ 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 - with network interface names as the keys and the namedtuple - 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. + 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: @@ -2071,105 +2364,121 @@ 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( - _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" -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 + (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 OSX this function requires root privileges. + On macOS this function requires root privileges. """ + _check_conn_kind(kind) return _psplatform.net_connections(kind) -def 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 namedtuples for each address - assigned to the NIC. Each namedtuple includes 5 fields: +def net_if_addrs() -> dict[str, list[snicaddr]]: + """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. - - 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. + The named tuple includes 5 fields (addresses may be None): - Note: you can have more than one address of the same family - associated with each interface. + - 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 """ - has_enums = sys.version_info >= (3, 4) - 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 - _psplatform.AF_LINK == fam): - # 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: # https://github.com/giampaolo/psutil/issues/786 separator = ":" if POSIX else "-" while addr.count(separator) < 5: - addr += "%s00" % separator - ret[name].append(_common.snic(fam, addr, mask, broadcast, ptp)) + addr += f"{separator}00" + + nt = _ntp.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) -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: + 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() @@ -2179,17 +2488,22 @@ def net_if_stats(): # ===================================================================== -# Linux +# Linux, macOS if hasattr(_psplatform, "sensors_temperatures"): - def sensors_temperatures(fahrenheit=False): - """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 - configuration). + 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). + 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 @@ -2209,8 +2523,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) @@ -2220,8 +2533,8 @@ def convert(n): # Linux if hasattr(_psplatform, "sensors_fans"): - def sensors_fans(): - """Return fans speed. Each entry is a namedtuple + def sensors_fans() -> dict[str, list[sfan]]: + """Return fans speed. Each entry is a named tuple representing a certain hardware sensor. All speed are expressed in RPM (rounds per minute). """ @@ -2230,17 +2543,17 @@ def sensors_fans(): __all__.append("sensors_fans") -# Linux, Windows, FreeBSD, OSX +# 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. - 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() @@ -2253,22 +2566,27 @@ 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. +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). + """ 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. + 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() @@ -2280,73 +2598,77 @@ def users(): if WINDOWS: - def win_service_iter(): - """Return a generator yielding a WindowsService instance for all - Windows services installed. + 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. + + Raise `NoSuchProcess` if no service with such name exists. """ return _psplatform.win_service_get(name) # ===================================================================== +# --- malloc / heap +# ===================================================================== + + +# Linux + glibc, Windows, macOS, FreeBSD, NetBSD +if hasattr(_psplatform, "heap_info"): + + def heap_info() -> pheap: + """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. -def test(): # pragma: no cover - """List info of all currently running processes emulating ps aux - output. + - `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() -> None: + """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") + + +# ===================================================================== + + +def _set_debug(value): + """Enable or disable `PSUTIL_DEBUG` option, which prints debugging + messages to stderr. """ - today_day = datetime.date.today() - templ = "%-10s %5s %4s %7s %7s %-13s %5s %7s %s" - attrs = ['pid', 'memory_percent', 'name', 'cpu_times', 'create_time', - 'memory_info'] - if POSIX: - attrs.append('uids') - attrs.append('terminal') - print(templ % ("USER", "PID", "%MEM", "VSZ", "RSS", "TTY", "START", "TIME", - "COMMAND")) - for p in process_iter(attrs=attrs, ad_value=''): - 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 = '' - cputime = time.strftime("%M:%S", - time.localtime(sum(p.info['cpu_times']))) - try: - user = p.username() - 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 '?' - print(templ % ( - user[:10], - p.info['pid'], - memp, - vms, - rss, - p.info.get('terminal', '') or '?', - ctime, - cputime, - p.info['name'].strip() or '?')) - - -del memoize, memoize_when_activated, division, deprecated_method -if sys.version_info[0] < 3: - del num, x - -if __name__ == "__main__": - test() + import psutil._common + + psutil._common.PSUTIL_DEBUG = bool(value) + _psplatform.cext.set_debug(bool(value)) + + +del memoize_when_activated diff --git a/psutil/_common.py b/psutil/_common.py index 05dbb4ce65..dc1a5dbf32 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -2,15 +2,13 @@ # 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.""" +"""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. +Note: this module is imported by setup.py, so it should not import +psutil or third-party modules. +""" -from __future__ import division - -import contextlib -import errno +import collections import functools import os import socket @@ -18,11 +16,10 @@ import sys import threading import warnings -from collections import defaultdict -from collections import namedtuple from socket import AF_INET from socket import SOCK_DGRAM from socket import SOCK_STREAM + try: from socket import AF_INET6 except ImportError: @@ -32,39 +29,27 @@ except ImportError: AF_UNIX = None -if sys.version_info >= (3, 4): - import enum -else: - enum = None -# can't take it from _common.py as this script is imported by setup.py -PY3 = sys.version_info[0] == 3 +PSUTIL_DEBUG = bool(os.getenv('PSUTIL_DEBUG')) +_DEFAULT = object() +# fmt: off __all__ = [ - # constants - 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'OSX', 'POSIX', 'SUNOS', - 'WINDOWS', + # OS constants + 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'MACOS', 'OSX', 'POSIX', + 'SUNOS', 'WINDOWS', + # other constants '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', - 'CONN_NONE', 'CONN_SYN_RECV', 'CONN_SYN_SENT', 'CONN_TIME_WAIT', - # net constants - 'NIC_DUPLEX_FULL', 'NIC_DUPLEX_HALF', 'NIC_DUPLEX_UNKNOWN', - # 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', - # named tuples - 'pconn', 'pcputimes', 'pctxsw', 'pgids', 'pio', 'pionice', 'popenfile', - 'pthread', 'puids', 'sconn', 'scpustats', 'sdiskio', 'sdiskpart', - 'sdiskusage', 'snetio', 'snic', 'snicstats', 'sswap', 'suser', # 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', + 'bytes2human', 'conn_to_ntuple', 'debug', + # shell utils + 'hilite', 'term_supports_colors', 'print_color', ] +# fmt: on # =================================================================== @@ -75,159 +60,21 @@ POSIX = os.name == "posix" WINDOWS = os.name == "nt" LINUX = sys.platform.startswith("linux") -OSX = sys.platform.startswith("darwin") -FREEBSD = sys.platform.startswith("freebsd") +MACOS = sys.platform.startswith("darwin") +OSX = MACOS # deprecated alias +FREEBSD = sys.platform.startswith(("freebsd", "midnightbsd")) 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") - -# =================================================================== -# --- 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" # FreeBSD, OSX -STATUS_LOCKED = "locked" # FreeBSD -STATUS_WAITING = "waiting" # FreeBSD -STATUS_SUSPENDED = "suspended" # NetBSD - -# Process.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() -if enum is None: - 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__) - -# sensors_battery() -if enum is None: - 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__) - -# --- 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" - - -# =================================================================== -# --- namedtuples -# =================================================================== - -# --- for system functions - -# 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() -snic = namedtuple('snic', ['family', 'address', 'netmask', 'broadcast', 'ptp']) -# psutil.net_if_stats() -snicstats = namedtuple('snicstats', ['isup', 'duplex', 'speed', 'mtu']) -# 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_battery() -sfan = namedtuple('sfan', ['label', 'current']) - -# --- 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.connections() -pconn = namedtuple('pconn', ['fd', 'family', 'type', 'laddr', 'raddr', - 'status']) - -# psutil.connections() and psutil.Process.connections() -addr = namedtuple('addr', ['ip', 'port']) +ENCODING_ERRS = sys.getfilesystemencodeerrors() # =================================================================== -# --- Process.connections() 'kind' parameter mapping +# --- Process.net_connections() 'kind' parameter mapping # =================================================================== @@ -248,12 +95,115 @@ class BatteryTime(enum.IntEnum): "udp6": ([AF_INET6], [SOCK_DGRAM]), }) -if AF_UNIX is not None: - conn_tmap.update({ - "unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM]), - }) +if AF_UNIX is not None and not SUNOS: + conn_tmap.update({"unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM])}) -del AF_INET, AF_UNIX, SOCK_STREAM, SOCK_DGRAM + +# ===================================================================== +# --- Exceptions +# ===================================================================== + + +class Error(Exception): + """Base exception class. All other psutil exceptions inherit + from this one. + """ + + __module__ = 'psutil' + + def _infodict(self, attrs): + info = {} + for name in attrs: + value = getattr(self, name, None) + if value or (name == "pid" and value == 0): + info[name] = value + return info + + def __str__(self): + # invoked on `raise Error` + info = self._infodict(("pid", "ppid", "name")) + if info: + details = "({})".format( + ", ".join([f"{k}={v!r}" for k, v in info.items()]) + ) + else: + details = None + return " ".join([x for x in (getattr(self, "msg", ""), details) if x]) + + def __repr__(self): + # invoked on `repr(Error)` + info = self._infodict(("pid", "ppid", "name", "seconds", "msg")) + details = ", ".join([f"{k}={v!r}" for k, v in info.items()]) + return f"psutil.{self.__class__.__name__}({details})" + + +class NoSuchProcess(Error): + """Exception raised when a process with a certain PID doesn't + or no longer exists. + """ + + __module__ = 'psutil' + + def __init__(self, pid, name=None, msg=None): + Error.__init__(self) + self.pid = pid + self.name = name + self.msg = msg or "process no longer exists" + + def __reduce__(self): + return (self.__class__, (self.pid, self.name, self.msg)) + + +class ZombieProcess(NoSuchProcess): + """Exception raised when querying a zombie process. This is + raised on macOS, BSD and Solaris only, and not always: depending + on the query the OS may be able to succeed anyway. + On Linux all zombie processes are querable (hence this is never + raised). Windows doesn't have zombie processes. + """ + + __module__ = 'psutil' + + def __init__(self, pid, name=None, ppid=None, msg=None): + NoSuchProcess.__init__(self, pid, name, msg) + self.ppid = ppid + self.msg = msg or "PID still exists but it's a zombie" + + 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.""" + + __module__ = 'psutil' + + def __init__(self, pid=None, name=None, msg=None): + Error.__init__(self) + self.pid = pid + 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 + is still alive. + """ + + __module__ = 'psutil' + + def __init__(self, seconds, pid=None, name=None): + Error.__init__(self) + self.seconds = seconds + self.pid = pid + self.name = name + self.msg = f"timeout after {seconds} seconds" + + def __reduce__(self): + return (self.__class__, (self.seconds, self.pid, self.name)) # =================================================================== @@ -264,47 +214,15 @@ 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 -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() - >>> - """ - @functools.wraps(fun) - def wrapper(*args, **kwargs): - key = (args, frozenset(sorted(kwargs.items()))) - try: - return cache[key] - except KeyError: - ret = cache[key] = fun(*args, **kwargs) - 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. @@ -312,7 +230,7 @@ def memoize_when_activated(fun): accepting no arguments. >>> class Foo: - ... @memoize + ... @memoize_when_activated ... def foo() ... print(1) ... @@ -324,35 +242,53 @@ def memoize_when_activated(fun): 1 >>> >>> # activated - >>> foo.cache_activate() + >>> foo.cache_activate(self) >>> foo() 1 >>> foo() >>> foo() >>> """ + @functools.wraps(fun) def wrapper(self): - if not wrapper.cache_activated: - return fun(self) - else: + try: + # case 1: we previously entered oneshot() ctx + ret = self._cache[fun] + except AttributeError: + # case 2: we never entered oneshot() ctx try: - ret = cache[fun] - except KeyError: - ret = cache[fun] = fun(self) - return ret + return fun(self) + except Exception as err: + 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: + raise err from None + try: + self._cache[fun] = ret + except AttributeError: + # multi-threading race condition, see: + # https://github.com/giampaolo/psutil/issues/1948 + pass + return ret - def cache_activate(): - """Activate cache.""" - wrapper.cache_activated = True + def cache_activate(proc): + """Activate cache. Expects a Process instance. Cache will be + stored as a "_cache" instance attribute. + """ + proc._cache = {} - def cache_deactivate(): + def cache_deactivate(proc): """Deactivate and clear cache.""" - wrapper.cache_activated = False - cache.clear() + try: + del proc._cache + except AttributeError: + pass - cache = {} - wrapper.cache_activated = False wrapper.cache_activate = cache_activate wrapper.cache_deactivate = cache_deactivate return wrapper @@ -361,13 +297,13 @@ def cache_deactivate(): def isfile_strict(path): """Same as os.path.isfile() but does not swallow EACCES / EPERM exceptions, see: - http://mail.python.org/pipermail/python-dev/2012-June/120787.html + http://mail.python.org/pipermail/python-dev/2012-June/120787.html. """ try: st = os.stat(path) - 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) @@ -375,30 +311,28 @@ 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) - except OSError as err: - if err.errno in (errno.EPERM, errno.EACCES): - raise + except PermissionError: + raise + except OSError: return False else: 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: 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 @@ -420,7 +354,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() @@ -434,46 +368,114 @@ 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, AttributeError): - 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.AddressType(num) - except (ValueError, AttributeError): - 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.""" + from . import _ntuples as ntp + from ._enums import ConnectionStatus + + if fam in {socket.AF_INET, AF_INET6}: + if laddr: + laddr = ntp.addr(*laddr) + if raddr: + raddr = ntp.addr(*raddr) + if type_ == socket.SOCK_STREAM and fam in {AF_INET, AF_INET6}: + status = status_map.get(status, ConnectionStatus.CONN_NONE) + else: + status = ConnectionStatus.CONN_NONE # ignore whatever C returned to us + fam = sockfam_to_enum(fam) + type_ = socktype_to_enum(type_) + if pid is None: + return ntp.pconn(fd, fam, type_, laddr, raddr, status) + else: + return ntp.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. + 'replacement' 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) + msg = ( + f"{fun.__name__}() is deprecated and will be removed; use" + f" {replacement}() instead" + ) if fun.__doc__ is None: fun.__doc__ = msg @functools.wraps(fun) def inner(self, *args, **kwargs): - warnings.warn(msg, category=FutureWarning, stacklevel=2) + warnings.warn(msg, category=DeprecationWarning, stacklevel=2) return getattr(self, replacement)(*args, **kwargs) + return inner + return outer +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). @@ -490,15 +492,15 @@ 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 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] @@ -506,7 +508,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. @@ -517,7 +519,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] @@ -573,3 +575,172 @@ def wrap_numbers(input_dict, name): _wn = _WrapNumbers() wrap_numbers.cache_clear = _wn.cache_clear wrap_numbers.cache_info = _wn.cache_info + + +# The read buffer size for open() builtin. This (also) dictates how +# much data we read(2) when iterating over file lines as in: +# >>> with open(file) as f: +# ... for line in f: +# ... ... +# Default per-line buffer size for binary files is 1K. For text files +# is 8K. We use a bigger buffer (32K) in order to have more consistent +# results when reading /proc pseudo files on Linux, see: +# https://github.com/giampaolo/psutil/issues/2050 +# 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): + """Open a file in text mode by using the proper FS encoding and + en/decoding error handlers. + """ + # See: + # https://github.com/giampaolo/psutil/issues/675 + # https://github.com/giampaolo/psutil/pull/733 + fobj = open( # noqa: SIM115 + fname, + buffering=FILE_READ_BUFFER_SIZE, + encoding=ENCODING, + errors=ENCODING_ERRS, + ) + try: + # Dictates per-line read(2) buffer size. Defaults is 8k. See: + # https://github.com/giampaolo/psutil/issues/2050#issuecomment-1013387546 + fobj._CHUNK_SIZE = FILE_READ_BUFFER_SIZE + except AttributeError: + pass + except Exception: + fobj.close() + raise + + return fobj + + +def cat(fname, fallback=_DEFAULT, _open=open_text): + """Read entire file content and return it as a string. File is + opened in text mode. If specified, `fallback` is the value + returned in case of error, either if the file does not exist or + it can't be read(). + """ + if fallback is _DEFAULT: + with _open(fname) as f: + return f.read() + else: + try: + with _open(fname) as f: + return f.read() + except 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: https://code.activestate.com/recipes/578019-bytes-to-human-human-to-bytes-converter/?in=user-4178764. + + >>> bytes2human(10000) + '9.8K' + >>> bytes2human(100001221) + '95.4M' + """ + symbols = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') + prefix = {} + for i, s in enumerate(symbols[1:]): + prefix[s] = 1 << (i + 1) * 10 + for symbol in reversed(symbols[1:]): + if abs(n) >= prefix[symbol]: + value = float(n) / prefix[symbol] + return format % locals() + return format % dict(symbol=symbols[0], value=n) + + +def get_procfs_path(): + """Return updated psutil.PROCFS_PATH constant.""" + return sys.modules['psutil'].PROCFS_PATH + + +def decode(s): + return s.decode(encoding=ENCODING, errors=ENCODING_ERRS) + + +# ===================================================================== +# --- shell utils +# ===================================================================== + + +@functools.lru_cache +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: + sys.stdout.fileno() + except Exception: # noqa: BLE001 + return False + return True + + +def hilite(s, color=None, bold=False, force_color=False): + """Return an highlighted version of 'string'.""" + if not term_supports_colors(force_color=force_color): + return s + attr = [] + colors = dict( + blue='34', + brown='33', + darkgrey='30', + green='32', + grey='37', + lightblue='36', + red='31', + violet='35', + yellow='93', + ) + colors[None] = '29' + try: + color = colors[color] + except KeyError: + msg = f"invalid color {color!r}; choose amongst {list(colors)}" + raise ValueError(msg) from None + attr.append(color) + if bold: + attr.append('1') + return f"\x1b[{';'.join(attr)}m{s}\x1b[0m" + + +def print_color( + s, color=None, bold=False, file=sys.stdout +): # pragma: no cover + """Print a colorized version of string.""" + if term_supports_colors(): + s = hilite(s, color=color, bold=bold) + print(s, file=file, flush=True) + + +def debug(msg): + """If PSUTIL_DEBUG env var is set, print a debug message to stderr.""" + if PSUTIL_DEBUG: + import inspect + + fname, lineno, _, _lines, _index = inspect.getframeinfo( + inspect.currentframe().f_back + ) + if isinstance(msg, Exception): + if isinstance(msg, OSError): + # ...because str(exc) may contain info about the file name + msg = f"ignoring {msg}" + else: + msg = f"ignoring {msg!r}" + print( # noqa: T201 + f"psutil-debug [{fname}:{lineno}]> {msg}", file=sys.stderr + ) diff --git a/psutil/_compat.py b/psutil/_compat.py deleted file mode 100644 index 08aefe4b76..0000000000 --- a/psutil/_compat.py +++ /dev/null @@ -1,241 +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.""" - -import collections -import functools -import os -import sys - -__all__ = ["PY3", "long", "xrange", "unicode", "basestring", "u", "b", - "lru_cache", "which"] - -PY3 = sys.version_info[0] == 3 - -if PY3: - long = int - xrange = range - unicode = str - basestring = str - - def u(s): - return s - - def b(s): - return s.encode("latin-1") -else: - long = long - xrange = xrange - unicode = unicode - basestring = basestring - - def u(s): - return unicode(s, "unicode_escape") - - def b(s): - return s - - -# --- 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): - __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=(object(), ), - fasttypes=set((int, str, frozenset, type(None))), - 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 = dict() - 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 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/_exceptions.py b/psutil/_exceptions.py deleted file mode 100644 index c08e6d83c8..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 OSX, 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/_ntuples.py b/psutil/_ntuples.py new file mode 100644 index 0000000000..c95d85b11b --- /dev/null +++ b/psutil/_ntuples.py @@ -0,0 +1,527 @@ +# 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 warnings +from collections import namedtuple +from typing import TYPE_CHECKING +from typing import NamedTuple + +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 +from ._common import LINUX +from ._common import MACOS +from ._common import NETBSD +from ._common import OPENBSD +from ._common import SUNOS +from ._common import WINDOWS + +# =================================================================== +# --- system functions +# =================================================================== + + +# psutil.swap_memory() +class sswap(NamedTuple): + total: int + used: int + free: int + percent: float + sin: int + sout: int + + +# psutil.disk_usage() +class sdiskusage(NamedTuple): + total: int + used: int + free: int + percent: float + + +# psutil.disk_io_counters() +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() +class sdiskpart(NamedTuple): + device: str + mountpoint: str + fstype: str + opts: str + + +# psutil.net_io_counters() +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() +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() +class sconn(NamedTuple): + fd: int + family: socket.AddressFamily + type: socket.SocketKind + laddr: addr | tuple | str + raddr: addr | tuple | str + status: ConnectionStatus + pid: int | None + + +# psutil.net_if_addrs() +class snicaddr(NamedTuple): + family: socket.AddressFamily + address: str | None + netmask: str | None + broadcast: str | None + ptp: str | None + + +# psutil.net_if_stats() +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: + 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): + ctx_switches: int + interrupts: int + soft_interrupts: int + syscalls: int + + +# psutil.cpu_freq() +class scpufreq(NamedTuple): + current: float + min: float | None + max: float | None + + +# psutil.sensors_temperatures() +class shwtemp(NamedTuple): + label: str + current: float | None + high: float | None + critical: float | None + + +# psutil.sensors_battery() +class sbattery(NamedTuple): + percent: float + secsleft: int | BatteryTime + power_plugged: bool | None + + +# psutil.sensors_fans() +class sfan(NamedTuple): + label: str + current: int + + +if LINUX or WINDOWS or MACOS or BSD: + + # 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 WINDOWS: + cached: int + wired: int + elif MACOS: + active: int + inactive: int + wired: int + + +# =================================================================== +# --- Process class +# =================================================================== + + +# psutil.Process.cpu_times() +class pcputimes(NamedTuple): + user: float + system: float + children_user: float + children_system: float + if LINUX: + iowait: float + + +# psutil.Process.open_files() +class popenfile(NamedTuple): + path: str + fd: int + if LINUX: + position: int + mode: str + flags: int + + +# psutil.Process.threads() +class pthread(NamedTuple): + id: int + user_time: float + system_time: float + + +# psutil.Process.uids() +class puids(NamedTuple): + real: int + effective: int + saved: int + + +# psutil.Process.gids() +class pgids(NamedTuple): + real: int + effective: int + saved: int + + +# psutil.Process.io_counters() +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() +class pionice(NamedTuple): + ioclass: ProcessIOPriority + value: int + + +# psutil.Process.ctx_switches() +class pctxsw(NamedTuple): + voluntary: int + involuntary: int + + +# psutil.Process.page_faults() +class ppagefaults(NamedTuple): + minor: int + major: int + + +# psutil.Process().memory_footprint() +if LINUX or MACOS or WINDOWS: + + class pfootprint(NamedTuple): + uss: int + if LINUX: + pss: int + swap: int + + +# psutil.Process.net_connections() +class pconn(NamedTuple): + fd: int + family: socket.AddressFamily + type: socket.SocketKind + laddr: addr | tuple | str + raddr: addr | tuple | str + status: ConnectionStatus + + +# 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 + + +# =================================================================== +# --- Process memory_info() / memory_info_ex() / memory_full_info() +# =================================================================== + +if LINUX: + + # psutil.Process().memory_info() + class pmem(NamedTuple): + rss: int + vms: int + shared: int + text: int + data: int + + @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 = namedtuple( + "pmem_ex", + pmem._fields + + ( + "peak_rss", + "peak_vms", + "rss_anon", + "rss_file", + "rss_shmem", + "swap", + "hugetlb", + ), + ) + + # psutil.Process().memory_full_info() + pfullmem = namedtuple("pfullmem", pmem._fields + ("uss", "pss", "swap")) + +elif WINDOWS: + + # psutil.Process.memory_info() + class pmem( # noqa: SLOT002 + 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 {} + return inst + + def __getattr__(self, name): + depr = self.__dict__["_deprecated"] + if name in depr: + 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" + elif name == "num_page_faults": + msg += "; use page_faults() instead" + warnings.warn(msg, DeprecationWarning, stacklevel=2) + return depr[name] + + msg = f"{self.__class__.__name__} object has no attribute {name!r}" + raise AttributeError(msg) + + # psutil.Process.memory_info_ex() + pmem_ex = namedtuple( + "pmem_ex", + pmem._fields + + ( + "virtual", + "peak_virtual", + "paged_pool", + "nonpaged_pool", + "peak_paged_pool", + "peak_nonpaged_pool", + ), + ) + + # psutil.Process.memory_full_info() + pfullmem = namedtuple("pfullmem", pmem._fields + ("uss",)) + +elif MACOS: + + # psutil.Process.memory_info() + class pmem(NamedTuple): + rss: int + vms: int + + # psutil.Process.memory_info_ex() + 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 = namedtuple("pfullmem", pmem._fields + ("uss",)) + +elif BSD: + + # psutil.Process.memory_info() + class pmem(NamedTuple): + rss: int + vms: int + text: int + data: int + stack: int + peak_rss: int + + # psutil.Process.memory_info_ex() + pmem_ex = pmem + + # psutil.Process.memory_full_info() + pfullmem = pmem + +elif SUNOS or AIX: + + # psutil.Process.memory_info() + 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/_psaix.py b/psutil/_psaix.py index 662f306c3c..429d40686d 100644 --- a/psutil/_psaix.py +++ b/psutil/_psaix.py @@ -6,32 +6,27 @@ """AIX platform implementation.""" -import errno +import functools import glob import os import re import subprocess import sys -from collections import namedtuple -from socket import AF_INET -from . import _common +from . import _ntuples as ntp from . import _psposix from . import _psutil_aix as cext -from . import _psutil_posix as cext_posix -from ._common import AF_INET6 +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import 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 -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess - +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessStatus __extra__all__ = ["PROCFS_PATH"] @@ -42,31 +37,33 @@ 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 +PAGE_SIZE = cext.getpagesize() +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( @@ -77,37 +74,8 @@ nice=4, num_threads=5, status=6, - ttynr=7) - - -# ===================================================================== -# --- 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']) -# psutil.Process.memory_maps(grouped=True) -pmmap_grouped = namedtuple('pmmap_grouped', ['path', 'rss', 'anon', 'locked']) -# psutil.Process.memory_maps(grouped=False) -pmmap_ext = namedtuple( - 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) - - -# ===================================================================== -# --- utils -# ===================================================================== - - -def get_procfs_path(): - """Return updated psutil.PROCFS_PATH constant.""" - return sys.modules['psutil'].PROCFS_PATH + ttynr=7, +) # ===================================================================== @@ -116,9 +84,9 @@ def get_procfs_path(): 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) + return ntp.svmem(total, avail, percent, inuse, free) def swap_memory(): @@ -126,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) # ===================================================================== @@ -135,15 +103,15 @@ 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)]) + 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""" + """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(): @@ -155,16 +123,14 @@ def cpu_count_logical(): return None -def cpu_count_physical(): - cmd = "lsdev -Cc processor" - p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) +def cpu_count_cores(): + cmd = ["lsdev", "-Cc", "processor"] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() - if PY3: - stdout, stderr = [x.decode(sys.stdout.encoding) - for x in (stdout, stderr)] + stdout, stderr = (x.decode(sys.stdout.encoding) for x in (stdout, stderr)) if p.returncode != 0: - raise RuntimeError("%r command error\n%s" % (cmd, stderr)) + msg = f"{cmd!r} command error\n{stderr}" + raise RuntimeError(msg) processors = stdout.strip().splitlines() return len(processors) or None @@ -172,8 +138,7 @@ def cpu_count_physical(): 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) # ===================================================================== @@ -201,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 @@ -211,71 +176,77 @@ def disk_partitions(all=False): # ===================================================================== -net_if_addrs = cext_posix.net_if_addrs -net_io_counters = cext.net_io_counters +net_if_addrs = cext.net_if_addrs + +if HAS_NET_IO_COUNTERS: + net_io_counters = cext.net_io_counters 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] + families, types = conn_tmap[kind] rawlist = cext.net_connections(_pid) - ret = set() + ret = [] for item in rawlist: fd, fam, type_, laddr, raddr, status, pid = item if fam not in families: continue if type_ not in types: continue - status = TCP_STATUSES[status] - if fam in (AF_INET, AF_INET6): - if laddr: - laddr = _common.addr(*laddr) - if raddr: - raddr = _common.addr(*raddr) - fam = sockfam_to_enum(fam) - type_ = socktype_to_enum(type_) - if _pid == -1: - nt = _common.sconn(fd, fam, type_, laddr, raddr, status, pid) - else: - nt = _common.pconn(fd, fam, type_, laddr, raddr, status) - ret.add(nt) - return list(ret) + nt = conn_to_ntuple( + fd, + fam, + type_, + laddr, + raddr, + status, + TCP_STATUSES, + pid=pid if _pid == -1 else None, + ) + ret.append(nt) + return ret def net_if_stats(): """Get NIC stats (isup, duplex, speed, mtu).""" - duplex_map = {"Full": NIC_DUPLEX_FULL, - "Half": NIC_DUPLEX_HALF} - names = set([x[0] for x in net_if_addrs()]) + 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: - isup, mtu = cext.net_if_stats(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. # 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("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) - duplex = duplex_map.get(duplex, NIC_DUPLEX_UNKNOWN) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + output_flags = ','.join(flags) + isup = 'running' in flags + duplex = duplex_map.get(duplex, NicDuplex.NIC_DUPLEX_UNKNOWN) + ret[name] = ntp.snicstats(isup, duplex, speed, mtu, output_flags) return ret @@ -290,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') @@ -303,7 +274,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 @@ -328,32 +299,28 @@ def wrap_exceptions(fun): EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): + pid, ppid, name = self.pid, self._ppid, self._name 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) 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 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(pid): + raise NoSuchProcess(pid, name) from err + raise ZombieProcess(pid, name, ppid) from err + except PermissionError as err: + raise AccessDenied(pid, name) from err + return wrapper -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_procfs_path"] + __slots__ = ["_cache", "_name", "_ppid", "_procfs_path", "pid"] def __init__(self, pid): self.pid = pid @@ -362,23 +329,19 @@ def __init__(self, pid): self._procfs_path = get_procfs_path() def oneshot_enter(self): - self._proc_name_and_args.cache_activate() - self._proc_basic_info.cache_activate() - self._proc_cred.cache_activate() + self._proc_oneshot.cache_activate(self) + self._proc_cred.cache_activate(self) def oneshot_exit(self): - self._proc_name_and_args.cache_deactivate() - self._proc_basic_info.cache_deactivate() - self._proc_cred.cache_deactivate() - - @memoize_when_activated - def _proc_name_and_args(self): - return cext.proc_name_and_args(self.pid, self._procfs_path) + self._proc_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 def _proc_cred(self): return cext.proc_cred(self.pid, self._procfs_path) @@ -387,52 +350,63 @@ def _proc_cred(self): def name(self): if self.pid == 0: return "swapper" - # note: this is limited to 15 characters - return self._proc_name_and_args()[0].rstrip("\x00") + # note: max 16 characters + return cext.proc_name(self.pid, self._procfs_path).rstrip("\x00") @wrap_exceptions def exe(self): # there is no way to get executable path in AIX other than to guess, # and guessing is more complex than what's in the wrapping class - exe = self.cmdline()[0] + cmdline = self.cmdline() + if not cmdline: + return '' + exe = cmdline[0] if os.path.sep in exe: # relative or absolute path if not os.path.isabs(exe): # if cwd has changed, we're out of luck - this may be wrong! exe = os.path.abspath(os.path.join(self.cwd(), exe)) - if (os.path.isabs(exe) and - os.path.isfile(exe) and - os.access(exe, os.X_OK)): + if ( + os.path.isabs(exe) + and os.path.isfile(exe) + and os.access(exe, os.X_OK) + ): return exe # not found, move to search in PATH using basename only exe = os.path.basename(exe) # search for exe name PATH for path in os.environ["PATH"].split(":"): possible_exe = os.path.abspath(os.path.join(path, exe)) - if (os.path.isfile(possible_exe) and - os.access(possible_exe, os.X_OK)): + if os.path.isfile(possible_exe) and os.access( + possible_exe, os.X_OK + ): return possible_exe return '' @wrap_exceptions def cmdline(self): - return self._proc_name_and_args()[1].split(' ') + return cext.proc_args(self.pid) + + @wrap_exceptions + def environ(self): + return cext.proc_environ(self.pid) @wrap_exceptions def create_time(self): - 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: + @wrap_exceptions 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 @@ -441,11 +415,11 @@ 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 - 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 @@ -454,42 +428,42 @@ def 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 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): - self._ppid = self._proc_basic_info()[proc_info_map['ppid']] + self._ppid = self._proc_oneshot()[proc_info_map['ppid']] return self._ppid @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): - 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 ntp.pcputimes(*t) @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)) + 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: @@ -500,41 +474,40 @@ 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 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(f"{procfs_path}/{self.pid}") # raise NSP or AD + return "" @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 pmem(rss, vms) - - memory_full_info = memory_info + return ntp.pmem(rss, vms) @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, '?') 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("(\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() @@ -542,32 +515,33 @@ 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 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))) + return len(os.listdir(f"{self._procfs_path}/{self.pid}/fd")) @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): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) - @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 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) from err + raise + return ntp.pio(rc, wc, rb, wb) diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 83f38d55e9..ba653cfc89 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -8,28 +8,26 @@ import errno import functools import os -import xml.etree.ElementTree as ET +from collections import defaultdict from collections import namedtuple -from socket import AF_INET -from . import _common +from . import _ntuples as ntp from . import _psposix from . import _psutil_bsd as cext -from . import _psutil_posix as cext_posix -from ._common import AF_INET6 -from ._common import conn_tmap from ._common import FREEBSD -from ._common import memoize -from ._common import memoize_when_activated from ._common import NETBSD from ._common import OPENBSD -from ._common import sockfam_to_enum -from ._common import socktype_to_enum -from ._common import usage_percent -from ._compat import which -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import debug +from ._common import memoize_when_activated +from ._enums import BatteryTime +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessStatus __extra__all__ = [] @@ -41,26 +39,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 or NETBSD: +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. @@ -68,104 +66,38 @@ # 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.SACTIVE: _common.STATUS_RUNNING, - cext.SDYING: _common.STATUS_ZOMBIE, - cext.SSTOP: _common.STATUS_STOPPED, - cext.SZOMB: _common.STATUS_ZOMBIE, - cext.SDEAD: _common.STATUS_DEAD, - cext.SSUSPENDED: _common.STATUS_SUSPENDED, # unique to NetBSD + 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, } -if NETBSD: - PAGESIZE = os.sysconf("SC_PAGESIZE") -else: - PAGESIZE = os.sysconf("SC_PAGE_SIZE") -AF_LINK = cext_posix.AF_LINK - -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, - name=24, -) - - -# ===================================================================== -# --- named tuples -# ===================================================================== +PAGESIZE = cext.getpagesize() +AF_LINK = cext.AF_LINK - -# 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']) +HAS_PROC_NUM_THREADS = hasattr(cext, "proc_num_threads") # ===================================================================== @@ -174,30 +106,22 @@ def virtual_memory(): - """System virtual memory as a namedtuple.""" - mem = cext.virtual_mem() - total, free, active, inactive, wired, cached, buffers, shared = mem - if NETBSD: - # 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 - elif line.startswith(b'MemShared:'): - shared = 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) + d = cext.virtual_mem() + return ntp.svmem(**d) 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) + """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) +if hasattr(cext, "heap_info"): + heap_info = cext.heap_info + heap_trim = cext.heap_trim # ===================================================================== @@ -206,38 +130,19 @@ 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 scputimes(user, nice, system, idle, irq) + return ntp.scputimes(user, system, idle, nice, irq) -if hasattr(cext, "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__: - raise NotImplementedError("supported only starting from FreeBSD 8") - per_cpu_times.__called__ = True - return [cpu_times()] - - per_cpu_times.__called__ = False +def per_cpu_times(): + """Return system CPU times as a named tuple.""" + ret = [] + for cpu_t in cext.per_cpu_times(): + user, nice, system, idle, irq = cpu_t + item = ntp.scputimes(user, system, idle, nice, irq) + ret.append(item) + return ret def cpu_count_logical(): @@ -246,36 +151,16 @@ 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.""" - # 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() - if s is not None: - # get rid of padding chars appended at the end of the string - index = s.rfind("") - if index != -1: - s = s[:index + 9] - root = ET.fromstring(s) - try: - ret = len(root.findall('group/children/group/cpu')) or None - finally: - # needed otherwise it will memleak - root.clear() - if not ret: - # If logical CPUs are 1 it's obvious we'll have only 1 - # physical CPU. - if cpu_count_logical() == 1: - return 1 - return ret + + def cpu_count_cores(): + """Return the number of CPU cores in the system.""" + return cext.cpu_count_cores() def cpu_stats(): @@ -283,29 +168,47 @@ 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() - elif NETBSD: - # XXX - # Note about intrs: the C extension returns 0. intrs - # can be determined via /proc/stat; it has the same value as - # soft_intrs thought so the kernel is faking it (?). - # - # Note about syscalls: the C extension always sets it to 0 (?). - # + ctxsw, intrs, soft_intrs, syscalls, _traps = cext.cpu_stats() + elif NETBSD or 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() - with open('/proc/stat', 'rb') as f: - for line in f: - if line.startswith(b'intr'): - intrs = int(line.split()[1]) - 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 = \ - cext.cpu_stats() - return _common.scpustats(ctxsw, intrs, soft_intrs, syscalls) + ) + return ntp.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(ntp.scpufreq(current, min_freq, max_freq)) + return ret + +elif OPENBSD: + + def cpu_freq(): + curr = float(cext.cpu_freq()) + return [ntp.scpufreq(curr, 0.0, 0.0)] # ===================================================================== @@ -314,15 +217,15 @@ def cpu_stats(): 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 + https://github.com/giampaolo/psutil/issues/906. """ retlist = [] partitions = cext.disk_partitions() for partition in partitions: device, mountpoint, fstype, opts = partition - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + ntuple = ntp.sdiskpart(device, mountpoint, fstype, opts) retlist.append(ntuple) return retlist @@ -337,7 +240,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(): @@ -345,61 +248,39 @@ 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.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: + raise + else: + duplex = NicDuplex(duplex) + output_flags = ','.join(flags) + isup = 'running' in flags + ret[name] = ntp.snicstats(isup, duplex, speed, mtu, output_flags) return ret def net_connections(kind): """System-wide network connections.""" - if OPENBSD: - ret = [] - for pid in pids(): - try: - cons = Process(pid).connections(kind) - except (NoSuchProcess, ZombieProcess): - continue - else: - for conn in cons: - conn = list(conn) - conn.append(pid) - ret.append(_common.sconn(*conn)) - return ret - - if kind not in _common.conn_tmap: - raise ValueError("invalid %r kind argument; choose between %s" - % (kind, ', '.join([repr(x) for x in conn_tmap]))) families, types = conn_tmap[kind] ret = set() - if NETBSD: - rawlist = cext.net_connections(-1) - else: - rawlist = cext.net_connections() + if OPENBSD: + rawlist = cext.net_connections(-1, families, types) + elif NETBSD: + rawlist = cext.net_connections(-1, kind) + else: # FreeBSD + rawlist = cext.net_connections(families, types) + for item in rawlist: fd, fam, type, laddr, raddr, status, pid = item - # TODO: apply filter at C level - if fam in families and type in types: - try: - status = TCP_STATUSES[status] - except KeyError: - # XXX: Not sure why this happens. I saw this occurring - # with IPv6 sockets opened by 'vim'. Those sockets - # have a very short lifetime so maybe the kernel - # can't initialize their status? - status = TCP_STATUSES[cext.PSUTIL_CONN_NONE] - if fam in (AF_INET, AF_INET6): - if laddr: - laddr = _common.addr(*laddr) - if raddr: - raddr = _common.addr(*raddr) - fam = sockfam_to_enum(fam) - type = socktype_to_enum(type) - nt = _common.sconn(fd, fam, type, laddr, raddr, status, pid) - ret.add(nt) + nt = conn_to_ntuple( + fd, fam, type, laddr, raddr, status, TCP_STATUSES, pid + ) + ret.add(nt) return list(ret) @@ -419,12 +300,28 @@ 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 _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.""" + 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 = f"Core {cpu}" + ret["coretemp"].append(ntp.shwtemp(name, current, high, high)) + except NotImplementedError: + pass + + return ret # ===================================================================== @@ -437,18 +334,39 @@ 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.""" + """Return currently connected users as a list of named tuples.""" retlist = [] rawlist = cext.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) + nt = ntp.suser(user, tty or None, hostname, tstamp, pid) retlist.append(nt) return retlist @@ -458,7 +376,7 @@ def users(): # ===================================================================== -@memoize +@functools.lru_cache def _pid_0_exists(): try: Process(0).name() @@ -480,9 +398,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 @@ -490,7 +408,20 @@ def pid_exists(pid): return pid in pids() 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 @@ -498,93 +429,101 @@ def wrap_exceptions(fun): """Decorator which translates bare OSError exceptions into NoSuchProcess and AccessDenied. """ + @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 as err: + 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 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 + if pid == 0 and 0 in pids(): + raise AccessDenied(pid, name) from err + raise err from None + return wrapper @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 EnvironmentError as err: + 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 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 cext.proc_is_zombie(inst.pid): + raise ZombieProcess(pid, name, ppid) from err + else: + raise NoSuchProcess(pid, name) from err + except PermissionError as err: + raise AccessDenied(pid, name) from err -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid"] + __slots__ = ["_cache", "_name", "_ppid", "pid"] def __init__(self, pid): self.pid = pid self._name = None self._ppid = None + def _assert_alive(self): + """Raise NSP if the process disappeared on us.""" + # For those C function who do not raise NSP, possibly returning + # incorrect or incomplete result. + cext.proc_name(self.pid) + + @wrap_exceptions @memoize_when_activated def oneshot(self): - """Retrieves multiple process info in one shot as a raw tuple.""" - ret = cext.proc_oneshot_info(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.oneshot.cache_activate(self) def oneshot_exit(self): - self.oneshot.cache_deactivate() + self.oneshot.cache_deactivate(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 def exe(self): if FREEBSD: + if self.pid == 0: + return '' # else NSP return cext.proc_exe(self.pid) elif NETBSD: if self.pid == 0: # /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/+/ - # 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 + cmdline = self.cmdline() if cmdline: - return which(cmdline[0]) + return shutil.which(cmdline[0]) or "" else: return "" @@ -593,26 +532,33 @@ def cmdline(self): if OPENBSD and self.pid == 0: return [] # ...else it crashes elif NETBSD: - # XXX - most of the times the underlying sysctl() call on Net - # and Open BSD returns a truncated string. - # Also /proc/pid/cmdline behaves the same so it looks - # like this is a kernel bug. + # XXX - most of the times the underlying sysctl() call on + # NetBSD and OpenBSD returns a truncated string. Also + # /proc/pid/cmdline behaves the same so it looks like this + # is a kernel bug. try: return cext.proc_cmdline(self.pid) except OSError as err: - if err.errno == errno.EINVAL: + if err.errno in {errno.EINVAL, errno.EFAULT}: + debug(f"cmdline(): ignoring {err!r}") + pid, name, ppid = self.pid, self._name, self._ppid + if cext.proc_is_zombie(self.pid): + raise ZombieProcess(pid, name, ppid) from err if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - else: - raise ZombieProcess(self.pid, self._name, self._ppid) + raise NoSuchProcess(pid, name, ppid) from err + return [] else: raise else: return cext.proc_cmdline(self.pid) + @wrap_exceptions + def environ(self): + return cext.proc_environ(self.pid) + @wrap_exceptions def terminal(self): - tty_nr = self.oneshot()[kinfo_proc_map['ttynr']] + tty_nr = self.oneshot()["ttynr"] tmap = _psposix.get_terminal_map() try: return tmap[tty_nr] @@ -621,69 +567,69 @@ 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 _common.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 _common.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() - return _common.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 = self.oneshot() + return ntp.pcputimes( + 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() - return 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']]) - - memory_full_info = memory_info + d = self.oneshot() + return ntp.pmem( + rss=d["rss"], + vms=d["vms"], + text=d["memtext"], + data=d["memdata"], + stack=d["memstack"], + peak_rss=d["peak_rss"], + ) @wrap_exceptions - def create_time(self): - return self.oneshot()[kinfo_proc_map['create_time']] + def create_time(self, monotonic=False): + ctime = self.oneshot()["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): - if hasattr(cext, "proc_num_threads"): - # FreeBSD + if HAS_PROC_NUM_THREADS: + # FreeBSD / NetBSD return cext.proc_num_threads(self.pid) else: return len(self.threads()) @wrap_exceptions 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']]) + d = self.oneshot() + return ntp.pctxsw(d["ctx_switches_vol"], d["ctx_switches_unvol"]) + + @wrap_exceptions + def page_faults(self): + d = self.oneshot() + return ntp.ppagefaults(d["min_faults"], d["maj_faults"]) @wrap_exceptions def threads(self): @@ -691,96 +637,59 @@ 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: - # On OpenBSD the underlying C function does not raise NSP - # in case the process is gone (and the returned list may - # incomplete). - self.name() # raise NSP if the process disappeared on us + self._assert_alive() return retlist @wrap_exceptions - def connections(self, kind='inet'): - if kind not in conn_tmap: - raise ValueError("invalid %r kind argument; choose between %s" - % (kind, ', '.join([repr(x) for x in conn_tmap]))) + def net_connections(self, kind='inet'): + families, types = conn_tmap[kind] + ret = [] if NETBSD: - families, types = conn_tmap[kind] - ret = set() - rawlist = cext.net_connections(self.pid) - for item in rawlist: - fd, fam, type, laddr, raddr, status, pid = item - assert pid == self.pid - if fam in families and type in types: - try: - status = TCP_STATUSES[status] - except KeyError: - status = TCP_STATUSES[cext.PSUTIL_CONN_NONE] - if fam in (AF_INET, AF_INET6): - if laddr: - laddr = _common.addr(*laddr) - if raddr: - raddr = _common.addr(*raddr) - fam = sockfam_to_enum(fam) - type = socktype_to_enum(type) - nt = _common.pconn(fd, fam, type, laddr, raddr, status) - ret.add(nt) - # On NetBSD the underlying C function does not raise NSP - # in case the process is gone (and the returned list may - # incomplete). - self.name() # raise NSP if the process disappeared on us - return list(ret) + rawlist = cext.net_connections(self.pid, kind) + elif OPENBSD: + rawlist = cext.net_connections(self.pid, families, types) + else: + rawlist = cext.proc_net_connections(self.pid, families, types) - families, types = conn_tmap[kind] - rawlist = cext.proc_connections(self.pid, families, types) - ret = [] for item in rawlist: - fd, fam, type, laddr, raddr, status = item - if fam in (AF_INET, AF_INET6): - if laddr: - laddr = _common.addr(*laddr) - if raddr: - raddr = _common.addr(*raddr) - fam = sockfam_to_enum(fam) - type = socktype_to_enum(type) - status = TCP_STATUSES[status] - nt = _common.pconn(fd, fam, type, laddr, raddr, status) + fd, fam, type, laddr, raddr, status = item[:6] + if FREEBSD: + if (fam not in families) or (type not in types): + continue + nt = conn_to_ntuple( + fd, fam, type, laddr, raddr, status, TCP_STATUSES + ) ret.append(nt) - if OPENBSD: - # On OpenBSD the underlying C function does not raise NSP - # in case the process is gone (and the returned list may - # incomplete). - self.name() # raise NSP if the process disappeared on us + + self._assert_alive() return ret @wrap_exceptions 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): - 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): - 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 _common.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): @@ -788,52 +697,29 @@ def cwd(self): # sometimes we get an empty string, in which case we turn # it into None if OPENBSD and self.pid == 0: - return None # ...else it would raise EINVAL - elif NETBSD: - with wrap_exceptions_procfs(self): - return os.readlink("/proc/%s/cwd" % self.pid) - elif hasattr(cext, 'proc_open_files'): - # FreeBSD < 8 does not support functions based on - # kinfo_getfile() and kinfo_getvmmap() - return cext.proc_cwd(self.pid) or None - else: - raise NotImplementedError( - "supported only starting from FreeBSD 8" if - FREEBSD else "") + return "" # ...else it would raise EINVAL + return cext.proc_cwd(self.pid) 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') - - def _not_implemented(self): - raise NotImplementedError + 'mmap', 'addr, perms path rss, private, ref_count, shadow_count' + ) - # FreeBSD < 8 does not support functions based on kinfo_getfile() - # and kinfo_getvmmap() - if hasattr(cext, '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 + @wrap_exceptions + def open_files(self): + """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] - # FreeBSD < 8 does not support functions based on kinfo_getfile() - # and kinfo_getvmmap() - if hasattr(cext, 'proc_num_fds'): - @wrap_exceptions - def num_fds(self): - """Return the number of file descriptors opened by this process.""" - ret = cext.proc_num_fds(self.pid) - if NETBSD: - # On NetBSD the underlying C function does not raise NSP - # in case the process is gone. - self.name() # raise NSP if the process disappeared on us - return ret - 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 @@ -845,14 +731,14 @@ def cpu_affinity_get(self): @wrap_exceptions def cpu_affinity_set(self, cpus): - # Pre-emptively check if CPUs are valid because the C + # preemptively 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: @@ -860,14 +746,30 @@ 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( - "invalid CPU #%i (choose between %s)" % ( - cpu, allcpus)) + msg = ( + f"invalid CPU {cpu!r} (choose between" + f" {allcpus})" + ) + raise ValueError(msg) from err raise @wrap_exceptions def memory_maps(self): return cext.proc_memory_maps(self.pid) + + @wrap_exceptions + def rlimit(self, resource, limits=None): + if limits is None: + return cext.proc_getrlimit(self.pid, resource) + else: + if len(limits) != 2: + 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 78c03d5c5c..2cbc60b673 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -4,63 +4,49 @@ """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 -import traceback import warnings from collections import defaultdict -from collections import namedtuple -from . import _common +from . import _ntuples as ntp 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 AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import bcat +from ._common import cat +from ._common import debug +from ._common import decode +from ._common import get_procfs_path from ._common import isfile_strict -from ._common import memoize from ._common import memoize_when_activated -from ._common import 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 socktype_to_enum from ._common import supports_ipv6 from ._common import usage_percent -from ._compat import b -from ._compat import basestring -from ._compat import long -from ._compat import PY3 -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess - -if sys.version_info >= (3, 4): - import enum -else: - enum = None +from ._enums import BatteryTime +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessIOPriority +from ._enums import ProcessStatus - -__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", ] +__extra__all__ = ['PROCFS_PATH'] # ===================================================================== @@ -69,154 +55,79 @@ POWER_SUPPLY_PATH = "/sys/class/power_supply" -HAS_SMAPS = os.path.exists('/proc/%s/smaps' % os.getpid()) -HAS_PRLIMIT = hasattr(cext, "linux_prlimit") -_DEFAULT = object() - -# RLIMIT_* constants, not guaranteed to be present on all kernels -if HAS_PRLIMIT: - for name in dir(cext): - if name.startswith('RLIM'): - __extra__all__.append(name) +HAS_PROC_SMAPS = os.path.exists(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") # Number of clock ticks per second CLOCK_TICKS = os.sysconf("SC_CLK_TCK") -PAGESIZE = os.sysconf("SC_PAGE_SIZE") -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 +PAGESIZE = cext.getpagesize() LITTLE_ENDIAN = sys.byteorder == 'little' -SECTOR_SIZE_FALLBACK = 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 - -# ioprio_* constants http://linux.die.net/man/2/ioprio_get -if enum is None: - 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__) - -# taken from /fs/proc/array.c +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 +# 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 + +AddressFamily = enum.IntEnum( + 'AddressFamily', {'AF_LINK': int(socket.AF_PACKET)} +) +AF_LINK = AddressFamily.AF_LINK + + +# 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 + "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, } -# ===================================================================== -# --- named tuples -# ===================================================================== - - -# 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']) - - # ===================================================================== # --- utils # ===================================================================== -def open_binary(fname, **kwargs): - return open(fname, "rb", **kwargs) - - -def open_text(fname, **kwargs): - """On Python 3 opens a file in text mode by using fs encoding and - a proper en/decoding errors handler. - On Python 2 this is just an alias for open(name, 'rt'). - """ - if PY3: - # See: - # https://github.com/giampaolo/psutil/issues/675 - # https://github.com/giampaolo/psutil/pull/733 - kwargs.setdefault('encoding', ENCODING) - kwargs.setdefault('errors', ENCODING_ERRS) - return open(fname, "rt", **kwargs) - - -if PY3: - def decode(s): - return s.decode(encoding=ENCODING, errors=ENCODING_ERRS) -else: - def decode(s): - return s - - -def get_procfs_path(): - """Return updated psutil.PROCFS_PATH constant.""" - return sys.modules['psutil'].PROCFS_PATH - - def readlink(path): """Wrapper around os.readlink().""" - assert isinstance(path, basestring), path + 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 @@ -247,66 +158,22 @@ 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 is_storage_device(name): + """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. """ - try: - with open("/sys/block/%s/queue/hw_sector_size" % partition, "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 - - -@memoize -def set_scputimes_ntuple(procfs_path): - """Set 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('%s/stat' % procfs_path) as f: - values = f.readline().split()[1:] - fields = ['user', 'nice', 'system', 'idle', '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') - 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: - if fallback is not _DEFAULT: - return fallback - else: - raise - - -try: - set_scputimes_ntuple("/proc") -except Exception: - # Don't want to crash at import time. - traceback.print_exc() - scputimes = namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0) + # Re-adapted from iostat source code, see: + # 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 + if including_virtual: + path = f"/sys/block/{name}" + else: + path = f"/sys/block/{name}/device" + return os.access(path, os.F_OK) # ===================================================================== @@ -316,38 +183,48 @@ def cat(fname, fallback=_DEFAULT, binary=True): def calculate_avail_vmem(mems): """Fallback for kernels < 3.14 where /proc/meminfo does not provide - "MemAvailable:" column, see: - https://blog.famzah.net/2014/09/24/ + "MemAvailable", see: + https://blog.famzah.net/2014/09/24/. + This code reimplements the algorithm outlined here: https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/ commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773 - XXX: on recent kernels this calculation differs by ~1.5% than - "MemAvailable:" as it's calculated slightly differently, see: - https://gitlab.com/procps-ng/procps/issues/42 - https://github.com/famzah/linux-memavailable-procfs/issues/2 + We use this function also when "MemAvailable" returns 0 (possibly a + kernel bug, see: https://github.com/giampaolo/psutil/issues/1915). + In that case this routine matches "free" CLI tool result ("available" + column). + + XXX: on recent kernels this calculation may differ by ~1.5% compared + to "MemAvailable:", as it's calculated slightly differently. It is still way more realistic than doing (free + cached) though. + See: + * https://gitlab.com/procps-ng/procps/issues/42 + * https://github.com/famzah/linux-memavailable-procfs/issues/2 """ - # Fallback for very old distros. According to - # 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 + # Note about "fallback" value. According to: + # 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 + # "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( + 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()) - except IOError: + f = open_binary(f"{get_procfs_path()}/zoneinfo") + except OSError: return fallback # kernel 2.6.13 watermark_low = 0 @@ -357,7 +234,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 @@ -369,23 +245,15 @@ 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 mimics 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 = {} - 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 @@ -410,8 +278,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: @@ -433,10 +300,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') @@ -446,12 +314,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. @@ -463,36 +325,55 @@ 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/24fd2605c51fcc/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. 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") - warnings.warn(msg, RuntimeWarning) - - return svmem(total, avail, percent, used, free, - active, inactive, buffers, cached, shared, slab) + "was" if len(missing_fields) == 1 else "were", + ) + warnings.warn(msg, RuntimeWarning, stacklevel=2) + + return ntp.svmem( + total, + avail, + percent, + used, + free, + active, + inactive, + buffers, + cached, + shared, + slab, + ) 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 @@ -512,12 +393,14 @@ def swap_memory(): percent = usage_percent(used, total, round_=1) # get pgin/pgouts try: - f = open_binary("%s/vmstat" % get_procfs_path()) - except IOError as err: + 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) - warnings.warn(msg, RuntimeWarning) + msg = ( + "'sin' and 'sout' swap memory stats couldn't " + f"be determined and were set to 0 ({err})" + ) + warnings.warn(msg, RuntimeWarning, stacklevel=2) sin = sout = 0 else: with f: @@ -535,11 +418,17 @@ def swap_memory(): # we might get here when dealing with exotic Linux # flavors, see: # https://github.com/giampaolo/psutil/issues/313 - msg = "'sin' and 'sout' swap memory stats couldn't " \ - "be determined and were set to 0" - warnings.warn(msg, RuntimeWarning) + msg = "'sin' and 'sout' swap memory stats couldn't " + msg += "be determined and were set to 0" + warnings.warn(msg, RuntimeWarning, stacklevel=2) sin = sout = 0 - return _common.sswap(total, used, free, percent, sin, sout) + 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 # ===================================================================== @@ -548,37 +437,51 @@ 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, - [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() - 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] - return scputimes(*fields) + nfields = len(ntp.scputimes._fields) + raw = [float(x) / CLOCK_TICKS for x in values[1 : nfields + 1]] + 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(): - """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() - set_scputimes_ntuple(procfs_path) cpus = [] - with open_binary('%s/stat' % procfs_path) as f: + 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(scputimes._fields) + 1] - fields = [float(x) / CLOCK_TICKS for x in fields] - entry = 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 @@ -590,7 +493,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 @@ -600,7 +503,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): @@ -612,34 +515,51 @@ def cpu_count_logical(): return num -def cpu_count_physical(): - """Return the number of physical cores in the system.""" +def cpu_count_cores(): + """Return the number of CPU cores in the system.""" + # Method #1 + ls = set() + # These 2 files are the same but */core_cpus_list is newer while + # */thread_siblings_list is deprecated and may disappear in the future. + # https://www.kernel.org/doc/Documentation/admin-guide/cputopology.rst + # https://github.com/giampaolo/psutil/pull/1727#issuecomment-707624964 + # https://lkml.org/lkml/2019/2/26/41 + p1 = "/sys/devices/system/cpu/cpu[0-9]*/topology/core_cpus_list" + p2 = "/sys/devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list" + for path in glob.glob(p1) or glob.glob(p2): + with open_binary(path) as f: + ls.add(f.read().strip()) + result = len(ls) + if result != 0: + return result + + # Method #2 mapping = {} current_info = {} - with open_binary('%s/cpuinfo' % get_procfs_path()) as f: + with open_binary(f"{get_procfs_path()}/cpuinfo") as f: for line in f: line = line.strip().lower() if not line: # new section - if (b'physical id' in current_info and - b'cpu cores' in current_info): - mapping[current_info[b'physical id']] = \ - current_info[b'cpu cores'] + try: + mapping[current_info[b'physical id']] = current_info[ + b'cpu cores' + ] + except KeyError: + pass current_info = {} - else: + elif line.startswith((b'physical id', b'cpu cores')): # ongoing section - if (line.startswith(b'physical id') or - line.startswith(b'cpu cores')): - key, value = line.split(b'\t:', 1) - current_info[key] = int(value) + key, value = line.split(b':', 1) + current_info[key.strip()] = 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(): """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 @@ -650,66 +570,89 @@ 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) + return ntp.scpustats(ctx_switches, interrupts, soft_interrupts, syscalls) + +def _cpu_get_cpuinfo_freq(): + """Return current CPU frequency from cpuinfo if available.""" + with open_binary(f"{get_procfs_path()}/cpuinfo") as f: + 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( + "/sys/devices/system/cpu/cpu0/cpufreq" +): -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. Contrarily to other OSes, Linux updates these values in real-time. """ - # scaling_* files seem preferable to cpuinfo_*, see: - # http://unix.stackexchange.com/a/87537/168884 + cpuinfo_freqs = _cpu_get_cpuinfo_freq() + paths = glob.glob( + "/sys/devices/system/cpu/cpufreq/policy[0-9]*" + ) or glob.glob("/sys/devices/system/cpu/cpu[0-9]*/cpufreq") + paths.sort(key=lambda x: int(re.search(r"[0-9]+", x).group())) ret = [] - ls = glob.glob("/sys/devices/system/cpu/cpufreq/policy*") - if ls: - # Sort the list so that '10' comes after '2'. This should - # ensure the CPU order is consistent with other CPU functions - # having a 'percpu' argument and returning results for multiple - # CPUs (cpu_times(), cpu_percent(), cpu_times_percent()). - ls.sort(key=lambda x: int(os.path.basename(x)[6:])) - else: - # https://github.com/giampaolo/psutil/issues/981 - ls = glob.glob("/sys/devices/system/cpu/cpu[0-9]*/cpufreq") - ls.sort(key=lambda x: int(re.search('[0-9]+', x).group(0))) - pjoin = os.path.join - for path in ls: - curr = cat(pjoin(path, "scaling_cur_freq"), fallback=None) + for i, path in enumerate(paths): + if len(paths) == len(cpuinfo_freqs): + # take cached value from cpuinfo if available, see: + # https://github.com/giampaolo/psutil/issues/1851 + curr = cpuinfo_freqs[i] * 1000 + else: + curr = bcat(pjoin(path, "scaling_cur_freq"), fallback=None) if curr is None: # Likely an old RedHat, see: # https://github.com/giampaolo/psutil/issues/1071 - curr = cat(pjoin(path, "cpuinfo_cur_freq"), fallback=None) + curr = bcat(pjoin(path, "cpuinfo_cur_freq"), fallback=None) if curr is None: - raise NotImplementedError( - "can't find current frequency file") + 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(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(cat(pjoin(path, "scaling_max_freq"))) / 1000 - min_ = int(cat(pjoin(path, "scaling_min_freq"))) / 1000 - ret.append(_common.scpufreq(curr, min_, max_)) + max_ = int(bcat(pjoin(path, "scaling_max_freq"))) / 1000 + min_ = int(bcat(pjoin(path, "scaling_min_freq"))) / 1000 + ret.append(ntp.scpufreq(curr, min_, max_)) return ret +else: + + def cpu_freq(): + """Alternate implementation using /proc/cpuinfo. + min and max frequencies are not available and are set to None. + """ + return [ntp.scpufreq(x, 0.0, 0.0) for x in _cpu_get_cpuinfo_freq()] + # ===================================================================== # --- network # ===================================================================== -net_if_addrs = cext_posix.net_if_addrs +net_if_addrs = cext.net_if_addrs 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". @@ -722,6 +665,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) @@ -744,20 +689,23 @@ 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)) - except OSError as err: + 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) - if err.errno in (errno.ENOENT, errno.ESRCH): - continue - elif err.errno == errno.EINVAL: + continue + except OSError as err: + if err.errno == errno.EINVAL: # not a link continue - else: - raise + if err.errno == errno.ENAMETOOLONG: + # file name too long + debug(err) + continue + raise else: if inode.startswith('socket:['): # the process is using a socket @@ -770,7 +718,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 @@ -778,9 +726,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 @@ -806,8 +752,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: @@ -815,28 +760,25 @@ def decode_address(addr, family): else: ip = socket.inet_ntop(family, base64.b16decode(ip)) else: # IPv6 - # old version - let's keep it, just in case... - # ip = ip.decode('hex') - # return socket.inet_ntop(socket.AF_INET6, - # ''.join(ip[i:i+4][::-1] for i in xrange(0, 16, 4))) ip = base64.b16decode(ip) try: # see: https://github.com/giampaolo/psutil/issues/201 if LITTLE_ENDIAN: ip = socket.inet_ntop( socket.AF_INET6, - struct.pack('>4I', *struct.unpack('<4I', ip))) + struct.pack('>4I', *struct.unpack('<4I', ip)), + ) else: ip = socket.inet_ntop( socket.AF_INET6, - struct.pack('<4I', *struct.unpack('<4I', ip))) + struct.pack('<4I', *struct.unpack('<4I', ip)), + ) except ValueError: # see: https://github.com/giampaolo/psutil/issues/623 if not supports_ipv6(): - raise _Ipv6UnsupportedError - else: - raise - return _common.addr(ip, port) + raise _Ipv6UnsupportedError from None + raise + return ntp.addr(ip, port) @staticmethod def process_inet(file, family, type_, inodes, filter_pid=None): @@ -844,22 +786,25 @@ def process_inet(file, family, type_, inodes, filter_pid=None): if file.endswith('6') and not os.path.exists(file): # IPv6 not supported return - with open_text(file, buffering=BIGFILE_BUFFERING) as f: + with open_text(file) as f: f.readline() # skip the first line for lineno, line in enumerate(f, 1): try: - _, laddr, raddr, status, _, _, _, _, _, inode = \ + _, laddr, raddr, status, _, _, _, _, _, inode = ( line.split()[:10] + ) except ValueError: - raise RuntimeError( - "error while parsing %s; malformed line %s %r" % ( - file, lineno, line)) + msg = ( + f"error while parsing {file}; malformed line" + f" {lineno} {line!r}" + ) + 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 # # 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: @@ -870,10 +815,10 @@ 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 = 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) @@ -881,7 +826,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() @@ -891,10 +836,11 @@ 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)) - if inode in inodes: + msg = ( + f"error while parsing {file}; malformed line {line!r}" + ) + raise RuntimeError(msg) # noqa: B904 + if inode in inodes: # noqa: SIM108 # With UNIX sockets we can have a single inode # referencing many file descriptors. pairs = inodes[inode] @@ -904,22 +850,16 @@ def process_unix(file, family, inodes, filter_pid=None): if filter_pid is not None and filter_pid != pid: continue else: - if len(tokens) == 8: - path = tokens[-1] - else: - path = "" - type_ = int(type_) + path = tokens[-1] if len(tokens) == 8 else '' + type_ = 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): - 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) @@ -929,83 +869,105 @@ def retrieve(self, kind, pid=None): else: inodes = self.get_all_inodes() ret = set() - for f, family, type_ in self.tmap[kind]: - if family in (socket.AF_INET, socket.AF_INET6): + for proto_name, family, type_ in self.tmap[kind]: + path = f"{self._procfs_path}/net/{proto_name}" + if family in {socket.AF_INET, socket.AF_INET6}: ls = self.process_inet( - "%s/net/%s" % (self._procfs_path, f), - family, type_, inodes, filter_pid=pid) + path, family, type_, inodes, filter_pid=pid + ) else: - ls = self.process_unix( - "%s/net/%s" % (self._procfs_path, f), - family, inodes, filter_pid=pid) + ls = self.process_unix(path, family, inodes, filter_pid=pid) for fd, family, type_, laddr, raddr, status, bound_pid in ls: if pid: - conn = _common.pconn(fd, family, type_, laddr, raddr, - status) + conn = ntp.pconn(fd, family, type_, laddr, raddr, status) else: - conn = _common.sconn(fd, family, type_, laddr, raddr, - status, bound_pid) + conn = ntp.sconn( + fd, family, type_, laddr, raddr, status, bound_pid + ) ret.add(conn) 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(): """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:]: colon = line.rfind(':') assert colon > 0, repr(line) name = line[:colon].strip() - 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) + 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, # unused + ) = 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: NicDuplex.NIC_DUPLEX_FULL, + cext.DUPLEX_HALF: NicDuplex.NIC_DUPLEX_HALF, + cext.DUPLEX_UNKNOWN: NicDuplex.NIC_DUPLEX_UNKNOWN, + } 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.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: + raise + debug(err) + else: + output_flags = ','.join(flags) + isup = 'running' in flags + ret[name] = ntp.snicstats( + isup, duplex_map[duplex], speed, mtu, output_flags + ) return ret @@ -1017,35 +979,12 @@ 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: + 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.: @@ -1056,64 +995,206 @@ def get_partitions(): # ...unless (Linux 2.6) the line refers to a partition instead # of a disk, in which case the line has less fields (7): # "3 1 hda1 8 8 8 8" + # 4.18+ has 4 fields added: + # "3 0 hda 8 8 8 8 8 8 8 8 8 8 8 0 0 0 0" + # 5.5 has 2 more fields. # See: # https://www.kernel.org/doc/Documentation/iostats.txt # https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats - 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) - - 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) + with open_text(f"{get_procfs_path()}/diskstats") as f: + lines = f.readlines() + for line in lines: + fields = line.split() + flen = len(fields) + # fmt: off + if flen == 15: + # Linux 2.4 + name = fields[3] + reads = int(fields[2]) + (reads_merged, rbytes, rtime, writes, writes_merged, + wbytes, wtime, _, busy_time, _) = map(int, fields[4:14]) + elif flen == 14 or flen >= 18: + # 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: + 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 + + 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) + # 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(f"{get_procfs_path()}/diskstats"): + gen = read_procfs() + elif os.path.exists('/sys/block'): + gen = read_sysfs() + else: + msg = ( + f"{get_procfs_path()}/diskstats nor /sys/block are available on" + " this system" + ) + raise NotImplementedError(msg) + + 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): + # 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 + + rbytes *= DISK_SECTOR_SIZE + wbytes *= DISK_SECTOR_SIZE + retdict[name] = (reads, writes, rbytes, wbytes, rtime, wtime, + reads_merged, writes_merged, busy_time) + # fmt: on + return retdict +class RootFsDeviceFinder: + """disk_partitions() may return partitions with device == "/dev/root" + or "rootfs". This container class uses different strategies to try to + obtain the real device path. Resources: + https://bootlin.com/blog/find-root-device/ + https://www.systutorials.com/how-to-find-the-disk-where-root-is-on-in-bash-on-linux/. + """ + + __slots__ = ['major', 'minor'] + + def __init__(self): + dev = os.stat("/").st_dev + self.major = os.major(dev) + self.minor = os.minor(dev) + + def ask_proc_partitions(self): + with open_text(f"{get_procfs_path()}/partitions") 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 f"/dev/{name}" + + def ask_sys_dev_block(self): + 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 f"/dev/{name}" + + def ask_sys_class_block(self): + needle = f"{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 f"/dev/{name}" + + def find(self): + path = None + if path is None: + try: + path = self.ask_proc_partitions() + except OSError as err: + debug(err) + if path is None: + try: + path = self.ask_sys_dev_block() + except OSError as err: + debug(err) + if path is None: + try: + path = self.ask_sys_class_block() + 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. + 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.""" + """Return mounted disk partitions as a list of named tuples.""" fstypes = set() - with open_text("%s/filesystems" % get_procfs_path()) as f: - for line in f: - line = line.strip() - if not line.startswith("nodev"): - fstypes.add(line.strip()) - else: - # ignore all lines starting with "nodev" except "nodev zfs" - fstype = line.split("\t")[1] - if fstype == "zfs": - fstypes.add("zfs") + procfs_path = get_procfs_path() + if not all: + with open_text(f"{procfs_path}/filesystems") as f: + for line in f: + line = line.strip() + if not line.startswith("nodev"): + fstypes.add(line.strip()) + else: + # ignore all lines starting with "nodev" except "nodev zfs" + fstype = line.split("\t")[1] + if fstype == "zfs": + fstypes.add("zfs") + + # See: https://github.com/giampaolo/psutil/issues/1307 + if procfs_path == "/proc" and os.path.isfile('/etc/mtab'): + mounts_path = os.path.realpath("/etc/mtab") + else: + mounts_path = os.path.realpath(f"{procfs_path}/self/mounts") retlist = [] - partitions = cext.disk_partitions() + partitions = cext.disk_partitions(mounts_path) for partition in partitions: device, mountpoint, fstype, opts = partition if device == 'none': device = '' + if device in {"/dev/root", "rootfs"}: + device = RootFsDeviceFinder().find() or device if not all: - if device == '' or fstype not in fstypes: + 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 @@ -1141,37 +1222,103 @@ 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/ + # 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(r"/sys/devices/platform/coretemp.*/hwmon/") + for name in basenames2: + altname = repl.sub('/sys/class/hwmon/', name) + if altname not in basenames: + basenames.append(name) for base in basenames: try: path = base + '_input' - current = float(cat(path)) / 1000.0 + current = float(bcat(path)) / 1000.0 path = os.path.join(os.path.dirname(base), 'name') - unit_name = cat(path, binary=False) - except (IOError, OSError) as err: + unit_name = cat(path).strip() + except (OSError, ValueError): # 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 - warnings.warn("ignoring %r for file %r" % (err, path), - RuntimeWarning) + # 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: - 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)) - return ret + # Indication that no sensors were detected in /sys/class/hwmon/ + if not basenames: + basenames = glob.glob('/sys/class/thermal/thermal_zone*') + basenames = sorted(set(basenames)) + + for base in basenames: + try: + path = os.path.join(base, 'temp') + current = float(bcat(path)) / 1000.0 + path = os.path.join(base, 'type') + unit_name = cat(path).strip() + except (OSError, ValueError) as err: + debug(err) + continue + + trip_paths = glob.glob(base + '/trip_point*') + 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: + path = os.path.join(base, trip_point + "_type") + trip_type = cat(path, fallback='').strip() + if trip_type == 'critical': + critical = bcat( + os.path.join(base, trip_point + "_temp"), fallback=None + ) + elif trip_type == 'high': + high = bcat( + os.path.join(base, trip_point + "_temp"), fallback=None + ) + + if high is not None: + try: + high = float(high) / 1000.0 + except ValueError: + high = None + if critical is not None: + try: + critical = float(critical) / 1000.0 + except ValueError: + critical = None + + ret[unit_name].append(('', current, high, critical)) + + return dict(ret) def sensors_fans(): @@ -1191,17 +1338,16 @@ 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(cat(base + '_input')) - except (IOError, OSError) as err: - warnings.warn("ignoring %r" % err, RuntimeWarning) + current = int(bcat(base + '_input')) + except 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) - ret[unit_name].append(_common.sfan(label, current)) + unit_name = cat(os.path.join(os.path.dirname(base), 'name')).strip() + label = cat(base + '_label', fallback='').strip() + ret[unit_name].append(ntp.sfan(label, current)) return dict(ret) @@ -1211,50 +1357,50 @@ def sensors_battery(): Implementation note: it appears /sys/class/power_supply/BAT0/ directory structure may vary and provide files with the same meaning but under different names, see: - https://github.com/giampaolo/psutil/issues/966 + https://github.com/giampaolo/psutil/issues/966. """ null = object() - def multi_cat(*paths): + def multi_bcat(*paths): """Attempt to read the content of multiple files which may not exist. If none of them exist return None. """ for path in paths: - ret = cat(path, fallback=null) + ret = bcat(path, fallback=null) if ret != null: - return int(ret) if ret.isdigit() else ret + try: + return int(ret) + except ValueError: + return ret.strip() return None - bats = [x for x in os.listdir(POWER_SUPPLY_PATH) if x.startswith('BAT')] + bats = [ + x + for x in os.listdir(POWER_SUPPLY_PATH) + if x.startswith('BAT') or 'battery' in x.lower() + ] if not bats: return None # Get the first available battery. Usually this is "BAT0", except # 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_cat( - root + "/energy_now", - root + "/charge_now") - power_now = multi_cat( - root + "/power_now", - root + "/current_now") - energy_full = multi_cat( - root + "/energy_full", - root + "/charge_full") - if energy_now is None or power_now is None: - return None + energy_now = multi_bcat(root + "/energy_now", root + "/charge_now") + power_now = multi_bcat(root + "/power_now", root + "/current_now") + energy_full = multi_bcat(root + "/energy_full", root + "/charge_full") + time_to_empty = multi_bcat(root + "/time_to_empty_now") # Percent. If we have energy_full the percentage will be more # accurate compared to reading /capacity file (float vs. int). - if energy_full is not None: + if energy_full is not None and energy_now is not None: try: percent = 100.0 * energy_now / energy_full except ZeroDivisionError: percent = 0.0 else: - percent = int(cat(root + "/capacity", fallback=-1)) + percent = float(cat(root + "/capacity", fallback=-1)) if percent == -1: return None @@ -1262,31 +1408,35 @@ def multi_cat(*paths): # Note: AC0 is not always available and sometimes (e.g. CentOS7) # it's called "AC". power_plugged = None - online = multi_cat( + online = multi_bcat( os.path.join(POWER_SUPPLY_PATH, "AC0/online"), - os.path.join(POWER_SUPPLY_PATH, "AC/online")) + os.path.join(POWER_SUPPLY_PATH, "AC/online"), + ) if online is not None: power_plugged = online == 1 else: - status = cat(root + "/status", fallback="", binary=False).lower() + status = cat(root + "/status", fallback="").strip().lower() if status == "discharging": power_plugged = False - elif status in ("charging", "full"): + elif status in {"charging", "full"}: 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 - else: + secsleft = BatteryTime.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 + secsleft = BatteryTime.POWER_TIME_UNKNOWN + elif time_to_empty is not None: + secsleft = int(time_to_empty * 60) + if secsleft < 0: + secsleft = BatteryTime.POWER_TIME_UNKNOWN + else: + secsleft = BatteryTime.POWER_TIME_UNKNOWN - return _common.sbattery(percent, secsleft, power_plugged) + return ntp.sbattery(percent, secsleft, power_plugged) # ===================================================================== @@ -1295,35 +1445,25 @@ def multi_cat(*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: - 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' - nt = _common.suser(user, tty or None, hostname, tstamp, pid) + user, tty, hostname, tstamp, pid = item + nt = ntp.suser(user, tty or None, hostname, tstamp, pid) retlist.append(nt) return retlist 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) + return float(line.strip().split()[1]) + msg = f"line 'btime' not found in {path}" + raise RuntimeError(msg) # ===================================================================== @@ -1333,7 +1473,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): @@ -1355,7 +1496,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:"): @@ -1363,8 +1504,9 @@ 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) - except (EnvironmentError, ValueError): + msg = f"'Tgid' line not found in {path}" + raise ValueError(msg) + except (OSError, ValueError): return pid in pids() @@ -1376,136 +1518,193 @@ 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 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): - raise + except (FileNotFoundError, ProcessLookupError): + pass + except PermissionError as err: + raise AccessDenied(pid) from err else: rpar = data.rfind(b')') - dset = data[rpar + 2:].split() + dset = data[rpar + 2 :].split() ppid = int(dset[1]) ret[pid] = ppid return ret def wrap_exceptions(fun): - """Decorator which translates bare OSError and IOError exceptions - into NoSuchProcess and AccessDenied. + """Decorator which translates bare OSError exceptions into + NoSuchProcess and AccessDenied. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): + pid, name = self.pid, self._name 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)): - 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. + except PermissionError as err: + raise AccessDenied(pid, name) from err + except ProcessLookupError as err: + self._raise_if_zombie() + 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}/{pid}/stat"): + raise NoSuchProcess(pid, name) from err raise + return wrapper -class Process(object): +class Process: """Linux process implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_procfs_path"] + __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): + # 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(f"{self._procfs_path}/{self.pid}/stat") + except OSError: + return False + else: + rpar = data.rfind(b')') + status = data[rpar + 2 : rpar + 3] + return status == b"Z" + + def _raise_if_zombie(self): + if self._is_zombie(): + raise ZombieProcess(self.pid, self._name, self._ppid) + + def _raise_if_not_alive(self): + """Raise NSP if the process disappeared on us.""" + # For those C function who do not raise NSP, possibly returning + # incorrect or incomplete result. + os.stat(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): - """Parse /proc/{pid}/stat file. Return a list of fields where - process name is in position 0. + """Parse /proc/{pid}/stat file and return a dict with various + process info. Using "man proc" as a reference: where "man proc" refers to - position N, always substract 2 (e.g starttime pos 22 in - 'man proc' == pos 20 in the list returned here). + position N always subtract 3 (e.g ppid position 4 in + 'man proc' == position 1 in here). The return value is cached in case oneshot() ctx manager is in use. """ - with open_binary("%s/%s/stat" % (self._procfs_path, self.pid)) as f: - data = f.read() + data = bcat(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 occurence of ")". + # the first occurrence of "(" and the last occurrence of ")". rpar = data.rfind(b')') - name = data[data.find(b'(') + 1:rpar] - others = data[rpar + 2:].split() - return [name] + others + name = data[data.find(b'(') + 1 : rpar] + fields = data[rpar + 2 :].split() + + ret = {} + ret['name'] = name + ret['status'] = fields[0] + ret['ppid'] = fields[1] + ret['ttynr'] = fields[4] + ret['minflt'] = fields[7] + ret['majflt'] = fields[9] + 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] + 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 + @wrap_exceptions @memoize_when_activated def _read_status_file(self): """Read /proc/{pid}/stat file and return its content. 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), - buffering=BIGFILE_BUFFERING) as f: + with open_binary(f"{self._procfs_path}/{self.pid}/smaps") as f: return f.read().strip() def oneshot_enter(self): - self._parse_stat_file.cache_activate() - self._read_status_file.cache_activate() - self._read_smaps_file.cache_activate() + self._parse_stat_file.cache_activate(self) + self._read_status_file.cache_activate(self) + self._read_smaps_file.cache_activate(self) def oneshot_exit(self): - self._parse_stat_file.cache_deactivate() - self._read_status_file.cache_deactivate() - self._read_smaps_file.cache_deactivate() + self._parse_stat_file.cache_deactivate(self) + self._read_status_file.cache_deactivate(self) + self._read_smaps_file.cache_deactivate(self) @wrap_exceptions def name(self): - name = self._parse_stat_file()[0] - 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): - try: - return readlink("%s/%s/exe" % (self._procfs_path, self.pid)) - except OSError as err: - if err.errno in (errno.ENOENT, errno.ESRCH): - # no such file error; might be raised also if the - # path actually exists for system processes with - # low pids (about 0-20) - if os.path.lexists("%s/%s" % (self._procfs_path, self.pid)): - return "" - else: - if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - else: - raise ZombieProcess(self.pid, self._name, self._ppid) - if err.errno in (errno.EPERM, errno.EACCES): - raise AccessDenied(self.pid, self._name) - raise + return self._readlink( + f"{self._procfs_path}/{self.pid}/exe", fallback="" + ) @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 + 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 @@ -1517,78 +1716,104 @@ def cmdline(self): sep = '\x00' if data.endswith('\x00') else ' ' if data.endswith(sep): data = data[:-1] - return [x for x in data.split(sep)] + cmdline = data.split(sep) + # Sometimes last char is a null byte '\0' but the args are + # separated by spaces, see: https://github.com/giampaolo/psutil/ + # issues/1179#issuecomment-552984549 + if sep == '\x00' and len(cmdline) == 1 and ' ' in data: + cmdline = data.split(' ') + return cmdline @wrap_exceptions def environ(self): - 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) @wrap_exceptions def terminal(self): - tty_nr = int(self._parse_stat_file()[5]) + tty_nr = int(self._parse_stat_file()['ttynr']) tmap = _psposix.get_terminal_map() try: return tmap[tty_nr] except KeyError: return None - if os.path.exists('/proc/%s/io' % os.getpid()): + # May not be available on old kernels. + 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: # https://github.com/giampaolo/psutil/issues/1004 line = line.strip() if line: - name, value = line.split(b': ') - fields[name] = int(value) + try: + name, value = line.split(b': ') + except ValueError: + # https://github.com/giampaolo/psutil/issues/1004 + continue + else: + fields[name] = int(value) if not fields: - raise RuntimeError("%s file was empty" % fname) - return pio( - fields[b'syscr'], # read syscalls - fields[b'syscw'], # write syscalls - fields[b'read_bytes'], # read bytes - fields[b'write_bytes'], # write bytes - fields[b'rchar'], # read chars - fields[b'wchar'], # write chars - ) - else: - def io_counters(self): - raise NotImplementedError("couldn't find /proc/%s/io (kernel " - "too old?)" % self.pid) + msg = f"{fname} file was empty" + raise RuntimeError(msg) + try: + return ntp.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: + msg = ( + f"{err.args[0]!r} field was not found in {fname}; found" + f" fields are {fields!r}" + ) + raise ValueError(msg) from None @wrap_exceptions def cpu_times(self): values = self._parse_stat_file() - utime = float(values[12]) / CLOCK_TICKS - stime = float(values[13]) / CLOCK_TICKS - children_utime = float(values[14]) / CLOCK_TICKS - children_stime = float(values[15]) / CLOCK_TICKS - return _common.pcputimes(utime, stime, children_utime, children_stime) + utime = float(values['utime']) / CLOCK_TICKS + stime = float(values['stime']) / CLOCK_TICKS + children_utime = float(values['children_utime']) / CLOCK_TICKS + children_stime = float(values['children_stime']) / CLOCK_TICKS + iowait = float(values['blkio_ticks']) / CLOCK_TICKS + return ntp.pcputimes( + utime, stime, children_utime, children_stime, iowait + ) @wrap_exceptions def cpu_num(self): """What CPU the process is on.""" - return int(self._parse_stat_file()[37]) + return int(self._parse_stat_file()['cpu_num']) @wrap_exceptions def wait(self, timeout=None): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) @wrap_exceptions - def create_time(self): - values = self._parse_stat_file() - # 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 + def create_time(self, monotonic=False): + # 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 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): @@ -1603,27 +1828,81 @@ 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: - 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) + 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] + ) + return ntp.pmem(rss, vms, shared, text, data) - # /proc/pid/smaps does not exist on kernels < 2.6.14 or if - # CONFIG_MMU kernel configuration option is not enabled. - if HAS_SMAPS: + @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): + # /proc/pid/smaps_rollup was added to Linux in 2017. Faster + # than /proc/pid/smaps. It reports higher PSS than */smaps + # (from 1k up to 200k higher; tested against all processes). + # IMPORTANT: /proc/pid/smaps_rollup is weird, because it + # raises ESRCH / ENOENT for many PIDs, even if they're alive + # (also as root). In that case we'll use /proc/pid/smaps as + # fallback, which is slower but has a +50% success rate + # compared to /proc/pid/smaps_rollup. + uss = pss = swap = 0 + with open_binary( + f"{self._procfs_path}/{self.pid}/smaps_rollup" + ) as f: + for line in f: + if line.startswith(b"Private_"): + # Private_Clean, Private_Dirty, Private_Hugetlb + uss += int(line.split()[1]) * 1024 + elif line.startswith(b"Pss:"): + pss = int(line.split()[1]) * 1024 + elif line.startswith(b"Swap:"): + swap = int(line.split()[1]) * 1024 + return (uss, pss, swap) @wrap_exceptions - def memory_full_info( - self, - # Gets Private_Clean, Private_Dirty, Private_Hugetlb. - _private_re=re.compile(br"\nPrivate.*:\s+(\d+)"), - _pss_re=re.compile(br"\nPss\:\s+(\d+)"), - _swap_re=re.compile(br"\nSwap\:\s+(\d+)")): - basic_mem = self.memory_info() + def _parse_smaps( + self, + # Gets Private_Clean, Private_Dirty, Private_Hugetlb. + _private_re=re.compile(br"\nPrivate.*:\s+(\d+)"), + _pss_re=re.compile(br"\nPss\:\s+(\d+)"), + _swap_re=re.compile(br"\nSwap\:\s+(\d+)"), + ): + # /proc/pid/smaps does not exist on kernels < 2.6.14 or if + # CONFIG_MMU kernel configuration option is not enabled. + # Note: using 3 regexes is faster than reading the file # line by line. - # XXX: on Python 3 the 2 regexes are 30% slower than on - # Python 2 though. Figure out why. # # You might be tempted to calculate USS by subtracting # the "shared" value from the "resident" value in @@ -1638,19 +1917,33 @@ 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 pfullmem(*basic_mem + (uss, pss, swap)) + return (uss, pss, swap) - else: - memory_full_info = memory_info + @wrap_exceptions + 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() - if HAS_SMAPS: + uss, pss, swap = fetch() + return ntp.pfootprint(uss, pss, swap) + + if HAS_PROC_SMAPS: @wrap_exceptions def memory_maps(self): """Return process's mapped memory regions as a list of named tuples. Fields are explained in 'man proc'; here is an updated - (Apr 2012) version: http://goo.gl/fmebo + (Apr 2012) version: 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. """ + def get_blocks(lines, current_block): data = {} for line in lines: @@ -1662,18 +1955,19 @@ 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 - else: - raise ValueError("don't know how to inte" - "rpret line %r" % line) + 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() - # 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 = [] @@ -1682,22 +1976,23 @@ 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: - if PY3: - path = decode(path) + path = decode(path) path = path.strip() - if (path.endswith(' (deleted)') and not - path_exists_strict(path)): + if path.endswith(' (deleted)') and not path_exists_strict( + path + ): path = path[:-10] - ls.append(( - decode(addr), decode(perms), path, - data[b'Rss:'], + item = ( + decode(addr), + decode(perms), + path, + data.get(b'Rss:', 0), data.get(b'Size:', 0), data.get(b'Pss:', 0), data.get(b'Shared_Clean:', 0), @@ -1706,227 +2001,208 @@ 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), + ) + ls.append(item) 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 page_faults(self): + values = self._parse_stat_file() + return ntp.ppagefaults(int(values['minflt']), int(values['majflt'])) @wrap_exceptions def cwd(self): - try: - return readlink("%s/%s/cwd" % (self._procfs_path, self.pid)) - except OSError as err: - # https://github.com/giampaolo/psutil/issues/986 - if err.errno in (errno.ENOENT, errno.ESRCH): - if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - else: - raise ZombieProcess(self.pid, self._name, self._ppid) - raise + return self._readlink( + f"{self._procfs_path}/{self.pid}/cwd", fallback="" + ) @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)) - else: - return _common.pctxsw(int(ctxsw[0]), int(ctxsw[1])) + 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) + return ntp.pctxsw(int(ctxsw[0]), int(ctxsw[1])) @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]) @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() - except IOError as err: - if err.errno == errno.ENOENT: - # no such file or directory; it means thread - # disappeared on us - hit_enoent = True - continue - raise + except (FileNotFoundError, ProcessLookupError): + # no such file or directory or no such process; + # it means thread disappeared on us + hit_enoent = True + continue # ignore the first two values ("pid (exe)") - st = st[st.find(b')') + 2:] + st = st[st.find(b')') + 2 :] values = st.split(b' ') utime = float(values[11]) / CLOCK_TICKS stime = float(values[12]) / CLOCK_TICKS - ntuple = _common.pthread(int(thread_id), utime, stime) + ntuple = ntp.pthread(int(thread_id), utime, stime) retlist.append(ntuple) if hit_enoent: - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (self._procfs_path, self.pid)) + self._raise_if_not_alive() return retlist @wrap_exceptions 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]) # 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) - @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: + msg = ( + f"invalid CPU {cpu!r}; choose between" + f" {eligible_cpus!r}" + ) + 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) from err + 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): ioclass, value = cext.proc_ioprio_get(self.pid) - if enum is not None: - ioclass = IOPriority(ioclass) - return _common.pionice(ioclass, value) + ioclass = ProcessIOPriority(ioclass) + return ntp.pionice(ioclass, value) @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) - + if value and ioclass in { + ProcessIOPriority.IOPRIO_CLASS_IDLE, + ProcessIOPriority.IOPRIO_CLASS_NONE, + }: + 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) return cext.proc_ioprio_set(self.pid, ioclass, value) - if HAS_PRLIMIT: + if hasattr(resource, "prlimit"): + @wrap_exceptions - def rlimit(self, resource, limits=None): + def rlimit(self, resource_, limits=None): # If pid is 0 prlimit() applies to the calling process and # we don't want that. We should never get here though as # PID 0 is not supported on Linux. if self.pid == 0: - raise ValueError("can't use prlimit() against PID 0 process") + msg = "can't use prlimit() against PID 0 process" + raise ValueError(msg) try: if limits is None: # get - return cext.linux_prlimit(self.pid, resource) + return resource.prlimit(self.pid, resource_) else: # set if len(limits) != 2: - raise ValueError( - "second argument must be a (soft, hard) tuple, " - "got %s" % repr(limits)) - soft, hard = limits - cext.linux_prlimit(self.pid, resource, soft, hard) + msg = ( + "second argument must be a (soft, hard) " + f"tuple, got {limits!r}" + ) + raise ValueError(msg) + resource.prlimit(self.pid, resource_, limits) except OSError as err: - if err.errno == errno.ENOSYS and pid_exists(self.pid): + if err.errno == errno.ENOSYS: # I saw this happening on Travis: # https://travis-ci.org/giampaolo/psutil/jobs/51368273 - raise ZombieProcess(self.pid, self._name, self._ppid) - else: - raise + self._raise_if_zombie() + raise @wrap_exceptions def status(self): - letter = self._parse_stat_file()[1] - if PY3: - letter = letter.decode() + letter = self._parse_stat_file()['status'] + letter = letter.decode() # XXX is '?' legit? (we're not supposed to return it anyway) return PROC_STATUSES.get(letter, '?') @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 OSError as err: + except (FileNotFoundError, ProcessLookupError): # ENOENT == file which is gone in the meantime - if err.errno in (errno.ENOENT, errno.ESRCH): - hit_enoent = True - continue - elif err.errno == errno.EINVAL: + hit_enoent = True + continue + except OSError as err: + if err.errno == errno.EINVAL: # not a link continue - else: - raise + if err.errno == errno.ENAMETOOLONG: + # file name too long + debug(err) + continue + raise else: # If path is not an absolute there's no way to tell # whether it's a regular file or not, so we skip it. @@ -1934,53 +2210,47 @@ 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]) flags = int(f.readline().split()[1], 8) - except IOError as err: - if err.errno == errno.ENOENT: - # fd gone in the meantime; does not - # necessarily mean the process disappeared - # on us. - hit_enoent = True - else: - raise + except (FileNotFoundError, ProcessLookupError): + # fd gone in the meantime; process may + # still be alive + hit_enoent = True else: mode = file_flags_to_mode(flags) - ntuple = popenfile( - path, int(fd), int(pos), mode, flags) + ntuple = ntp.popenfile( + path, int(fd), int(pos), mode, flags + ) retlist.append(ntuple) if hit_enoent: - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (self._procfs_path, self.pid)) + self._raise_if_not_alive() return retlist @wrap_exceptions - def connections(self, kind='inet'): - ret = _connections.retrieve(kind, self.pid) - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (self._procfs_path, self.pid)) + def net_connections(self, kind='inet'): + ret = _net_connections.retrieve(kind, self.pid) + self._raise_if_not_alive() return ret @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): - 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+)')): 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 193f63e002..bdb914063f 100644 --- a/psutil/_psosx.py +++ b/psutil/_psosx.py @@ -2,31 +2,29 @@ # 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 contextlib import errno import functools import os -from socket import AF_INET -from collections import namedtuple -from . import _common +from . import _ntuples as ntp from . import _psposix from . import _psutil_osx as cext -from . import _psutil_posix as cext_posix -from ._common import AF_INET6 +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import debug from ._common import isfile_strict from ._common import memoize_when_activated from ._common import parse_environ_block -from ._common import sockfam_to_enum -from ._common import socktype_to_enum from ._common import usage_percent -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess - +from ._enums import BatteryTime +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessStatus __extra__all__ = [] @@ -36,81 +34,32 @@ # ===================================================================== -PAGESIZE = os.sysconf("SC_PAGE_SIZE") -AF_LINK = cext_posix.AF_LINK +PAGESIZE = cext.getpagesize() +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, } -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, - pfaults=4, - pageins=5, - numthreads=6, - volctxsw=7, -) - - -# ===================================================================== -# --- named tuples -# ===================================================================== - - -# 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', )) -# 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)) - # ===================================================================== # --- memory @@ -118,20 +67,24 @@ def virtual_memory(): - """System virtual memory as a namedtuple.""" - total, active, inactive, wired, free = cext.virtual_mem() - avail = inactive + free - used = active + inactive + wired - percent = usage_percent((total - avail), total, round_=1) - return svmem(total, avail, percent, used, free, - active, inactive, wired) + """System virtual memory as a named tuple.""" + d = cext.virtual_mem() + d["percent"] = usage_percent( + (d["total"] - d["available"]), d["total"], round_=1 + ) + return ntp.svmem(**d) 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) + d = cext.swap_mem() + d["percent"] = usage_percent(d["used"], d["total"], round_=1) + return ntp.sswap(**d) + + +# malloc / heap functions +heap_info = cext.heap_info +heap_trim = cext.heap_trim # ===================================================================== @@ -140,17 +93,17 @@ 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 scputimes(user, nice, system, idle) + return ntp.scputimes(user, system, idle, nice) 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 - item = scputimes(user, nice, system, idle) + item = ntp.scputimes(user, system, idle, nice) ret.append(item) return ret @@ -160,26 +113,28 @@ def cpu_count_logical(): return cext.cpu_count_logical() -def cpu_count_physical(): - """Return the number of physical CPUs in the system.""" - return cext.cpu_count_phys() +def cpu_count_cores(): + """Return the number of CPU cores in the system.""" + return cext.cpu_count_cores() def cpu_stats(): - ctx_switches, interrupts, soft_interrupts, syscalls, traps = \ + ctx_switches, interrupts, soft_interrupts, syscalls, _traps = ( cext.cpu_stats() - return _common.scpustats( - ctx_switches, interrupts, soft_interrupts, syscalls) + ) + return ntp.scpustats(ctx_switches, interrupts, soft_interrupts, syscalls) -def cpu_freq(): - """Return CPU frequency. - On OSX 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 [ntp.scpufreq(curr, min_, max_)] # ===================================================================== @@ -192,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: @@ -202,7 +157,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 @@ -213,8 +168,7 @@ def disk_partitions(all=False): def sensors_battery(): - """Return battery information. - """ + """Return battery information.""" try: percent, minsleft, power_plugged = cext.sensors_battery() except NotImplementedError: @@ -222,12 +176,12 @@ 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 _common.sbattery(percent, secsleft, power_plugged) + return ntp.sbattery(percent, secsleft, power_plugged) # ===================================================================== @@ -236,24 +190,24 @@ 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'): """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(): try: - cons = Process(pid).connections(kind) + cons = Process(pid).net_connections(kind) except NoSuchProcess: continue else: if cons: for c in cons: c = list(c) + [pid] - ret.append(_common.sconn(*c)) + ret.append(ntp.sconn(*c)) return ret @@ -262,12 +216,19 @@ 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.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: + raise + else: + duplex = NicDuplex(duplex) + output_flags = ','.join(flags) + isup = 'running' in flags + ret[name] = ntp.snicstats(isup, duplex, speed, mtu, output_flags) return ret @@ -281,8 +242,31 @@ 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.""" + """Return currently connected users as a list of named tuples.""" retlist = [] rawlist = cext.users() for item in rawlist: @@ -291,7 +275,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 @@ -304,16 +288,16 @@ 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: 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 @@ -324,127 +308,93 @@ def wrap_exceptions(fun): """Decorator which translates bare OSError exceptions into NoSuchProcess and AccessDenied. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): + pid, ppid, name = self.pid, self._ppid, self._name 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 - return wrapper + except ProcessLookupError as err: + 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 - -@contextlib.contextmanager -def catch_zombie(proc): - """There are some poor C APIs which incorrectly raise ESRCH when - the process is still alive or it's a zombie, or even RuntimeError - (those who don't set errno). This is here in order to solve: - https://github.com/giampaolo/psutil/issues/1044 - """ - try: - yield - except (OSError, RuntimeError) as err: - if isinstance(err, RuntimeError) or err.errno == errno.ESRCH: - try: - # status() is not supposed to lie and correctly detect - # zombies so if it raises ESRCH it's true. - status = proc.status() - except NoSuchProcess: - raise err - else: - if status == _common.STATUS_ZOMBIE: - raise ZombieProcess(proc.pid, proc._name, proc._ppid) - else: - raise AccessDenied(proc.pid, proc._name) - else: - raise + return wrapper -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid"] + __slots__ = ["_cache", "_name", "_ppid", "pid"] def __init__(self, pid): self.pid = pid self._name = None self._ppid = None + @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) - assert len(ret) == len(kinfo_proc_map) - return ret + return cext.proc_oneshot_kinfo(self.pid) + @wrap_exceptions @memoize_when_activated - def _get_pidtaskinfo(self): + def _oneshot_pidtaskinfo(self): # Note: should work for PIDs owned by user only. - with catch_zombie(self): - ret = cext.proc_pidtaskinfo_oneshot(self.pid) - assert len(ret) == len(pidtaskinfo_map) - return ret + return cext.proc_oneshot_pidtaskinfo(self.pid) def oneshot_enter(self): - self._get_kinfo_proc.cache_activate() - self._get_pidtaskinfo.cache_activate() + self._oneshot_kinfo.cache_activate(self) + self._oneshot_pidtaskinfo.cache_activate(self) def oneshot_exit(self): - self._get_kinfo_proc.cache_deactivate() - self._get_pidtaskinfo.cache_deactivate() + 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()["name"] return name if name is not None else cext.proc_name(self.pid) @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): - self._ppid = self._get_kinfo_proc()[kinfo_proc_map['ppid']] + self._ppid = self._oneshot_kinfo()["ppid"] return self._ppid @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): - rawtuple = self._get_kinfo_proc() - return _common.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._get_kinfo_proc() - return _common.puids( - rawtuple[kinfo_proc_map['rgid']], - rawtuple[kinfo_proc_map['egid']], - rawtuple[kinfo_proc_map['sgid']]) + d = self._oneshot_kinfo() + return ntp.pgids(d["rgid"], d["egid"], d["sgid"]) @wrap_exceptions def terminal(self): - tty_nr = self._get_kinfo_proc()[kinfo_proc_map['ttynr']] + tty_nr = self._oneshot_kinfo()["ttynr"] tmap = _psposix.get_terminal_map() try: return tmap[tty_nr] @@ -453,78 +403,70 @@ def terminal(self): @wrap_exceptions def memory_info(self): - rawtuple = self._get_pidtaskinfo() - return pmem( - rawtuple[pidtaskinfo_map['rss']], - rawtuple[pidtaskinfo_map['vms']], - rawtuple[pidtaskinfo_map['pfaults']], - rawtuple[pidtaskinfo_map['pageins']], - ) + d = self._oneshot_pidtaskinfo() + return ntp.pmem(d["rss"], d["vms"]) + + @wrap_exceptions + def memory_info_ex(self): + return cext.proc_memory_info_ex(self.pid) @wrap_exceptions - def memory_full_info(self): - basic_mem = self.memory_info() + def memory_footprint(self): uss = cext.proc_memory_uss(self.pid) - return pfullmem(*basic_mem + (uss, )) + return ntp.pfootprint(uss) + + @wrap_exceptions + def page_faults(self): + d = self._oneshot_pidtaskinfo() + return ntp.ppagefaults(d["minor_faults"], d["major_faults"]) @wrap_exceptions def cpu_times(self): - rawtuple = self._get_pidtaskinfo() - return _common.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): - return self._get_kinfo_proc()[kinfo_proc_map['ctime']] + def create_time(self, monotonic=False): + ctime = self._oneshot_kinfo()["ctime"] + if not monotonic: + ctime = adjust_proc_create_time(ctime) + return ctime @wrap_exceptions 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']] - return _common.pctxsw(vol, 0) + vol = self._oneshot_pidtaskinfo()["volctxsw"] + return ntp.pctxsw(vol, 0) @wrap_exceptions def num_threads(self): - return self._get_pidtaskinfo()[pidtaskinfo_map['numthreads']] + return self._oneshot_pidtaskinfo()["num_threads"] @wrap_exceptions 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) + ntuple = ntp.popenfile(path, fd) files.append(ntuple) return files @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]))) + def net_connections(self, kind='inet'): families, types = conn_tmap[kind] - with catch_zombie(self): - 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 - 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 @@ -532,40 +474,31 @@ 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): - return _psposix.wait_pid(self.pid, timeout, self._name) + return _psposix.wait_pid(self.pid, timeout) @wrap_exceptions def nice_get(self): - with catch_zombie(self): - return cext_posix.getpriority(self.pid) + return cext.proc_priority_get(self.pid) @wrap_exceptions def nice_set(self, value): - with catch_zombie(self): - return cext_posix.setpriority(self.pid, value) + return cext.proc_priority_set(self.pid, value) @wrap_exceptions def status(self): - code = self._get_kinfo_proc()[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, '?') @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) + ntuple = ntp.pthread(thread_id, utime, stime) retlist.append(ntuple) return retlist - - @wrap_exceptions - def memory_maps(self): - with catch_zombie(self): - return cext.proc_memory_maps(self.pid) diff --git a/psutil/_psposix.py b/psutil/_psposix.py index 9c3fac27ef..384ba1b97a 100644 --- a/psutil/_psposix.py +++ b/psutil/_psposix.py @@ -4,18 +4,23 @@ """Routines common to all posix systems.""" +import enum import errno +import functools import glob import os -import sys +import select +import signal import time -from ._common import memoize -from ._common import sdiskusage +from . import _ntuples as ntp +from ._common import MACOS +from ._common import TimeoutExpired +from ._common import debug from ._common import usage_percent -from ._compat import PY3 -from ._compat import unicode -from ._exceptions import TimeoutExpired + +if MACOS: + from . import _psutil_osx __all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map'] @@ -32,88 +37,271 @@ def pid_exists(pid): return True try: os.kill(pid, 0) - except OSError as err: - if err.errno == errno.ESRCH: - # ESRCH == No such process - return False - elif err.errno == errno.EPERM: - # EPERM clearly means there's a process to deny access to - return True - else: - # According to "man 2 kill" possible error values are - # (EINVAL, EPERM, ESRCH) therefore we should never get - # here. If we do let's be explicit in considering this - # an error. - raise err + except ProcessLookupError: + return False + except PermissionError: + # EPERM clearly means there's a process to deny access to + return True + # According to "man 2 kill" possible error values are + # (EINVAL, EPERM, ESRCH) else: return True -def wait_pid(pid, timeout=None, proc_name=None): - """Wait for process with pid 'pid' to terminate and return its - exit status code as an integer. +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) + except ValueError: + return num + - If pid is not a children of os.getpid() (current process) just - waits until the process disappears and return None. +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 + # 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)) + # 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) + # # until the process state doesn't change. + # # It may make sense to catch/enable this since stopped PIDs + # # ignore SIGTERM. + # interval = sleep(interval) + # continue + # if os.WIFCONTINUED(status): + # # Process was resumed via SIGCONT and waitpid() was called + # # with WCONTINUED flag. + # interval = sleep(interval) + # continue - If pid does not exist at all return None immediately. + # Should never happen. + msg = f"unknown process exit status {status!r}" + raise ValueError(msg) - Raise TimeoutExpired on timeout expired. + +def wait_pid_posix( + pid, + timeout=None, + _waitpid=os.waitpid, + _timer=getattr(time, 'monotonic', time.time), # noqa: B008 + _min=min, + _sleep=time.sleep, + _pid_exists=pid_exists, +): + """Wait for a process PID to terminate. + + If the process terminated normally by calling exit(3) or _exit(2), + or by returning from main(), the return value is the positive integer + passed to *exit(). + + If it was terminated by a signal it returns 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 at all return None immediately. + + If timeout is specified and process is still alive raise + TimeoutExpired. + + If timeout=0 either return immediately or raise TimeoutExpired + (non-blocking). """ - def check_timeout(delay): - if timeout is not None: - 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) + interval = 0.0001 + max_interval = 0.04 + flags = 0 + stop_at = None + 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) + flags |= os.WNOHANG + if timeout != 0: + stop_at = _timer() + timeout + + 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) + _sleep(interval) + return _min(interval * 2, max_interval) - delay = 0.0001 + # See: https://linux.die.net/man/2/waitpid while True: try: - retpid, status = waitcall() - except OSError as err: - if err.errno == errno.EINTR: - delay = check_timeout(delay) - continue - elif err.errno == errno.ECHILD: - # This has two meanings: - # - pid is not a child of os.getpid() in which case - # we keep polling until it's gone - # - pid never existed in the first place - # In both cases we'll eventually return None as we - # can't determine its exit status code. - while True: - if pid_exists(pid): - delay = check_timeout(delay) - else: - return - else: - raise + retpid, status = os.waitpid(pid, flags) + except ChildProcessError: + # This has two meanings: + # - PID is not a child of os.getpid() in which case + # we keep polling until it's gone + # - PID never existed in the first place + # In both cases we'll eventually return None as we + # can't determine its exit status code. + while _pid_exists(pid): + interval = sleep_or_timeout(interval) + return None else: if retpid == 0: - # WNOHANG was used, pid is still running - delay = check_timeout(delay) - 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): - return os.WEXITSTATUS(status) + # WNOHANG flag was used and PID is still running. + interval = sleep_or_timeout(interval) else: - # should never happen - raise ValueError("unknown process exit status %r" % status) + 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: + # 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 + # 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 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( + 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() + + +@functools.lru_cache +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: + 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) + return True + + +@functools.lru_cache +def can_use_kqueue(): + # Availability: macOS, BSD + names = ( + "kqueue", + "KQ_EV_ADD", + "KQ_EV_ONESHOT", + "KQ_FILTER_PROC", + "KQ_NOTE_EXIT", + ) + 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): + # 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_open(): + 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): @@ -123,33 +311,19 @@ 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: - # 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) + total = st.f_blocks * st.f_frsize # Remaining free space usable by root. - avail_to_root = (st.f_bfree * st.f_frsize) + avail_to_root = st.f_bfree * st.f_frsize # Remaining free space usable by user. - avail_to_user = (st.f_bavail * st.f_frsize) + avail_to_user = st.f_bavail * st.f_frsize # Total space being used in general. - used = (total - avail_to_root) + used = total - avail_to_root + if MACOS: + # see: https://github.com/giampaolo/psutil/pull/2152 + used = _psutil_osx.disk_usage_used(path, used) # Total space which is available to user (same as 'total' but # for the user). total_user = used + avail_to_user @@ -161,14 +335,15 @@ 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( - total=total, used=used, free=avail_to_user, percent=usage_percent_user) + return ntp.sdiskusage( + total=total, used=used, free=avail_to_user, percent=usage_percent_user + ) -@memoize +@functools.lru_cache def get_terminal_map(): """Get a map of device-id -> path as a dict. - Used by Process.terminal() + Used by Process.terminal(). """ ret = {} ls = glob.glob('/dev/tty*') + glob.glob('/dev/pts/*') @@ -176,7 +351,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 e2f33a3ae1..8707981727 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 @@ -12,24 +13,27 @@ from collections import namedtuple from socket import AF_INET -from . import _common +from . import _ntuples as ntp 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 +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 from ._common import memoize_when_activated from ._common import sockfam_to_enum from ._common import socktype_to_enum from ._common import usage_percent -from ._compat import b -from ._compat import PY3 -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess +from ._enums import ConnectionStatus +from ._enums import NicDuplex +from ._enums import ProcessStatus - -__extra__all__ = ["CONN_IDLE", "CONN_BOUND", "PROCFS_PATH"] +__extra__all__ = ["PROCFS_PATH"] # ===================================================================== @@ -37,38 +41,36 @@ # ===================================================================== -PAGE_SIZE = os.sysconf('SC_PAGE_SIZE') -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" -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( @@ -83,40 +85,8 @@ uid=8, euid=9, gid=10, - egid=11) - - -# ===================================================================== -# --- 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)) - - -# ===================================================================== -# --- utils -# ===================================================================== - - -def get_procfs_path(): - """Return updated psutil.PROCFS_PATH constant.""" - return sys.modules['psutil'].PROCFS_PATH + egid=11, +) # ===================================================================== @@ -132,7 +102,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(): @@ -140,32 +110,39 @@ 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(['/usr/bin/env', 'PATH=/usr/sbin:/sbin:%s' % - os.environ['PATH'], 'swap', '-l'], - stdout=subprocess.PIPE) - stdout, stderr = p.communicate() - if PY3: - stdout = stdout.decode(sys.stdout.encoding) + p = subprocess.Popen( + [ + '/usr/bin/env', + f"PATH=/usr/sbin:/sbin:{os.environ['PATH']}", + 'swap', + '-l', + ], + stdout=subprocess.PIPE, + ) + 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: - raise RuntimeError('no swap device(s) configured') + msg = 'no swap device(s) configured' + raise RuntimeError(msg) total = free = 0 for line in lines: line = line.split() - t, f = line[-2:] + t, f = line[3:5] total += int(int(t) * 512) free += int(int(f) * 512) used = total - free percent = usage_percent(used, total, round_=1) - return _common.sswap(total, used, free, percent, - sin * PAGE_SIZE, sout * PAGE_SIZE) + return ntp.sswap( + total, used, free, percent, sin * PAGE_SIZE, sout * PAGE_SIZE + ) # ===================================================================== @@ -174,15 +151,15 @@ 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)]) + 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""" + """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(): @@ -194,17 +171,16 @@ def cpu_count_logical(): return None -def cpu_count_physical(): - """Return the number of physical CPUs in the system.""" - return cext.cpu_count_phys() +def cpu_count_cores(): + """Return the number of CPU cores in the system.""" + return cext.cpu_count_cores() def cpu_stats(): """Return various CPU stats as a named tuple.""" - ctx_switches, interrupts, syscalls, traps = cext.cpu_stats() + 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) # ===================================================================== @@ -230,9 +206,14 @@ def disk_partitions(all=False): # Differently from, say, Linux, we don't have a list of # common fs types so the best we can do, AFAIK, is to # filter by filesystem having a total size > 0. - if not disk_usage(mountpoint).total: + try: + if not disk_usage(mountpoint).total: + continue + except OSError as err: + # https://github.com/giampaolo/psutil/issues/1674 + debug(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 @@ -243,7 +224,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): @@ -251,13 +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). """ - 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] + families, types = conn_tmap[kind] rawlist = cext.net_connections(_pid) ret = set() for item in rawlist: @@ -266,18 +241,19 @@ def net_connections(kind, _pid=-1): continue if type_ not in types: continue - if fam in (AF_INET, AF_INET6): + # 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) @@ -287,9 +263,8 @@ 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) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + duplex = NicDuplex(duplex) + ret[name] = ntp.snicstats(isup, duplex, speed, mtu, '') return ret @@ -304,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') @@ -317,7 +292,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 @@ -329,7 +304,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): @@ -342,33 +318,34 @@ def wrap_exceptions(fun): EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): + pid, ppid, name = self.pid, self._ppid, self._name try: return fun(self, *args, **kwargs) - except EnvironmentError as err: - if self.pid == 0: - if 0 in pids(): - raise AccessDenied(self.pid, self._name) - else: - raise + 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 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) + if not pid_exists(pid): + raise NoSuchProcess(pid, name) 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 + raise raise + return wrapper -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_procfs_path"] + __slots__ = ["_cache", "_name", "_ppid", "_procfs_path", "pid"] def __init__(self, pid): self.pid = pid @@ -376,32 +353,42 @@ def __init__(self, pid): self._ppid = None self._procfs_path = get_procfs_path() + def _assert_alive(self): + """Raise NSP if the process disappeared on us.""" + # For those C function who do not raise NSP, possibly returning + # incorrect or incomplete result. + os.stat(f"{self._procfs_path}/{self.pid}") + def oneshot_enter(self): - self._proc_name_and_args.cache_activate() - self._proc_basic_info.cache_activate() - self._proc_cred.cache_activate() + self._oneshot.cache_activate(self) + self._proc_name_and_args.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._oneshot.cache_deactivate(self) + self._proc_name_and_args.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) + 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_oneshot(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): @@ -411,10 +398,9 @@ 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 + 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. @@ -423,7 +409,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): @@ -431,32 +417,32 @@ 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): - 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, # 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): - self._ppid = self._proc_basic_info()[proc_info_map['ppid']] + self._ppid = self._oneshot()[proc_info_map['ppid']] return self._ppid @wrap_exceptions @@ -464,20 +450,20 @@ 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 _common.puids(real, effective, saved) + return ntp.puids(real, effective, saved) @wrap_exceptions 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 _common.puids(real, effective, saved) + return ntp.puids(real, effective, saved) @wrap_exceptions def cpu_times(self): @@ -495,7 +481,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): @@ -505,49 +491,40 @@ 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: - 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 + return os.readlink(f"{procfs_path}/{self.pid}/path/{x}") + except FileNotFoundError: + hit_enoent = True + continue if hit_enoent: - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (procfs_path, self.pid)) + self._assert_alive() @wrap_exceptions def cwd(self): # /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("%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 + return os.readlink(f"{procfs_path}/{self.pid}/path/cwd") + except FileNotFoundError: + os.stat(f"{procfs_path}/{self.pid}") # raise NSP or AD + return "" @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 pmem(rss, vms) - - memory_full_info = memory_info + return ntp.pmem(rss, vms) @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, '?') @@ -555,14 +532,15 @@ 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) try: utime, stime = cext.query_process_thread( - self.pid, tid, procfs_path) - except EnvironmentError as err: + self.pid, tid, procfs_path + ) + 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. @@ -578,11 +556,10 @@ def threads(self): continue raise else: - nt = _common.pthread(tid, utime, stime) + nt = ntp.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 @@ -590,43 +567,41 @@ 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: 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))) + retlist.append(ntp.popenfile(file, int(fd))) if hit_enoent: - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (procfs_path, self.pid)) + self._assert_alive() return retlist def _get_unix_sockets(self, pid): """Get UNIX sockets used by process by parsing 'pfiles' output.""" # TODO: rewrite this in C (...but the damn netstat source code # does not include this part! Argh!!) - cmd = "pfiles %s" % pid - p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + cmd = ["pfiles", str(pid)] + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) stdout, stderr = p.communicate() - if PY3: - stdout, stderr = [x.decode(sys.stdout.encoding) - for x in (stdout, stderr)] + stdout, stderr = ( + x.decode(sys.stdout.encoding) for x in (stdout, stderr) + ) if p.returncode != 0: if 'permission denied' in stderr.lower(): raise AccessDenied(self.pid, self._name) 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): @@ -640,10 +615,17 @@ 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 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 @@ -652,12 +634,13 @@ def 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'): - ret.extend([_common.pconn(*conn) for conn in - self._get_unix_sockets(self.pid)]) + if kind in {'all', 'unix'}: + 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') @@ -666,8 +649,7 @@ 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 f"{hex(start)[2:].strip('L')}-{hex(end)[2:].strip('L')}" procfs_path = self._procfs_path retlist = [] @@ -691,35 +673,39 @@ 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 # 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) + name = f"{procfs_path}/{self.pid}/path/{name}" hit_enoent = True else: 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 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): - return _common.pctxsw( - *cext.proc_num_ctx_switches(self.pid, self._procfs_path)) + return ntp.pctxsw( + *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, self._name) + return _psposix.wait_pid(self.pid, timeout) diff --git a/psutil/_psutil_aix.c b/psutil/_psutil_aix.c index 916254d5a4..de398e484b 100644 --- a/psutil/_psutil_aix.c +++ b/psutil/_psutil_aix.c @@ -6,35 +6,35 @@ * 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.threads 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_61/com.ibm.aix.files/proc.htm - * - libperfstat: http://www-01.ibm.com/support/knowledgecenter/ - * ssw_aix_61/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 -#include +#include #include -#include #include #include +#include +#include #include +#include #include #include #include @@ -46,19 +46,17 @@ #include #include #include +#include +#include "arch/all/init.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) +#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; @@ -71,12 +69,12 @@ 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) { close(fd); - PyErr_SetString(PyExc_RuntimeError, "structure size mismatch"); + psutil_runtime_error("psutil_file_to_struct() size mismatch"); return 0; } close(fd); @@ -84,131 +82,225 @@ 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_basic_info(PyObject *self, PyObject *args) { +psutil_proc_oneshot(PyObject *self, PyObject *args) { int pid; char path[100]; psinfo_t info; 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))) + str_format(path, sizeof(path), "%s/%i/psinfo", procfs_path, pid); + 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 { - sprintf(path, "%s/%i/status", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&status, sizeof(status))) + } + else { + str_format(path, sizeof(path), "%s/%i/status", procfs_path, pid); + 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 + ); } -/* - * Return process name and args as a Python tuple. - */ 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)) + 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))) + str_format(path, sizeof(path), "%s/%i/psinfo", procfs_path, pid); + 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); + 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(procbuf), argbuf, ARG_MAX); + if (ret == -1) { + psutil_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') { + if (!pylist_append_obj(py_retlist, PyUnicode_DecodeFSDefault(curarg))) + goto error; + 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); return NULL; } +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(procbuf), envbuf, ARG_MAX); + if (ret == -1) { + psutil_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_CLEAR(py_key); + Py_CLEAR(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 -/* - * 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; PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; perfstat_thread_t *threadt = NULL; perfstat_id_t id; int i, rc, thread_count; 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 */ + // 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; } - /* Allocate enough memory */ - threadt = (perfstat_thread_t *)calloc(thread_count, - sizeof(perfstat_thread_t)); + // Allocate enough memory + 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); + psutil_oserror(); goto error; } @@ -216,56 +308,57 @@ 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); return NULL; } -#endif +#endif // CURR_VERSION_THREAD +#ifdef CURR_VERSION_PROCESS static PyObject * psutil_proc_io_counters(PyObject *self, PyObject *args) { long pid; 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); rc = perfstat_process(&id, &procinfo, sizeof(perfstat_process_t), 1); if (rc <= 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); 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 // 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; @@ -273,23 +366,23 @@ 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))) + 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() - 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) + ); } -/* - * 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; @@ -297,20 +390,23 @@ 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))) + 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("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 + ); } -/* - * 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; @@ -320,107 +416,39 @@ 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); 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 */ + py_tuple = Py_BuildValue( + "LL", + (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 NoSuchProcess(""); + return psutil_oserror_nsp("psutil_read_process_table (no PID found)"); } -/* - * 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( - "(OOOfOi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (float)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_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(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. - */ 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; PyObject *py_retlist = PyList_New(0); if (py_retlist == NULL) @@ -428,30 +456,30 @@ 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); 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) - 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) + if (!py_mountp) 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_DECREF(py_dev); - Py_DECREF(py_mountp); - Py_DECREF(py_tuple); + } + Py_CLEAR(py_dev); + Py_CLEAR(py_mountp); mt = getmntent(file); } endmntent(file); @@ -460,7 +488,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); @@ -468,9 +495,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { } -/* - * Return a list of tuples for network I/O statistics. - */ +#if defined(CURR_VERSION_NETINTERFACE) && CURR_VERSION_NETINTERFACE >= 3 static PyObject * psutil_net_io_counters(PyObject *self, PyObject *args) { perfstat_netinterface_t *statp = NULL; @@ -483,42 +508,46 @@ 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); + NULL, NULL, sizeof(perfstat_netinterface_t), 0 + ); if (tot == 0) { // no network interfaces - return empty dict return py_retdict; } if (tot < 0) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_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); + psutil_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)) @@ -536,10 +565,11 @@ psutil_net_io_counters(PyObject *self, PyObject *args) { Py_DECREF(py_retdict); return NULL; } +#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; @@ -548,14 +578,14 @@ 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); if (sock == -1) goto error; - 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); @@ -584,7 +614,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; } @@ -594,6 +624,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) { @@ -602,38 +633,43 @@ 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); } -/* - * Return a Python list of tuple representing per-cpu times - */ 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); - PyObject *py_cputime = NULL; if (py_retlist == NULL) return NULL; - /* get the number of cpus in ncpu */ + // 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 ncpu = perfstat_cpu(NULL, NULL, sizeof(perfstat_cpu_t), 0); - if (ncpu <= 0){ - PyErr_SetFromErrno(PyExc_OSError); + if (ncpu <= 0) { + psutil_oserror(); goto error; } - /* allocate enough memory to hold the ncpu structures */ - cpu = (perfstat_cpu_t *) malloc(ncpu * sizeof(perfstat_cpu_t)); + // allocate enough memory to hold the ncpu structures + cpu = (perfstat_cpu_t *)malloc(ncpu * sizeof(perfstat_cpu_t)); if (cpu == NULL) { PyErr_NoMemory(); goto error; @@ -643,28 +679,27 @@ 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; } 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); - if (!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; - if (PyList_Append(py_retlist, py_cputime)) - 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); @@ -672,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(); @@ -686,26 +718,24 @@ 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) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } - /* Allocate enough memory */ - diskt = (perfstat_disk_t *)calloc(disk_count, - sizeof(perfstat_disk_t)); + // Allocate enough memory + 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); + psutil_oserror(); goto error; } @@ -717,12 +747,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); } @@ -738,60 +767,55 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) { } -/* - * Return virtual memory usage statistics. - */ 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( - NULL, &memory, sizeof(perfstat_memory_total_t), 1); - if (rc <= 0){ - PyErr_SetFromErrno(PyExc_OSError); + NULL, &memory, sizeof(perfstat_memory_total_t), 1 + ); + if (rc <= 0) { + psutil_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 ); } -/* - * Return stats about swap memory. - */ 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( - NULL, &memory, sizeof(perfstat_memory_total_t), 1); - if (rc <= 0){ - PyErr_SetFromErrno(PyExc_OSError); + NULL, &memory, sizeof(perfstat_memory_total_t), 1 + ); + if (rc <= 0) { + psutil_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 ); } -/* - * Return CPU statistics. - */ static PyObject * psutil_cpu_stats(PyObject *self, PyObject *args) { int ncpu, rc, i; @@ -804,15 +828,15 @@ 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){ - PyErr_SetFromErrno(PyExc_OSError); + if (ncpu <= 0) { + psutil_oserror(); goto error; } - /* allocate enough memory to hold the ncpu structures */ - cpu = (perfstat_cpu_t *) malloc(ncpu * sizeof(perfstat_cpu_t)); + // allocate enough memory to hold the ncpu structures + cpu = (perfstat_cpu_t *)malloc(ncpu * sizeof(perfstat_cpu_t)); if (cpu == NULL) { PyErr_NoMemory(); goto error; @@ -822,7 +846,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; } @@ -835,13 +859,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) @@ -850,57 +868,40 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { } -/* - * define the psutil C module methods and initialize the module. - */ -static PyMethodDef -PsutilMethods[] = -{ +// define the psutil C module methods and initialize the module. +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_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_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, - "Return process threads"}, + {"proc_threads", psutil_proc_threads, METH_VARARGS}, #endif - {"proc_io_counters", psutil_proc_io_counters, METH_VARARGS, - "Get process I/O counters."}, - {"proc_num_ctx_switches", psutil_proc_num_ctx_switches, METH_VARARGS, - "Get process I/O counters."}, +#ifdef CURR_VERSION_PROCESS + {"proc_io_counters", psutil_proc_io_counters, METH_VARARGS}, +#endif + {"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"}, - {"net_io_counters", psutil_net_io_counters, METH_VARARGS, - "Return a Python dict of tuples for network I/O statistics."}, - {"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"}, + {"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}, + {"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}, +#endif + {"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_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, + {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, + {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} }; @@ -910,13 +911,11 @@ 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 +#define GETSTATE(m) ((struct module_state *)PyModule_GetState(m)) -#if PY_MAJOR_VERSION >= 3 +#ifdef __cplusplus +extern "C" { +#endif static int psutil_aix_traverse(PyObject *m, visitproc visit, void *arg) { @@ -942,47 +941,65 @@ static struct PyModuleDef moduledef = { NULL }; -#define INITERROR return NULL - -PyMODINIT_FUNC PyInit__psutil_aix(void) -#else -#define INITERROR return +PyMODINIT_FUNC +PyInit__psutil_aix(void) { + PyObject *mod = PyModule_Create(&moduledef); + if (mod == NULL) + return NULL; -void init_psutil_aix(void) -#endif -{ -#if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("_psutil_aix", PsutilMethods); -#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; -#if PY_MAJOR_VERSION >= 3 - return module; +#ifdef Py_GIL_DISABLED + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) + return NULL; #endif + + 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; + 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 +} +#endif diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 9a2ed04bc5..f26d524b36 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -1,1098 +1,200 @@ /* - * 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. - * - * 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() */ -#if defined(PSUTIL_NETBSD) - #define _KMEMUSER -#endif - #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #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 // for TCP connection states -#include // for inet_ntop() - -#include +#include // BSD version +#include // for TCP connection states -#include // net io counters -#include -#include - -#include // process open files/connections -#include - -#include "_psutil_common.h" -#include "_psutil_posix.h" +#include "arch/all/init.h" +#include "arch/bsd/init.h" #ifdef PSUTIL_FREEBSD - #include "arch/freebsd/specific.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/init.h" #elif PSUTIL_OPENBSD - #include "arch/openbsd/specific.h" - - #include - #include // for VREG - #define _KERNEL // for DTYPE_VNODE - #include - #undef _KERNEL - #include // for CPUSTATES & CP_* +#include "arch/openbsd/init.h" #elif PSUTIL_NETBSD - #include "arch/netbsd/specific.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) - -#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 - - -/* - * 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; - - // 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"); - } - 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 = Py_BuildValue("i", proclist->ki_pid); -#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) - py_pid = Py_BuildValue("i", proclist->p_pid); -#endif - 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: - 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) { - long pid; - long rss; - long vms; - long memtext; - long memdata; - long memstack; - int oncpu; - kinfo_proc kp; - long pagesize = sysconf(_SC_PAGESIZE); - char str[1000]; - PyObject *py_name; - PyObject *py_retlist; - - if (! PyArg_ParseTuple(args, "l", &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; - } - - // 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 - - // Return a single big tuple with all process info. - py_retlist = Py_BuildValue( - "(lillllllidllllddddlllllbO)", -#ifdef PSUTIL_FREEBSD - // - (long)kp.ki_ppid, // (long) 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) 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) - // - (long)kp.p_ppid, // (long) 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 - ); - - if (py_retlist != NULL) { - // XXX shall we decref() also in case of Py_BuildValue() error? - Py_DECREF(py_name); - } - return py_retlist; -} - - -/* - * Return process name from kinfo_proc as a Python string. - */ -static PyObject * -psutil_proc_name(PyObject *self, PyObject *args) { - long pid; - kinfo_proc kp; - char str[1000]; - - if (! PyArg_ParseTuple(args, "l", &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 cmdline as a Python list of cmdline arguments. - */ -static PyObject * -psutil_proc_cmdline(PyObject *self, PyObject *args) { - long pid; - PyObject *py_retlist = NULL; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - py_retlist = psutil_get_cmdline(pid); - if (py_retlist == NULL) - return NULL; - return Py_BuildValue("N", py_retlist); -} - - -/* - * Return the number of logical CPUs in the system. - * XXX this could be shared with OSX - */ -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) { - long 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, "l", &pid)) - goto error; - if (psutil_kinfo_proc(pid, &kipp) == -1) - goto error; - - errno = 0; - freep = kinfo_getfile(pid, &cnt); - if (freep == NULL) { - psutil_raise_for_pid(pid, "kinfo_getfile()"); - 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_DECREF(py_path); - Py_DECREF(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); +#include "arch/netbsd/init.h" #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)); - if (flags & MNT_GJOURNAL) - strlcat(opts, ",gjournal", sizeof(opts)); - 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)); - if (flags & MNT_NFS4ACLS) - strlcat(opts, ",nfs4acls", sizeof(opts)); -#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_DECREF(py_dev); - Py_DECREF(py_mountp); - Py_DECREF(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_DECREF(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; - - if (py_retlist == NULL) - return NULL; - -#if (defined(__FreeBSD_version) && (__FreeBSD_version < 900000)) || PSUTIL_OPENBSD - struct utmp ut; - FILE *fp; - - fp = fopen(_PATH_UTMP, "r"); - if (fp == NULL) { - PyErr_SetFromErrno(PyExc_OSError); - 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( - "(OOOfi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (float)ut.ut_time, // start time -#ifdef PSUTIL_OPENBSD - -1 // process id (set to None later) -#else - ut.ut_pid // process id -#endif - ); - if (!py_tuple) { - fclose(fp); - goto error; - } - if (PyList_Append(py_retlist, py_tuple)) { - fclose(fp); - goto error; - } - Py_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(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; - py_tuple = Py_BuildValue( - "(OOOfi)", - 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 - ); - - if (!py_tuple) { - endutxent(); - goto error; - } - if (PyList_Append(py_retlist, py_tuple)) { - endutxent(); - goto error; - } - Py_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(py_tuple); - } - - endutxent(); -#endif - 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; -} - - -/* - * 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, - "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"}, -#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) - {"proc_connections", psutil_proc_connections, METH_VARARGS, - "Return connections opened by process"}, - {"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"}, - {"proc_open_files", psutil_proc_open_files, METH_VARARGS, - "Return files opened by process as a list of (path, fd) tuples"}, -#endif + {"proc_cmdline", psutil_proc_cmdline, METH_VARARGS}, + {"proc_cwd", psutil_proc_cwd, METH_VARARGS}, + {"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_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) - {"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."}, - {"cpu_count_phys", psutil_cpu_count_phys, METH_VARARGS, - "Return an XML string to determine the number physical CPUs."}, + {"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 // --- 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_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}, + {"swap_mem", psutil_swap_mem, METH_VARARGS}, #if defined(PSUTIL_FREEBSD) || defined(PSUTIL_NETBSD) - {"net_connections", psutil_net_connections, METH_VARARGS, - "Return system-wide open connections."}, + {"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 + {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, +#if defined(PSUTIL_FREEBSD) || defined(PSUTIL_OPENBSD) + {"cpu_freq", psutil_cpu_freq, METH_VARARGS}, #endif #if defined(PSUTIL_FREEBSD) - {"sensors_battery", psutil_sensors_battery, METH_VARARGS, - "Return battery information."}, + {"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 - // --- others - {"set_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, + {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, + {"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) -#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 = { +static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, - "psutil_bsd", + "_psutil_bsd", + NULL, + -1, + mod_methods, + NULL, NULL, - sizeof(struct module_state), - PsutilMethods, NULL, - psutil_bsd_traverse, - psutil_bsd_clear, NULL }; -#define INITERROR return NULL +PyObject * +PyInit__psutil_bsd(void) { + PyObject *v; + PyObject *mod = PyModule_Create(&moduledef); + if (mod == NULL) + return NULL; -PyMODINIT_FUNC PyInit__psutil_bsd(void) +#ifdef Py_GIL_DISABLED + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) + return NULL; +#endif -#else -#define INITERROR return + if (psutil_setup() != 0) + return NULL; + if (psutil_posix_add_constants(mod) != 0) + return NULL; + if (psutil_posix_add_methods(mod) != 0) + return NULL; -void init_psutil_bsd(void) -#endif -{ -#if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("_psutil_bsd", PsutilMethods); -#endif - PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); - // process status constants + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) + return NULL; + // 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); -#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)) + 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)) + 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) - 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)) + 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)) + return NULL; +#endif + if (PyModule_AddIntConstant(mod, "SONPROC", LSONPROC)) + return NULL; // unique to NetBSD - PyModule_AddIntConstant(module, "SSUSPENDED", LSSUSPENDED); + if (PyModule_AddIntConstant(mod, "SSUSPENDED", LSSUSPENDED)) + return NULL; #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); - // PSUTIL_CONN_NONE - PyModule_AddIntConstant(module, "PSUTIL_CONN_NONE", 128); - - psutil_setup(); + 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_RECEIVED", 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", 128)) + return NULL; - if (module == NULL) - INITERROR; -#if PY_MAJOR_VERSION >= 3 - return module; -#endif + return mod; } diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c deleted file mode 100644 index e08f011c20..0000000000 --- a/psutil/_psutil_common.c +++ /dev/null @@ -1,104 +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. - * - * Routines common to all platforms. - */ - -#include -#include - - -// Global vars. -int PSUTIL_DEBUG = 0; -int PSUTIL_TESTING = 0; - - -/* - * 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 - - -/* - * 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. - */ -PyObject * -AccessDenied(const char *msg) { - PyObject *exc; - exc = PyObject_CallFunction( - PyExc_OSError, "(is)", EACCES, strlen(msg) ? msg : strerror(EACCES)); - PyErr_SetObject(PyExc_OSError, exc); - Py_XDECREF(exc); - return NULL; -} - - -/* - * 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; - Py_INCREF(Py_None); - return Py_None; -} - - -/* - * 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; - va_start(argptr, format); - fprintf(stderr, "psutil-dubug> "); - vfprintf(stderr, format, argptr); - fprintf(stderr, "\n"); - va_end(argptr); -} - - -/* - * Called on module import on all platforms. - */ -void -psutil_setup(void) { - if (getenv("PSUTIL_DEBUG") != NULL) - PSUTIL_DEBUG = 1; - if (getenv("PSUTIL_TESTING") != NULL) - PSUTIL_TESTING = 1; -} diff --git a/psutil/_psutil_common.h b/psutil/_psutil_common.h deleted file mode 100644 index e107166a12..0000000000 --- a/psutil/_psutil_common.h +++ /dev/null @@ -1,30 +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 PSUTIL_PSUTIL_COMMON_H -#define PSUTIL_PSUTIL_COMMON_H - -#include - -extern int PSUTIL_TESTING; -extern int PSUTIL_DEBUG; - -// a signaler for connections without an actual status -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); -#endif - -PyObject* AccessDenied(const char *msg); -PyObject* NoSuchProcess(const char *msg); - -PyObject* psutil_set_testing(PyObject *self, PyObject *args); -void psutil_debug(const char* format, ...); -void psutil_setup(void); - -#endif // PSUTIL_PSUTIL_COMMON_H diff --git a/psutil/_psutil_linux.c b/psutil/_psutil_linux.c index d1f0d14551..a8b5401477 100644 --- a/psutil/_psutil_linux.c +++ b/psutil/_psutil_linux.c @@ -2,722 +2,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. - * - * Linux-specific functions. */ +// Linux-specific functions. + #ifndef _GNU_SOURCE - #define _GNU_SOURCE 1 +#define _GNU_SOURCE 1 #endif #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) +#include // DUPLEX_* -// Linux >= 2.6.36 (supposedly) and glibc >= 13 -#define PSUTIL_HAVE_PRLIMIT \ - (LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 36)) && \ - (__GLIBC__ >= 2 && __GLIBC_MINOR__ >= 13) && \ - defined(__NR_prlimit64) - -#if PSUTIL_HAVE_PRLIMIT - #define _FILE_OFFSET_BITS 64 - #include - #include -#endif - -#include "_psutil_common.h" -#include "_psutil_posix.h" +#include "arch/all/init.h" // May happen on old RedHat versions, see: // https://github.com/giampaolo/psutil/issues/607 #ifndef DUPLEX_UNKNOWN - #define DUPLEX_UNKNOWN 0xff -#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); -} - -#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) { - long pid; - int ioprio, ioclass, iodata; - if (! PyArg_ParseTuple(args, "l", &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) { - long pid; - int ioprio, ioclass, iodata; - int retval; - - if (! PyArg_ParseTuple(args, "lii", &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 - - -#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) { - long 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)) - 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 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) - 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 - */ -static PyObject * -psutil_disk_partitions(PyObject *self, PyObject *args) { - FILE *file = NULL; - struct mntent *entry; - PyObject *py_dev = NULL; - PyObject *py_mountp = NULL; - PyObject *py_tuple = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - - // MOUNTED constant comes from mntent.h and it's == '/etc/mtab' - Py_BEGIN_ALLOW_THREADS - file = setmntent(MOUNTED, "r"); - Py_END_ALLOW_THREADS - if ((file == 0) || (file == NULL)) { - PyErr_SetFromErrnoWithFilename(PyExc_OSError, MOUNTED); - 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_DECREF(py_dev); - Py_DECREF(py_mountp); - Py_DECREF(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 - * The dual implementation exists because of: - * https://github.com/giampaolo/psutil/issues/536 - */ - -#ifdef CPU_ALLOC - -static PyObject * -psutil_proc_cpu_affinity_get(PyObject *self, PyObject *args) { - int cpu, ncpus, count, cpucount_s; - long pid; - size_t setsize; - cpu_set_t *mask = NULL; - PyObject *py_list = NULL; - - if (!PyArg_ParseTuple(args, "l", &pid)) - return NULL; - ncpus = NCPUS_START; - while (1) { - setsize = CPU_ALLOC_SIZE(ncpus); - mask = CPU_ALLOC(ncpus); - if (mask == NULL) - 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); +#define DUPLEX_UNKNOWN 0xff #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; -} -#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 - */ -static PyObject * -psutil_proc_cpu_affinity_set(PyObject *self, PyObject *args) { - cpu_set_t cpu_set; - size_t len; - long pid; - int i, seq_len; - PyObject *py_cpu_set; - PyObject *py_cpu_seq = NULL; - - if (!PyArg_ParseTuple(args, "lO", &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; - } - - 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); - 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()) { - if (!PyErr_Occurred()) - PyErr_SetString(PyExc_ValueError, "invalid CPU value"); - goto error; - } - CPU_SET(value, &cpu_set); - } - - - len = sizeof(cpu_set); - if (sched_setaffinity(pid, len, &cpu_set)) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - Py_DECREF(py_cpu_seq); - Py_RETURN_NONE; - -error: - if (py_cpu_seq != NULL) - Py_DECREF(py_cpu_seq); - return NULL; -} - - -/* - * 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( - "(OOOfOi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (float)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_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(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; - 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) - goto error; - 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 = ethcmd.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 { - goto error; - } - } - - close(sock); - py_retlist = Py_BuildValue("[ii]", duplex, speed); - if (!py_retlist) - goto error; - return py_retlist; - -error: - if (sock != -1) - close(sock); - return PyErr_SetFromErrno(PyExc_OSError); -} - - -/* - * Define the psutil C module methods and initialize the module. - */ -static PyMethodDef -PsutilMethods[] = { +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}, +#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 - {"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."}, - // --- 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"}, - - // --- linux specific - - {"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."}, + {"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 - {"set_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, - + {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, + {"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) -#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 = { +static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, - "psutil_linux", + "_psutil_linux", + NULL, + -1, + mod_methods, + NULL, 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 -{ - PyObject *v; -#if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("_psutil_linux", PsutilMethods); -#endif - - PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); -#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); +PyObject * +PyInit__psutil_linux(void) { + PyObject *mod = PyModule_Create(&moduledef); + if (mod == NULL) + return NULL; -#if defined(HAVE_LONG_LONG) - if (sizeof(RLIM_INFINITY) > sizeof(long)) { - v = PyLong_FromLongLong((PY_LONG_LONG) RLIM_INFINITY); - } else +#ifdef Py_GIL_DISABLED + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) + return NULL; #endif - { - v = PyLong_FromLong((long) RLIM_INFINITY); - } - if (v) { - PyModule_AddObject(module, "RLIM_INFINITY", v); - } -#ifdef RLIMIT_MSGQUEUE - PyModule_AddIntConstant(module, "RLIMIT_MSGQUEUE", RLIMIT_MSGQUEUE); -#endif -#ifdef RLIMIT_NICE - PyModule_AddIntConstant(module, "RLIMIT_NICE", RLIMIT_NICE); -#endif -#ifdef RLIMIT_RTPRIO - PyModule_AddIntConstant(module, "RLIMIT_RTPRIO", RLIMIT_RTPRIO); -#endif -#ifdef RLIMIT_RTTIME - PyModule_AddIntConstant(module, "RLIMIT_RTTIME", RLIMIT_RTTIME); -#endif -#ifdef RLIMIT_SIGPENDING - PyModule_AddIntConstant(module, "RLIMIT_SIGPENDING", RLIMIT_SIGPENDING); -#endif -#endif - PyModule_AddIntConstant(module, "DUPLEX_HALF", DUPLEX_HALF); - PyModule_AddIntConstant(module, "DUPLEX_FULL", DUPLEX_FULL); - PyModule_AddIntConstant(module, "DUPLEX_UNKNOWN", DUPLEX_UNKNOWN); + if (psutil_setup() != 0) + return NULL; + if (psutil_posix_add_constants(mod) != 0) + return NULL; + if (psutil_posix_add_methods(mod) != 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 (module == NULL) - INITERROR; -#if PY_MAJOR_VERSION >= 3 - return module; -#endif + return mod; } diff --git a/psutil/_psutil_osx.c b/psutil/_psutil_osx.c index 55dd64ca5d..76997e334f 100644 --- a/psutil/_psutil_osx.c +++ b/psutil/_psutil_osx.c @@ -1,2046 +1,137 @@ /* - * 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. - * - * OS X platform-specific module methods for _psutil_osx */ #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include // needed for old macOS versions +#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "arch/all/init.h" +#include "arch/osx/init.h" -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "_psutil_common.h" -#include "_psutil_posix.h" -#include "arch/osx/process_info.h" +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_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}, + {"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_threads", psutil_proc_threads, METH_VARARGS}, + // clang-format on -#define PSUTIL_TV2DOUBLE(t) ((t).tv_sec + (t).tv_usec / 1000000.0) + // --- 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}, + {"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}, + {"sensors_battery", psutil_sensors_battery, METH_VARARGS}, + {"swap_mem", psutil_swap_mem, METH_VARARGS}, + {"virtual_mem", psutil_virtual_mem, METH_VARARGS}, + // --- others + {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, + {"set_debug", psutil_set_debug, METH_VARARGS}, -/* - * 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(); + {NULL, NULL, 0, NULL} +}; - 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; -} +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_psutil_osx", + NULL, + -1, + mod_methods, + NULL, + NULL, + NULL, + NULL +}; -/* - * 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_pid = NULL; - PyObject *py_retlist = PyList_New(0); - if (py_retlist == NULL) +PyObject * +PyInit__psutil_osx(void) { + PyObject *mod = PyModule_Create(&moduledef); + if (mod == 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"); - } - 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); - } - return py_retlist; - -error: - Py_XDECREF(py_pid); - Py_DECREF(py_retlist); - if (orig_address != NULL) - free(orig_address); - return NULL; -} - - -/* - * Return multiple process info as a Python tuple 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. - */ -static PyObject * -psutil_proc_kinfo_oneshot(PyObject *self, PyObject *args) { - long pid; - struct kinfo_proc kp; - PyObject *py_name; - PyObject *py_retlist; - - if (! PyArg_ParseTuple(args, "l", &pid)) +#ifdef Py_GIL_DISABLED + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) return NULL; - if (psutil_get_kinfo_proc(pid, &kp) == -1) - return NULL; - - py_name = PyUnicode_DecodeFSDefault(kp.kp_proc.p_comm); - 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_retlist = Py_BuildValue( - "lllllllidiO", - (long)kp.kp_eproc.e_ppid, // (long) 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 - kp.kp_eproc.e_tdev, // (int) 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); - } - return py_retlist; -} - - -/* - * Return multiple process info as a Python tuple 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. - */ -static PyObject * -psutil_proc_pidtaskinfo_oneshot(PyObject *self, PyObject *args) { - long pid; - struct proc_taskinfo pti; +#endif - if (! PyArg_ParseTuple(args, "l", &pid)) + if (psutil_setup() != 0) return NULL; - if (psutil_proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)) <= 0) + if (psutil_setup_osx() != 0) return NULL; - - 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 - // Note about memory: determining other mem stats on OSX 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 - // 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 - ); -} - - -/* - * Return process name from kinfo_proc as a Python string. - */ -static PyObject * -psutil_proc_name(PyObject *self, PyObject *args) { - long pid; - struct kinfo_proc kp; - - if (! PyArg_ParseTuple(args, "l", &pid)) + if (psutil_posix_add_constants(mod) != 0) return NULL; - if (psutil_get_kinfo_proc(pid, &kp) == -1) + if (psutil_posix_add_methods(mod) != 0) return NULL; - return PyUnicode_DecodeFSDefault(kp.kp_proc.p_comm); -} - -/* - * Return process current working directory. - * Raises NSP in case of zombie process. - */ -static PyObject * -psutil_proc_cwd(PyObject *self, PyObject *args) { - long pid; - struct proc_vnodepathinfo pathinfo; - - if (! PyArg_ParseTuple(args, "l", &pid)) + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) return NULL; - - if (psutil_proc_pidinfo( - pid, PROC_PIDVNODEPATHINFO, 0, &pathinfo, sizeof(pathinfo)) <= 0) - { + // 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)) return NULL; - } - - return PyUnicode_DecodeFSDefault(pathinfo.pvi_cdir.vip_path); -} - - -/* - * Return path of the process executable. - */ -static PyObject * -psutil_proc_exe(PyObject *self, PyObject *args) { - long pid; - char buf[PATH_MAX]; - int ret; - - if (! PyArg_ParseTuple(args, "l", &pid)) + if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) return NULL; - errno = 0; - ret = proc_pidpath((pid_t)pid, &buf, sizeof(buf)); - if (ret == 0) { - if (pid == 0) - AccessDenied(""); - else - psutil_raise_for_pid(pid, "proc_pidpath()"); + if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) return NULL; - } - return PyUnicode_DecodeFSDefault(buf); -} - - -/* - * Return process cmdline as a Python list of cmdline arguments. - */ -static PyObject * -psutil_proc_cmdline(PyObject *self, PyObject *args) { - long pid; - PyObject *py_retlist = NULL; - - if (! PyArg_ParseTuple(args, "l", &pid)) + if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) 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. - */ -static PyObject * -psutil_proc_environ(PyObject *self, PyObject *args) { - long pid; - PyObject *py_retdict = NULL; - - if (! PyArg_ParseTuple(args, "l", &pid)) + if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) return NULL; - - // get the environment block, defined in arch/osx/process_info.c - py_retdict = psutil_get_environ(pid); - return py_retdict; -} - - -/* - * Return a list of tuples for every process memory maps. - * 'procstat' cmdline utility has been used as an example. - */ -static PyObject * -psutil_proc_memory_maps(PyObject *self, PyObject *args) { - char buf[PATH_MAX]; - char addr_str[34]; - char perms[8]; - int pagesize = getpagesize(); - long pid; - kern_return_t err = KERN_SUCCESS; - mach_port_t task = MACH_PORT_NULL; - uint32_t depth = 1; - vm_address_t address = 0; - vm_size_t size = 0; - - PyObject *py_tuple = NULL; - PyObject *py_path = NULL; - PyObject *py_list = PyList_New(0); - - if (py_list == NULL) + // connection status constants + if (PyModule_AddIntConstant(mod, "TCPS_CLOSED", TCPS_CLOSED)) return NULL; - - 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)"); - } - goto error; - } - - while (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, - (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 (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() return value seems meaningless - // so we do what we can in order to not continue in case - // of error. - errno = 0; - proc_regionfilename((pid_t)pid, address, buf, sizeof(buf)); - if ((errno != 0) || ((sizeof(buf)) <= 0)) { - // TODO temporary - psutil_debug("proc_regionfilename() failed"); - 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. - */ -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 physical CPUs in the system. - */ -static PyObject * -psutil_cpu_count_phys(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. - */ -bool -psutil_in_shared_region(mach_vm_address_t addr, cpu_type_t type) { - mach_vm_address_t base; - mach_vm_address_t size; - - switch (type) { - case CPU_TYPE_ARM: - base = SHARED_REGION_BASE_ARM; - size = SHARED_REGION_SIZE_ARM; - break; - case CPU_TYPE_I386: - base = SHARED_REGION_BASE_I386; - size = SHARED_REGION_SIZE_I386; - break; - case CPU_TYPE_X86_64: - base = SHARED_REGION_BASE_X86_64; - size = SHARED_REGION_SIZE_X86_64; - break; - default: - return false; - } - - return base <= addr && addr < (base + size); -} - - -/* - * Returns the USS (unique set size) 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) { - long pid; - int err; - size_t len; - 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; - kern_return_t kr; - vm_size_t page_size; - mach_vm_address_t addr = MACH_VM_MIN_ADDRESS; - mach_port_t task = MACH_PORT_NULL; - vm_region_top_info_data_t info; - mach_port_t object_name; - - if (! PyArg_ParseTuple(args, "l", &pid)) + if (PyModule_AddIntConstant(mod, "TCPS_CLOSING", TCPS_CLOSING)) 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 (PyModule_AddIntConstant(mod, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT)) return NULL; - } - - len = sizeof(cpu_type); - if (sysctlbyname("sysctl.proc_cputype", &cpu_type, &len, NULL, 0) != 0) - return PyErr_SetFromErrno(PyExc_OSError); - - // 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) { - kr = mach_vm_region( - 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; - } - else if (kr != KERN_SUCCESS) { - PyErr_Format( - PyExc_RuntimeError, - "mach_vm_region(VM_REGION_TOP_INFO) syscall failed"); - return NULL; - } - - if (psutil_in_shared_region(addr, cpu_type) && - info.share_mode != SM_PRIVATE) { - continue; - } - - switch (info.share_mode) { -#ifdef SM_LARGE_PAGE - case SM_LARGE_PAGE: - // NB: Large pages are not shareable and always resident. -#endif - case SM_PRIVATE: - private_pages += info.private_pages_resident; - private_pages += info.shared_pages_resident; - break; - case SM_COW: - private_pages += info.private_pages_resident; - if (info.ref_count == 1) { - // Treat copy-on-write pages as private if they only - // have one reference. - private_pages += info.shared_pages_resident; - } - break; - case SM_SHARED: - default: - break; - } - } - - 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 system virtual memory stats. - * See: - * http://opensource.apple.com/source/system_cmds/system_cmds-498.2/ - * vm_stat.tproj/vm_stat.c - */ -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; - int pagesize = 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"); + if (PyModule_AddIntConstant(mod, "TCPS_LISTEN", TCPS_LISTEN)) return NULL; - } - - // vm - if (!psutil_sys_vminfo(&vm)) + if (PyModule_AddIntConstant(mod, "TCPS_ESTABLISHED", TCPS_ESTABLISHED)) return NULL; - - 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.free_count - vm.speculative_count) * pagesize - ); -} - - -/* - * 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; - int pagesize = 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"); + if (PyModule_AddIntConstant(mod, "TCPS_SYN_SENT", TCPS_SYN_SENT)) return NULL; - } - if (!psutil_sys_vminfo(&vmstat)) + if (PyModule_AddIntConstant(mod, "TCPS_SYN_RECEIVED", TCPS_SYN_RECEIVED)) 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 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 - */ -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) + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1)) 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_DECREF(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; -} - - -/* - * Retrieve CPU frequency. - */ -static PyObject * -psutil_cpu_freq(PyObject *self, PyObject *args) { - int64_t curr; - int64_t min; - 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); - - return Py_BuildValue( - "KKK", - curr / 1000 / 1000, - min / 1000 / 1000, - max / 1000 / 1000); -} - - -/* - * 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) + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2)) 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_DECREF(py_dev); - Py_DECREF(py_mountp); - Py_DECREF(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 process threads - */ -static PyObject * -psutil_proc_threads(PyObject *self, PyObject *args) { - long 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; - thread_info_data_t thinfo_basic; - 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) - 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(""); - 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) { - AccessDenied(""); - } - else { - // otherwise throw a runtime error with appropriate error code - PyErr_Format(PyExc_RuntimeError, - "task_info(TASK_BASIC_INFO) syscall failed"); - } - goto error; - } - - err = task_threads(task, &thread_list, &thread_count); - if (err != KERN_SUCCESS) { - PyErr_Format(PyExc_RuntimeError, "task_threads() syscall failed"); - goto error; - } - - 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); - if (kr != KERN_SUCCESS) { - PyErr_Format(PyExc_RuntimeError, - "thread_info(THREAD_BASIC_INFO) syscall failed"); - goto error; - } - - basic_info_th = (thread_basic_info_t)thinfo_basic; - 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 - ); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_DECREF(py_tuple); - } - - ret = vm_deallocate(task, (vm_address_t)thread_list, - thread_count * sizeof(int)); - if (ret != KERN_SUCCESS) - PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); - - 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(int)); - if (ret != KERN_SUCCESS) - PyErr_WarnEx(PyExc_RuntimeWarning, "vm_deallocate() failed", 2); - } - return NULL; -} - - -/* - * Return process open files as a Python tuple. - * References: - * - lsof source code: http://goo.gl/SYW79 and http://goo.gl/m78fd - * - /usr/include/sys/proc_info.h - */ -static PyObject * -psutil_proc_open_files(PyObject *self, PyObject *args) { - long pid; - int pidinfo_result; - int iterations; - 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; - - if (py_retlist == NULL) - return NULL; - - if (! PyArg_ParseTuple(args, "l", &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) - goto error; - - iterations = (pidinfo_result / PROC_PIDLISTFD_SIZE); - - for (i = 0; i < iterations; i++) { - fdp_pointer = &fds_pointer[i]; - - 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)); - - // --- errors checking - if ((nb <= 0) || nb < sizeof(vi)) { - if ((errno == ENOENT) || (errno == EBADF)) { - // no such file or directory or bad file descriptor; - // let's assume the file has been closed or removed - continue; - } - else { - psutil_raise_for_pid( - pid, "proc_pidinfo(PROC_PIDFDVNODEPATHINFO)"); - goto error; - } - } - // --- /errors checking - - // --- construct python list - 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)) - goto error; - Py_DECREF(py_tuple); - py_tuple = NULL; - Py_DECREF(py_path); - py_path = NULL; - // --- /construct python list - } - } - - free(fds_pointer); - return py_retlist; - -error: - Py_XDECREF(py_tuple); - Py_XDECREF(py_path); - Py_DECREF(py_retlist); - if (fds_pointer != NULL) - free(fds_pointer); - return NULL; // exception has already been set earlier -} - - -/* - * 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 - * - /usr/include/sys/proc_info.h - */ -static PyObject * -psutil_proc_connections(PyObject *self, PyObject *args) { - long pid; - int pidinfo_result; - int iterations; - 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; - PyObject *py_raddr = NULL; - PyObject *py_af_filter = NULL; - PyObject *py_type_filter = NULL; - - if (py_retlist == NULL) - return NULL; - - 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)) { - PyErr_SetString(PyExc_TypeError, "arg 2 or 3 is not a sequence"); - 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) - goto error; - - iterations = (pidinfo_result / PROC_PIDLISTFD_SIZE); - for (i = 0; i < iterations; i++) { - py_tuple = NULL; - py_laddr = NULL; - py_raddr = NULL; - fdp_pointer = &fds_pointer[i]; - - if (fdp_pointer->proc_fdtype == PROX_FDTYPE_SOCKET) { - errno = 0; - nb = proc_pidfdinfo((pid_t)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 - continue; - } - else { - psutil_raise_for_pid( - pid, "proc_pidinfo(PROC_PIDFDSOCKETINFO)"); - goto error; - } - } - // --- /errors checking - - // - int fd, family, type, lport, rport, state; - char lip[200], rip[200]; - int inseq; - PyObject *py_family; - PyObject *py_type; - - fd = (int)fdp_pointer->proc_fd; - family = si.psi.soi_family; - type = si.psi.soi_type; - - // apply filters - py_family = PyLong_FromLong((long)family); - inseq = PySequence_Contains(py_af_filter, py_family); - Py_DECREF(py_family); - if (inseq == 0) - continue; - py_type = PyLong_FromLong((long)type); - inseq = PySequence_Contains(py_type_filter, py_type); - Py_DECREF(py_type); - 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, - &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)); - } - 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) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - lport = ntohs(si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_lport); - rport = ntohs(si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_fport); - if (type == SOCK_STREAM) - state = (int)si.psi.soi_proto.pri_tcp.tcpsi_state; - else - state = PSUTIL_CONN_NONE; - - 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; - - // construct the python list - 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)) - goto error; - Py_DECREF(py_tuple); - } - else if (family == AF_UNIX) { - py_laddr = PyUnicode_DecodeFSDefault( - 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); - if (!py_raddr) - goto error; - // construct the python list - py_tuple = Py_BuildValue( - "(iiiOOi)", - fd, family, type, - py_laddr, - py_raddr, - PSUTIL_CONN_NONE); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_DECREF(py_tuple); - Py_DECREF(py_laddr); - Py_DECREF(py_raddr); - } - } - } - - free(fds_pointer); - return py_retlist; - -error: - Py_XDECREF(py_tuple); - Py_XDECREF(py_laddr); - Py_XDECREF(py_raddr); - Py_DECREF(py_retlist); - if (fds_pointer != NULL) - free(fds_pointer); - return NULL; -} - - -/* - * Return number of file descriptors opened by process. - * Raises NSP in case of zombie process. - */ -static PyObject * -psutil_proc_num_fds(PyObject *self, PyObject *args) { - long pid; - int pidinfo_result; - int num; - struct proc_fdinfo *fds_pointer; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - - pidinfo_result = proc_pidinfo((pid_t)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); - if (pidinfo_result <= 0) { - free(fds_pointer); - return PyErr_SetFromErrno(PyExc_OSError); - } - - num = (pidinfo_result / PROC_PIDLISTFD_SIZE); - free(fds_pointer); - return Py_BuildValue("i", num); -} - - -/* - * 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_IFLIST2; // 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; ) { - 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_DECREF(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_retdict = PyDict_New(); - PyObject *py_disk_info = NULL; - - if (py_retdict == NULL) + if (PyModule_AddIntConstant(mod, "TCPS_LAST_ACK", TCPS_LAST_ACK)) 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 OS X 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_DECREF(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) + if (PyModule_AddIntConstant(mod, "TCPS_TIME_WAIT", TCPS_TIME_WAIT)) 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( - "(OOOfi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (float)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_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(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 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)); + if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE)) 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. - */ -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. - */ -static PyMethodDef -PsutilMethods[] = { - // --- 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_memory_maps", psutil_proc_memory_maps, METH_VARARGS, - "Return a list of tuples for every process's memory map"}, - - // --- 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_phys", psutil_cpu_count_phys, METH_VARARGS, - "Return number of physical CPUs 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."}, - - // --- others - {"set_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, - - {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_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 -{ -#if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("_psutil_osx", PsutilMethods); -#endif - 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 - 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); - // 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); - - psutil_setup(); - - if (module == NULL) - INITERROR; -#if PY_MAJOR_VERSION >= 3 - return module; -#endif + return mod; } diff --git a/psutil/_psutil_posix.c b/psutil/_psutil_posix.c deleted file mode 100644 index cc827273ca..0000000000 --- a/psutil/_psutil_posix.c +++ /dev/null @@ -1,710 +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 -#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 -#elif defined(PSUTIL_BSD) || defined(PSUTIL_OSX) - #include - #include - #include - #include - #include - #include -#elif defined(PSUTIL_SUNOS) - #include - #include -#elif defined(PSUTIL_AIX) - #include -#endif - -#include "_psutil_common.h" - -/* - * Check if PID exists. Return values: - * 1: exists - * 0: does not exist - * -1: error (Python exception is set) - */ -int -psutil_pid_exists(long 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 - } - -#if defined(PSUTIL_OSX) - ret = kill((pid_t)pid , 0); -#else - ret = kill(pid , 0); -#endif - - 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. - */ -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(""); - } - else { - PyErr_Format(PyExc_RuntimeError, "%s syscall failed", syscall_name); - } - return 0; -} - - -/* - * Given a PID return process priority as a Python integer. - */ -static PyObject * -psutil_posix_getpriority(PyObject *self, PyObject *args) { - long pid; - int priority; - errno = 0; - - if (! PyArg_ParseTuple(args, "l", &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) { - long pid; - int priority; - int retval; - - if (! PyArg_ParseTuple(args, "li", &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_DECREF(py_tuple); - Py_DECREF(py_address); - Py_DECREF(py_netmask); - Py_DECREF(py_broadcast); - Py_DECREF(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 = 0; - 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 - 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)); - 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 != 0) - close(sock); - return PyErr_SetFromErrno(PyExc_OSError); -} - - -/* - * Inspect NIC flags, returns a bool indicating whether the NIC is - * running. References: - * http://www.i-scream.org/libstatgrab/ - */ -static PyObject * -psutil_net_if_flags(PyObject *self, PyObject *args) { - char *nic_name; - int sock = 0; - 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; - - 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_UP) != 0) - return Py_BuildValue("O", Py_True); - else - return Py_BuildValue("O", Py_False); - -error: - if (sock != 0) - close(sock); - return PyErr_SetFromErrno(PyExc_OSError); -} - - -/* - * net_if_stats() OSX/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) - // 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 - 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 = 0; - 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) - goto error; - 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); - -error: - if (sock != 0) - close(sock); - return PyErr_SetFromErrno(PyExc_OSError); -} -#endif // net_if_stats() OSX/BSD implementation - - -/* - * define the psutil C module methods and initialize the module. - */ -static PyMethodDef -PsutilMethods[] = { - {"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_flags", psutil_net_if_flags, METH_VARARGS, - "Retrieve NIC flags"}, -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) - {"net_if_duplex_speed", psutil_net_if_duplex_speed, METH_VARARGS, - "Return NIC stats."}, -#endif - {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_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 -{ -#if PY_MAJOR_VERSION >= 3 - PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("_psutil_posix", PsutilMethods); -#endif - -#if defined(PSUTIL_BSD) || defined(PSUTIL_OSX) || defined(PSUTIL_SUNOS) || defined(PSUTIL_AIX) - PyModule_AddIntConstant(module, "AF_LINK", AF_LINK); -#endif - - if (module == NULL) - INITERROR; -#if PY_MAJOR_VERSION >= 3 - return module; -#endif -} diff --git a/psutil/_psutil_posix.h b/psutil/_psutil_posix.h deleted file mode 100644 index fe25b36695..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. - */ - -int psutil_pid_exists(long pid); -void psutil_raise_for_pid(long pid, char *msg); diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index 99069f56af..644ec52da1 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -2,1767 +2,158 @@ * 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: - * 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 =\ -*/ +// 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 NEW_MIB_COMPLIANT 1 #define _STRUCTURED_PROC 1 +#define NEW_MIB_COMPLIANT 1 #include #if !defined(_LP64) && _FILE_OFFSET_BITS == 64 -# undef _FILE_OFFSET_BITS -# undef _LARGEFILE64_SOURCE +#undef _FILE_OFFSET_BITS +#undef _LARGEFILE64_SOURCE #endif -#include -#include -#include -#include -#include -#include // for MNTTAB -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include // fabs() - -#include "_psutil_common.h" -#include "_psutil_posix.h" - -#include "arch/solaris/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. - */ -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 delemiter dm. - * Omit empty records. - */ -static int -cstrings_array_to_string(char **joined, char ** array, size_t count, char dm) { - int i; - size_t total_length = 0; - size_t item_length = 0; - char *result = NULL; - char *last = NULL; - - if (!array || !joined) - return 0; +#include - 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) - 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)) +#ifdef Py_GIL_DISABLED + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) return NULL; +#endif - 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(""); - 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)) + if (psutil_setup() != 0) return NULL; - sprintf(path, "%s/%i/status", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (psutil_posix_add_constants(mod) != 0) 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 = NULL; - 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)) + if (psutil_posix_add_methods(mod) != 0) return NULL; - sprintf(path, "%s/%i/lpsinfo", procfs_path, pid); - fd = open(path, O_RDONLY); - if (fd == -1) { - PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); + if (PyModule_AddIntConstant(mod, "version", PSUTIL_VERSION)) 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)) + if (PyModule_AddIntConstant(mod, "SSLEEP", SSLEEP)) return NULL; - sprintf(path, "%s/%i/cred", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (PyModule_AddIntConstant(mod, "SRUN", SRUN)) 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)) + if (PyModule_AddIntConstant(mod, "SZOMB", SZOMB)) return NULL; - sprintf(path, "%s/%i/usage", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (PyModule_AddIntConstant(mod, "SSTOP", SSTOP)) 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)) + if (PyModule_AddIntConstant(mod, "SIDL", SIDL)) return NULL; - sprintf(path, "%s/%i/usage", procfs_path, pid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + if (PyModule_AddIntConstant(mod, "SONPROC", SONPROC)) 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)) +#ifdef SWAIT + if (PyModule_AddIntConstant(mod, "SWAIT", SWAIT)) return NULL; - sprintf(path, "%s/%i/lwp/%i/lwpstatus", procfs_path, pid, tid); - if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) +#else + // sys/proc.h started defining SWAIT somewhere + // after Update 3 and prior to Update 5 included. + if (PyModule_AddIntConstant(mod, "SWAIT", 0)) 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); +#endif + // for process tty + if (PyModule_AddIntConstant(mod, "PRNODEV", PRNODEV)) return NULL; - } - if (num == 0) { - PyErr_SetString(PyExc_RuntimeError, "no swap devices configured"); + if (PyModule_AddIntConstant(mod, "TCPS_CLOSED", TCPS_CLOSED)) return NULL; - } - if ((st = malloc(num * sizeof(swapent_t) + sizeof (int))) == NULL) { - PyErr_SetString(PyExc_RuntimeError, "malloc failed"); + if (PyModule_AddIntConstant(mod, "TCPS_CLOSING", TCPS_CLOSING)) return NULL; - } - if ((path = malloc(num * MAXPATHLEN)) == NULL) { - PyErr_SetString(PyExc_RuntimeError, "malloc failed"); + if (PyModule_AddIntConstant(mod, "TCPS_CLOSE_WAIT", TCPS_CLOSE_WAIT)) 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); + if (PyModule_AddIntConstant(mod, "TCPS_LISTEN", TCPS_LISTEN)) 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"); + if (PyModule_AddIntConstant(mod, "TCPS_ESTABLISHED", TCPS_ESTABLISHED)) 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) + if (PyModule_AddIntConstant(mod, "TCPS_SYN_SENT", TCPS_SYN_SENT)) 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( - "(OOOfOi)", - py_username, // username - py_tty, // tty - py_hostname, // hostname - (float)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_DECREF(py_username); - Py_DECREF(py_tty); - Py_DECREF(py_hostname); - Py_DECREF(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) + if (PyModule_AddIntConstant(mod, "TCPS_SYN_RCVD", TCPS_SYN_RCVD)) 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_DECREF(py_dev); - Py_DECREF(py_mountp); - Py_DECREF(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) + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_1", TCPS_FIN_WAIT_1)) 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_DECREF(py_cputime); - py_cputime = NULL; - } - } - - 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) + if (PyModule_AddIntConstant(mod, "TCPS_FIN_WAIT_2", TCPS_FIN_WAIT_2)) 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_DECREF(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) + if (PyModule_AddIntConstant(mod, "TCPS_LAST_ACK", TCPS_LAST_ACK)) 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_DECREF(py_path); - Py_DECREF(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) + if (PyModule_AddIntConstant(mod, "TCPS_TIME_WAIT", TCPS_TIME_WAIT)) 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 - 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_DECREF(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; - 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) + // sunos specific + if (PyModule_AddIntConstant(mod, "TCPS_IDLE", TCPS_IDLE)) 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; - } - - int 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 + sizeof tor, &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; - } - - 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; - - // contruct 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_DECREF(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; - - // contruct 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_DECREF(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); - 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_DECREF(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_DECREF(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"); + // sunos specific + if (PyModule_AddIntConstant(mod, "TCPS_BOUND", TCPS_BOUND)) return NULL; - } - return Py_BuildValue("f", boot_time); -} - - -/* - * Return the number of physical CPU cores on the system. - */ -static PyObject * -psutil_cpu_count_phys(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) + if (PyModule_AddIntConstant(mod, "PSUTIL_CONN_NONE", PSUTIL_CONN_NONE)) 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; - - 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_DECREF(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[] = { - // --- 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"}, - - // --- 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_phys", psutil_cpu_count_phys, METH_VARARGS, - "Return the number of physical CPUs 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"}, - // --- others - {"set_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, - - {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_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, - NULL, - psutil_sunos_traverse, - psutil_sunos_clear, - 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 - PyObject *module = PyModule_Create(&moduledef); -#else - PyObject *module = Py_InitModule("_psutil_sunos", PsutilMethods); -#endif - PyModule_AddIntConstant(module, "version", PSUTIL_VERSION); - - 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); - PyModule_AddIntConstant(module, "SWAIT", SWAIT); - - 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); - // sunos specific - PyModule_AddIntConstant(module, "TCPS_IDLE", TCPS_IDLE); - // sunos specific - 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 - return module; -#endif + return mod; } diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index bb993dbd43..892b74c11e 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -2,3721 +2,106 @@ * 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 */ -// Fixes clash between winsock2.h and windows.h -#define WIN32_LEAN_AND_MEAN +// 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 -#include -#include -#include -#include -#include -#include -#include -#if (_WIN32_WINNT >= 0x0600) // Windows Vista and above -#include -#endif -#include -#include -#include -#include -#include - -// Link with Iphlpapi.lib -#pragma comment(lib, "IPHLPAPI.lib") - -#include "_psutil_common.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" - -#ifdef __MINGW32__ -#include "arch/windows/glpi.h" -#endif - - -/* - * ============================================================================ - * 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 -#define BYTESWAP_USHORT(x) ((((USHORT)(x) << 8) | ((USHORT)(x) >> 8)) & 0xffff) -#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); - -typedef BOOL (WINAPI *LPFN_GLPI) - (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION, PDWORD); - -// 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 -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; -} - - -/* - * ============================================================================ - * Public Python API - * ============================================================================ - */ - -// Raised by Process.wait(). -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. - */ -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; - HINSTANCE hKernel32; - psutil_GetTickCount64 = NULL; - - 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 betwee 01-01-1970 - 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 - // 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 - hKernel32 = GetModuleHandleW(L"KERNEL32"); - psutil_GetTickCount64 = (void*)GetProcAddress(hKernel32, "GetTickCount64"); - if (psutil_GetTickCount64 != NULL) { - // Windows >= Vista - uptime = psutil_GetTickCount64() / (ULONGLONG)1000.00f; - return Py_BuildValue("K", pt - uptime); - } - else { - // Windows XP. - // GetTickCount() time will wrap around to zero if the - // system is run continuously for 49.7 days. - uptime = GetTickCount() / (LONGLONG)1000.00f; - return Py_BuildValue("L", pt - uptime); - } -} - - -/* - * Return 1 if PID exists in the current process list, else 0. - */ -static PyObject * -psutil_pid_exists(PyObject *self, PyObject *args) { - long pid; - int status; - - if (! PyArg_ParseTuple(args, "l", &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 = Py_BuildValue("I", proclist[i]); - if (!py_pid) - goto error; - if (PyList_Append(py_retlist, py_pid)) - goto error; - Py_DECREF(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 err; - long pid; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - if (pid == 0) - return AccessDenied(""); - - hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); - 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(""); - } - else { - PyErr_SetFromWindowsErr(0); - } - return NULL; - } - - // kill the process - if (! TerminateProcess(hProcess, SIGTERM)) { - err = GetLastError(); - // See: https://github.com/giampaolo/psutil/issues/1099 - if (err != ERROR_ACCESS_DENIED) { - CloseHandle(hProcess); - PyErr_SetFromWindowsErr(err); - 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; - long pid; - long timeout; - - if (! PyArg_ParseTuple(args, "ll", &pid, &timeout)) - return NULL; - if (pid == 0) - return AccessDenied(""); - - 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 - return PyErr_SetFromWindowsErr(0); - } - - // 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) { - CloseHandle(hProcess); - PyErr_SetFromWindowsErr(0); - return NULL; - } - if (retVal == WAIT_TIMEOUT) { - CloseHandle(hProcess); - PyErr_SetString(TimeoutExpired, - "WaitForSingleObject() returned WAIT_TIMEOUT"); - return NULL; - } - if (retVal == WAIT_ABANDONED) { - psutil_debug("WaitForSingleObject() -> WAIT_ABANDONED"); - CloseHandle(hProcess); - PyErr_SetString(TimeoutAbandoned, - "WaitForSingleObject() returned WAIT_ABANDONED"); - 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) { - CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(GetLastError()); - } - 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_cpu_times(PyObject *self, PyObject *args) { - long pid; - HANDLE hProcess; - FILETIME ftCreate, ftExit, ftKernel, ftUser; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - - hProcess = psutil_handle_from_pid(pid); - 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(""); - } - else { - return PyErr_SetFromWindowsErr(0); - } - } - - CloseHandle(hProcess); - - /* - * User and kernel times are represented as a FILETIME structure - * wich 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( - "(dd)", - (double)(ftUser.dwHighDateTime * 429.4967296 + \ - ftUser.dwLowDateTime * 1e-7), - (double)(ftKernel.dwHighDateTime * 429.4967296 + \ - ftKernel.dwLowDateTime * 1e-7) - ); -} - - -/* - * 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) { - long pid; - long long unix_time; - HANDLE hProcess; - FILETIME ftCreate, ftExit, ftKernel, ftUser; - - if (! PyArg_ParseTuple(args, "l", &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); - 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(""); - } - else { - return PyErr_SetFromWindowsErr(0); - } - } - - 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(""); - } - 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 the number of logical CPUs. - */ -static PyObject * -psutil_cpu_count_logical(PyObject *self, PyObject *args) { - SYSTEM_INFO system_info; - system_info.dwNumberOfProcessors = 0; - - GetSystemInfo(&system_info); - if (system_info.dwNumberOfProcessors == 0) - Py_RETURN_NONE; // mimic os.cpu_count() - else - return Py_BuildValue("I", system_info.dwNumberOfProcessors); -} - - -/* - * Return the number of physical CPU cores. - */ -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; - DWORD length = 0; - DWORD offset = 0; - int ncpus = 0; - - glpi = (LPFN_GLPI)GetProcAddress(GetModuleHandle(TEXT("kernel32")), - "GetLogicalProcessorInformation"); - if (glpi == NULL) - goto return_none; - - while (1) { - rc = glpi(buffer, &length); - if (rc == FALSE) { - if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - if (buffer) - free(buffer); - buffer = (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION)malloc( - length); - if (NULL == buffer) { - PyErr_NoMemory(); - return NULL; - } - } - else { - goto return_none; - } - } - else { - break; - } - } - - ptr = buffer; - while (offset + sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION) <= length) { - if (ptr->Relationship == RelationProcessorCore) - ncpus += 1; - offset += sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION); - ptr++; - } - - free(buffer); - if (ncpus == 0) - goto return_none; - else - return Py_BuildValue("i", ncpus); - -return_none: - // mimic os.cpu_count() - if (buffer != NULL) - free(buffer); - Py_RETURN_NONE; -} - - -/* - * Return process cmdline as a Python list of cmdline arguments. - */ -static PyObject * -psutil_proc_cmdline(PyObject *self, PyObject *args) { - long pid; - int pid_return; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - if ((pid == 0) || (pid == 4)) - return Py_BuildValue("[]"); - - 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 process cmdline as a Python list of cmdline arguments. - */ -static PyObject * -psutil_proc_environ(PyObject *self, PyObject *args) { - long pid; - int pid_return; - - if (! PyArg_ParseTuple(args, "l", &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(""); - if (pid_return == -1) - return NULL; - - return psutil_get_environ(pid); -} - - -/* - * Return process executable path. - */ -static PyObject * -psutil_proc_exe(PyObject *self, PyObject *args) { - long pid; - HANDLE hProcess; - wchar_t exe[MAX_PATH]; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - hProcess = psutil_handle_from_pid_waccess(pid, PROCESS_QUERY_INFORMATION); - if (NULL == hProcess) - return NULL; - if (GetProcessImageFileNameW(hProcess, exe, MAX_PATH) == 0) { - CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); - } - 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) { - long pid; - int ok; - PROCESSENTRY32W pentry; - HANDLE hSnapShot; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, pid); - if (hSnapShot == INVALID_HANDLE_VALUE) - return PyErr_SetFromWindowsErr(0); - pentry.dwSize = sizeof(PROCESSENTRY32W); - ok = Process32FirstW(hSnapShot, &pentry); - if (! ok) { - CloseHandle(hSnapShot); - return PyErr_SetFromWindowsErr(0); - } - while (ok) { - if (pentry.th32ProcessID == pid) { - CloseHandle(hSnapShot); - return PyUnicode_FromWideChar( - pentry.szExeFile, wcslen(pentry.szExeFile)); - } - ok = Process32NextW(hSnapShot, &pentry); - } - - CloseHandle(hSnapShot); - NoSuchProcess(""); - return NULL; -} - - -/* - * Return process memory information as a Python tuple. - */ -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; - - hProcess = psutil_handle_from_pid(pid); - if (NULL == hProcess) - return NULL; - - if (! GetProcessMemoryInfo(hProcess, (PPROCESS_MEMORY_COUNTERS)&cnt, - sizeof(cnt))) { - CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); - } - -#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 - // 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)private); -#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)private); -#endif -} - - - -/** - * 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 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; - 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; - - proc = psutil_handle_from_pid(pid); - if (proc == 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 + (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; - } - - 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++; - } - } - - // GetSystemInfo has no return value. - GetSystemInfo(&system_info); - total = private_pages * system_info.dwPageSize; - py_result = Py_BuildValue("K", total); - -done: - if (proc) { - CloseHandle(proc); - } - - if (info_array) { - free(info_array); - } - - return py_result; -} - - -/* - * Return a Python integer indicating the total amount of physical memory - * in bytes. - */ -static PyObject * -psutil_virtual_mem(PyObject *self, PyObject *args) { - MEMORYSTATUSEX memInfo; - memInfo.dwLength = sizeof(MEMORYSTATUSEX); - - if (! GlobalMemoryStatusEx(&memInfo)) - return PyErr_SetFromWindowsErr(0); - 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 -} - -/* - * 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) { - // 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; - _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *sppi = NULL; - SYSTEM_INFO si; - UINT i; - PyObject *py_tuple = NULL; - PyObject *py_retlist = PyList_New(0); - - if (py_retlist == NULL) - return NULL; - - // 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; - } - - // retrives number of processors - GetSystemInfo(&si); - - // 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)); - if (sppi == NULL) { - PyErr_NoMemory(); - goto error; - } - - // gets cpu time informations - status = NtQuerySystemInformation( - SystemProcessorPerformanceInformation, - sppi, - si.dwNumberOfProcessors * sizeof - (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), - NULL); - if (status != 0) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - // computes system global times summing each - // processor value - idle = user = kernel = interrupt = dpc = 0; - for (i = 0; i < si.dwNumberOfProcessors; 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_DECREF(py_tuple); - } - - free(sppi); - FreeLibrary(hNtDll); - return py_retlist; - -error: - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - if (sppi) - free(sppi); - if (hNtDll) - FreeLibrary(hNtDll); - return NULL; -} - - -/* - * Return process current working directory as a Python string. - */ -static PyObject * -psutil_proc_cwd(PyObject *self, PyObject *args) { - long pid; - int pid_return; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - - pid_return = psutil_pid_is_running(pid); - if (pid_return == 0) - return NoSuchProcess(""); - if (pid_return == -1) - return NULL; - - return psutil_get_cwd(pid); -} - - -/* - * 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_SetFromWindowsErr(0); - return FALSE; - } - - // Fill in the size of the structure before using it - te32.dwSize = sizeof(THREADENTRY32); - - if (! Thread32First(hThreadSnap, &te32)) { - PyErr_SetFromWindowsErr(0); - 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_SetFromWindowsErr(0); - CloseHandle(hThread); - CloseHandle(hThreadSnap); - return FALSE; - } - if (suspend == 1) { - if (SuspendThread(hThread) == (DWORD) - 1) { - PyErr_SetFromWindowsErr(0); - CloseHandle(hThread); - CloseHandle(hThreadSnap); - return FALSE; - } - } - else { - if (ResumeThread(hThread) == (DWORD) - 1) { - PyErr_SetFromWindowsErr(0); - 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) { - long pid; - int suspend = 1; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - if (! psutil_proc_suspend_or_resume(pid, suspend)) - return NULL; - Py_RETURN_NONE; -} - - -static PyObject * -psutil_proc_resume(PyObject *self, PyObject *args) { - long pid; - int suspend = 0; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - if (! psutil_proc_suspend_or_resume(pid, suspend)) - return NULL; - Py_RETURN_NONE; -} - - -static PyObject * -psutil_proc_threads(PyObject *self, PyObject *args) { - HANDLE hThread; - THREADENTRY32 te32 = {0}; - long 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, "l", &pid)) - goto error; - if (pid == 0) { - // raise AD instead of returning 0 as procexp is able to - // retrieve useful information somehow - AccessDenied(""); - goto error; - } - - pid_return = psutil_pid_is_running(pid); - if (pid_return == 0) { - NoSuchProcess(""); - goto error; - } - if (pid_return == -1) - goto error; - - hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); - if (hThreadSnap == INVALID_HANDLE_VALUE) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - // Fill in the size of the structure before using it - te32.dwSize = sizeof(THREADENTRY32); - - if (! Thread32First(hThreadSnap, &te32)) { - PyErr_SetFromWindowsErr(0); - 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_SetFromWindowsErr(0); - goto error; - } - - /* - * User and kernel times are represented as a FILETIME structure - * wich 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 * 429.4967296 + \ - ftUser.dwLowDateTime * 1e-7), - (double)(ftKernel.dwHighDateTime * 429.4967296 + \ - ftKernel.dwLowDateTime * 1e-7)); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_DECREF(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) { - long pid; - HANDLE processHandle; - DWORD access = PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION; - PyObject *py_retlist; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - - processHandle = psutil_handle_from_pid_waccess(pid, access); - if (processHandle == NULL) - return NULL; - py_retlist = psutil_get_open_files(pid, processHandle); - CloseHandle(processHandle); - if (py_retlist == NULL) - return PyErr_SetFromWindowsErr(0); - return py_retlist; -} - - -/* - 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. - */ -static PyObject * -psutil_proc_username(PyObject *self, PyObject *args) { - long 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; - PyObject *py_domain; - PyObject *py_tuple; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - - processHandle = psutil_handle_from_pid_waccess( - pid, PROCESS_QUERY_INFORMATION); - if (processHandle == NULL) - return NULL; - - if (!OpenProcessToken(processHandle, TOKEN_QUERY, &tokenHandle)) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - CloseHandle(processHandle); - processHandle = NULL; - - // Get the user SID. - bufferSize = 0x100; - while (1) { - user = malloc(bufferSize); - if (user == NULL) { - PyErr_NoMemory(); - goto error; - } - if (!GetTokenInformation(tokenHandle, TokenUser, user, bufferSize, - &bufferSize)) - { - if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - free(user); - continue; - } - else { - PyErr_SetFromWindowsErr(0); - goto error; - } - } - break; - } - - CloseHandle(tokenHandle); - tokenHandle = NULL; - - // resolve the SID to a name - nameSize = 0x100; - domainNameSize = 0x100; - while (1) { - name = malloc(nameSize * sizeof(WCHAR)); - if (name == NULL) { - PyErr_NoMemory(); - goto error; - } - domainName = malloc(domainNameSize * sizeof(WCHAR)); - if (domainName == NULL) { - PyErr_NoMemory(); - goto error; - } - if (!LookupAccountSidW(NULL, user->User.Sid, name, &nameSize, - domainName, &domainNameSize, &nameUse)) - { - if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - free(name); - free(domainName); - continue; - } - else { - PyErr_SetFromWindowsErr(0); - goto error; - } - } - break; - } - - py_domain = PyUnicode_FromWideChar(domainName, wcslen(domainName)); - if (! py_domain) - goto error; - py_username = PyUnicode_FromWideChar(name, wcslen(name)); - 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(name); - free(domainName); - free(user); - - return py_tuple; - -error: - if (processHandle != NULL) - CloseHandle(processHandle); - if (tokenHandle != NULL) - CloseHandle(tokenHandle); - if (name != NULL) - free(name); - if (domainName != NULL) - free(domainName); - if (user != NULL) - free(user); - Py_XDECREF(py_domain); - Py_XDECREF(py_username); - Py_XDECREF(py_tuple); - return NULL; -} - - -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, - 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. - // - // Also, since we may loop a theoretically unbounded number of times here, - // release the GIL while we're doing this. - 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) - { - *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; - } - } - Py_END_ALLOW_THREADS; - return error; -} - - -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, - 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. - // - // Also, since we may loop a theoretically unbounded number of times here, - // release the GIL while we're doing this. - 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) - { - *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; - } - } - Py_END_ALLOW_THREADS; - return error; -} - - -/* - * 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; - 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; - 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); - - if (! PyArg_ParseTuple(args, "lOO", &pid, &py_af_filter, &py_type_filter)) - { - _psutil_conn_decref_objs(); - return NULL; - } - - 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; - } - } - - // 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"); - _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(getExtendedTcpTable, - AF_INET, &table, &tableSize); - if (error == ERROR_NOT_ENOUGH_MEMORY) { - PyErr_NoMemory(); - goto error; - } - - if (error == NO_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; - 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; - 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_DECREF(py_conn_tuple); - } - } - else { - PyErr_SetFromWindowsErr(error); - goto error; - } - - 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)) - { - table = NULL; - 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 == ERROR_NOT_ENOUGH_MEMORY) { - PyErr_NoMemory(); - goto error; - } - - if (error == NO_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); - 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); - 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_DECREF(py_conn_tuple); - } - } - else { - PyErr_SetFromWindowsErr(error); - goto error; - } - - 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(getExtendedUdpTable, - AF_INET, &table, &tableSize); - if (error == ERROR_NOT_ENOUGH_MEMORY) { - PyErr_NoMemory(); - goto error; - } - - if (error == NO_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; - 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_DECREF(py_conn_tuple); - } - } - else { - PyErr_SetFromWindowsErr(error); - goto error; - } - - 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)) - { - table = NULL; - 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 == ERROR_NOT_ENOUGH_MEMORY) { - PyErr_NoMemory(); - goto error; - } - - if (error == NO_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); - 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_DECREF(py_conn_tuple); - } - } - else { - PyErr_SetFromWindowsErr(error); - goto error; - } - - 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. - */ -static PyObject * -psutil_proc_priority_get(PyObject *self, PyObject *args) { - long pid; - DWORD priority; - HANDLE hProcess; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - hProcess = psutil_handle_from_pid(pid); - if (hProcess == NULL) - return NULL; - priority = GetPriorityClass(hProcess); - CloseHandle(hProcess); - if (priority == 0) - return PyErr_SetFromWindowsErr(0); - return Py_BuildValue("i", priority); -} - - -/* - * Set process priority. - */ -static PyObject * -psutil_proc_priority_set(PyObject *self, PyObject *args) { - long pid; - int priority; - int retval; - HANDLE hProcess; - DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; - - if (! PyArg_ParseTuple(args, "li", &pid, &priority)) - return NULL; - hProcess = psutil_handle_from_pid_waccess(pid, access); - if (hProcess == NULL) - return NULL; - retval = SetPriorityClass(hProcess, priority); - CloseHandle(hProcess); - if (retval == 0) - return PyErr_SetFromWindowsErr(0); - Py_RETURN_NONE; -} - - -#if (_WIN32_WINNT >= 0x0600) // Windows Vista -/* - * Get process IO priority as a Python integer. - */ -static PyObject * -psutil_proc_io_priority_get(PyObject *self, PyObject *args) { - long pid; - HANDLE hProcess; - PULONG IoPriority; - - _NtQueryInformationProcess NtQueryInformationProcess = - (_NtQueryInformationProcess)GetProcAddress( - GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess"); - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - hProcess = psutil_handle_from_pid(pid); - if (hProcess == NULL) - return NULL; - - NtQueryInformationProcess( - hProcess, - ProcessIoPriority, - &IoPriority, - sizeof(ULONG), - NULL - ); - CloseHandle(hProcess); - return Py_BuildValue("i", IoPriority); -} - - -/* - * Set process IO priority. - */ -static PyObject * -psutil_proc_io_priority_set(PyObject *self, PyObject *args) { - long pid; - int prio; - HANDLE hProcess; - - _NtSetInformationProcess NtSetInformationProcess = - (_NtSetInformationProcess)GetProcAddress( - GetModuleHandleA("ntdll.dll"), "NtSetInformationProcess"); - - if (NtSetInformationProcess == NULL) { - PyErr_SetString(PyExc_RuntimeError, - "couldn't get NtSetInformationProcess syscall"); - return NULL; - } - - if (! PyArg_ParseTuple(args, "li", &pid, &prio)) - return NULL; - hProcess = psutil_handle_from_pid_waccess(pid, PROCESS_ALL_ACCESS); - if (hProcess == NULL) - return NULL; - - NtSetInformationProcess( - hProcess, - ProcessIoPriority, - (PVOID)&prio, - sizeof((PVOID)prio) - ); - - CloseHandle(hProcess); - Py_RETURN_NONE; -} -#endif - - -/* - * 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, "l", &pid)) - return NULL; - hProcess = psutil_handle_from_pid(pid); - if (NULL == hProcess) - return NULL; - if (! GetProcessIoCounters(hProcess, &IoCounters)) { - CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); - } - 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, "l", &pid)) - return NULL; - hProcess = psutil_handle_from_pid(pid); - if (hProcess == NULL) { - return NULL; - } - if (GetProcessAffinityMask(hProcess, &proc_mask, &system_mask) == 0) { - CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); - } - - 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 dwDesiredAccess = \ - PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; - DWORD_PTR mask; - -#ifdef _WIN64 - if (! PyArg_ParseTuple(args, "lK", &pid, &mask)) -#else - if (! PyArg_ParseTuple(args, "lk", &pid, &mask)) -#endif - { - return NULL; - } - hProcess = psutil_handle_from_pid_waccess(pid, dwDesiredAccess); - if (hProcess == NULL) - return NULL; - - if (SetProcessAffinityMask(hProcess, mask) == 0) { - CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); - } - - CloseHandle(hProcess); - Py_RETURN_NONE; -} - - -/* - * Return True if one of the process threads is in a waiting or - * suspended status. - */ -static PyObject * -psutil_proc_is_suspended(PyObject *self, PyObject *args) { - DWORD pid; - ULONG i; - PSYSTEM_PROCESS_INFORMATION process; - PVOID buffer; - - 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++) { - if (process->Threads[i].ThreadState != Waiting || - process->Threads[i].WaitReason != Suspended) - { - free(buffer); - Py_RETURN_FALSE; - } - } - free(buffer); - Py_RETURN_TRUE; -} - - -/* - * 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_SetFromWindowsErr(0); - 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; - -#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(); - 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; - -#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, - (pIfRow->OutUcastPkts + pIfRow->OutNUcastPkts), - (pIfRow->InUcastPkts + pIfRow->InNUcastPkts), - pIfRow->InErrors, - 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; - - 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_XDECREF(py_nic_name); - Py_XDECREF(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 - */ -static PyObject * -psutil_disk_io_counters(PyObject *self, PyObject *args) { - DISK_PERFORMANCE_WIN_2008 diskPerformance; - DWORD dwSize; - HANDLE hDevice = NULL; - char szDevice[MAX_PATH]; - char szDeviceDisplay[MAX_PATH]; - int devNum; - int i; - size_t ioctrlSize; - BOOL WINAPI 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_XDECREF(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_DECREF(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_DECREF(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 - */ -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; - DWORD count; - DWORD i; - DWORD sessionId; - DWORD bytes; - 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; - - if (py_retlist == NULL) - return NULL; - - hInstWinSta = LoadLibraryA("winsta.dll"); - WinStationQueryInformationW = (PWINSTATIONQUERYINFORMATIONW) \ - GetProcAddress(hInstWinSta, "WinStationQueryInformationW"); - - if (WTSEnumerateSessions(hServer, 0, 1, &sessions, &count) == 0) { - PyErr_SetFromWindowsErr(0); - 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); - - buffer_user = NULL; - buffer_addr = NULL; - - // username - bytes = 0; - if (WTSQuerySessionInformationW(hServer, sessionId, WTSUserName, - &buffer_user, &bytes) == 0) { - PyErr_SetFromWindowsErr(0); - goto error; - } - if (bytes <= 2) - continue; - - // address - bytes = 0; - if (WTSQuerySessionInformation(hServer, sessionId, WTSClientAddress, - &buffer_addr, &bytes) == 0) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - address = (PWTS_CLIENT_ADDRESS)buffer_addr; - if (address->AddressFamily == 0) { // AF_INET - sprintf_s(address_str, - _countof(address_str), - "%u.%u.%u.%u", - address->Address[0], - address->Address[1], - address->Address[2], - address->Address[3]); - py_address = Py_BuildValue("s", address_str); - if (!py_address) - goto error; - } - else { - py_address = Py_None; - } - - // login time - if (!WinStationQueryInformationW(hServer, - sessionId, - WinStationInformation, - &station_info, - sizeof(station_info), - &returnLen)) - { - 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); - if (!py_tuple) - goto error; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_XDECREF(py_username); - Py_XDECREF(py_address); - Py_XDECREF(py_tuple); - } - - WTSFreeMemory(sessions); - WTSFreeMemory(buffer_user); - WTSFreeMemory(buffer_addr); - FreeLibrary(hInstWinSta); - return py_retlist; - -error: - Py_XDECREF(py_username); - Py_XDECREF(py_tuple); - Py_XDECREF(py_address); - Py_DECREF(py_retlist); - - if (hInstWinSta != NULL) - FreeLibrary(hInstWinSta); - if (sessions != NULL) - WTSFreeMemory(sessions); - if (buffer_user != NULL) - WTSFreeMemory(buffer_user); - if (buffer_addr != NULL) - WTSFreeMemory(buffer_addr); - return NULL; -} - - -/* - * 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, "l", &pid)) - return NULL; - hProcess = psutil_handle_from_pid(pid); - if (NULL == hProcess) - return NULL; - if (! GetProcessHandleCount(hProcess, &handleCount)) { - CloseHandle(hProcess); - return PyErr_SetFromWindowsErr(0); - } - CloseHandle(hProcess); - return Py_BuildValue("k", handleCount); -} - - -/* - * 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; - SIZE_T mem_private; - 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; - } - -#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", -#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 - mem_private // private - ); - - free(buffer); - return py_retlist; -} - - -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) { -#ifdef _WIN64 - MEMORY_BASIC_INFORMATION64 basicInfo; -#else - MEMORY_BASIC_INFORMATION basicInfo; -#endif - DWORD pid; - HANDLE hProcess = NULL; - PVOID baseAddress; - PVOID previousAllocationBase; - WCHAR mappedFileName[MAX_PATH]; - SYSTEM_INFO system_info; - LPVOID maxAddr; - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - PyObject *py_str = NULL; - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) - goto error; - hProcess = psutil_handle_from_pid(pid); - if (NULL == hProcess) - goto error; - - GetSystemInfo(&system_info); - maxAddr = system_info.lpMaximumApplicationAddress; - baseAddress = NULL; - previousAllocationBase = 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_DECREF(py_tuple); - Py_DECREF(py_str); - } - previousAllocationBase = basicInfo.AllocationBase; - 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 = Py_BuildValue("I", pe.th32ProcessID); - if (py_pid == NULL) - goto error; - py_ppid = Py_BuildValue("I", pe.th32ParentProcessID); - if (py_ppid == NULL) - goto error; - if (PyDict_SetItem(py_retdict, py_pid, py_ppid)) - goto error; - Py_DECREF(py_pid); - Py_DECREF(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; -} +#include "arch/all/init.h" +#include "arch/windows/init.h" -/* - * 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; -#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; - - 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_DECREF(py_tuple); - Py_DECREF(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; -#if (_WIN32_WINNT >= 0x0600) // Windows Vista and above - 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; - } -#endif - } - 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_DECREF(py_tuple); - Py_DECREF(py_address); - Py_DECREF(py_netmask); - - pUnicast = pUnicast->Next; - } - } - Py_DECREF(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_DECREF(py_nic_name); - Py_DECREF(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) { - // 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; - _SYSTEM_INTERRUPT_INFORMATION *InterruptInformation = NULL; - SYSTEM_INFO si; - UINT i; - 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; - } - - // retrives number of processors - GetSystemInfo(&si); - - // get syscalls / ctx switches - spi = (_SYSTEM_PERFORMANCE_INFORMATION *) \ - malloc(si.dwNumberOfProcessors * \ - sizeof(_SYSTEM_PERFORMANCE_INFORMATION)); - if (spi == NULL) { - PyErr_NoMemory(); - goto error; - } - status = NtQuerySystemInformation( - SystemPerformanceInformation, - spi, - si.dwNumberOfProcessors * sizeof(_SYSTEM_PERFORMANCE_INFORMATION), - NULL); - if (status != 0) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - // get DPCs - InterruptInformation = \ - malloc(sizeof(_SYSTEM_INTERRUPT_INFORMATION) * - si.dwNumberOfProcessors); - if (InterruptInformation == NULL) { - PyErr_NoMemory(); - goto error; - } - - status = NtQuerySystemInformation( - SystemInterruptInformation, - InterruptInformation, - si.dwNumberOfProcessors * sizeof(SYSTEM_INTERRUPT_INFORMATION), - NULL); - if (status != 0) { - PyErr_SetFromWindowsErr(0); - goto error; - } - for (i = 0; i < si.dwNumberOfProcessors; i++) { - dpcs += InterruptInformation[i].DpcCount; - } - - // get interrupts - sppi = (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION *) \ - malloc(si.dwNumberOfProcessors * \ - sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); - if (sppi == NULL) { - PyErr_NoMemory(); - goto error; - } - - status = NtQuerySystemInformation( - SystemProcessorPerformanceInformation, - sppi, - si.dwNumberOfProcessors * sizeof - (_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), - NULL); - if (status != 0) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - for (i = 0; i < si.dwNumberOfProcessors; i++) { - interrupts += sppi[i].InterruptCount; - } - - // done - free(spi); - free(InterruptInformation); - free(sppi); - FreeLibrary(hNtDll); - 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); - if (hNtDll) - FreeLibrary(hNtDll); - return NULL; -} - - -/* - * Return CPU frequency. - */ -static PyObject * -psutil_cpu_freq(PyObject *self, PyObject *args) { - PROCESSOR_POWER_INFORMATION *ppi; - NTSTATUS ret; - size_t size; - LPBYTE pBuffer = NULL; - ULONG current; - ULONG max; - unsigned int num_cpus; - SYSTEM_INFO system_info; - system_info.dwNumberOfProcessors = 0; - - // Get the number of CPUs. - GetSystemInfo(&system_info); - if (system_info.dwNumberOfProcessors == 0) - num_cpus = 1; - else - num_cpus = system_info.dwNumberOfProcessors; - - // Allocate size. - size = num_cpus * 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. - */ -static PyObject * -psutil_sensors_battery(PyObject *self, PyObject *args) { - SYSTEM_POWER_STATUS sps; - - if (GetSystemPowerStatus(&sps) == 0) - return PyErr_SetFromWindowsErr(0); - 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 - ); -} +#define GETSTATE(m) ((struct module_state *)PyModule_GetState(m)) // ------------------------ Python init --------------------------- -static PyMethodDef -PsutilMethods[] = { +static PyMethodDef PsutilMethods[] = { // --- per-process functions - - {"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_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, - "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, - "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_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."}, -#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, - "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"}, + {"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_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}, + {"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_oneshot", psutil_proc_oneshot, 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_phys", psutil_cpu_count_phys, METH_VARARGS, - "Returns the number of physical CPUs 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."}, - {"sensors_battery", psutil_sensors_battery, METH_VARARGS, - "Return battery metrics usage."}, + {"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}, + {"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}, + {"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}, // --- 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 - {"win32_QueryDosDevice", psutil_win32_QueryDosDevice, METH_VARARGS, - "QueryDosDevice binding"}, + // --- direct Windows APIs + {"GetPerformanceInfo", psutil_GetPerformanceInfo, METH_VARARGS}, + {"QueryDosDevice", psutil_QueryDosDevice, METH_VARARGS}, // --- others - {"set_testing", psutil_set_testing, METH_NOARGS, - "Set psutil in testing mode"}, + {"check_pid_range", psutil_check_pid_range, METH_VARARGS}, + {"set_debug", psutil_set_debug, METH_VARARGS}, {NULL, NULL, 0, NULL} }; @@ -3726,21 +111,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; } @@ -3757,126 +136,159 @@ static struct PyModuleDef moduledef = { NULL }; -#define INITERROR return NULL -PyMODINIT_FUNC PyInit__psutil_windows(void) +PyMODINIT_FUNC +PyInit__psutil_windows(void) { + PyObject *mod = PyModule_Create(&moduledef); + if (mod == NULL) + return NULL; -#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); +#ifdef Py_GIL_DISABLED + if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED)) + return NULL; #endif - if (module == NULL) { - INITERROR; - } - - st = GETSTATE(module); - st->error = PyErr_NewException("_psutil_windows.Error", NULL, NULL); - if (st->error == NULL) { - Py_DECREF(module); - INITERROR; - } + if (psutil_setup() != 0) + return NULL; + if (psutil_setup_windows() != 0) + return NULL; + if (psutil_set_se_debug() != 0) + return NULL; - // Exceptions. + // Exceptions TimeoutExpired = PyErr_NewException( - "_psutil_windows.TimeoutExpired", NULL, NULL); - Py_INCREF(TimeoutExpired); - PyModule_AddObject(module, "TimeoutExpired", TimeoutExpired); + "_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); - Py_INCREF(TimeoutAbandoned); - PyModule_AddObject(module, "TimeoutAbandoned", TimeoutAbandoned); + "_psutil_windows.TimeoutAbandoned", NULL, NULL + ); + if (TimeoutAbandoned == NULL) + return NULL; + if (PyModule_AddObject(mod, "TimeoutAbandoned", TimeoutAbandoned)) + return NULL; // 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); - - // set SeDebug for the current process - psutil_set_se_debug(); - psutil_setup(); + 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; -#if PY_MAJOR_VERSION >= 3 - return module; -#endif + return mod; } diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index ab727cba30..0190f36c93 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -4,203 +4,117 @@ """Windows platform implementation.""" +from __future__ import annotations + import contextlib -import errno +import enum import functools import os +import signal 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 +from ._common import TimeoutExpired +from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import debug +from ._common import isfile_strict +from ._common import memoize_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 ProcessIOPriority +from ._enums import ProcessPriority +from ._enums import ProcessStatus + 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 # See: https://github.com/giampaolo/psutil/issues/811 - # It must be noted that psutil can still (kind of) work - # on outdated systems if compiled / installed from sources, - # but if we get here it means this this was a wheel (or exe). msg = "this Windows version is too old (< Windows Vista); " msg += "psutil 3.4.2 is the latest version which supports Windows " - msg += "2000, XP and 2003 server; it may be possible that psutil " - msg += "will work if compiled from sources though" - raise RuntimeError(msg) + msg += "2000, XP and 2003 server" + raise RuntimeError(msg) from err else: raise -from ._common import conn_tmap -from ._common import ENCODING -from ._common import ENCODING_ERRS -from ._common import isfile_strict -from ._common import memoize_when_activated -from ._common import parse_environ_block -from ._common import sockfam_to_enum -from ._common import socktype_to_enum -from ._common import usage_percent -from ._compat import long -from ._compat import lru_cache -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 -from ._psutil_windows import IDLE_PRIORITY_CLASS -from ._psutil_windows import NORMAL_PRIORITY_CLASS -from ._psutil_windows import REALTIME_PRIORITY_CLASS - -if sys.version_info >= (3, 4): - import enum -else: - enum = None # process priority constants, import from __init__.py: # http://msdn.microsoft.com/en-us/library/ms686219(v=vs.85).aspx -__extra__all__ = [ - "win_service_iter", "win_service_get", - "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", -] +__extra__all__ = ["win_service_iter", "win_service_get", "AF_LINK"] # ===================================================================== # --- globals # ===================================================================== -CONN_DELETE_TCB = "DELETE_TCB" -ACCESS_DENIED_ERRSET = frozenset([errno.EPERM, errno.EACCES, - cext.ERROR_ACCESS_DENIED]) -NO_SUCH_SERVICE_ERRSET = frozenset([cext.ERROR_INVALID_NAME, - cext.ERROR_SERVICE_DOES_NOT_EXIST]) - +ERROR_PARTIAL_COPY = 299 +PYPY = '__pypy__' in sys.builtin_module_names -if enum is None: - AF_LINK = -1 -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, - 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, } -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 - - globals().update(Priority.__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, -) - - -# ===================================================================== -# --- named tuples -# ===================================================================== - - -# 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']) - # ===================================================================== # --- utils # ===================================================================== -@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" + "\Device\HarddiskVolume1\Windows\systemew\file.txt" or + "\??\C:\Windows\systemew\file.txt" into: - "C:\Windows\systemew\file.txt" + "C:\Windows\systemew\file.txt". """ + if s.startswith('\\\\'): + return s rawdrive = '\\'.join(s.split('\\')[:3]) - driveletter = cext.win32_QueryDosDevice(rawdrive) - return os.path.join(driveletter, s[len(rawdrive):]) + if rawdrive in {"\\??\\UNC", "\\Device\\Mup"}: + rawdrive = '\\'.join(s.split('\\')[:5]) + driveletter = '\\\\' + '\\'.join(s.split('\\')[3:5]) + elif rawdrive.startswith('\\??\\'): + driveletter = s.split('\\')[2] + else: + driveletter = cext.QueryDosDevice(rawdrive) + remainder = s[len(rawdrive) :] + 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 - else: - if isinstance(s, str): - return s - else: - return s.encode(ENCODING, ENCODING_ERRS) +@functools.lru_cache +def getpagesize(): + return cext.getpagesize() # ===================================================================== @@ -209,26 +123,50 @@ def py2_strencode(s): def virtual_memory(): - """System virtual memory as a namedtuple.""" - mem = cext.virtual_mem() - totphys, availphys, totpagef, availpagef, totvirt, freevirt = mem - # - total = totphys - avail = availphys - free = availphys + """System virtual memory as a named tuple.""" + info = cext.GetPerformanceInfo() + 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 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.""" - mem = cext.virtual_mem() - total = mem[2] - free = mem[3] - used = total - free - percent = usage_percent(used, total, round_=1) - return _common.sswap(total, used, free, percent, 0, 0) + info = cext.GetPerformanceInfo() + 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"] * pagesize # 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. + # instead, we calculate page file usage based on performance counter + if total > 0: + percentswap = cext.swap_percent() + used = int(0.01 * percentswap * total) + else: + percentswap = 0.0 + used = 0 + + free = total - used + percent = round(percentswap, 1) + return ntp.sswap(total, used, free, percent, 0, 0) + + +# malloc / heap functions +heap_info = cext.heap_info +heap_trim = cext.heap_trim # ===================================================================== @@ -241,20 +179,24 @@ 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") - total, free = cext.disk_usage(path) - used = total - free + 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 _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] # ===================================================================== @@ -268,16 +210,19 @@ 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(user, system, idle, - percpu_summed.interrupt, percpu_summed.dpc) + percpu_summed = ntp.scputimes( + *[sum(n) for n in zip(*cext.per_cpu_times())] + ) + return ntp.scputimes( + 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 = 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 @@ -287,17 +232,16 @@ 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(): """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) + return ntp.scpustats(ctx_switches, interrupts, soft_interrupts, syscalls) def cpu_freq(): @@ -306,7 +250,34 @@ 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 +_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_initialized + + if _loadavg_initialized: + return _getloadavg_impl() + + with _lock: + if not _loadavg_initialized: + cext.init_loadavg_counter() + _loadavg_initialized = True + + return _getloadavg_impl() # ===================================================================== @@ -318,25 +289,21 @@ 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() 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) @@ -346,13 +313,9 @@ 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) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + duplex = NicDuplex(duplex) + ret[name] = ntp.snicstats(isup, duplex, speed, mtu, '') return ret @@ -360,18 +323,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() # ===================================================================== @@ -382,8 +339,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) @@ -392,11 +348,11 @@ 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 _common.sbattery(percent, secsleft, power_plugged) + return ntp.sbattery(percent, secsleft, power_plugged) # ===================================================================== @@ -404,31 +360,20 @@ def sensors_battery(): # ===================================================================== -_last_btime = 0 - - def boot_time(): - """The system boot time expressed in seconds since the epoch.""" - # 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()) - if abs(ret - _last_btime) <= 1: - return _last_btime - else: - _last_btime = ret - return ret + """The system boot time expressed in seconds since the epoch. This + also includes the time spent during hibernate / suspend. + """ + return cext.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: user, hostname, tstamp = item - user = py2_strencode(user) - nt = _common.suser(user, None, hostname, tstamp, None) + nt = ntp.suser(user, None, hostname, tstamp, None) retlist.append(nt) return retlist @@ -441,7 +386,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): @@ -451,41 +396,42 @@ def win_service_get(name): return service -class WindowsService(object): +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 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): + 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): 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)) + display_name=display_name, + binpath=binpath, + username=username, + start_type=start_type, + ) def _query_status(self): with self._wrap_exceptions(): @@ -501,46 +447,48 @@ def _wrap_exceptions(self): """ try: yield - except WindowsError as err: - if err.errno in ACCESS_DENIED_ERRSET: - raise AccessDenied( - pid=None, name=self._name, - msg="service %r is not querable (not enough privileges)" % - self._name) - elif err.errno in NO_SUCH_SERVICE_ERRSET or \ - err.winerror in NO_SUCH_SERVICE_ERRSET: - raise NoSuchProcess( - pid=None, name=self._name, - msg="service %r does not exist)" % self._name) + except OSError as err: + name = self._name + if is_permission_err(err): + msg = ( + f"service {name!r} is not querable (not enough privileges)" + ) + 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 {name!r} does not exist" + raise NoSuchProcess(pid=None, name=name, msg=msg) from err else: raise # 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". """ @@ -548,23 +496,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 py2_strencode(cext.winservice_query_descr(self.name())) + 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. """ @@ -625,27 +573,70 @@ 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 isinstance(exc, PermissionError) or exc.winerror in { + cext.ERROR_ACCESS_DENIED, + cext.ERROR_PRIVILEGE_NOT_HELD, + } + + +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 isinstance(exc, ProcessLookupError): + 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) from err + + 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 _ in range(times): # retries for roughly 1 second + try: + return fun(self, *args, **kwargs) + except OSError as _: + err = _ + if err.winerror == ERROR_PARTIAL_COPY: + time.sleep(delay) + delay = min(delay * 2, 0.04) + continue + raise + msg = ( + 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) + return wrapper -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid"] + __slots__ = ["_cache", "_name", "_ppid", "pid"] def __init__(self, pid): self.pid = pid @@ -655,21 +646,20 @@ def __init__(self, pid): # --- oneshot() stuff def oneshot_enter(self): - self.oneshot_info.cache_activate() + self._oneshot.cache_activate(self) + self.exe.cache_activate(self) def oneshot_exit(self): - self.oneshot_info.cache_deactivate() + self._oneshot.cache_deactivate(self) + self.exe.cache_deactivate(self) @memoize_when_activated - def oneshot_info(self): + def _oneshot(self): """Return multiple information about this process as a - raw tuple. + raw dict. """ - ret = cext.proc_info(self.pid) - assert len(ret) == len(pinfo_map) - return ret + return cext.proc_oneshot(self.pid) - @wrap_exceptions def name(self): """Return process name, which on Windows is always the final part of the executable. @@ -678,86 +668,132 @@ def name(self): # and process-hacker. if self.pid == 0: return "System Idle Process" - elif self.pid == 4: + if self.pid == 4: return "System" - else: - try: - # Note: this will fail with AD for most PIDs owned - # by another user but it's faster. - return py2_strencode(os.path.basename(self.exe())) - except AccessDenied: - return py2_strencode(cext.proc_name(self.pid)) + return os.path.basename(self.exe()) @wrap_exceptions + @memoize_when_activated def exe(self): - # Note: os.path.exists(path) may return False even if the file - # is there, see: - # http://stackoverflow.com/questions/3112546/os-path-exists-lies - - # see https://github.com/giampaolo/psutil/issues/414 - # see https://github.com/giampaolo/psutil/issues/528 - if self.pid in (0, 4): - raise AccessDenied(self.pid, self._name) - return py2_strencode(convert_dos_path(cext.proc_exe(self.pid))) + if PYPY: + try: + exe = cext.proc_exe(self.pid) + except 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: + debug(f"{err!r} translated into AccessDenied") + raise AccessDenied(self.pid, self._name) from err + raise + else: + exe = cext.proc_exe(self.pid) + if exe.startswith('\\'): + return convert_dos_path(exe) + return exe # May be "Registry", "MemCompression", ... @wrap_exceptions + @retry_error_partial_copy def cmdline(self): - ret = cext.proc_cmdline(self.pid) - if PY3: - return ret + if cext.WINVER >= cext.WINDOWS_8_1: + # PEB method detects cmdline changes but requires more + # privileges: https://github.com/giampaolo/psutil/pull/1398 + try: + return cext.proc_cmdline(self.pid, use_peb=True) + except OSError as err: + if is_permission_err(err): + return cext.proc_cmdline(self.pid, use_peb=False) + else: + raise 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: 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: 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() - 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']], - ) + # to get this from cext.proc_oneshot() + debug("attempting memory_info() fallback (slower)") + info = self._oneshot() + 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"], # adjust name + } raise @wrap_exceptions 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 pmem(*(rss, vms, ) + t) + d = self._get_raw_meminfo() + return ntp.pmem( + rss=d["WorkingSetSize"], + vms=d["PrivateUsage"], + peak_rss=d["PeakWorkingSetSize"], + peak_vms=d["PeakPagefileUsage"], + _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"], + # moved to page_faults() + "num_page_faults": d["PageFaultCount"], + }, + ) @wrap_exceptions - def memory_full_info(self): - basic_mem = self.memory_info() + 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 + def memory_footprint(self): uss = cext.proc_memory_uss(self.pid) - return pfullmem(*basic_mem + (uss, )) + uss *= getpagesize() + return ntp.pfootprint(uss) + + @wrap_exceptions + def page_faults(self): + t = cext.proc_page_faults(self.pid) + return ntp.ppagefaults(*t) def memory_maps(self): try: @@ -765,17 +801,10 @@ 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) from err else: for addr, perm, path, rss in raw: path = convert_dos_path(path) - if not PY3: - assert isinstance(path, unicode), type(path) - path = py2_strencode(path) addr = hex(addr) yield (addr, perm, path, rss) @@ -785,7 +814,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) + elif sig in {signal.CTRL_C_EVENT, signal.CTRL_BREAK_EVENT}: + os.kill(self.pid, sig) + else: + msg = ( + "only SIGTERM, CTRL_C_EVENT and CTRL_BREAK_EVENT signals " + "are supported on Windows" + ) + raise ValueError(msg) @wrap_exceptions def wait(self, timeout=None): @@ -803,18 +841,16 @@ 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 # 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, @@ -832,70 +868,74 @@ 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) + return f"{domain}\\{user}" @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() + 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: - return cext.proc_create_time(self.pid) + _user, _system, created = cext.proc_times(self.pid) + return created except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - return self.oneshot_info()[pinfo_map['create_time']] + if is_permission_err(err): + if fast_only: + raise + debug("attempting create_time() fallback (slower)") + return self._oneshot()["create_time"] raise @wrap_exceptions def num_threads(self): - return self.oneshot_info()[pinfo_map['num_threads']] + return self._oneshot()["num_threads"] @wrap_exceptions 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 @wrap_exceptions def cpu_times(self): try: - user, system = cext.proc_cpu_times(self.pid) + user, system, _created = cext.proc_times(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - info = self.oneshot_info() - user = info[pinfo_map['user_time']] - system = info[pinfo_map['kernel_time']] - else: + if not is_permission_err(err): raise + debug("attempting cpu_times() fallback (slower)") + info = self._oneshot() + user = info["user_time"] + system = info["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): - return cext.proc_suspend(self.pid) + cext.proc_suspend_or_resume(self.pid, True) @wrap_exceptions def resume(self): - return cext.proc_resume(self.pid) + cext.proc_suspend_or_resume(self.pid, False) @wrap_exceptions + @retry_error_partial_copy def cwd(self): - if self.pid in (0, 4): + 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 path = cext.proc_cwd(self.pid) - return py2_strencode(os.path.normpath(path)) + return os.path.normpath(path) @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: @@ -903,88 +943,87 @@ 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): - if not PY3: - _file = py2_strencode(_file) - ntuple = _common.popenfile(_file, -1) + for file in raw_file_names: + file = convert_dos_path(file) + if isfile_strict(file): + ntuple = ntp.popenfile(file, -1) ret.add(ntuple) 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 def nice_get(self): value = cext.proc_priority_get(self.pid) - if enum is not None: - value = Priority(value) + value = ProcessPriority(value) return value @wrap_exceptions def nice_set(self, value): return cext.proc_priority_set(self.pid, value) - # available on Windows >= Vista - if hasattr(cext, "proc_io_priority_get"): - @wrap_exceptions - def ionice_get(self): - return cext.proc_io_priority_get(self.pid) - - @wrap_exceptions - def ionice_set(self, value, _): - if _: - raise TypeError("set_proc_ionice() on Windows takes only " - "1 argument (2 given)") - if value not in (2, 1, 0): - raise ValueError("value must be 2 (normal), 1 (low) or 0 " - "(very low); got %r" % value) - return cext.proc_io_priority_set(self.pid, value) + @wrap_exceptions + def ionice_get(self): + ret = cext.proc_io_priority_get(self.pid) + ret = ProcessIOPriority(ret) + return ret + + @wrap_exceptions + def ionice_set(self, ioclass, value): + if value: + msg = "value argument not accepted on Windows" + raise TypeError(msg) + if ioclass not in ProcessIOPriority: + msg = f"{ioclass} is not a valid priority" + raise ValueError(msg) + cext.proc_io_priority_set(self.pid, ioclass) @wrap_exceptions def io_counters(self): try: ret = cext.proc_io_counters(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - info = self.oneshot_info() - ret = ( - info[pinfo_map['io_rcount']], - info[pinfo_map['io_wcount']], - info[pinfo_map['io_rbytes']], - info[pinfo_map['io_wbytes']], - info[pinfo_map['io_count_others']], - info[pinfo_map['io_bytes_others']], - ) - else: + if not is_permission_err(err): raise - return pio(*ret) + debug("attempting io_counters() fallback (slower)") + info = self._oneshot() + ret = ( + info["io_rcount"], + info["io_wcount"], + info["io_rbytes"], + info["io_wbytes"], + info["io_count_others"], + info["io_bytes_others"], + ) + return ntp.pio(*ret) @wrap_exceptions 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): def from_bitmask(x): - return [i for i in xrange(64) if (1 << i) & x] + return [i for i in range(64) if (1 << i) & x] + bitmask = cext.proc_cpu_affinity_get(self.pid) return from_bitmask(bitmask) @wrap_exceptions def cpu_affinity_set(self, value): - def to_bitmask(l): - if not l: - raise ValueError("invalid argument %r" % l) + def to_bitmask(ls): + if not ls: + msg = f"invalid argument {ls!r}" + raise ValueError(msg) out = 0 - for b in l: - out |= 2 ** b + for b in ls: + out |= 2**b return out # SetProcessAffinityMask() states that ERROR_INVALID_PARAMETER @@ -993,11 +1032,11 @@ def to_bitmask(l): allcpus = list(range(len(per_cpu_times()))) for cpu in value: if cpu not in allcpus: - if not isinstance(cpu, (int, long)): - raise TypeError( - "invalid CPU %r; an integer is required" % cpu) - else: - raise ValueError("invalid CPU %r" % cpu) + if not isinstance(cpu, int): + msg = f"invalid CPU {cpu!r}; an integer is required" + raise TypeError(msg) + msg = f"invalid CPU {cpu!r}" + raise ValueError(msg) bitmask = to_bitmask(value) cext.proc_cpu_affinity_set(self.pid, bitmask) @@ -1007,12 +1046,13 @@ def num_handles(self): try: return cext.proc_num_handles(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - return self.oneshot_info()[pinfo_map['num_handles']] + if is_permission_err(err): + debug("attempting num_handles() fallback (slower)") + return self._oneshot()["num_handles"] raise @wrap_exceptions def num_ctx_switches(self): - ctx_switches = self.oneshot_info()[pinfo_map['ctx_switches']] + ctx_switches = self._oneshot()["ctx_switches"] # only voluntary ctx switches are supported - return _common.pctxsw(ctx_switches, 0) + return ntp.pctxsw(ctx_switches, 0) diff --git a/psutil/arch/aix/common.c b/psutil/arch/aix/common.c index 6115a15db5..5e28288eca 100644 --- a/psutil/arch/aix/common.c +++ b/psutil/arch/aix/common.c @@ -8,43 +8,47 @@ #include #include #include + +#include "../../arch/all/init.h" #include "common.h" -/* psutil_kread() - read from kernel memory */ + +// 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) { - 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) { - 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; } + 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 +58,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)); @@ -70,10 +76,10 @@ 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; *num = np; return processes; -} \ No newline at end of file +} 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 1a819365ab..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 @@ -19,17 +18,17 @@ #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)) +#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,14 +49,14 @@ void freeifaddrs(struct ifaddrs *ifp) } -int getifaddrs(struct ifaddrs **ifap) -{ +int +getifaddrs(struct ifaddrs **ifap) { int sd, ifsize; 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; @@ -66,11 +66,11 @@ int 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; - 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) @@ -114,7 +115,7 @@ int 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) { @@ -146,4 +147,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..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; @@ -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.c b/psutil/arch/aix/net_connections.c index 69b4389201..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 @@ -26,20 +25,16 @@ #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) + +#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; @@ -53,8 +48,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; @@ -84,10 +79,10 @@ 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 */ + // Read file structure if (psutil_kread(Kd, fp, (char *)&f, sizeof(f))) { return NULL; } @@ -95,7 +90,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; } @@ -104,7 +99,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))) { @@ -112,7 +107,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))) { @@ -121,20 +116,21 @@ 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) { - 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))) { + 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 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; } @@ -148,7 +144,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); } } @@ -158,48 +154,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) { - PyErr_SetString(PyExc_RuntimeError, "unp_socket mismatch"); + if ((KA_T)f.f_data != (KA_T)unp.unp_socket) { + psutil_runtime_error("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; } @@ -216,11 +241,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); @@ -233,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) @@ -248,12 +273,13 @@ 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 */ + // 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/aix/net_connections.h b/psutil/arch/aix/net_connections.h index 222bcaf354..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__ */ \ 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 09f320ff57..7382280274 100644 --- a/psutil/arch/aix/net_kernel_structs.h +++ b/psutil/arch/aix/net_kernel_structs.h @@ -13,11 +13,12 @@ * 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 #include +#include #include #include #include @@ -30,81 +31,79 @@ #define tcpcb64 tcpcb #define unpcb64 unpcb #define mbuf64 mbuf -#else - 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 \ No newline at end of file +#endif // __64BIT__ diff --git a/psutil/arch/all/errors.c b/psutil/arch/all/errors.c new file mode 100644 index 0000000000..039b8f98a2 --- /dev/null +++ b/psutil/arch/all/errors.c @@ -0,0 +1,109 @@ +/* + * 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 + +#include "init.h" + +#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(); + str_format(msg, sizeof(msg), "(originated from %s)", syscall); + PyErr_SetFromWindowsErrWithFilename(err, msg); +#else + PyObject *exc; + 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); +#endif + return NULL; +} + + +// Set OSError(errno=ESRCH) ("No such process"). +PyObject * +psutil_oserror_nsp(const char *syscall) { + PyObject *exc; + char msg[MSG_SIZE]; + + 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); + return NULL; +} + + +// Set OSError(errno=EACCES) ("Permission denied"). +PyObject * +psutil_oserror_ad(const char *syscall) { + PyObject *exc; + char msg[MSG_SIZE]; + + 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); + 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 new file mode 100644 index 0000000000..67c446b2b6 --- /dev/null +++ b/psutil/arch/all/init.c @@ -0,0 +1,52 @@ +/* + * 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 + +#include "init.h" + +int PSUTIL_DEBUG = 0; +int PSUTIL_TESTING = 0; +int PSUTIL_CONN_NONE = 128; + +#ifdef Py_GIL_DISABLED +PyMutex utxent_lock = {0}; +#endif + + +// 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; +} + + +// Called on module import on all platforms. +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 new file mode 100644 index 0000000000..61734fd5ae --- /dev/null +++ b/psutil/arch/all/init.h @@ -0,0 +1,147 @@ +/* + * 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). + +// clang-format off +#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_OPENBSD) + #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 +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; + #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 +// clang-format on + + +// ==================================================================== +// --- Backward compatibility with missing Python.h APIs +// ==================================================================== + +// --- _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. +// 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 +// 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) + + +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 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); +double psutil_usage_percent(double used, double total, int round_); + +// ==================================================================== +// --- Exposed to Python +// ==================================================================== + +#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); diff --git a/psutil/arch/all/pids.c b/psutil/arch/all/pids.c new file mode 100644 index 0000000000..858aa09279 --- /dev/null +++ b/psutil/arch/all/pids.c @@ -0,0 +1,69 @@ +/* + * 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 "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 + DWORD *pids_array = NULL; +#else + pid_t *pids_array = NULL; +#endif + int pids_count = 0; + int i; + PyObject *py_retlist = PyList_New(0); + + if (!py_retlist) + return NULL; + + if (_psutil_pids(&pids_array, &pids_count) != 0) + goto error; + + if (pids_count == 0) { + psutil_runtime_error("no PIDs found"); + goto error; + } + + for (i = 0; i < pids_count; i++) { + if (!pylist_append_obj(py_retlist, PyLong_FromPid(pids_array[i]))) + goto error; + } + + free(pids_array); + return py_retlist; + +error: + Py_DECREF(py_retlist); + free(pids_array); + return NULL; +} +#endif diff --git a/psutil/arch/all/str.c b/psutil/arch/all/str.c new file mode 100644 index 0000000000..c4b90962da --- /dev/null +++ b/psutil/arch/all/str.c @@ -0,0 +1,109 @@ +/* + * 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 +#include + +#include "init.h" + + +static int +_error(const char *msg) { + if (PSUTIL_TESTING) { + fprintf(stderr, "CRITICAL: %s\n", msg); + fflush(stderr); + exit(EXIT_FAILURE); // terminate execution + } + else { + // 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 +// 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) + return _error("str_format: invalid arg 'size' = 0"); + + 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; +} + + +// 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/all/utils.c b/psutil/arch/all/utils.c new file mode 100644 index 0000000000..dda570ecaa --- /dev/null +++ b/psutil/arch/all/utils.c @@ -0,0 +1,85 @@ +#include +#include +#include + + +// 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 + 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; +} + + +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/bsd/cpu.c b/psutil/arch/bsd/cpu.c new file mode 100644 index 0000000000..4cced2d9c9 --- /dev/null +++ b/psutil/arch/bsd/cpu.c @@ -0,0 +1,59 @@ +/* + * 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 // CP_* on OpenBSD + +#include "../../arch/all/init.h" + + +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 = psutil_sysctlbyname("kern.cp_time", &cpu_time, size); +#elif PSUTIL_OPENBSD + int mib[] = {CTL_KERN, KERN_CPTIME}; + ret = psutil_sysctl(mib, 2, &cpu_time, size); +#endif + 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/bsd/disk.c b/psutil/arch/bsd/disk.c new file mode 100644 index 0000000000..1d7efc9d6b --- /dev/null +++ b/psutil/arch/bsd/disk.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. + */ + +#include +#if PSUTIL_NETBSD // getvfsstat() +#include +#include +#else // getfsstat() +#include +#include +#include +#endif + +#include "../../arch/all/init.h" + + +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; + + 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) { + psutil_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) { + psutil_oserror(); + goto error; + } + + for (i = 0; i < num; i++) { + 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) + str_append(opts, sizeof(opts), "ro"); + else + str_append(opts, sizeof(opts), "rw"); + if (flags & MNT_SYNCHRONOUS) + str_append(opts, sizeof(opts), ",sync"); + if (flags & MNT_NOEXEC) + str_append(opts, sizeof(opts), ",noexec"); + if (flags & MNT_NOSUID) + str_append(opts, sizeof(opts), ",nosuid"); + if (flags & MNT_ASYNC) + str_append(opts, sizeof(opts), ",async"); + if (flags & MNT_NOATIME) + str_append(opts, sizeof(opts), ",noatime"); + if (flags & MNT_SOFTDEP) + str_append(opts, sizeof(opts), ",softdep"); +#ifdef PSUTIL_FREEBSD + if (flags & MNT_UNION) + str_append(opts, sizeof(opts), ",union"); + if (flags & MNT_SUIDDIR) + str_append(opts, sizeof(opts), ",suiddir"); + if (flags & MNT_NOSYMFOLLOW) + str_append(opts, sizeof(opts), ",nosymfollow"); +#ifdef MNT_GJOURNAL + if (flags & MNT_GJOURNAL) + str_append(opts, sizeof(opts), ",gjournal"); +#endif + if (flags & MNT_MULTILABEL) + str_append(opts, sizeof(opts), ",multilabel"); + if (flags & MNT_ACLS) + str_append(opts, sizeof(opts), ",acls"); + if (flags & MNT_NOCLUSTERR) + str_append(opts, sizeof(opts), ",noclusterr"); + if (flags & MNT_NOCLUSTERW) + str_append(opts, sizeof(opts), ",noclusterw"); +#ifdef MNT_NFS4ACLS + if (flags & MNT_NFS4ACLS) + str_append(opts, sizeof(opts), ",nfs4acls"); +#endif +#elif PSUTIL_NETBSD + if (flags & MNT_NODEV) + str_append(opts, sizeof(opts), ",nodev"); + if (flags & MNT_UNION) + str_append(opts, sizeof(opts), ",union"); + if (flags & MNT_NOCOREDUMP) + str_append(opts, sizeof(opts), ",nocoredump"); +#ifdef MNT_RELATIME + if (flags & MNT_RELATIME) + str_append(opts, sizeof(opts), ",relatime"); +#endif + if (flags & MNT_IGNORE) + str_append(opts, sizeof(opts), ",ignore"); +#ifdef MNT_DISCARD + if (flags & MNT_DISCARD) + str_append(opts, sizeof(opts), ",discard"); +#endif +#ifdef MNT_EXTATTR + if (flags & MNT_EXTATTR) + str_append(opts, sizeof(opts), ",extattr"); +#endif + if (flags & MNT_LOG) + str_append(opts, sizeof(opts), ",log"); + if (flags & MNT_SYMPERM) + str_append(opts, sizeof(opts), ",symperm"); + if (flags & MNT_NODEVMTIME) + str_append(opts, sizeof(opts), ",nodevmtime"); +#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; + 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); + } + + free(fs); + return py_retlist; + +error: + Py_XDECREF(py_dev); + Py_XDECREF(py_mountp); + Py_DECREF(py_retlist); + if (fs != NULL) + free(fs); + return NULL; +} diff --git a/psutil/arch/bsd/heap.c b/psutil/arch/bsd/heap.c new file mode 100644 index 0000000000..334a613cb4 --- /dev/null +++ b/psutil/arch/bsd/heap.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. + */ + +#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; + + // 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) + 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.c b/psutil/arch/bsd/init.c new file mode 100644 index 0000000000..d3f00f5f20 --- /dev/null +++ b/psutil/arch/bsd/init.c @@ -0,0 +1,27 @@ +/* + * 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" + + +void +convert_kvm_err(const char *syscall, char *errbuf) { + char fullmsg[512]; + + 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) + psutil_oserror_ad(fullmsg); + else + psutil_runtime_error(fullmsg); +} diff --git a/psutil/arch/bsd/init.h b/psutil/arch/bsd/init.h new file mode 100644 index 0000000000..6cc86f46ca --- /dev/null +++ b/psutil/arch/bsd/init.h @@ -0,0 +1,35 @@ +/* + * 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 + + +#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); +#endif + +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); +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); +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..7a0d838ae8 --- /dev/null +++ b/psutil/arch/bsd/mem.c @@ -0,0 +1,92 @@ +/* + * 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; + } + } + + // Get swap in/out. +#if defined(PSUTIL_NETBSD) + struct uvmexp_sysctl uv; + int mib[] = {CTL_VM, VM_UVMEXP2}; +#else // OpenBSD + struct uvmexp uv; + int mib[] = {CTL_VM, VM_UVMEXP}; +#endif + if (psutil_sysctl(mib, 2, &uv, sizeof(uv)) != 0) + goto error; + sin = (uint64_t)uv.pgswapin * pagesize; + sout = (uint64_t)uv.pgswapout * pagesize; + + 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/bsd/net.c b/psutil/arch/bsd/net.c new file mode 100644 index 0000000000..efa2fc2520 --- /dev/null +++ b/psutil/arch/bsd/net.c @@ -0,0 +1,91 @@ +/* + * 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 "../../arch/all/init.h" + + +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 (psutil_sysctl_malloc(mib, 6, &buf, &len) != 0) + 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) != 0) + goto error; + Py_CLEAR(py_ifc_info); + } + } + + free(buf); + return py_retdict; + +error: + Py_XDECREF(py_ifc_info); + Py_DECREF(py_retdict); + free(buf); + return NULL; +} diff --git a/psutil/arch/bsd/proc.c b/psutil/arch/bsd/proc.c new file mode 100644 index 0000000000..985daae4b7 --- /dev/null +++ b/psutil/arch/bsd/proc.c @@ -0,0 +1,434 @@ +/* + * 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 +#ifdef PSUTIL_FREEBSD +#include +#include +#endif + +#include "../../arch/all/init.h" + + +// 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; + long rss; + long vms; + long memtext; + long memdata; + long memstack; + long peak_rss; + int oncpu; +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 kp; +#else + struct kinfo_proc kp; +#endif + long pagesize = psutil_getpagesize(); + char name_buf[256]; // buffer for process name + PyObject *py_name = NULL; + PyObject *dict = PyDict_New(); + + if (!dict) + return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + if (psutil_kinfo_proc(pid, &kp) == -1) + goto error; + + // Process name +#ifdef PSUTIL_FREEBSD + str_format(name_buf, sizeof(name_buf), "%s", kp.ki_comm); +#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) + str_format(name_buf, sizeof(name_buf), "%s", kp.p_comm); +#endif + 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); + } + + // 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; + 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/ + // 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 + + // 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? + // 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 + // 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; + 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 (!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; + 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; +#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; + 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 + // 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; + + // clang-format on + Py_DECREF(py_name); + return dict; + +error: + Py_XDECREF(py_name); + Py_DECREF(dict); + return NULL; +} + + +PyObject * +psutil_proc_name(PyObject *self, PyObject *args) { + pid_t pid; +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 kp; +#else + struct kinfo_proc kp; +#endif + 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 + str_format(str, sizeof(str), "%s", kp.ki_comm); +#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD) + str_format(str, sizeof(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) { + psutil_oserror_nsp("kvm_getprocs"); + goto error; + } + if (cnt <= 0) { + psutil_oserror_nsp(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 (!((p)->ki_flag & P_INMEM) || ((p)->ki_flag & P_SYSTEM)) { +#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: + psutil_oserror_ad("kvm_getenvv -> EPERM"); + break; + case ESRCH: + psutil_oserror_nsp("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'. + str_format( + errbuf, + sizeof(errbuf), + "kvm_getenvv(pid=%ld, ki_uid=%d) -> ENOMEM", + pid, + p->ki_uid + ); + psutil_oserror_ad(errbuf); + break; +#endif + default: + str_format( + errbuf, sizeof(errbuf), "kvm_getenvv(pid=%ld)", pid + ); + psutil_oserror_wsyscall(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 +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; +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 kipp; +#else + struct kinfo_proc kipp; +#endif + 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) + 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; + } + + 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; + if (!pylist_append_fmt(py_retlist, "(Oi)", py_path, fd)) + goto error; + Py_CLEAR(py_path); + } + } + free(freep); + return py_retlist; + +error: + Py_XDECREF(py_path); + Py_DECREF(py_retlist); + if (freep != NULL) + free(freep); + return NULL; +} diff --git a/psutil/arch/bsd/proc_utils.c b/psutil/arch/bsd/proc_utils.c new file mode 100644 index 0000000000..4b1023d0b4 --- /dev/null +++ b/psutil/arch/bsd/proc_utils.c @@ -0,0 +1,124 @@ +/* + * 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 +#ifdef PSUTIL_FREEBSD +#include +#endif + +#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) + return 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/bsd/sys.c b/psutil/arch/bsd/sys.c new file mode 100644 index 0000000000..8205dd2af5 --- /dev/null +++ b/psutil/arch/bsd/sys.c @@ -0,0 +1,24 @@ +/* + * 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 "../../arch/all/init.h" + + +// 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; + + 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/cpu.c b/psutil/arch/freebsd/cpu.c new file mode 100644 index 0000000000..78e54aa846 --- /dev/null +++ b/psutil/arch/freebsd/cpu.c @@ -0,0 +1,155 @@ +/* + * 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 + +#include "../../arch/all/init.h" + + +PyObject * +psutil_per_cpu_times(PyObject *self, PyObject *args) { + int maxcpus; + int mib[2]; + int ncpu; + size_t size; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + // retrieve the number of CPUs currently online + mib[0] = CTL_HW; + mib[1] = HW_NCPU; + if (psutil_sysctl(mib, 2, &ncpu, sizeof(ncpu)) != 0) { + goto error; + } + + // allocate buffer dynamically based on actual CPU count + long(*cpu_time)[CPUSTATES] = malloc(ncpu * sizeof(*cpu_time)); + if (!cpu_time) { + PyErr_NoMemory(); + goto error; + } + + // get per-cpu times using ncpu count + size = ncpu * sizeof(*cpu_time); + if (psutil_sysctlbyname("kern.cp_times", cpu_time, size) == -1) { + free(cpu_time); + goto error; + } + + for (int i = 0; i < ncpu; i++) { + 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; + } + } + + free(cpu_time); + return py_retlist; + +error: + Py_DECREF(py_retlist); + return NULL; +} + + +PyObject * +psutil_cpu_count_cores(PyObject *self, PyObject *args) { + int num; + + if (psutil_sysctlbyname("kern.smp.cores", &num, sizeof(num)) != 0) + Py_RETURN_NONE; + return Py_BuildValue("i", num); +} + + +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 (psutil_sysctlbyname("vm.stats.sys.v_soft", &v_soft, size) != 0) + return NULL; + if (psutil_sysctlbyname("vm.stats.sys.v_intr", &v_intr, size) != 0) + return NULL; + if (psutil_sysctlbyname("vm.stats.sys.v_syscall", &v_syscall, size) != 0) + return NULL; + if (psutil_sysctlbyname("vm.stats.sys.v_trap", &v_trap, size) != 0) + return NULL; + if (psutil_sysctlbyname("vm.stats.sys.v_swtch", &v_swtch, size) != 0) + return NULL; + + 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] = {0}; + size_t size; + + if (!PyArg_ParseTuple(args, "i", &core)) + return NULL; + + // https://www.unix.com/man-page/FreeBSD/4/cpufreq/ + size = sizeof(current); + 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); + 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)"); + + return Py_BuildValue("is", current, available_freq_levels); + +error: + if (errno == ENOENT) + PyErr_SetString(PyExc_NotImplementedError, "unable to read frequency"); + else + psutil_oserror(); + return NULL; +} diff --git a/psutil/arch/freebsd/disk.c b/psutil/arch/freebsd/disk.c new file mode 100644 index 0000000000..8a46d74f54 --- /dev/null +++ b/psutil/arch/freebsd/disk.c @@ -0,0 +1,89 @@ +/* + * 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 "../../arch/all/init.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) { + psutil_runtime_error("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) { + psutil_runtime_error("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]; + str_format( + 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/init.h b/psutil/arch/freebsd/init.h new file mode 100644 index 0000000000..7ecb8790c3 --- /dev/null +++ b/psutil/arch/freebsd/init.h @@ -0,0 +1,34 @@ +/* + * 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 +#include + +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_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 new file mode 100644 index 0000000000..ac3a234db7 --- /dev/null +++ b/psutil/arch/freebsd/mem.c @@ -0,0 +1,163 @@ +/* + * 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 "../../arch/all/init.h" + +#ifndef _PATH_DEVNULL +#define _PATH_DEVNULL "/dev/null" +#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; + 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 (dict == NULL) + return NULL; + + 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; + } + + if (psutil_sysctl(mib, 2, &vm, sizeof(vm)) != 0) + 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; +} + + +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 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"); + goto error; + } + + if (kvm_getswapinfo(kd, kvmsw, 1, 0) < 0) { + kvm_close(kd); + psutil_runtime_error("kvm_getswapinfo() syscall failed"); + goto error; + } + + kvm_close(kd); + + if (sbn("vm.stats.vm.v_swapin", &swapin, sizeof(swapin)) != 0) + goto error; + if (sbn("vm.stats.vm.v_swapout", &swapout, sizeof(swapout)) != 0) + goto error; + if (sbn("vm.stats.vm.v_vnodein", &nodein, sizeof(nodein)) != 0) + goto error; + if (sbn("vm.stats.vm.v_vnodeout", &nodeout, sizeof(nodeout)) != 0) + 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/freebsd/pids.c b/psutil/arch/freebsd/pids.c new file mode 100644 index 0000000000..051eb66b87 --- /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) { + psutil_runtime_error("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 new file mode 100644 index 0000000000..236f4a329b --- /dev/null +++ b/psutil/arch/freebsd/proc.c @@ -0,0 +1,557 @@ +/* + * 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" + + +// remove spaces from string +static void +psutil_remove_spaces(char *str) { + char *p1 = str; + char *p2 = str; + do + while (*p2 == ' ') + p2++; + while ((*p1++ = *p2++)); +} + + +PyObject * +psutil_proc_cmdline(PyObject *self, PyObject *args) { + pid_t pid; + int mib[4]; + char *procargs = NULL; + size_t size = 0; + size_t pos = 0; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_ARGS; + mib[3] = pid; + + 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 + // separator + if (size > 0) { + while (pos < size) { + if (!pylist_append_obj( + py_retlist, PyUnicode_DecodeFSDefault(&procargs[pos]) + )) + goto error; + pos += strlen(&procargs[pos]) + 1; + } + } + + free(procargs); + return py_retlist; + +error: + Py_XDECREF(py_retlist); + if (procargs != NULL) + free(procargs); + 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 +PyObject * +psutil_proc_exe(PyObject *self, PyObject *args) { + pid_t pid; + char pathname[PATH_MAX]; + int error; + int mib[4]; + int ret; + size_t size; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PATHNAME; + mib[3] = pid; + + size = sizeof(pathname); + error = sysctl(mib, 4, pathname, &size, NULL, 0); + if (error == -1) { + // see: https://github.com/giampaolo/psutil/issues/907 + if (errno == ENOENT) { + return PyUnicode_DecodeFSDefault(""); + } + else { + return psutil_oserror_wsyscall("sysctl(KERN_PROC_PATHNAME)"); + } + } + if (size == 0 || strlen(pathname) == 0) { + ret = psutil_pid_exists(pid); + if (ret == -1) { + psutil_oserror(); + return NULL; + } + else if (ret == 0) + return psutil_oserror_nsp("psutil_pid_exists -> 0"); + else + str_copy(pathname, sizeof(pathname), ""); + } + + return PyUnicode_DecodeFSDefault(pathname); +} + + +PyObject * +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)) + return NULL; + if (psutil_kinfo_proc(pid, &kp) == -1) + return NULL; + return Py_BuildValue("l", (long)kp.ki_numthreads); +} + + +PyObject * +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 + pid_t pid; + int mib[4]; + struct kinfo_proc *kip = NULL; + struct kinfo_proc *kipp = NULL; + unsigned int i; + size_t size = 0; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + + // we need to re-query for thread information, so don't use *kipp + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID | KERN_PROC_INC_THREAD; + mib[3] = pid; + + if (psutil_sysctl_malloc(mib, 4, (char **)&kip, &size) != 0) + goto error; + + // subtle check: size == 0 means no such process + if (size == 0) { + psutil_oserror_nsp("sysctl (size = 0)"); + goto error; + } + + for (i = 0; i < size / sizeof(*kip); i++) { + kipp = &kip[i]; + 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; + } + } + + free(kip); + return py_retlist; + +error: + Py_DECREF(py_retlist); + free(kip); + return NULL; +} + +PyObject * +psutil_proc_cwd(PyObject *self, PyObject *args) { + pid_t pid; + struct kinfo_file *freep = NULL; + struct kinfo_file *kif; + struct kinfo_proc kipp; + PyObject *py_path = NULL; + + int i, cnt; + + 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) { + psutil_raise_for_pid(pid, "kinfo_getfile()"); + goto error; + } + + for (i = 0; i < cnt; i++) { + kif = &freep[i]; + if (kif->kf_fd == KF_FD_TYPE_CWD) { + py_path = PyUnicode_DecodeFSDefault(kif->kf_path); + if (!py_path) + goto error; + break; + } + } + /* + * For lower pids it seems we can't retrieve any information + * (lsof can't do that it either). Since this happens even + * as root we return an empty string instead of AccessDenied. + */ + if (py_path == NULL) + py_path = PyUnicode_DecodeFSDefault(""); + free(freep); + return py_path; + +error: + Py_XDECREF(py_path); + if (freep != NULL) + free(freep); + return NULL; +} + + +PyObject * +psutil_proc_num_fds(PyObject *self, PyObject *args) { + pid_t pid; + int cnt; + + struct kinfo_file *freep; + struct kinfo_proc kipp; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &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()"); + return NULL; + } + free(freep); + + return Py_BuildValue("i", cnt); +} + + +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. + pid_t pid; + int ptrwidth; + int i, cnt; + char addr[1000]; + char perms[4]; + char *path; + struct kinfo_proc kp; + 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); + + if (py_retlist == NULL) + return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + if (psutil_kinfo_proc(pid, &kp) == -1) + goto error; + + errno = 0; + freep = kinfo_getvmmap(pid, &cnt); + if (freep == NULL) { + psutil_raise_for_pid(pid, "kinfo_getvmmap()"); + goto error; + } + for (i = 0; i < cnt; i++) { + kve = &freep[i]; + addr[0] = '\0'; + perms[0] = '\0'; + str_format( + addr, + sizeof(addr), + "%#*jx-%#*jx", + ptrwidth, + (uintmax_t)kve->kve_start, + ptrwidth, + (uintmax_t)kve->kve_end + ); + psutil_remove_spaces(addr); + str_append( + perms, + sizeof(perms), + kve->kve_protection & KVME_PROT_READ ? "r" : "-" + ); + str_append( + perms, + sizeof(perms), + kve->kve_protection & KVME_PROT_WRITE ? "w" : "-" + ); + str_append( + perms, + sizeof(perms), + kve->kve_protection & KVME_PROT_EXEC ? "x" : "-" + ); + + if (strlen(kve->kve_path) == 0) { + switch (kve->kve_type) { + case KVME_TYPE_NONE: + path = "[none]"; + break; + case KVME_TYPE_DEFAULT: + path = "[default]"; + break; + case KVME_TYPE_VNODE: + path = "[vnode]"; + break; + case KVME_TYPE_SWAP: + path = "[swap]"; + break; + case KVME_TYPE_DEVICE: + path = "[device]"; + break; + case KVME_TYPE_PHYS: + path = "[phys]"; + break; + 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; + default: + path = "[?]"; + break; + } + } + else { + path = kve->kve_path; + } + + py_path = PyUnicode_DecodeFSDefault(path); + if (!py_path) + goto error; + if (!pylist_append_fmt( + py_retlist, + "ssOKKii", + addr, // "start-end" address + perms, // "rwx" permissions + py_path, // path + (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 + )) + { + goto error; + } + Py_DECREF(py_path); + py_path = NULL; + } + free(freep); + return py_retlist; + +error: + Py_XDECREF(py_path); + Py_DECREF(py_retlist); + if (freep != NULL) + free(freep); + return NULL; +} + + +// 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) { + pid_t pid; + int ret; + int i; + cpuset_t mask; + PyObject *py_retlist; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + ret = cpuset_getaffinity( + CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, sizeof(mask), &mask + ); + if (ret != 0) + return psutil_oserror(); + + py_retlist = PyList_New(0); + if (py_retlist == NULL) + return NULL; + + for (i = 0; i < CPU_SETSIZE; i++) { + if (CPU_ISSET(i, &mask)) { + if (!pylist_append_fmt(py_retlist, "i", i)) + goto error; + } + } + + return py_retlist; + +error: + Py_DECREF(py_retlist); + return NULL; +} + + +// 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) { + pid_t pid; + int i; + int seq_len; + int ret; + cpuset_t cpu_set; + PyObject *py_cpu_set; + PyObject *py_cpu_seq = NULL; + + 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"); + if (!py_cpu_seq) + return NULL; + seq_len = PySequence_Fast_GET_SIZE(py_cpu_seq); + + // calculate the mask + CPU_ZERO(&cpu_set); + for (i = 0; i < seq_len; i++) { + PyObject *item = PySequence_Fast_GET_ITEM(py_cpu_seq, i); + long value = PyLong_AsLong(item); + if (value == -1 || PyErr_Occurred()) + goto error; + CPU_SET(value, &cpu_set); + } + + // set affinity + ret = cpuset_setaffinity( + CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, sizeof(cpu_set), &cpu_set + ); + if (ret != 0) { + psutil_oserror(); + goto error; + } + + Py_DECREF(py_cpu_seq); + Py_RETURN_NONE; + +error: + if (py_cpu_seq != NULL) + Py_DECREF(py_cpu_seq); + return NULL; +} + + +// An emulation of Linux prlimit(). Returns a (soft, hard) tuple. +PyObject * +psutil_proc_getrlimit(PyObject *self, PyObject *args) { + pid_t pid; + int resource; + 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; + + if (psutil_sysctl(name, 5, &rlp, sizeof(rlp)) != 0) + return NULL; + +#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 psutil_oserror(); + Py_RETURN_NONE; +} diff --git a/psutil/arch/freebsd/proc_socks.c b/psutil/arch/freebsd/proc_socks.c index a458a01e53..cb554eedbd 100644 --- a/psutil/arch/freebsd/proc_socks.c +++ b/psutil/arch/freebsd/proc_socks.c @@ -3,23 +3,23 @@ * 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 -#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 "../../_psutil_common.h" -#include "../../_psutil_posix.h" +#include "../../arch/all/init.h" // The tcplist fetching and walking is borrowed from netstat/inet.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; @@ -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,11 +199,10 @@ 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. - long pid; + pid_t pid; int i; int cnt; struct kinfo_file *freep = NULL; @@ -192,7 +215,6 @@ psutil_proc_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; @@ -202,8 +224,12 @@ 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; @@ -218,16 +244,15 @@ psutil_proc_connections(PyObject *self, PyObject *args) { tcplist = psutil_fetch_tcplist(); if (tcplist == NULL) { - PyErr_SetFromErrno(PyExc_OSError); + psutil_oserror(); goto error; } 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; py_laddr = NULL; py_raddr = NULL; @@ -245,8 +270,9 @@ psutil_proc_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) { @@ -258,35 +284,49 @@ psutil_proc_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 @@ -299,20 +339,21 @@ psutil_proc_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. @@ -324,30 +365,34 @@ psutil_proc_connections(PyObject *self, PyObject *args) { #else 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); + str_format( + 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( - "(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) + 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; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_DECREF(py_tuple); + } Py_DECREF(py_laddr); + py_laddr = NULL; } } } @@ -356,7 +401,6 @@ psutil_proc_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/proc_socks.h b/psutil/arch/freebsd/proc_socks.h deleted file mode 100644 index a7996b1074..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_connections(PyObject* self, PyObject* args); diff --git a/psutil/arch/freebsd/sensors.c b/psutil/arch/freebsd/sensors.c new file mode 100644 index 0000000000..8259f88e3b --- /dev/null +++ b/psutil/arch/freebsd/sensors.c @@ -0,0 +1,77 @@ +/* + * 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 "../../arch/all/init.h" + + +#define DECIKELVIN_2_CELSIUS(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 (psutil_sysctlbyname("hw.acpi.battery.life", &percent, size) != 0) + goto error; + if (psutil_sysctlbyname("hw.acpi.battery.time", &minsleft, size) != 0) + goto error; + if (psutil_sysctlbyname("hw.acpi.acline", &power_plugged, size) != 0) + goto error; + return Py_BuildValue("dii", (double)percent, minsleft, power_plugged); + +error: + // see: https://github.com/giampaolo/psutil/issues/1074 + if (errno == ENOENT) + PyErr_SetString(PyExc_NotImplementedError, "no battery"); + else + psutil_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; + 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. + 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); + + return Py_BuildValue("ii", current, tjmax); + +error: + if (errno == ENOENT) + PyErr_SetString(PyExc_NotImplementedError, "no temperature sensors"); + else + psutil_oserror(); + return NULL; +} diff --git a/psutil/arch/freebsd/specific.c b/psutil/arch/freebsd/specific.c deleted file mode 100644 index cf0b7df246..0000000000 --- a/psutil/arch/freebsd/specific.c +++ /dev/null @@ -1,1014 +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 specific to FreeBSD. - * Used by _psutil_bsd module methods. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include // needed for vmtotal struct -#include // for swap mem -#include // process open files, shared libs (kinfo_getvmmap), cwd -#include - -#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) -#ifndef _PATH_DEVNULL -#define _PATH_DEVNULL "/dev/null" -#endif - - -// ============================================================================ -// Utility functions -// ============================================================================ - - -int -psutil_kinfo_proc(const 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; - - size = sizeof(struct kinfo_proc); - if (sysctl((int *)mib, 4, proc, &size, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return -1; - } - - // sysctl stores 0 in the size if we can't find the process information. - if (size == 0) { - NoSuchProcess(""); - return -1; - } - return 0; -} - - -// remove spaces from string -static void psutil_remove_spaces(char *str) { - char *p1 = str; - char *p2 = str; - do - while (*p2 == ' ') - p2++; - while ((*p1++ = *p2++)); -} - - -// ============================================================================ -// 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 (use "free" from System framework). - // On success, the function returns 0. - // On error, the function returns a BSD errno value. - int err; - struct kinfo_proc *result; - int done; - int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0 }; - size_t length; - - 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 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); - - // Clean up and establish post conditions. - if (err != 0 && result != NULL) { - free(result); - result = NULL; - } - - *procList = result; - *procCount = length / sizeof(struct kinfo_proc); - - assert((err == 0) == (*procList != NULL)); - return err; -} - - -/* - * 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(long pid, size_t *argsize) { - int mib[4]; - int argmax; - size_t size = sizeof(argmax); - char *procargs = NULL; - - // 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) - return NULL; - - // Allocate space for the arguments. - procargs = (char *)malloc(argmax); - if (procargs == NULL) { - PyErr_NoMemory(); - return NULL; - } - - // 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) { - free(procargs); - PyErr_SetFromErrno(PyExc_OSError); - 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(long 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 (!py_arg) - goto error; - if (PyList_Append(py_retlist, py_arg)) - goto error; - Py_DECREF(py_arg); - pos = pos + strlen(&argstr[pos]) + 1; - } - } - - free(argstr); - return py_retlist; - -error: - Py_XDECREF(py_arg); - Py_DECREF(py_retlist); - if (argstr != NULL) - free(argstr); - 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 - */ -PyObject * -psutil_proc_exe(PyObject *self, PyObject *args) { - long pid; - char pathname[PATH_MAX]; - int error; - int mib[4]; - int ret; - size_t size; - const char *encoding_errs; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - - mib[0] = CTL_KERN; - mib[1] = KERN_PROC; - mib[2] = KERN_PROC_PATHNAME; - mib[3] = pid; - - size = sizeof(pathname); - error = sysctl(mib, 4, pathname, &size, NULL, 0); - if (error == -1) { - // see: https://github.com/giampaolo/psutil/issues/907 - if (errno == ENOENT) - return PyUnicode_DecodeFSDefault(""); - else - return PyErr_SetFromErrno(PyExc_OSError); - } - if (size == 0 || strlen(pathname) == 0) { - ret = psutil_pid_exists(pid); - if (ret == -1) - return NULL; - else if (ret == 0) - return NoSuchProcess(""); - else - strcpy(pathname, ""); - } - - return PyUnicode_DecodeFSDefault(pathname); -} - - -PyObject * -psutil_proc_num_threads(PyObject *self, PyObject *args) { - // Return number of threads used by process as a Python integer. - long pid; - struct kinfo_proc kp; - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - if (psutil_kinfo_proc(pid, &kp) == -1) - return NULL; - return Py_BuildValue("l", (long)kp.ki_numthreads); -} - - -PyObject * -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 - long pid; - int mib[4]; - struct kinfo_proc *kip = NULL; - struct kinfo_proc *kipp = NULL; - int error; - unsigned int i; - size_t size; - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) - goto error; - - // we need to re-query for thread information, so don't use *kipp - mib[0] = CTL_KERN; - mib[1] = KERN_PROC; - 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) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - if (size == 0) { - NoSuchProcess(""); - goto error; - } - - kip = malloc(size); - if (kip == NULL) { - PyErr_NoMemory(); - goto error; - } - - error = sysctl(mib, 4, kip, &size, NULL, 0); - if (error == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - if (size == 0) { - NoSuchProcess(""); - goto error; - } - - for (i = 0; i < size / sizeof(*kipp); 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) - 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); - if (kip != NULL) - free(kip); - return NULL; -} - - -PyObject * -psutil_cpu_count_phys(PyObject *self, PyObject *args) { - // Return an XML string from which we'll determine the number of - // physical 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. - */ -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 = 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)) - 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; - // 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)) - goto error; - if (sysctlbyname("vfs.bufspace", &buffers, &buffers_size, NULL, 0)) - goto error; - - size = sizeof(vm); - if (sysctl(mib, 2, &vm, &size, NULL, 0) != 0) - goto error; - - 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 - ); - -error: - return PyErr_SetFromErrno(PyExc_OSError); -} - - -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); - - 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) - 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; - - int pagesize = getpagesize(); - - 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 - ); - -error: - return PyErr_SetFromErrno(PyExc_OSError); -} - - -#if defined(__FreeBSD_version) && __FreeBSD_version >= 800000 -PyObject * -psutil_proc_cwd(PyObject *self, PyObject *args) { - long pid; - struct kinfo_file *freep = NULL; - struct kinfo_file *kif; - struct kinfo_proc kipp; - const char *encoding_errs; - PyObject *py_path = NULL; - - int i, cnt; - - if (! PyArg_ParseTuple(args, "l", &pid)) - goto error; - if (psutil_kinfo_proc(pid, &kipp) == -1) - goto error; - - errno = 0; - freep = kinfo_getfile(pid, &cnt); - if (freep == NULL) { - psutil_raise_for_pid(pid, "kinfo_getfile()"); - goto error; - } - - for (i = 0; i < cnt; i++) { - kif = &freep[i]; - if (kif->kf_fd == KF_FD_TYPE_CWD) { - py_path = PyUnicode_DecodeFSDefault(kif->kf_path); - if (!py_path) - goto error; - break; - } - } - /* - * For lower pids it seems we can't retrieve any information - * (lsof can't do that it either). Since this happens even - * as root we return an empty string instead of AccessDenied. - */ - if (py_path == NULL) - py_path = PyUnicode_DecodeFSDefault(""); - free(freep); - return py_path; - -error: - Py_XDECREF(py_path); - if (freep != NULL) - free(freep); - return NULL; -} -#endif - - -#if defined(__FreeBSD_version) && __FreeBSD_version >= 800000 -PyObject * -psutil_proc_num_fds(PyObject *self, PyObject *args) { - long pid; - int cnt; - - struct kinfo_file *freep; - struct kinfo_proc kipp; - - 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()"); - return NULL; - } - free(freep); - - return Py_BuildValue("i", cnt); -} -#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_SetFromErrno(PyExc_OSError); - } - 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_SetFromErrno(PyExc_OSError); - goto error; - } - - // per-cpu info - size = sizeof(cpu_time); - if (sysctlbyname("kern.cp_times", &cpu_time, &size, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - 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. - //'procstat' cmdline utility has been used as an example. - long pid; - int ptrwidth; - int i, cnt; - char addr[1000]; - char perms[4]; - char *path; - struct kinfo_proc kp; - 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); - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) - goto error; - if (psutil_kinfo_proc(pid, &kp) == -1) - goto error; - - errno = 0; - freep = kinfo_getvmmap(pid, &cnt); - if (freep == NULL) { - psutil_raise_for_pid(pid, "kinfo_getvmmap()"); - goto error; - } - for (i = 0; i < cnt; i++) { - py_tuple = NULL; - 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); - 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)); - - if (strlen(kve->kve_path) == 0) { - switch (kve->kve_type) { - case KVME_TYPE_NONE: - path = "[none]"; - break; - case KVME_TYPE_DEFAULT: - path = "[default]"; - break; - case KVME_TYPE_VNODE: - path = "[vnode]"; - break; - case KVME_TYPE_SWAP: - path = "[swap]"; - break; - case KVME_TYPE_DEVICE: - path = "[device]"; - break; - case KVME_TYPE_PHYS: - path = "[phys]"; - break; - case KVME_TYPE_DEAD: - path = "[dead]"; - break; - case KVME_TYPE_SG: - path = "[sg]"; - break; - case KVME_TYPE_UNKNOWN: - path = "[unknown]"; - break; - default: - path = "[?]"; - break; - } - } - else { - path = kve->kve_path; - } - - 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)) - goto error; - Py_DECREF(py_path); - Py_DECREF(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); - return NULL; -} - - -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 - long pid; - int ret; - int i; - cpuset_t mask; - PyObject* py_retlist; - PyObject* py_cpu_num; - - if (!PyArg_ParseTuple(args, "i", &pid)) - return NULL; - ret = cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, - sizeof(mask), &mask); - if (ret != 0) - return PyErr_SetFromErrno(PyExc_OSError); - - py_retlist = PyList_New(0); - if (py_retlist == NULL) - return NULL; - - 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)) - goto error; - } - } - - return py_retlist; - -error: - Py_XDECREF(py_cpu_num); - Py_DECREF(py_retlist); - return NULL; -} - - -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 - long pid; - int i; - int seq_len; - int ret; - cpuset_t cpu_set; - PyObject *py_cpu_set; - PyObject *py_cpu_seq = NULL; - - if (!PyArg_ParseTuple(args, "lO", &pid, &py_cpu_set)) - return NULL; - - py_cpu_seq = PySequence_Fast(py_cpu_set, "expected a sequence or integer"); - if (!py_cpu_seq) - return NULL; - seq_len = PySequence_Fast_GET_SIZE(py_cpu_seq); - - // calculate the mask - 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); - } - - // set affinity - ret = cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, - sizeof(cpu_set), &cpu_set); - if (ret != 0) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - Py_DECREF(py_cpu_seq); - Py_RETURN_NONE; - -error: - if (py_cpu_seq != NULL) - Py_DECREF(py_cpu_seq); - return NULL; -} - - -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)) - 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; - - return Py_BuildValue( - "IIIII", - v_swtch, // ctx switches - v_intr, // interrupts - v_soft, // software interrupts - v_syscall, // syscalls - v_trap // traps - ); - -error: - return PyErr_SetFromErrno(PyExc_OSError); -} - - -/* - * 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; -} diff --git a/psutil/arch/freebsd/specific.h b/psutil/arch/freebsd/specific.h deleted file mode 100644 index 0df66eccbf..0000000000 --- a/psutil/arch/freebsd/specific.h +++ /dev/null @@ -1,32 +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_cpu_count_phys(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); -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_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_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_sensors_battery(PyObject* self, PyObject* args); -#endif diff --git a/psutil/arch/freebsd/sys_socks.c b/psutil/arch/freebsd/sys_socks.c index e0e2046be6..69e0875308 100644 --- a/psutil/arch/freebsd/sys_socks.c +++ b/psutil/arch/freebsd/sys_socks.c @@ -3,62 +3,60 @@ * 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 #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 "../../_psutil_common.h" -#include "../../_psutil_posix.h" +#include "../../arch/all/init.h" -static struct xfile *psutil_xfiles; -static int psutil_nxfiles; +static int +psutil_populate_xfiles(struct xfile **psutil_xfiles, int *psutil_nxfiles) { + struct xfile *new_psutil_xfiles; + size_t len = sizeof(struct xfile); -int -psutil_populate_xfiles() { - size_t len; - - 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; + psutil_oserror(); + return -1; } 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; + return -1; } + *psutil_xfiles = new_psutil_xfiles; } - if (len > 0 && psutil_xfiles->xf_size != sizeof *psutil_xfiles) { - PyErr_Format(PyExc_RuntimeError, "struct xfile size mismatch"); - return 0; + if (len > 0 && (*psutil_xfiles)->xf_size != sizeof(struct xfile)) { + psutil_runtime_error("struct xfile size mismatch"); + return -1; } - psutil_nxfiles = len / sizeof *psutil_xfiles; - return 1; + *psutil_nxfiles = len / sizeof(struct xfile); + return 0; } -struct xfile * -psutil_get_file_from_sock(void *sock) { +static struct xfile * +psutil_get_file_from_sock( + kvaddr_t sock, struct xfile *psutil_xfiles, int psutil_nxfiles +) { struct xfile *xf; int n; @@ -72,7 +70,15 @@ psutil_get_file_from_sock(void *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 xfile *psutil_xfiles, + int psutil_nxfiles +) { struct xinpgen *xig, *exig; struct xinpcb *xip; struct xtcpcb *xtp; @@ -88,7 +94,6 @@ int psutil_gather_inet(int proto, PyObject *py_retlist) { int retry; int type; - PyObject *py_tuple = NULL; PyObject *py_laddr = NULL; PyObject *py_raddr = NULL; @@ -115,7 +120,7 @@ int psutil_gather_inet(int proto, PyObject *py_retlist) { if (sysctlbyname(varname, buf, &len, NULL, 0) == 0) break; if (errno != ENOMEM) { - PyErr_SetFromErrno(0); + psutil_oserror(); goto error; } bufsize *= 2; @@ -123,13 +128,13 @@ int psutil_gather_inet(int proto, PyObject *py_retlist) { 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--); for (;;) { - struct xfile *xf; + struct xfile *xf; int lport, rport, status, family; xig = (struct xinpgen *)(void *)((char *)xig + xig->xig_len); @@ -140,8 +145,7 @@ int psutil_gather_inet(int proto, PyObject *py_retlist) { 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; @@ -156,8 +160,7 @@ int psutil_gather_inet(int proto, PyObject *py_retlist) { 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 @@ -169,13 +172,21 @@ int psutil_gather_inet(int proto, PyObject *py_retlist) { status = PSUTIL_CONN_NONE; break; default: - PyErr_Format(PyExc_RuntimeError, "invalid proto"); + psutil_runtime_error("invalid proto"); goto error; } - char lip[200], rip[200]; + // filter + if ((inp->inp_vflag & INP_IPV4) && (include_v4 == 0)) + continue; + if ((inp->inp_vflag & INP_IPV6) && (include_v6 == 0)) + continue; + + char lip[INET6_ADDRSTRLEN], rip[INET6_ADDRSTRLEN]; - 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); @@ -202,35 +213,42 @@ int psutil_gather_inet(int proto, PyObject *py_retlist) { py_raddr = Py_BuildValue("()"); if (!py_raddr) goto error; - py_tuple = Py_BuildValue( - "(iiiNNii)", - xf->xf_fd, // fd - family, // family - type, // type - py_laddr, // laddr - py_raddr, // raddr - status, // status - xf->xf_pid); // pid - if (!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; - if (PyList_Append(py_retlist, py_tuple)) - goto error; - Py_DECREF(py_tuple); + } + py_laddr = NULL; + py_raddr = NULL; } 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; } -int psutil_gather_unix(int proto, PyObject *py_retlist) { +static int +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; @@ -242,7 +260,6 @@ int psutil_gather_unix(int proto, PyObject *py_retlist) { struct sockaddr_un *sun; char path[PATH_MAX]; - PyObject *py_tuple = NULL; PyObject *py_lpath = NULL; switch (proto) { @@ -271,22 +288,21 @@ int psutil_gather_unix(int proto, PyObject *py_retlist) { if (sysctlbyname(varname, buf, &len, NULL, 0) == 0) break; if (errno != ENOMEM) { - PyErr_SetFromErrno(0); + psutil_oserror(); goto error; } 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"); + psutil_runtime_error("struct xinpgen size mismatch"); goto error; } } 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) @@ -295,68 +311,155 @@ int 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; 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); + str_format( + 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 - 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 1; + return 0; error: - Py_XDECREF(py_tuple); Py_XDECREF(py_lpath); free(buf); - return 0; + return -1; +} + + +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. +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; 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 ((include_v6 = psutil_int_in_seq(AF_INET6, py_af_filter)) == -1) goto error; - if (psutil_gather_inet(IPPROTO_TCP, py_retlist) == 0) + if ((include_unix = psutil_int_in_seq(AF_UNIX, py_af_filter)) == -1) goto error; - if (psutil_gather_inet(IPPROTO_UDP, py_retlist) == 0) + if ((include_tcp = psutil_int_in_seq(SOCK_STREAM, py_type_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_udp = psutil_int_in_seq(SOCK_DGRAM, py_type_filter)) == -1) + goto error; + + psutil_xfiles = malloc(sizeof(struct xfile)); + if (psutil_xfiles == NULL) { + PyErr_NoMemory(); goto error; + } + + 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) + { + goto error_free_psutil_xfiles; + } + } + // UDP + if (include_udp == 1) { + if (psutil_gather_inet( + 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) + 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; } 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 new file mode 100644 index 0000000000..2fe1b4a8ef --- /dev/null +++ b/psutil/arch/linux/disk.c @@ -0,0 +1,74 @@ +/* + * 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" + +#include +#include + + +// 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_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) { + psutil_runtime_error("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; + 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); + } + endmntent(file); + return py_retlist; + +error: + if (file != NULL) + endmntent(file); + Py_XDECREF(py_dev); + Py_XDECREF(py_mountp); + Py_DECREF(py_retlist); + return NULL; +} 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 new file mode 100644 index 0000000000..60c45548ad --- /dev/null +++ b/psutil/arch/linux/init.h @@ -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 // __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); +PyObject *psutil_proc_ioprio_get(PyObject *self, PyObject *args); +PyObject *psutil_proc_ioprio_set(PyObject *self, PyObject *args); + +// 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); +#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/linux/mem.c b/psutil/arch/linux/mem.c new file mode 100644 index 0000000000..044e6042d4 --- /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 "../../arch/all/init.h" + + +PyObject * +psutil_linux_sysinfo(PyObject *self, PyObject *args) { + struct sysinfo info; + + if (sysinfo(&info) != 0) + return psutil_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/net.c b/psutil/arch/linux/net.c new file mode 100644 index 0000000000..fdacdff903 --- /dev/null +++ b/psutil/arch/linux/net.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 +#include +#include + +#include "../../arch/all/init.h" + +// 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 + +// * 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 psutil_oserror_wsyscall("socket()"); + str_copy(ifr.ifr_name, sizeof(ifr.ifr_name), nic_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) || (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; + } + else { + psutil_oserror_wsyscall("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/proc.c b/psutil/arch/linux/proc.c new file mode 100644 index 0000000000..9e1bb3bd60 --- /dev/null +++ b/psutil/arch/linux/proc.c @@ -0,0 +1,191 @@ +/* + * 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" + +#include +#include +#include +#include + + +// ==================================================================== +// --- process priority (niceness) +// ==================================================================== + + +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 psutil_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 psutil_oserror(); + Py_RETURN_NONE; +} + + +// ==================================================================== +// --- process CPU affinity +// ==================================================================== + + +#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; + 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 psutil_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 (!pylist_append_obj(py_list, PyLong_FromLong(cpu))) + goto error; + --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, + "sequence argument expected, got %R", + Py_TYPE(py_cpu_set) + ); + } + + 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; + } + long value = PyLong_AsLong(item); + 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 psutil_oserror(); + } + + Py_RETURN_NONE; +} +#endif // PSUTIL_HAS_CPU_AFFINITY diff --git a/psutil/arch/netbsd/cpu.c b/psutil/arch/netbsd/cpu.c new file mode 100644 index 0000000000..6142fe3800 --- /dev/null +++ b/psutil/arch/netbsd/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 +#include + +#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) + + +PyObject * +psutil_cpu_stats(PyObject *self, PyObject *args) { + struct uvmexp_sysctl uv; + int uvmexp_mib[] = {CTL_VM, VM_UVMEXP2}; + + // https://man.netbsd.org/man9/uvm.9 + if (psutil_sysctl(uvmexp_mib, 2, &uv, sizeof(uv)) != 0) + return NULL; + + return Py_BuildValue( + "KKKKKKK", + (uint64_t)uv.swtch, // ctx switches + (uint64_t)uv.intrs, // interrupts + (uint64_t)uv.softs, // soft interrupts + (uint64_t)uv.syscalls, // syscalls + (uint64_t)uv.traps, // traps + (uint64_t)uv.faults, // faults + (uint64_t)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_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + // retrieve the number of cpus + mib[0] = CTL_HW; + mib[1] = HW_NCPU; + if (psutil_sysctl(mib, 2, &ncpu, sizeof(ncpu)) != 0) + 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; + if (psutil_sysctl(mib, 3, &cpu_time, sizeof(cpu_time)) != 0) + goto error; + 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; + } + } + + return py_retlist; + +error: + Py_DECREF(py_retlist); + return NULL; +} diff --git a/psutil/arch/netbsd/disk.c b/psutil/arch/netbsd/disk.c new file mode 100644 index 0000000000..7f817248cb --- /dev/null +++ b/psutil/arch/netbsd/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. + */ + +// 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 + +#include "../../arch/all/init.h" + + +PyObject * +psutil_disk_io_counters(PyObject *self, PyObject *args) { + 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); + + // Use helper to allocate and fill buffer + if (psutil_sysctl_malloc(mib, 3, (char **)&stats, &stats_len) == -1) + goto error; + + dk_ndrive = (int)(stats_len / sizeof(struct io_sysctl)); + + 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/init.h b/psutil/arch/netbsd/init.h new file mode 100644 index 0000000000..2949da7856 --- /dev/null +++ b/psutil/arch/netbsd/init.h @@ -0,0 +1,26 @@ +/* + * 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 +#include + +int _psutil_pids(pid_t **pids_array, int *pids_count); + +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_virtual_mem(PyObject *self, PyObject *args); diff --git a/psutil/arch/netbsd/mem.c b/psutil/arch/netbsd/mem.c new file mode 100644 index 0000000000..870aa1f0dc --- /dev/null +++ b/psutil/arch/netbsd/mem.c @@ -0,0 +1,93 @@ +/* + * 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 +#include + +#include "../../arch/all/init.h" + + +PyObject * +psutil_virtual_mem(PyObject *self, PyObject *args) { + struct uvmexp_sysctl uv; + 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 buffers, shared, used, avail; + double percent; + long pagesize = psutil_getpagesize(); + PyObject *dict = PyDict_New(); + + if (dict == NULL) + return NULL; + + if (psutil_sysctl(uvmexp_mib, 2, &uv, sizeof(uv)) != 0) + goto error; + if (psutil_sysctl(vmmeter_mib, 2, &vmdata, sizeof(vmdata)) != 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; + + // Updated by kernel every 5 secs. We have: + // u_int32_t t_vmshr; /* shared virtual memory */ + // u_int32_t t_avmshr; /* active shared virtual memory */ + // u_int32_t t_rmshr; /* shared real memory */ + // We return `t_rmshr` (real shared). Reason: report physical + // resource pressure rather than just kernel bookkeeping. + shared = (unsigned long long)vmdata.t_rmshr * pagesize; + + // Note: on OpenBSD 'cached' and 'buffers' are aliases; not on NetBSD. + buffers = (unsigned long long)uv.filepages << uv.pageshift; + + // same as 'vmstat -s' (2 distinct values) + cached = (unsigned long long)(uv.filepages + uv.execpages) * pagesize; + + // 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, "buffers", "K", buffers) + | pydict_add(dict, "wired", "K", wired) + | pydict_add(dict, "cached", "K", cached) + | pydict_add(dict, "shared", "K", shared))) + goto error; + + return dict; + +error: + Py_DECREF(dict); + return NULL; +} diff --git a/psutil/arch/netbsd/pids.c b/psutil/arch/netbsd/pids.c new file mode 100644 index 0000000000..ebf5127f06 --- /dev/null +++ b/psutil/arch/netbsd/pids.c @@ -0,0 +1,63 @@ +/* + * 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_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) { + psutil_runtime_error("kvm_openfiles() failed: %s", errbuf); + return -1; + } + + result = kvm_getproc2( + kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc2), &cnt + ); + if (result == NULL) { + psutil_runtime_error("kvm_getproc2() failed"); + kvm_close(kd); + return -1; + } + + if (cnt == 0) { + psutil_runtime_error("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 new file mode 100644 index 0000000000..8aa4953067 --- /dev/null +++ b/psutil/arch/netbsd/proc.c @@ -0,0 +1,287 @@ +/* + * 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 "../../arch/all/init.h" + + +PyObject * +psutil_proc_cwd(PyObject *self, PyObject *args) { + long pid; + char path[MAXPATHLEN]; + + 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}; + 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) + goto error; + path[len] = '\0'; +#endif + + return PyUnicode_DecodeFSDefault(path); + +error: + if (errno == ENOENT) + psutil_oserror_nsp("sysctl -> ENOENT"); + else + psutil_oserror(); + return NULL; +} + + +// XXX: This is no longer used as per +// https://github.com/giampaolo/psutil/pull/557#issuecomment-171912820 +// Current implementation uses /proc instead. +// Left here just in case. +/* +PyObject * +psutil_proc_exe(PyObject *self, PyObject *args) { +#if __NetBSD_Version__ >= 799000000 + pid_t pid; + char pathname[MAXPATHLEN]; + int error; + int mib[4]; + int ret; + size_t size; + + if (! PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (pid == 0) { + // else returns ENOENT + return PyUnicode_FromString(""); + } + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC_ARGS; + mib[2] = pid; + mib[3] = KERN_PROC_PATHNAME; + + size = sizeof(pathname); + error = sysctl(mib, 4, NULL, &size, NULL, 0); + if (error == -1) { + psutil_oserror(); + return NULL; + } + + error = sysctl(mib, 4, pathname, &size, NULL, 0); + if (error == -1) { + psutil_oserror(); + return NULL; + } + if (size == 0 || strlen(pathname) == 0) { + ret = psutil_pid_exists(pid); + if (ret == -1) + return NULL; + else if (ret == 0) + return psutil_oserror_nsp("psutil_pid_exists -> 0"); + else + str_copy(pathname, sizeof(pathname), ""); + } + + return PyUnicode_DecodeFSDefault(pathname); +#else + return Py_BuildValue("s", ""); +#endif +} +*/ + + +PyObject * +psutil_proc_num_threads(PyObject *self, PyObject *args) { + long pid; + struct kinfo_proc2 kp; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (psutil_kinfo_proc(pid, &kp) == -1) + return NULL; + return Py_BuildValue("l", (long)kp.p_nlwps); +} + + +PyObject * +psutil_proc_threads(PyObject *self, PyObject *args) { + pid_t pid; + int mib[5]; + int i, nlwps; + ssize_t st; + size_t size; + struct kinfo_lwp *kl = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + + mib[0] = CTL_KERN; + mib[1] = KERN_LWP; + mib[2] = pid; + mib[3] = sizeof(struct kinfo_lwp); + mib[4] = 0; + + // first query size + st = sysctl(mib, 5, NULL, &size, NULL, 0); + if (st == -1) { + psutil_oserror(); + goto error; + } + if (size == 0) { + psutil_oserror_nsp("sysctl (size = 0)"); + goto error; + } + + // set slot count for NetBSD KERN_LWP + mib[4] = size / sizeof(size_t); + + if (psutil_sysctl_malloc(mib, __arraycount(mib), (char **)&kl, &size) != 0) + { + goto error; + } + if (size == 0) { + psutil_oserror_nsp("sysctl (size = 0)"); + goto error; + } + + 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: return 2 "user" times, no "system" time available + 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; + } + } + + free(kl); + return py_retlist; + +error: + Py_DECREF(py_retlist); + if (kl != NULL) + free(kl); + return NULL; +} + + +PyObject * +psutil_proc_cmdline(PyObject *self, PyObject *args) { + pid_t pid; + int mib[4]; + int st; + int attempt = 0; + int max_attempts = 50; + size_t len = 0; + size_t pos = 0; + char *procargs = NULL; + PyObject *py_retlist = PyList_New(0); + + 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; + + while (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 + // psutil_sysctl_malloc() sets a Python exception before + // returning -1; clear it before retrying or returning so we + // don't leave a pending exception on a non-NULL return value. + PyErr_Clear(); + 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 + ); + PyErr_Clear(); + return py_retlist; + } + } + goto error; + } + break; + } + + if (len > 0) { + while (pos < len) { + if (!pylist_append_obj( + py_retlist, PyUnicode_DecodeFSDefault(&procargs[pos]) + )) + goto error; + pos = pos + strlen(&procargs[pos]) + 1; + } + } + + free(procargs); + PyErr_Clear(); + return py_retlist; + +error: + Py_DECREF(py_retlist); + if (procargs != NULL) + free(procargs); + return NULL; +} + + +PyObject * +psutil_proc_num_fds(PyObject *self, PyObject *args) { + long pid; + int cnt; + + struct kinfo_file *freep; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + errno = 0; + freep = kinfo_getfile(pid, &cnt); + if (freep == NULL) { + psutil_raise_for_pid(pid, "kinfo_getfile()"); + return NULL; + } + free(freep); + + return Py_BuildValue("i", cnt); +} diff --git a/psutil/arch/netbsd/socks.c b/psutil/arch/netbsd/socks.c index f370f09466..d383027cc0 100644 --- a/psutil/arch/netbsd/socks.c +++ b/psutil/arch/netbsd/socks.c @@ -7,43 +7,21 @@ */ #include -#include -#include -#include #include #include -#include -#include -#include -#include #include #include #include -#include - -#include "../../_psutil_common.h" -#include "../../_psutil_posix.h" - - -// address family filter -enum af_filter { - INET, - INET4, - INET6, - TCP, - TCP4, - TCP6, - UDP, - UDP4, - UDP6, - UNIX, - ALL, -}; + +#include "../../arch/all/init.h" + // kinfo_file results struct kif { SLIST_ENTRY(kif) kifs; struct kinfo_file *kif; + char *buf; + int has_buf; }; // kinfo_file results list @@ -54,36 +32,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 +64,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 +86,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; @@ -123,8 +97,8 @@ psutil_get_files(void) { mib[5] = 0; if (sysctl(mib, 6, NULL, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return -1; + psutil_oserror(); + goto error; } offset = len % sizeof(off_t); @@ -132,33 +106,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; + psutil_oserror(); + 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); + else { + free(buf); } - */ return 0; + +error: + if (buf != NULL) + free(buf); + return -1; } @@ -167,25 +154,25 @@ 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; memset(mib, 0, sizeof(mib)); if (sysctlnametomib(name, mib, &namelen) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return -1; + psutil_oserror(); + goto error; } if (sysctl(mib, __arraycount(mib), NULL, &len, NULL, 0) == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return -1; + psutil_oserror(); + goto error; } if ((pcb = malloc(len)) == NULL) { PyErr_NoMemory(); - return -1; + goto error; } memset(pcb, 0, len); @@ -193,114 +180,126 @@ 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; + psutil_oserror(); + 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); + } } - - /* - // 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); + else { + free(pcb); } - */ return 0; + +error: + if (pcb != NULL) + free(pcb); + return -1; } // 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; } @@ -312,11 +311,11 @@ 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; pid_t pid; - PyObject *py_tuple = NULL; PyObject *py_laddr = NULL; PyObject *py_raddr = NULL; PyObject *py_retlist = PyList_New(0); @@ -324,14 +323,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; @@ -344,37 +345,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); } @@ -386,29 +391,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; - strcpy(laddr, sun_src->sun_path); - strcpy(raddr, sun_dst->sun_path); + struct sockaddr_un *sun_src = (struct sockaddr_un *)&kp->kpcb + ->ki_src; + struct sockaddr_un *sun_dst = (struct sockaddr_un *)&kp->kpcb + ->ki_dst; + 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) + if (!py_laddr) goto error; py_raddr = PyUnicode_DecodeFSDefault(raddr); - if (! py_raddr) + if (!py_raddr) goto error; } else { @@ -416,22 +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; } } @@ -440,8 +447,10 @@ psutil_net_connections(PyObject *self, PyObject *args) { return py_retlist; error: - Py_XDECREF(py_tuple); + psutil_kiflist_clear(); + psutil_kpcblist_clear(); + Py_DECREF(py_retlist); Py_XDECREF(py_laddr); Py_XDECREF(py_raddr); - return 0; + return NULL; } diff --git a/psutil/arch/netbsd/socks.h b/psutil/arch/netbsd/socks.h deleted file mode 100644 index 9e6a97c0a8..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_proc_connections(PyObject *, PyObject *); -PyObject *psutil_net_connections(PyObject *, PyObject *); diff --git a/psutil/arch/netbsd/specific.c b/psutil/arch/netbsd/specific.c deleted file mode 100644 index cab60d6082..0000000000 --- a/psutil/arch/netbsd/specific.c +++ /dev/null @@ -1,652 +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. - * - * 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 -#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 "specific.h" -#include "../../_psutil_common.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) - - -// ============================================================================ -// Utility functions -// ============================================================================ - - -int -psutil_kinfo_proc(pid_t pid, kinfo_proc *proc) { - // Fills a kinfo_proc struct based on process pid. - int ret; - int mib[6]; - size_t size = sizeof(kinfo_proc); - - 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) { - PyErr_SetFromErrno(PyExc_OSError); - return -1; - } - // sysctl stores 0 in the size if we can't find the process information. - if (size == 0) { - NoSuchProcess(""); - return -1; - } - return 0; -} - - -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; -} - - -// XXX: This is no longer used as per -// https://github.com/giampaolo/psutil/pull/557#issuecomment-171912820 -// Current implementation uses /proc instead. -// Left here just in case. -/* -PyObject * -psutil_proc_exe(PyObject *self, PyObject *args) { -#if __NetBSD_Version__ >= 799000000 - pid_t pid; - char pathname[MAXPATHLEN]; - int error; - int mib[4]; - int ret; - size_t size; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - if (pid == 0) { - // else returns ENOENT - return Py_BuildValue("s", ""); - } - - mib[0] = CTL_KERN; - mib[1] = KERN_PROC_ARGS; - mib[2] = pid; - mib[3] = KERN_PROC_PATHNAME; - - size = sizeof(pathname); - error = sysctl(mib, 4, NULL, &size, NULL, 0); - if (error == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - error = sysctl(mib, 4, pathname, &size, NULL, 0); - if (error == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - if (size == 0 || strlen(pathname) == 0) { - ret = psutil_pid_exists(pid); - if (ret == -1) - return NULL; - else if (ret == 0) - return NoSuchProcess(""); - else - strcpy(pathname, ""); - } - - return PyUnicode_DecodeFSDefault(pathname); -#else - return Py_BuildValue("s", ""); -#endif -} -*/ - -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; - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - if (psutil_kinfo_proc(pid, &kp) == -1) - return NULL; - return Py_BuildValue("l", (long)kp.p_nlwps); -} - -PyObject * -psutil_proc_threads(PyObject *self, PyObject *args) { - pid_t pid; - int mib[5]; - int i, nlwps; - ssize_t st; - size_t size; - struct kinfo_lwp *kl = NULL; - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) - goto error; - - mib[0] = CTL_KERN; - mib[1] = KERN_LWP; - mib[2] = pid; - mib[3] = sizeof(struct kinfo_lwp); - mib[4] = 0; - - st = sysctl(mib, 5, NULL, &size, NULL, 0); - if (st == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - if (size == 0) { - NoSuchProcess(""); - goto error; - } - - 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); - goto error; - } - if (size == 0) { - NoSuchProcess(""); - goto error; - } - - nlwps = (int)(size / sizeof(struct kinfo_lwp)); - for (i = 0; i < nlwps; i++) { - 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; - -error: - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - if (kl != NULL) - free(kl); - return NULL; -} - - -// ============================================================================ -// APIS -// ============================================================================ - -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; - // 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 == NULL) { - PyErr_Format( - PyExc_RuntimeError, "kvm_openfiles() syscall failed: %s", errbuf); - return errno; - } - - 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; - } - - *procCount = (size_t)cnt; - - size_t mlen = cnt * sizeof(kinfo_proc); - - if ((*procList = malloc(mlen)) == NULL) { - PyErr_NoMemory(); - kvm_close(kd); - return errno; - } - - memcpy(*procList, result, mlen); - assert(*procList != NULL); - kvm_close(kd); - - return 0; -} - - -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; - - mib[0] = CTL_KERN; - mib[1] = KERN_ARGMAX; - - size = sizeof(argmax); - st = sysctl(mib, 2, &argmax, &size, NULL, 0); - if (st == -1) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - procargs = (char *)malloc(argmax); - 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); - if (st == -1) { - free(procargs); - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - *argsize = argmax; - 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) - 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 (!py_arg) - goto error; - if (PyList_Append(py_retlist, py_arg)) - goto error; - Py_DECREF(py_arg); - pos = pos + strlen(&argstr[pos]) + 1; - } - } - - free(argstr); - return py_retlist; - -error: - Py_XDECREF(py_arg); - Py_DECREF(py_retlist); - if (argstr != NULL) - free(argstr); - return NULL; -} - - -/* - * 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 = 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; - - 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 += swdev[i].se_nblks * DEV_BSIZE; - swap_free += (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}; - long pagesize = getpagesize(); - 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; - int cnt; - - struct kinfo_file *freep; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - - errno = 0; - freep = kinfo_getfile(pid, &cnt); - if (freep == NULL) { - psutil_raise_for_pid(pid, "kinfo_getfile()"); - return NULL; - } - free(freep); - - 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/specific.h deleted file mode 100644 index 96ad9f7d26..0000000000 --- a/psutil/arch/netbsd/specific.h +++ /dev/null @@ -1,28 +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_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); diff --git a/psutil/arch/openbsd/cpu.c b/psutil/arch/openbsd/cpu.c new file mode 100644 index 0000000000..1e75ade27d --- /dev/null +++ b/psutil/arch/openbsd/cpu.c @@ -0,0 +1,96 @@ +/* + * 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_* + +#include "../../arch/all/init.h" + + +PyObject * +psutil_per_cpu_times(PyObject *self, PyObject *args) { + int mib[3]; + int ncpu; + size_t len; + int i; + 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; + if (psutil_sysctl(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; + + if (psutil_sysctl(mib, 3, &cpu_time, sizeof(cpu_time)) != 0) + goto error; + + 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; + } + } + + return py_retlist; + +error: + Py_DECREF(py_retlist); + return NULL; +} + + +PyObject * +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) + 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_cpu_freq(PyObject *self, PyObject *args) { + int freq; + int mib[2] = {CTL_HW, HW_CPUSPEED}; + + // On VirtualBox I get "sysctl hw.cpuspeed=2593" (never changing), + // which appears to be expressed in Mhz. + if (psutil_sysctl(mib, 2, &freq, sizeof(freq)) != 0) + return NULL; + + return Py_BuildValue("i", freq); +} diff --git a/psutil/arch/openbsd/disk.c b/psutil/arch/openbsd/disk.c new file mode 100644 index 0000000000..36d980d70f --- /dev/null +++ b/psutil/arch/openbsd/disk.c @@ -0,0 +1,58 @@ +/* + * 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 "../../arch/all/init.h" + + +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; + + 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( + "(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/init.h b/psutil/arch/openbsd/init.h new file mode 100644 index 0000000000..8d6879fc0f --- /dev/null +++ b/psutil/arch/openbsd/init.h @@ -0,0 +1,22 @@ +/* + * 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 + +int _psutil_pids(pid_t **pids_array, int *pids_count); + +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_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 new file mode 100644 index 0000000000..3058862354 --- /dev/null +++ b/psutil/arch/openbsd/mem.c @@ -0,0 +1,124 @@ +/* + * 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 "../../arch/all/init.h" + + +// References +// ---------- +// +// top: +// https://github.com/openbsd/src/blob/master/usr.bin/top/machine.c +// zabbix: +// https://github.com/zabbix/zabbix/blob/master/src/libs/zbxsysinfo/openbsd/memory.c + +PyObject * +psutil_virtual_mem(PyObject *self, PyObject *args) { + int64_t _total; + unsigned long long free, active, inactive, wired, cached, shared; + unsigned long long total, avail, used, buffers; + 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}; + struct uvmexp uvmexp; + struct bcachestats bcstats; + struct vmtotal vmdata; + long pagesize = psutil_getpagesize(); + PyObject *dict = PyDict_New(); + + if (dict == NULL) + return NULL; + + 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; + + // psutil uses HW_PHYSMEM64, while "top" uses uvmexp.npages * + // pagesize, which is slightly smaller. HW_PHYSMEM64 reflects the + // physical (hardware) RAM, so prefer this value. This matches + // `sysctl hw.physmem`. + total = (unsigned long long)_total; + + // same as 'top' and 'vmstat -s' + free = (unsigned long long)uvmexp.free * pagesize; + + // same as 'top' and 'vmstat -s' + active = (unsigned long long)uvmexp.active * pagesize; + + // same as 'vmstat -s' + inactive = (unsigned long long)uvmexp.inactive * pagesize; + + // same as 'vmstat -s' + wired = (unsigned long long)uvmexp.wired * pagesize; + + // Updated by kernel every 5 secs. We have: + // u_int32_t t_vmshr; /* shared virtual memory */ + // u_int32_t t_avmshr; /* active shared virtual memory */ + // u_int32_t t_rmshr; /* shared real memory */ + // We return `t_rmshr` (real shared). Reason: report physical + // resource pressure rather than just kernel bookkeeping. + shared = (unsigned long long)vmdata.t_rmshr * pagesize; + + // "top" derives cached memory from `bcstats.numbufpages`, which + // technically corresponds to 'buffers' memory: + // https://github.com/openbsd/src/blob/master/usr.bin/top/machine.c + // + // Intuitively, 'cached' memory should instead be + // `uvmexp.vnodepages`, described as 'vnode page cache', but it's + // always 0, and the struct contains an "XXX" comment, suggesting + // that `vnodepages` should probably not be used. This article + // suggests that 'buffers' became the primary caching mechanism in + // the system: + // https://undeadly.org/cgi?action=article;sid=20140908113732 + // + // So, treat 'cached' and 'buffers' as aliases. See: + // https://github.com/giampaolo/psutil/issues/2813 + buffers = (unsigned long long)bcstats.numbufpages * pagesize; + cached = buffers; + + // Matches zabbix on FreeBSD (but not on OpenBSD). + avail = inactive + cached + free; + + // 'top' calculates this as `total - free`. Zabbix does `active + wired`. + // We do `active + wired + cached` (cached memory **is** used memory), + // which matches Zabbix on FreeBSD. + 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; +} diff --git a/psutil/arch/openbsd/pids.c b/psutil/arch/openbsd/pids.c new file mode 100644 index 0000000000..be7c493fb8 --- /dev/null +++ b/psutil/arch/openbsd/pids.c @@ -0,0 +1,63 @@ +/* + * 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) { + psutil_runtime_error("kvm_openfiles() failed: %s", errbuf); + return -1; + } + + result = kvm_getprocs( + kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &cnt + ); + if (result == NULL) { + psutil_runtime_error("kvm_getproc2() failed"); + kvm_close(kd); + return -1; + } + + if (cnt == 0) { + psutil_runtime_error("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 new file mode 100644 index 0000000000..bbdce4f067 --- /dev/null +++ b/psutil/arch/openbsd/proc.c @@ -0,0 +1,195 @@ +/* + * 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 + +#include "../../arch/all/init.h" + + +// TODO: refactor this (it's clunky) +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; + PyObject *py_retlist = PyList_New(0); + + 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; + + if (psutil_sysctl_malloc(mib, 4, &argv_buf, &argv_len) == -1) + goto error; + + argv = (char **)argv_buf; + + for (p = argv; *p != NULL; p++) { + if (!pylist_append_obj(py_retlist, PyUnicode_DecodeFSDefault(*p))) + goto error; + } + + free(argv_buf); + return py_retlist; + +error: + if (argv_buf != NULL) + free(argv_buf); + Py_DECREF(py_retlist); + return NULL; +} + + +PyObject * +psutil_proc_threads(PyObject *self, PyObject *args) { + // OpenBSD reference: + // https://github.com/janmojzis/pstree/blob/master/proc_kvm.c + // Note: this requires root access, else it will fail trying + // to access /dev/kmem. + pid_t pid; + kvm_t *kd = NULL; + int nentries, i; + char errbuf[4096]; + struct kinfo_proc *kp; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + + kd = kvm_openfiles(0, 0, 0, O_RDONLY, errbuf); + 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) { + 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) { + if (strstr(errbuf, "Permission denied") != NULL) + psutil_oserror_ad("kvm_getprocs"); + else + psutil_runtime_error("kvm_getprocs() syscall failed"); + goto error; + } + + for (i = 0; i < nentries; i++) { + if (kp[i].p_tid < 0) + continue; + if (kp[i].p_pid == pid) { + 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; + } + } + } + + kvm_close(kd); + return py_retlist; + +error: + Py_DECREF(py_retlist); + if (kd != NULL) + kvm_close(kd); + return NULL; +} + + +PyObject * +psutil_proc_num_fds(PyObject *self, PyObject *args) { + pid_t pid; + int cnt; + + struct kinfo_file *freep; + struct kinfo_proc kipp; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + if (psutil_kinfo_proc(pid, &kipp) == -1) + return NULL; + + freep = kinfo_getfile(pid, &cnt); + + 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); +} + + +PyObject * +psutil_proc_cwd(PyObject *self, PyObject *args) { + // Reference: + // https://github.com/openbsd/src/blob/588f7f8c69786211f2d16865c552afb91b1c7cba/bin/ps/print.c#L191 + pid_t pid; + struct kinfo_proc kp; + char path[MAXPATHLEN]; + size_t pathlen = sizeof path; + + 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}; + if (sysctl(name, 3, path, &pathlen, NULL, 0) != 0) { + if (errno == ENOENT) { + psutil_debug("sysctl(KERN_PROC_CWD) -> ENOENT converted to ''"); + return PyUnicode_FromString(""); + } + else { + psutil_oserror(); + return NULL; + } + } + return PyUnicode_DecodeFSDefault(path); +} diff --git a/psutil/arch/openbsd/socks.c b/psutil/arch/openbsd/socks.c new file mode 100644 index 0000000000..d6221f4bac --- /dev/null +++ b/psutil/arch/openbsd/socks.c @@ -0,0 +1,179 @@ +/* + * 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 "../../arch/all/init.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_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) { + psutil_oserror_wsyscall("kvm_getfiles"); + goto error; + } + + for (int i = 0; i < cnt; i++) { + const struct kinfo_file *kif = ikf + i; + 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 + 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; + } + py_laddr = NULL; + py_raddr = NULL; + } + // UNIX socket + else if (kif->so_family == AF_UNIX) { + py_lpath = PyUnicode_DecodeFSDefault(kif->unp_path); + if (!py_lpath) + goto error; + + 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_lpath = NULL; + } + } + + kvm_close(kd); + return py_retlist; + +error: + Py_XDECREF(py_laddr); + Py_XDECREF(py_raddr); + Py_XDECREF(py_lpath); + Py_DECREF(py_retlist); + if (kd != NULL) + kvm_close(kd); + return NULL; +} diff --git a/psutil/arch/openbsd/specific.c b/psutil/arch/openbsd/specific.c deleted file mode 100644 index 33ebdeecba..0000000000 --- a/psutil/arch/openbsd/specific.c +++ /dev/null @@ -1,791 +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. - * - * Platform-specific module methods for OpenBSD. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#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" - -#define PSUTIL_KPT2DOUBLE(t) (t ## _sec + t ## _usec / 1000000.0) -// #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) { - PyErr_SetFromErrno(PyExc_OSError); - return -1; - } - // sysctl stores 0 in the size if we can't find the process information. - if (size == 0) { - NoSuchProcess(""); - return -1; - } - return 0; -} - - -struct kinfo_file * -kinfo_getfile(long 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] = (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) { - free(kf); - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - - *cnt = (int)(len / sizeof(struct kinfo_file)); - return kf; -} - - -// ============================================================================ -// 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 (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 == NULL) { - return errno; - } - - result = kvm_getprocs(kd, KERN_PROC_ALL, 0, sizeof(struct kinfo_proc), &cnt); - if (result == NULL) { - kvm_close(kd); - err(1, NULL); - return errno; - } - - *procCount = (size_t)cnt; - - size_t mlen = cnt * sizeof(struct kinfo_proc); - - if ((*procList = malloc(mlen)) == NULL) { - kvm_close(kd); - err(1, NULL); - return errno; - } - - memcpy(*procList, result, mlen); - assert(*procList != NULL); - kvm_close(kd); - - return 0; -} - - -char ** -_psutil_get_argv(long pid) { - static char **argv; - int argv_mib[] = {CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_ARGV}; - size_t argv_size = 128; - // 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; - } - if ((argv = realloc(argv, argv_size)) == NULL) - continue; - if (sysctl(argv_mib, 4, argv, &argv_size, NULL, 0) == 0) - return argv; - if (errno == ENOMEM) - continue; - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } -} - - -// returns the command line as a python list object -PyObject * -psutil_get_cmdline(long 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); - if (!py_arg) - goto error; - if (PyList_Append(py_retlist, py_arg)) - goto error; - Py_DECREF(py_arg); - } - return py_retlist; - -error: - Py_XDECREF(py_arg); - Py_DECREF(py_retlist); - return NULL; -} - - -PyObject * -psutil_proc_threads(PyObject *self, PyObject *args) { - // OpenBSD reference: - // 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; - kvm_t *kd = NULL; - int nentries, i; - char errbuf[4096]; - struct kinfo_proc *kp; - PyObject *py_retlist = PyList_New(0); - PyObject *py_tuple = NULL; - - if (py_retlist == NULL) - return NULL; - if (! PyArg_ParseTuple(args, "l", &pid)) - goto error; - - kd = kvm_openfiles(0, 0, 0, O_RDONLY, errbuf); - if (! kd) { - if (strstr(errbuf, "Permission denied") != NULL) - AccessDenied(""); - else - PyErr_Format(PyExc_RuntimeError, "kvm_openfiles() syscall failed"); - goto error; - } - - kp = kvm_getprocs( - kd, KERN_PROC_PID | KERN_PROC_SHOW_THREADS | KERN_PROC_KTHREAD, pid, - sizeof(*kp), &nentries); - if (! kp) { - if (strstr(errbuf, "Permission denied") != NULL) - AccessDenied(""); - else - PyErr_Format(PyExc_RuntimeError, "kvm_getprocs() syscall failed"); - goto error; - } - - for (i = 0; i < nentries; i++) { - if (kp[i].p_tid < 0) - continue; - if (kp[i].p_pid == pid) { - py_tuple = Py_BuildValue( - "Idd", - 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)) - goto error; - Py_DECREF(py_tuple); - } - } - - kvm_close(kd); - return py_retlist; - -error: - Py_XDECREF(py_tuple); - Py_DECREF(py_retlist); - if (kd != NULL) - kvm_close(kd); - return NULL; -} - - -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 = 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) { - long pid; - int cnt; - - struct kinfo_file *freep; - struct kinfo_proc kipp; - - 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()"); - return NULL; - } - free(freep); - - return Py_BuildValue("i", cnt); -} - - -PyObject * -psutil_proc_cwd(PyObject *self, PyObject *args) { - // Reference: - // https://github.com/openbsd/src/blob/ - // 588f7f8c69786211f2d16865c552afb91b1c7cba/bin/ps/print.c#L191 - long pid; - struct kinfo_proc kp; - char path[MAXPATHLEN]; - size_t pathlen = sizeof path; - - if (! PyArg_ParseTuple(args, "l", &pid)) - return NULL; - if (psutil_kinfo_proc(pid, &kp) == -1) - return NULL; - - int name[] = { CTL_KERN, KERN_PROC_CWD, pid }; - if (sysctl(name, 3, path, &pathlen, NULL, 0) != 0) { - PyErr_SetFromErrno(PyExc_OSError); - return NULL; - } - 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) { - long 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, "lOO", &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; - } - - errno = 0; - freep = kinfo_getfile(pid, &cnt); - if (freep == NULL) { - psutil_raise_for_pid(pid, "kinfo_getfile()"); - 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; -} - - -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/specific.h deleted file mode 100644 index 4f870268d6..0000000000 --- a/psutil/arch/openbsd/specific.h +++ /dev/null @@ -1,27 +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(long 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); - -// -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/psutil/arch/openbsd/users.c b/psutil/arch/openbsd/users.c new file mode 100644 index 0000000000..0b943b7161 --- /dev/null +++ b/psutil/arch/openbsd/users.c @@ -0,0 +1,74 @@ +/* + * 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); + PyObject *py_username = NULL; + PyObject *py_tty = NULL; + PyObject *py_hostname = 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; + 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); + } + + fclose(fp); + return py_retlist; + +error: + fclose(fp); + Py_XDECREF(py_username); + Py_XDECREF(py_tty); + Py_XDECREF(py_hostname); + Py_DECREF(py_retlist); + return NULL; +} diff --git a/psutil/arch/osx/cpu.c b/psutil/arch/osx/cpu.c new file mode 100644 index 0000000000..7bfa7153a9 --- /dev/null +++ b/psutil/arch/osx/cpu.c @@ -0,0 +1,336 @@ +/* + * 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 +#include +#if defined(__arm64__) || defined(__aarch64__) +#include +#include +#endif + +#include "../../arch/all/init.h" + +// added in macOS 12 +#ifndef kIOMainPortDefault +#define kIOMainPortDefault 0 +#endif + +PyObject * +psutil_cpu_count_logical(PyObject *self, PyObject *args) { + int num; + + if (psutil_sysctlbyname("hw.logicalcpu", &num, sizeof(num)) != 0) + Py_RETURN_NONE; + return Py_BuildValue("i", num); +} + + +PyObject * +psutil_cpu_count_cores(PyObject *self, PyObject *args) { + int num; + + if (psutil_sysctlbyname("hw.physicalcpu", &num, sizeof(num)) != 0) + Py_RETURN_NONE; + 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 mport = mach_host_self(); + + if (mport == MACH_PORT_NULL) { + psutil_runtime_error("mach_host_self() returned MACH_PORT_NULL"); + return NULL; + } + + 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) { + return psutil_runtime_error( + "host_statistics(HOST_CPU_LOAD_INFO) syscall failed: %s", + mach_error_string(error) + ); + } + + 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) { + kern_return_t ret; + mach_msg_type_number_t count = HOST_VM_INFO_COUNT; + mach_port_t mport = mach_host_self(); + struct vmmeter vmstat; + + if (mport == MACH_PORT_NULL) { + psutil_runtime_error("mach_host_self() returned MACH_PORT_NULL"); + return NULL; + } + + ret = host_statistics(mport, HOST_VM_INFO, (host_info_t)&vmstat, &count); + mach_port_deallocate(mach_task_self(), mport); + + if (ret != KERN_SUCCESS) { + psutil_runtime_error( + "host_statistics(HOST_VM_INFO) failed: %s", mach_error_string(ret) + ); + return NULL; + } + + return Py_BuildValue( + "IIIII", + 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, +#endif + vmstat.v_trap + ); +} + + +#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. +static int +psutil_find_pmgr_entry(io_registry_entry_t *out_entry) { + kern_return_t status; + io_iterator_t iter = IO_OBJECT_NULL; + io_registry_entry_t entry = IO_OBJECT_NULL; + CFDictionaryRef matching = IOServiceMatching("AppleARMIODevice"); + int found = 0; + + if (!out_entry || !matching) + return 0; + + status = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iter); + if (status != KERN_SUCCESS || iter == IO_OBJECT_NULL) + return 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; + } + IOObjectRelease(entry); + } + + IOObjectRelease(iter); + + if (found) { + *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 = IO_OBJECT_NULL; + int ok = psutil_find_pmgr_entry(&entry); + if (entry != IO_OBJECT_NULL) + IOObjectRelease(entry); + if (ok) + Py_RETURN_TRUE; + Py_RETURN_FALSE; +} + + +PyObject * +psutil_cpu_freq(PyObject *self, PyObject *args) { + io_registry_entry_t entry = IO_OBJECT_NULL; + CFTypeRef pCoreRef = NULL; + CFTypeRef eCoreRef = NULL; + size_t pCoreLength = 0; + uint32_t pMin = 0, eMin = 0, min = 0, max = 0, curr = 0; + + if (!psutil_find_pmgr_entry(&entry)) { + psutil_runtime_error("'pmgr' entry not found in AppleARMIODevice"); + return NULL; + } + + pCoreRef = IORegistryEntryCreateCFProperty( + entry, CFSTR("voltage-states5-sram"), kCFAllocatorDefault, 0 + ); + eCoreRef = IORegistryEntryCreateCFProperty( + entry, CFSTR("voltage-states1-sram"), kCFAllocatorDefault, 0 + ); + + if (!pCoreRef || !eCoreRef || CFGetTypeID(pCoreRef) != CFDataGetTypeID() + || CFGetTypeID(eCoreRef) != CFDataGetTypeID() + || CFDataGetLength(pCoreRef) < 8 || CFDataGetLength(eCoreRef) < 4) + { + psutil_runtime_error("invalid CPU frequency data"); + goto cleanup; + } + + 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; + curr = max; + +cleanup: + if (pCoreRef) + CFRelease(pCoreRef); + if (eCoreRef) + CFRelease(eCoreRef); + if (entry != IO_OBJECT_NULL) + IOObjectRelease(entry); + + return Py_BuildValue( + "KKK", + (unsigned long long)(curr / 1000 / 1000), + (unsigned long long)(min / 1000 / 1000), + (unsigned long long)(max / 1000 / 1000) + ); +} + +#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; + 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_oserror_wsyscall("sysctl(HW_CPU_FREQ)"); + + 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("hw.cpufrequency_max", &max, sizeof(max)) != 0) { + max = 0; + psutil_debug("sysctlbyname('hw.cpufrequency_max') failed (set to 0)"); + } + + return Py_BuildValue( + "KKK", + (unsigned long long)(curr / 1000 / 1000), + (unsigned long long)(min / 1000 / 1000), + (unsigned long long)(max / 1000 / 1000) + ); +} + +#endif // ARM64 + +PyObject * +psutil_per_cpu_times(PyObject *self, PyObject *args) { + 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; + kern_return_t error; + mach_port_t mport = mach_host_self(); + PyObject *py_retlist = PyList_New(0); + + if (!py_retlist) + return NULL; + + if (mport == MACH_PORT_NULL) { + psutil_runtime_error("mach_host_self() returned NULL"); + goto 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) { + psutil_runtime_error( + "host_processor_info failed: %s", mach_error_string(error) + ); + goto error; + } + + cpu_load_info = (processor_cpu_load_info_data_t *)info_array; + + for (natural_t i = 0; i < cpu_count; i++) { + 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; + } + } + + vm_deallocate( + mach_task_self(), + (vm_address_t)info_array, + info_count * sizeof(integer_t) + ); + return py_retlist; + +error: + Py_XDECREF(py_retlist); + 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 new file mode 100644 index 0000000000..fe1ff3b2dc --- /dev/null +++ b/psutil/arch/osx/disk.c @@ -0,0 +1,388 @@ +/* + * 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 "../../arch/all/init.h" + + +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_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) { + psutil_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) { + psutil_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) + str_append(opts, sizeof(opts), "ro"); + else + str_append(opts, sizeof(opts), "rw"); + if (flags & MNT_SYNCHRONOUS) + str_append(opts, sizeof(opts), ",sync"); + if (flags & MNT_NOEXEC) + str_append(opts, sizeof(opts), ",noexec"); + if (flags & MNT_NOSUID) + str_append(opts, sizeof(opts), ",nosuid"); + if (flags & MNT_UNION) + str_append(opts, sizeof(opts), ",union"); + if (flags & MNT_ASYNC) + str_append(opts, sizeof(opts), ",async"); + if (flags & MNT_EXPORTED) + str_append(opts, sizeof(opts), ",exported"); + if (flags & MNT_LOCAL) + str_append(opts, sizeof(opts), ",local"); + if (flags & MNT_QUOTA) + str_append(opts, sizeof(opts), ",quota"); + if (flags & MNT_ROOTFS) + str_append(opts, sizeof(opts), ",rootfs"); + if (flags & MNT_DOVOLFS) + str_append(opts, sizeof(opts), ",dovolfs"); + if (flags & MNT_DONTBROWSE) + str_append(opts, sizeof(opts), ",dontbrowse"); + if (flags & MNT_IGNORE_OWNERSHIP) + str_append(opts, sizeof(opts), ",ignore-ownership"); + if (flags & MNT_AUTOMOUNTED) + str_append(opts, sizeof(opts), ",automounted"); + if (flags & MNT_JOURNALED) + str_append(opts, sizeof(opts), ",journaled"); + if (flags & MNT_NOUSERXATTR) + str_append(opts, sizeof(opts), ",nouserxattr"); + if (flags & MNT_DEFWRITE) + str_append(opts, sizeof(opts), ",defwrite"); + if (flags & MNT_UPDATE) + str_append(opts, sizeof(opts), ",update"); + if (flags & MNT_RELOAD) + str_append(opts, sizeof(opts), ",reload"); + if (flags & MNT_FORCE) + str_append(opts, sizeof(opts), ",force"); + if (flags & MNT_CMDFLAGS) + str_append(opts, sizeof(opts), ",cmdflags"); + // requires macOS >= 10.5 +#ifdef MNT_QUARANTINE + if (flags & MNT_QUARANTINE) + str_append(opts, sizeof(opts), ",quarantine"); +#endif +#ifdef MNT_MULTILABEL + if (flags & MNT_MULTILABEL) + str_append(opts, sizeof(opts), ",multilabel"); +#endif +#ifdef MNT_NOATIME + if (flags & MNT_NOATIME) + str_append(opts, sizeof(opts), ",noatime"); +#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; + 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); + } + + free(fs); + return py_retlist; + +error: + Py_XDECREF(py_dev); + Py_XDECREF(py_mountp); + 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 (!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; + } + +#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; + attrbuf.size = sizeof(attrbuf); + + 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; +} + + +PyObject * +psutil_disk_io_counters(PyObject *self, PyObject *args) { + 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(); + + if (py_retdict == NULL) + return NULL; + + if (IOServiceGetMatchingServices( + kIOMasterPortDefault, IOServiceMatching(kIOMediaClass), &disk_list + ) + != kIOReturnSuccess) + { + psutil_runtime_error("unable to get the list of disks"); + goto error; + } + + while ((disk = IOIteratorNext(disk_list)) != 0) { + py_disk_info = NULL; + parent_dict = NULL; + props_dict = NULL; + stats_dict = NULL; + parent = IO_OBJECT_NULL; + + if (IORegistryEntryGetParentEntry(disk, kIOServicePlane, &parent) + != kIOReturnSuccess) + { + psutil_runtime_error("unable to get the disk's parent"); + goto error; + } + + if (!IOObjectConformsTo(parent, "IOBlockStorageDriver")) { + IOObjectRelease(parent); + IOObjectRelease(disk); + continue; + } + + if (IORegistryEntryCreateCFProperties( + disk, + (CFMutableDictionaryRef *)&parent_dict, + kCFAllocatorDefault, + kNilOptions + ) + != kIOReturnSuccess) + { + psutil_runtime_error("unable to get the parent's properties"); + goto error; + } + + if (IORegistryEntryCreateCFProperties( + parent, + (CFMutableDictionaryRef *)&props_dict, + kCFAllocatorDefault, + kNilOptions + ) + != kIOReturnSuccess) + { + 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) { + psutil_runtime_error("unable to get disk name"); + goto error; + } + + const int kMaxDiskNameSize = 64; + char disk_name[kMaxDiskNameSize]; + if (!CFStringGetCString( + disk_name_ref, + disk_name, + kMaxDiskNameSize, + CFStringGetSystemEncoding() + )) + { + psutil_runtime_error("unable to convert disk name to C string"); + goto error; + } + + stats_dict = (CFDictionaryRef)CFDictionaryGetValue( + props_dict, CFSTR(kIOBlockStorageDriverStatisticsKey) + ); + if (stats_dict == NULL) { + psutil_runtime_error("unable to get disk stats"); + 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) + goto error; + + if (PyDict_SetItemString(py_retdict, disk_name, py_disk_info)) { + Py_CLEAR(py_disk_info); + goto error; + } + + Py_CLEAR(py_disk_info); + + if (parent_dict) + CFRelease(parent_dict); + if (props_dict) + CFRelease(props_dict); + IOObjectRelease(parent); + IOObjectRelease(disk); + } + + IOObjectRelease(disk_list); + return py_retdict; + +error: + Py_XDECREF(py_disk_info); + Py_DECREF(py_retdict); + 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/heap.c b/psutil/arch/osx/heap.c new file mode 100644 index 0000000000..0ab2a8c9ec --- /dev/null +++ b/psutil/arch/osx/heap.c @@ -0,0 +1,110 @@ +/* + * 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 "../../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; + +// 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); + + Py_RETURN_NONE; +} diff --git a/psutil/arch/osx/init.c b/psutil/arch/osx/init.c new file mode 100644 index 0000000000..1d9621160b --- /dev/null +++ b/psutil/arch/osx/init.c @@ -0,0 +1,33 @@ +/* + * 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" + + +uint64_t PSUTIL_HW_TBFREQUENCY; + +// Called on module import. +int +psutil_setup_osx(void) { + size_t size = sizeof(PSUTIL_HW_TBFREQUENCY); + + // 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 new file mode 100644 index 0000000000..ad9b070534 --- /dev/null +++ b/psutil/arch/osx/init.h @@ -0,0 +1,56 @@ +/* + * 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 + +extern uint64_t PSUTIL_HW_TBFREQUENCY; + +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); +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_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); +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); +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_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 new file mode 100644 index 0000000000..e7edbdaf03 --- /dev/null +++ b/psutil/arch/osx/mem.c @@ -0,0 +1,131 @@ +/* + * 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 +// See: +// https://github.com/apple-open-source/macos/blob/master/system_cmds/vm_stat/vm_stat.c + +#include +#include +#include +#include + +#include "../../arch/all/init.h" + + +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; + + mport = mach_host_self(); + if (mport == MACH_PORT_NULL) { + psutil_runtime_error("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); + if (ret != KERN_SUCCESS) { + psutil_runtime_error( + "host_statistics64(HOST_VM_INFO64) syscall failed: %s", + mach_error_string(ret) + ); + return -1; + } + return 0; +} + + +// 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; + 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(); + PyObject *dict = PyDict_New(); + + if (dict == NULL) + return NULL; + + // 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) + 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; + + 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; + +error: + Py_DECREF(dict); + return NULL; +} + + +PyObject * +psutil_swap_mem(PyObject *self, PyObject *args) { + struct xsw_usage totals; + vm_statistics64_data_t vmstat; + long pagesize = psutil_getpagesize(); + int mib[2] = {CTL_VM, VM_SWAPUSAGE}; + PyObject *dict = PyDict_New(); + + if (dict == NULL) + return NULL; + + if (psutil_sysctl(mib, 2, &totals, sizeof(totals)) != 0) + goto error; + if (psutil_sys_vminfo(&vmstat) != 0) + goto error; + + // 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; +} diff --git a/psutil/arch/osx/net.c b/psutil/arch/osx/net.c new file mode 100644 index 0000000000..12847320fc --- /dev/null +++ b/psutil/arch/osx/net.c @@ -0,0 +1,117 @@ +/* + * 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 "../../arch/all/init.h" + + +PyObject * +psutil_net_io_counters(PyObject *self, PyObject *args) { + char *buf = NULL, *lim, *next; + struct if_msghdr *ifm; + int mib[6]; + size_t len = 0; + PyObject *py_ifc_info = NULL; + PyObject *py_retdict = PyDict_New(); + + 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; + + if (psutil_sysctl_malloc(mib, 6, &buf, &len) != 0) + goto error; + + lim = buf + len; + + for (next = buf; next < lim;) { + if ((size_t)(lim - next) < sizeof(struct if_msghdr)) { + psutil_debug("struct if_msghdr 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 (skip entry)"); + break; + } + + 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 (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 (skip entry)"); + continue; + } + + char ifc_name[IFNAMSIZ]; + size_t namelen = sdl->sdl_nlen; + if (namelen >= IFNAMSIZ) + namelen = IFNAMSIZ - 1; + + memcpy(ifc_name, sdl->sdl_data, namelen); + ifc_name[namelen] = '\0'; + + py_ifc_info = Py_BuildValue( + "(KKKKKKKi)", + (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)) { + Py_CLEAR(py_ifc_info); + goto error; + } + + Py_CLEAR(py_ifc_info); + } + } + + 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/pids.c b/psutil/arch/osx/pids.c new file mode 100644 index 0000000000..debd8af4d6 --- /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) { + psutil_runtime_error("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 new file mode 100644 index 0000000000..9620d6c429 --- /dev/null +++ b/psutil/arch/osx/proc.c @@ -0,0 +1,1082 @@ +/* + * 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. + */ + +// Process related functions. Original code was moved in here from +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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; + int status; + struct kinfo_proc kp; + PyObject *py_name = NULL; + PyObject *dict = PyDict_New(); + + if (!dict) + return NULL; + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + if (psutil_get_kinfo_proc(pid, &kp) == -1) + goto error; + + py_name = PyUnicode_DecodeFSDefault(kp.kp_proc.p_comm); + 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_INCREF(Py_None); + py_name = Py_None; + } + + status = convert_status(&kp.kp_proc, &kp.kp_eproc); + + // 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 dict; + +error: + Py_XDECREF(py_name); + Py_DECREF(dict); + return NULL; +} + + +// 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; + struct proc_taskinfo pti; + unsigned long maj_faults; + unsigned long min_faults; + PyObject *dict = PyDict_New(); + + 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) + goto error; + + // 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; + min_faults = (unsigned long)pti.pti_faults - maj_faults; + + // 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; +} + + +PyObject * +psutil_proc_name(PyObject *self, PyObject *args) { + pid_t pid; + struct kinfo_proc kp; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + if (psutil_get_kinfo_proc(pid, &kp) == -1) + return NULL; + + return PyUnicode_DecodeFSDefault(kp.kp_proc.p_comm); +} + + +PyObject * +psutil_proc_cwd(PyObject *self, PyObject *args) { + pid_t pid; + struct proc_vnodepathinfo pathinfo; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + if (psutil_proc_pidinfo( + pid, PROC_PIDVNODEPATHINFO, 0, &pathinfo, sizeof(pathinfo) + ) + != 0) + { + return NULL; + } + + return PyUnicode_DecodeFSDefault(pathinfo.pvi_cdir.vip_path); +} + + +PyObject * +psutil_proc_exe(PyObject *self, PyObject *args) { + pid_t pid; + char buf[PATH_MAX]; + int ret; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + errno = 0; + ret = proc_pidpath(pid, &buf, sizeof(buf)); + if (ret == 0) { + if (pid == 0) { + psutil_oserror_ad("automatically set for PID 0"); + 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 PyUnicode_FromString(""); + } + else { + psutil_raise_for_pid(pid, "proc_pidpath()"); + return NULL; + } + } + return PyUnicode_DecodeFSDefault(buf); +} + + +// 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; + mach_vm_address_t size; + + switch (type) { + case CPU_TYPE_ARM: + base = SHARED_REGION_BASE_ARM; + size = SHARED_REGION_SIZE_ARM; + break; + case CPU_TYPE_I386: + base = SHARED_REGION_BASE_I386; + size = SHARED_REGION_SIZE_I386; + break; + case CPU_TYPE_X86_64: + base = SHARED_REGION_BASE_X86_64; + size = SHARED_REGION_SIZE_X86_64; + break; + default: + return false; + } + + return base <= addr && addr < (base + size); +} + + +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; +} + + +// 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; + cpu_type_t cpu_type; + size_t private_pages = 0; + mach_vm_size_t size = 0; + mach_msg_type_number_t info_count; + kern_return_t kr; + long pagesize = psutil_getpagesize(); + 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; + + if (psutil_task_for_pid(pid, &task) != 0) + return NULL; + + if (psutil_sysctlbyname("sysctl.proc_cputype", &cpu_type, sizeof(cpu_type)) + != 0) + { + mach_port_deallocate(mach_task_self(), task); + 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) { + prev_addr = addr; + info_count = VM_REGION_TOP_INFO_COUNT; // reset before each call + object_name = MACH_PORT_NULL; + + kr = mach_vm_region( + task, + &addr, + &size, + VM_REGION_TOP_INFO, + (vm_region_info_t)&info, + &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; + } + else if (kr != KERN_SUCCESS) { + psutil_runtime_error( + "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; + } + + switch (info.share_mode) { +#ifdef SM_LARGE_PAGE + case SM_LARGE_PAGE: + // NB: Large pages are not shareable and always resident. +#endif + case SM_PRIVATE: + private_pages += info.private_pages_resident; + private_pages += info.shared_pages_resident; + break; + case SM_COW: + private_pages += info.private_pages_resident; + if (info.ref_count == 1) { + // Treat copy-on-write pages as private if they only + // have one reference. + private_pages += info.shared_pages_resident; + } + break; + case SM_SHARED: + default: + break; + } + } + + mach_port_deallocate(mach_task_self(), task); + return Py_BuildValue("K", private_pages * pagesize); +} + + +PyObject * +psutil_proc_threads(PyObject *self, PyObject *args) { + pid_t pid; + kern_return_t kr; + mach_port_t task = MACH_PORT_NULL; + struct task_basic_info tasks_info; + thread_act_port_array_t thread_list = NULL; + thread_info_data_t thinfo_basic; + thread_basic_info_t basic_info_th; + mach_msg_type_number_t thread_count, thread_info_count, j; + + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + goto error; + + if (psutil_task_for_pid(pid, &task) != 0) + goto error; + + // 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) { + psutil_oserror_ad("task_info(TASK_BASIC_INFO)"); + } + else { + // otherwise throw a runtime error with appropriate error code + psutil_runtime_error("task_info(TASK_BASIC_INFO) syscall failed"); + } + goto error; + } + + kr = task_threads(task, &thread_list, &thread_count); + if (kr != KERN_SUCCESS) { + psutil_runtime_error("task_threads() syscall failed"); + goto error; + } + + 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 + ); + if (kr != KERN_SUCCESS) { + psutil_runtime_error( + "thread_info(THREAD_BASIC_INFO) syscall failed" + ); + goto error; + } + + basic_info_th = (thread_basic_info_t)thinfo_basic; + 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; + } + } + + // 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; + +error: + Py_XDECREF(py_retlist); + + if (thread_list != NULL) { + vm_deallocate( + mach_task_self(), + (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; +} + + +// 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; + 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_path = NULL; + + if (py_retlist == NULL) + return NULL; + + 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; + + for (i = 0; i < num_fds; i++) { + fdp_pointer = &fds_pointer[i]; + + 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) + ); + + // --- errors checking + if ((nb <= 0) || nb < sizeof(vi)) { + if ((errno == ENOENT) || (errno == EBADF)) { + // no such file or directory or bad file descriptor; + // let's assume the file has been closed or removed + continue; + } + else { + psutil_raise_for_pid( + pid, "proc_pidinfo(PROC_PIDFDVNODEPATHINFO)" + ); + goto error; + } + } + // --- /errors checking + + // --- construct python list + py_path = PyUnicode_DecodeFSDefault(vi.pvip.vip_path); + if (!py_path) + goto error; + if (!pylist_append_fmt( + py_retlist, "(Oi)", py_path, (int)fdp_pointer->proc_fd + )) + { + goto error; + } + Py_CLEAR(py_path); + // --- /construct python list + } + } + + free(fds_pointer); + return py_retlist; + +error: + Py_XDECREF(py_path); + Py_DECREF(py_retlist); + if (fds_pointer != NULL) + free(fds_pointer); + return NULL; // exception has already been set earlier +} + + +// 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; + int num_fds; + int i; + unsigned long nb; + 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_laddr = NULL; + PyObject *py_raddr = NULL; + PyObject *py_af_filter = NULL; + PyObject *py_type_filter = NULL; + + if (py_retlist == NULL) + return NULL; + + if (!PyArg_ParseTuple( + args, _Py_PARSE_PID "OO", &pid, &py_af_filter, &py_type_filter + )) + { + 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; + } + + fds_pointer = psutil_proc_list_fds(pid, &num_fds); + if (fds_pointer == NULL) + goto error; + + for (i = 0; i < num_fds; i++) { + py_laddr = NULL; + py_raddr = NULL; + 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) + ); + + // --- 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)" + ); + 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 { + psutil_raise_for_pid( + pid, "proc_pidinfo(PROC_PIDFDSOCKETINFO)" + ); + goto error; + } + } + // --- /errors checking + + // + int fd, family, type, lport, rport, state; + char lip[INET6_ADDRSTRLEN], rip[INET6_ADDRSTRLEN]; + int inseq; + PyObject *py_family; + PyObject *py_type; + + fd = (int)fdp_pointer->proc_fd; + family = si.psi.soi_family; + type = si.psi.soi_type; + + // apply filters + 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) { + 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_oserror_wsyscall("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_oserror_wsyscall("inet_ntop()"); + goto error; + } + } + else { + ntopret = inet_ntop( + AF_INET6, + &si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_laddr.ina_6, + lip, + sizeof(lip) + ); + if (!ntopret) { + psutil_oserror_wsyscall("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_oserror_wsyscall("inet_ntop()"); + goto error; + } + } + + lport = ntohs(si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_lport); + rport = ntohs(si.psi.soi_proto.pri_tcp.tcpsi_ini.insi_fport); + if (type == SOCK_STREAM) + state = (int)si.psi.soi_proto.pri_tcp.tcpsi_state; + else + state = PSUTIL_CONN_NONE; + + 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; + + if (!pylist_append_fmt( + py_retlist, + "(iiiNNi)", + fd, + family, + type, + py_laddr, + py_raddr, + state + )) + { + goto error; + } + py_laddr = NULL; + py_raddr = NULL; + } + else if (family == AF_UNIX) { + py_laddr = PyUnicode_DecodeFSDefault( + 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 + ); + if (!py_raddr) + goto error; + + if (!pylist_append_fmt( + py_retlist, + "(iiiOOi)", + fd, + family, + type, + py_laddr, + py_raddr, + PSUTIL_CONN_NONE + )) + { + goto error; + } + Py_CLEAR(py_laddr); + Py_CLEAR(py_raddr); + } + } + } + + free(fds_pointer); + return py_retlist; + +error: + Py_XDECREF(py_laddr); + Py_XDECREF(py_raddr); + Py_DECREF(py_retlist); + if (fds_pointer != NULL) + free(fds_pointer); + return NULL; +} + + +/* + * Return number of file descriptors opened by process. + * Raises NSP in case of zombie process. + */ +PyObject * +psutil_proc_num_fds(PyObject *self, PyObject *args) { + pid_t pid; + int num_fds; + struct proc_fdinfo *fds_pointer; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + fds_pointer = psutil_proc_list_fds(pid, &num_fds); + if (fds_pointer == NULL) + return NULL; + + free(fds_pointer); + return Py_BuildValue("i", num_fds); +} + + +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); + + 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 == 0) + 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') { + if (!pylist_append_obj( + py_retlist, PyUnicode_DecodeFSDefault(curr_arg) + )) + goto error; + // iterate to next arg and decrement # of args + curr_arg = arg_ptr; + nargs--; + } + } + + free(procargs); + return py_retlist; + +error: + 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; + size_t env_len; + PyObject *py_ret = NULL; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid)) + return NULL; + + // PID 0 (kernel_task) has no cmdline. + if (pid == 0) + goto empty; + + // Allocate buffer for process args. + argmax = psutil_sysctl_argmax(); + if (argmax == 0) + goto error; + + procargs = (char *)malloc(argmax); + if (procargs == NULL) { + PyErr_NoMemory(); + goto error; + } + + if (psutil_sysctl_procargs(pid, procargs, &argmax) != 0) + goto error; + + 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) + goto empty; + + // Skip null bytes until first argument. + while (arg_ptr < arg_end && *arg_ptr == '\0') + arg_ptr++; + + // Skip arguments. + while (arg_ptr < arg_end && nargs > 0) { + if (*arg_ptr++ == '\0') + nargs--; + } + + if (arg_ptr >= arg_end) + goto empty; + + env_start = 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') { + // 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; + + 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; + } + + 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; + } + + free(procargs); + free(procenv); + return py_ret; + +empty: + psutil_debug("set environ to empty"); + if (procargs != NULL) + free(procargs); + return PyUnicode_FromString(""); + +error: + Py_XDECREF(py_ret); + if (procargs != NULL) + free(procargs); + if (procenv != NULL) + free(procenv); + return NULL; +} diff --git a/psutil/arch/osx/proc_utils.c b/psutil/arch/osx/proc_utils.c new file mode 100644 index 0000000000..cd4bf1157c --- /dev/null +++ b/psutil/arch/osx/proc_utils.c @@ -0,0 +1,254 @@ +/* + * 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 "../../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; + } + 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; + } + 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; +} diff --git a/psutil/arch/osx/process_info.c b/psutil/arch/osx/process_info.c deleted file mode 100644 index 40c79a2cdc..0000000000 --- a/psutil/arch/osx/process_info.c +++ /dev/null @@ -1,361 +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 // for INT_MAX -#include -#include -#include -#include -#include -#include - -#include "process_info.h" -#include "../../_psutil_common.h" -#include "../../_psutil_posix.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 mib3[3] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL }; - size_t size, size2; - void *ptr; - int err; - int lim = 8; // some limit - - assert( procList != NULL); - assert(*procList == NULL); - assert(procCount != 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 *)mib3, 3, NULL, &size, NULL, 0) == -1) - return errno; - 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) - return ENOMEM; - - if (sysctl((int *)mib3, 3, ptr, &size, NULL, 0) == -1) { - err = errno; - free(ptr); - if (err != ENOMEM) - return err; - } - else { - *procList = (kinfo_proc *)ptr; - *procCount = size / sizeof(kinfo_proc); - return 0; - } - } - return ENOMEM; -} - - -// Read the maximum argument size for processes -int -psutil_get_argmax() { - int argmax; - int mib[] = { CTL_KERN, KERN_ARGMAX }; - size_t size = sizeof(argmax); - - if (sysctl(mib, 2, &argmax, &size, NULL, 0) == 0) - return argmax; - return 0; -} - - -// return process args as a python list -PyObject * -psutil_get_cmdline(long pid) { - int mib[3]; - int nargs; - size_t len; - char *procargs = NULL; - char *arg_ptr; - char *arg_end; - char *curr_arg; - size_t argmax; - - PyObject *py_arg = NULL; - PyObject *py_retlist = NULL; - - // special case for PID 0 (kernel_task) where cmdline cannot be fetched - if (pid == 0) - return Py_BuildValue("[]"); - - // read argmax and allocate memory for argument space. - argmax = psutil_get_argmax(); - if (! argmax) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - procargs = (char *)malloc(argmax); - if (NULL == procargs) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - // read argument space - mib[0] = CTL_KERN; - mib[1] = KERN_PROCARGS2; - 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. - if ((errno == EINVAL) && (psutil_pid_exists(pid))) - NoSuchProcess(""); - else - PyErr_SetFromErrno(PyExc_OSError); - 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_BuildValue("[]"); - } - - // 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; - 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); - 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 -PyObject * -psutil_get_environ(long pid) { - int mib[3]; - int nargs; - char *procargs = NULL; - char *procenv = NULL; - char *arg_ptr; - char *arg_end; - char *env_start; - size_t argmax; - PyObject *py_ret = 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_get_argmax(); - if (! argmax) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - procargs = (char *)malloc(argmax); - if (NULL == procargs) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - // read argument space - mib[0] = CTL_KERN; - mib[1] = KERN_PROCARGS2; - 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. - if ((errno == EINVAL) && (psutil_pid_exists(pid))) - NoSuchProcess(""); - else - PyErr_SetFromErrno(PyExc_OSError); - 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) - 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(long 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; - - // 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_SetFromErrno(PyExc_OSError); - return -1; - } - - // sysctl succeeds but len is zero, happens when process has gone away - if (len == 0) { - NoSuchProcess(""); - return -1; - } - return 0; -} - - -/* - * A wrapper around proc_pidinfo(). - * 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) { - errno = 0; - int ret = proc_pidinfo((int)pid, flavor, arg, pti, size); - if ((ret <= 0) || ((unsigned long)ret < sizeof(pti))) { - psutil_raise_for_pid(pid, "proc_pidinfo()"); - 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 bd2eef868a..0000000000 --- a/psutil/arch/osx/process_info.h +++ /dev/null @@ -1,17 +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_argmax(void); -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( - long pid, int flavor, uint64_t arg, void *pti, int size); -PyObject* psutil_get_cmdline(long pid); -PyObject* psutil_get_environ(long pid); diff --git a/psutil/arch/osx/sensors.c b/psutil/arch/osx/sensors.c new file mode 100644 index 0000000000..3ab97016f2 --- /dev/null +++ b/psutil/arch/osx/sensors.c @@ -0,0 +1,117 @@ +/* + * 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 + +#include "../../arch/all/init.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) { + psutil_runtime_error("IOPSCopyPowerSourcesInfo() syscall failed"); + goto error; + } + + power_sources_list = IOPSCopyPowerSourcesList(power_info); + if (!power_sources_list) { + psutil_runtime_error("IOPSCopyPowerSourcesList() syscall failed"); + goto error; + } + + if (CFArrayGetCount(power_sources_list) == 0) { + PyErr_SetString(PyExc_NotImplementedError, "no battery"); + goto error; + } + + power_sources_information = IOPSGetPowerSourceDescription( + power_info, CFArrayGetValueAtIndex(power_sources_list, 0) + ); + if (!power_sources_information) { + psutil_runtime_error("Failed to get power source description"); + goto error; + } + + capacity_ref = (CFNumberRef)CFDictionaryGetValue( + power_sources_information, CFSTR(kIOPSCurrentCapacityKey) + ); + if (!capacity_ref + || !CFNumberGetValue(capacity_ref, kCFNumberSInt32Type, &capacity)) + { + psutil_runtime_error( + "No battery capacity information in power sources info" + ); + goto error; + } + + ps_state_ref = (CFStringRef)CFDictionaryGetValue( + power_sources_information, CFSTR(kIOPSPowerSourceStateKey) + ); + if (!ps_state_ref) { + psutil_runtime_error("power source state info missing"); + goto error; + } + is_power_plugged = CFStringCompare( + 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 + )) + { + // 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( + "dii", (double)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/sys.c b/psutil/arch/osx/sys.c new file mode 100644 index 0000000000..3fafcac2b5 --- /dev/null +++ b/psutil/arch/osx/sys.c @@ -0,0 +1,28 @@ +/* + * 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 "../../arch/all/init.h" + + +PyObject * +psutil_boot_time(PyObject *self, PyObject *args) { + // fetch sysctl "kern.boottime" + int mib[2] = {CTL_KERN, KERN_BOOTTIME}; + struct timeval result; + time_t boot_time = 0; + + 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.c b/psutil/arch/posix/init.c new file mode 100644 index 0000000000..fa2a9ec994 --- /dev/null +++ b/psutil/arch/posix/init.c @@ -0,0 +1,224 @@ +/* + * 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 "init.h" + +PyObject *ZombieProcessError = NULL; + +// "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 + // 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()); +} + + +// 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 +#if defined(PSUTIL_OSX) || defined(PSUTIL_BSD) + {"proc_is_zombie", psutil_proc_is_zombie, METH_VARARGS}, +#endif + {NULL, NULL, 0, NULL} +}; + + +// 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++) { + 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; + } + } + + // 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 POSIX constants to main OS 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 new file mode 100644 index 0000000000..ec4391a664 --- /dev/null +++ b/psutil/arch/posix/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. + */ + +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 + 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 +#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 +// clang-format on + +// --- internal utils + +int psutil_pid_exists(pid_t pid); +long psutil_getpagesize(void); +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); +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_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/net.c b/psutil/arch/posix/net.c new file mode 100644 index 0000000000..49df43535e --- /dev/null +++ b/psutil/arch/posix/net.c @@ -0,0 +1,670 @@ +/* + * 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_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_RETURN_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. + // psutil_runtime_error(gai_strerror(err)); + // return NULL; + Py_RETURN_NONE; + } + else { + return PyUnicode_FromString(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_RETURN_NONE; + } + + // AF_PACKET or AF_LINK + if (len > 0) { + ptr = buf; + for (n = 0; n < len; ++n) { + str_format(ptr, sizeof(ptr), "%02x:", data[n] & 0xff); + ptr += 3; + } + *--ptr = '\0'; + return PyUnicode_FromString(buf); + } + else { + Py_RETURN_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_address = NULL; + PyObject *py_netmask = NULL; + PyObject *py_broadcast = NULL; + PyObject *py_ptp = NULL; + + if (py_retlist == NULL) + return NULL; + if (getifaddrs(&ifaddr) == -1) { + psutil_oserror(); + goto error; + } + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + 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 (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; + + 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; + if (!pylist_append_fmt( + py_retlist, + "(siOOOO)", + ifa->ifa_name, + family, + py_address, + py_netmask, + py_broadcast, + py_ptp + )) + { + goto error; + } + Py_CLEAR(py_address); + 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); + return py_retlist; + +error: + if (ifaddr != NULL) + freeifaddrs(ifaddr); + Py_DECREF(py_retlist); + 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; + struct ifreq ifr; + + if (!PyArg_ParseTuple(args, "s", &nic_name)) + return NULL; + + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock == -1) + goto error; + + str_copy(ifr.ifr_name, sizeof(ifr.ifr_name), nic_name); + ret = ioctl(sock, SIOCGIFMTU, &ifr); + if (ret == -1) + goto error; + close(sock); + + return Py_BuildValue("i", ifr.ifr_mtu); + +error: + if (sock != -1) + close(sock); + return psutil_oserror(); +} + +static int +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. +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_oserror_wsyscall("socket(SOCK_DGRAM)"); + goto error; + } + + str_copy(ifr.ifr_name, sizeof(ifr.ifr_name), nic_name); + ret = ioctl(sock, SIOCGIFFLAGS, &ifr); + if (ret == -1) { + psutil_oserror_wsyscall("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; + + str_copy(ifr.ifr_name, sizeof(ifr.ifr_name), nic_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 psutil_oserror(); +} + + +// 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: + // 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 psutil_oserror(); + str_copy(ifr.ifr_name, sizeof(ifr.ifr_name), nic_name); + + // speed / duplex + memset(&ifmed, 0, sizeof(struct ifmediareq)); + str_copy(ifmed.ifm_name, sizeof(ifmed.ifm_name), nic_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 // PSUTIL_HAS_NET_IF_DUPLEX_SPEED diff --git a/psutil/arch/posix/pids.c b/psutil/arch/posix/pids.c new file mode 100644 index 0000000000..368c53c91a --- /dev/null +++ b/psutil/arch/posix/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 "../../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 +// -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) + return has_pid_zero(); + + 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 new file mode 100644 index 0000000000..f8cd7e6a92 --- /dev/null +++ b/psutil/arch/posix/proc.c @@ -0,0 +1,91 @@ +/* + * 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" + + +#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 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; +} + + +// Get PID priority. +PyObject * +psutil_proc_priority_get(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 psutil_oserror(); + return Py_BuildValue("i", priority); +} + + +// Set PID priority. +PyObject * +psutil_proc_priority_set(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 psutil_oserror(); + Py_RETURN_NONE; +} diff --git a/psutil/arch/posix/sysctl.c b/psutil/arch/posix/sysctl.c new file mode 100644 index 0000000000..a868c92fe7 --- /dev/null +++ b/psutil/arch/posix/sysctl.c @@ -0,0 +1,243 @@ +/* + * 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" + +#ifdef PSUTIL_HAS_SYSCTL +#include +#include +#include + + +static const int MAX_RETRIES = 10; + + +// A thin wrapper on top of sysctl(). +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_oserror_wsyscall("sysctl()"); + return -1; + } + + if (len > buflen) { + psutil_runtime_error("sysctl() size mismatch"); + return -1; + } + + 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; + + 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) { + psutil_oserror_wsyscall("sysctl() malloc 1/3"); + 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); + 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_oserror_wsyscall("sysctl() malloc 2/3"); + return -1; + } + + psutil_debug("psutil_sysctl_malloc() retry"); + continue; + } + + // Other errors: clean up and give up + free(buffer); + psutil_oserror_wsyscall("sysctl() malloc 3/3"); + return -1; + } + + psutil_runtime_error("sysctl() buffer allocation retry limit exceeded"); + return -1; +} + + +// 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) { + return 0; + } + + if (argmax <= 0) { + psutil_runtime_error("sysctl(KERN_ARGMAX) return <= 0"); + return 0; + } + + return (size_t)argmax; +} + + +#ifdef PSUTIL_HAS_SYSCTLBYNAME +// A thin wrapper on top of sysctlbyname(). +int +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) { + str_format(errbuf, sizeof(errbuf), "sysctlbyname('%s')", name); + psutil_oserror_wsyscall(errbuf); + return -1; + } + + if (len > buflen) { + str_format( + errbuf, + sizeof(errbuf), + "sysctlbyname('%s') size mismatch: returned %zu, expected %zu", + name, + len, + buflen + ); + psutil_runtime_error(errbuf); + return -1; + } + + 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]; + + 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) { + str_format( + errbuf, sizeof(errbuf), "sysctlbyname('%s') malloc 1/3", name + ); + psutil_oserror_wsyscall(errbuf); + 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); + 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) { + str_format( + errbuf, + sizeof(errbuf), + "sysctlbyname('%s') malloc 2/3", + name + ); + psutil_oserror_wsyscall(errbuf); + return -1; + } + + psutil_debug("psutil_sysctlbyname_malloc() retry"); + continue; + } + + // Other errors: clean up and give up. + free(buffer); + str_format( + errbuf, sizeof(errbuf), "sysctlbyname('%s') malloc 3/3", name + ); + psutil_oserror_wsyscall(errbuf); + return -1; + } + + str_format( + errbuf, + sizeof(errbuf), + "sysctlbyname('%s') buffer allocation retry limit exceeded", + name + ); + psutil_runtime_error(errbuf); + return -1; +} +#endif // PSUTIL_HAS_SYSCTLBYNAME +#endif // PSUTIL_HAS_SYSCTL diff --git a/psutil/arch/posix/users.c b/psutil/arch/posix/users.c new file mode 100644 index 0000000000..bdf30864ce --- /dev/null +++ b/psutil/arch/posix/users.c @@ -0,0 +1,101 @@ +/* + * 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" + +#ifdef PSUTIL_HAS_POSIX_USERS +#include +#include + +#include + + +static void +setup() { + UTXENT_MUTEX_LOCK(); + setutxent(); +} + + +static void +teardown() { + endutxent(); + UTXENT_MUTEX_UNLOCK(); +} + + +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; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + setup(); + + while ((ut = getutxent()) != NULL) { + if (ut->ut_type != USER_PROCESS) + continue; + + py_username = PyUnicode_DecodeFSDefault(ut->ut_user); + if (!py_username) + goto error; + + py_tty = PyUnicode_DecodeFSDefault(ut->ut_line); + 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)) + { + py_hostname = PyUnicode_DecodeFSDefault("localhost"); + } + 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; + + 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); + } + + teardown(); + return py_retlist; + +error: + teardown(); + Py_XDECREF(py_username); + Py_XDECREF(py_tty); + Py_XDECREF(py_hostname); + Py_DECREF(py_retlist); + return NULL; +} +#endif // PSUTIL_HAS_POSIX_USERS diff --git a/psutil/arch/solaris/environ.c b/psutil/arch/solaris/environ.c deleted file mode 100644 index 1af4c12934..0000000000 --- a/psutil/arch/solaris/environ.c +++ /dev/null @@ -1,405 +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. - * - * Functions specific for Process.environ(). - */ - -#define NEW_MIB_COMPLIANT 1 -#define _STRUCTURED_PROC 1 - -#include - -#if !defined(_LP64) && _FILE_OFFSET_BITS == 64 -# undef _FILE_OFFSET_BITS -# undef _LARGEFILE64_SOURCE -#endif - -#include -#include -#include -#include - -#include "environ.h" - - -#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. - */ -static int -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); - fd = open(proc_path, O_RDONLY); - if (fd < 0) - PyErr_SetFromErrno(PyExc_OSError); - - return fd; -} - - -/* - * 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. - */ -static int -read_offt(int fd, off_t offset, char *buf, size_t buf_size) { - size_t to_read = buf_size; - size_t stored = 0; - int r; - - while (to_read) { - r = pread(fd, buf + stored, to_read, offset + stored); - if (r < 0) - goto error; - else if (r == 0) - break; - - to_read -= r; - stored += r; - } - - return stored; - - error: - PyErr_SetFromErrno(PyExc_OSError); - return -1; -} - - -/* - * 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. -*/ -static char * -read_cstring_offt(int fd, off_t offset) { - int r; - int i = 0; - off_t end = offset; - size_t len; - char buf[STRING_SEARCH_BUF_SIZE]; - char *result = NULL; - - if (lseek(fd, offset, SEEK_SET) == (off_t)-1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - - // Search end of string - for (;;) { - r = read(fd, buf, sizeof(buf)); - if (r == -1) { - PyErr_SetFromErrno(PyExc_OSError); - goto error; - } - else if (r == 0) { - 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); - - close(as); - return result; -} - - -/* - * 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 - */ -void -psutil_free_cstrings_array(char **array, size_t count) { - int i; - - if (!array) - return; - for (i=0; i -#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); - return (-1); -} diff --git a/psutil/arch/solaris/v10/ifaddrs.h b/psutil/arch/solaris/v10/ifaddrs.h deleted file mode 100644 index 0953a9b99a..0000000000 --- a/psutil/arch/solaris/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/psutil/arch/sunos/cpu.c b/psutil/arch/sunos/cpu.c new file mode 100644 index 0000000000..b1b08394fe --- /dev/null +++ b/psutil/arch/sunos/cpu.c @@ -0,0 +1,137 @@ +/* + * 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); + + if (py_retlist == NULL) + return NULL; + + kc = kstat_open(); + if (kc == NULL) { + 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) { + psutil_oserror(); + goto error; + } + 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; + } + } + } + + kstat_close(kc); + return py_retlist; + +error: + 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) { + 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) { + psutil_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/disk.c b/psutil/arch/sunos/disk.c new file mode 100644 index 0000000000..98ea64693c --- /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) { + psutil_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 psutil_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_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + + file = fopen(MNTTAB, "rb"); + if (file == NULL) { + psutil_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; + 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); + } + fclose(file); + return py_retlist; + +error: + Py_XDECREF(py_dev); + Py_XDECREF(py_mountp); + Py_DECREF(py_retlist); + if (file != NULL) + fclose(file); + return NULL; +} diff --git a/psutil/arch/sunos/environ.c b/psutil/arch/sunos/environ.c new file mode 100644 index 0000000000..6e87ae1895 --- /dev/null +++ b/psutil/arch/sunos/environ.c @@ -0,0 +1,347 @@ +/* + * 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. + */ + +#define _STRUCTURED_PROC 1 + +#include + +#include +#include +#include +#include +#include + +#include "../../arch/all/init.h" + + +#define STRING_SEARCH_BUF_SIZE 512 + + +// Open address space of specified process and return file descriptor. +static int +open_address_space(pid_t pid, const char *procfs_path) { + int fd; + char proc_path[PATH_MAX]; + + str_format(proc_path, PATH_MAX, "%s/%i/as", procfs_path, pid); + fd = open(proc_path, O_RDONLY); + if (fd < 0) + psutil_oserror(); + + return fd; +} + + +// 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; + size_t stored = 0; + int r; + + while (to_read) { + r = pread(fd, buf + stored, to_read, offset + stored); + if (r < 0) + goto error; + else if (r == 0) + break; + + to_read -= r; + stored += r; + } + + return stored; + +error: + psutil_oserror(); + return -1; +} + + +// 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; + int i = 0; + off_t end = offset; + size_t len; + char buf[STRING_SEARCH_BUF_SIZE]; + char *result = NULL; + + if (lseek(fd, offset, SEEK_SET) == (off_t)-1) { + psutil_oserror(); + goto error; + } + + // Search end of string + for (;;) { + r = read(fd, buf, sizeof(buf)); + if (r == -1) { + psutil_oserror(); + goto error; + } + else if (r == 0) { + break; + } + else { + for (i = 0; i < r; i++) + if (!buf[i]) + goto found; + } + + end += r; + } + +found: + len = end + i - offset; + + result = malloc(len + 1); + if (!result) { + PyErr_NoMemory(); + goto error; + } + + if (len) { + if (read_offt(fd, offset, result, len) < 0) { + goto error; + } + } + + result[len] = '\0'; + return result; + +error: + if (result) + free(result); + return 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; + char *pblock = NULL; + size_t pblock_size; + size_t i; + + assert(ptr_size == 4 || ptr_size == 8); + + if (!count) + goto error; + + pblock_size = ptr_size * count; + + pblock = malloc(pblock_size); + if (!pblock) { + PyErr_NoMemory(); + goto error; + } + + if (read_offt(fd, offset, pblock, pblock_size) != pblock_size) + goto error; + + result = (char **)calloc(count, sizeof(char *)); + if (!result) { + PyErr_NoMemory(); + goto error; + } + + for (i = 0; i < count; i++) { + result[i] = read_cstring_offt( + fd, + (ptr_size == 4 ? ((uint32_t *)pblock)[i] : ((uint64_t *)pblock)[i]) + ); + + if (!result[i]) + goto error; + } + + free(pblock); + return result; + +error: + if (result) + psutil_free_cstrings_array(result, i); + if (pblock) + free(pblock); + return NULL; +} + + +// 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) + return info.pr_dmodel == PR_MODEL_ILP32; +#else + return 1; +#endif +} + + +// 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. 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; + size_t r; + char buf[8]; + static const char zeros[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + + assert(ptr_size == 4 || ptr_size == 8); + + if (lseek(fd, offt, SEEK_SET) == (off_t)-1) + goto error; + + for (;; count++) { + r = read(fd, buf, ptr_size); + + if (r < 0) + goto error; + + if (r == 0) + break; + + if (r != ptr_size) { + psutil_runtime_error("pointer block is truncated"); + return -1; + } + + if (!memcmp(buf, zeros, ptr_size)) + break; + } + + return count; + +error: + psutil_oserror(); + return -1; +} + + +// 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; + char **result; + + if (!is_ptr_dereference_possible(info)) { + PyErr_SetString( + PyExc_NotImplementedError, + "can't get env of a 64 bit process from a 32 bit process" + ); + return NULL; + } + + if (!(info.pr_argv && info.pr_argc)) { + psutil_runtime_error("process doesn't have arguments block"); + + return NULL; + } + + as = open_address_space(info.pr_pid, procfs_path); + if (as < 0) + return NULL; + + result = read_cstrings_block( + as, info.pr_argv, ptr_size_by_psinfo(info), info.pr_argc + ); + + if (result && count) + *count = info.pr_argc; + + close(as); + + return result; +} + + +// 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; + int env_count; + int ptr_size; + char **result = NULL; + + if (!is_ptr_dereference_possible(info)) { + PyErr_SetString( + PyExc_NotImplementedError, + "can't get env of a 64 bit process from a 32 bit process" + ); + return NULL; + } + + as = open_address_space(info.pr_pid, procfs_path); + if (as < 0) + return NULL; + + ptr_size = ptr_size_by_psinfo(info); + + env_count = search_pointers_vector_size_offt(as, info.pr_envp, ptr_size); + + if (env_count >= 0 && count) + *count = env_count; + + if (env_count > 0) + result = read_cstrings_block(as, info.pr_envp, ptr_size, env_count); + + close(as); + return result; +} + + +// Free array of cstrings. +void +psutil_free_cstrings_array(char **array, size_t count) { + size_t i; + + if (!array) + return; + for (i = 0; i < count; i++) { + if (array[i]) { + free(array[i]); + } + } + free(array); +} diff --git a/psutil/arch/sunos/init.h b/psutil/arch/sunos/init.h new file mode 100644 index 0000000000..67f80fe67a --- /dev/null +++ b/psutil/arch/sunos/init.h @@ -0,0 +1,43 @@ +/* + * 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_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_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/mem.c b/psutil/arch/sunos/mem.c new file mode 100644 index 0000000000..a4e41482ae --- /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 "../../arch/all/init.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) { + psutil_oserror(); + return NULL; + } + if (num == 0) { + psutil_runtime_error("no swap devices configured"); + return NULL; + } + if ((st = malloc(num * sizeof(swapent_t) + sizeof (int))) == NULL) { + psutil_runtime_error("malloc failed"); + return NULL; + } + if ((path = malloc(num * MAXPATHLEN)) == NULL) { + psutil_runtime_error("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) { + psutil_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 psutil_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) { + 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 new file mode 100644 index 0000000000..73fd3f4190 --- /dev/null +++ b/psutil/arch/sunos/net.c @@ -0,0 +1,561 @@ +/* + * 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) { + psutil_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 + str_copy(ifr.lifr_name, sizeof(ifr.lifr_name), ksp->ks_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)) + { + psutil_runtime_error("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) { + psutil_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; + + 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 + + // 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); + psutil_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 +// 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_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) { + psutil_oserror(); + goto error; + } + ret = ioctl(sd, I_PUSH, "udp"); + if (ret == -1) { + psutil_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; + + mibhdr.len = 1; + 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) { + psutil_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) + { + 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) + { + psutil_runtime_error("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) { + psutil_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); + processed_pid = tp.tcpConnCreationProcess; + 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 + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET, + SOCK_STREAM, + py_laddr, + py_raddr, + state, + processed_pid + )) + { + goto error; + } + py_laddr = NULL; + py_raddr = NULL; + } + } +#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); + processed_pid = tp6.tcp6ConnCreationProcess; + 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 + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_STREAM, + py_laddr, + py_raddr, + state, + processed_pid + )) + { + goto error; + } + py_laddr = NULL; + py_raddr = NULL; + } + } +#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); + processed_pid = ude.udpCreationProcess; + 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; + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET, + SOCK_DGRAM, + py_laddr, + py_raddr, + PSUTIL_CONN_NONE, + processed_pid + )) + { + goto error; + } + py_laddr = NULL; + py_raddr = NULL; + } + } +#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); + processed_pid = ude6.udp6CreationProcess; + 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; + if (!pylist_append_fmt( + py_retlist, + "(iiiNNiI)", + -1, + AF_INET6, + SOCK_DGRAM, + py_laddr, + py_raddr, + PSUTIL_CONN_NONE, + processed_pid + )) + { + goto error; + } + py_laddr = NULL; + py_raddr = NULL; + } + } +#endif + free(databuf.buf); + } + + close(sd); + return py_retlist; + +error: + 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/proc.c b/psutil/arch/sunos/proc.c new file mode 100644 index 0000000000..3d6b3b697f --- /dev/null +++ b/psutil/arch/sunos/proc.c @@ -0,0 +1,586 @@ +/* + * 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" + + +// 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); + psutil_oserror(); + return 0; + } + if (nbytes != (ssize_t)size) { + close(fd); + psutil_runtime_error("read() file structure size mismatch"); + return 0; + } + close(fd); + return nbytes; +} + + +PyObject * +psutil_proc_oneshot(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; + + 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( + "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 + ); +} + + +// Return process name and args as a Python tuple. +PyObject * +psutil_proc_name_and_args(PyObject *self, PyObject *args) { + int pid; + char path[1000]; + psinfo_t info; + const char *procfs_path; + size_t i; + size_t argc; + char **argv = NULL; + PyObject *py_name = NULL; + PyObject *py_sep = NULL; + PyObject *py_arg = NULL; + PyObject *py_args_str = NULL; + PyObject *py_args_list = NULL; + PyObject *py_rettuple = NULL; + + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + return NULL; + str_format(path, sizeof(path), "%s/%i/psinfo", procfs_path, pid); + 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. + 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; + } + + 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_list) { + PyErr_Clear(); + py_args_str = PyUnicode_DecodeFSDefault(info.pr_psargs); + if (!py_args_str) + goto error; + + py_sep = PyUnicode_FromString(" "); + if (!py_sep) + goto error; + + 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_list); + + return py_rettuple; + +error: + psutil_free_cstrings_array(argv, argc); + Py_XDECREF(py_name); + Py_XDECREF(py_args_list); + Py_XDECREF(py_sep); + Py_XDECREF(py_arg); + Py_XDECREF(py_args_str); + Py_XDECREF(py_rettuple); + return NULL; +} + + +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; + + str_format(path, sizeof(path), "%s/%i/psinfo", procfs_path, pid); + if (!psutil_file_to_struct(path, (void *)&info, sizeof(info))) + goto error; + + if (!info.pr_envp) { + psutil_oserror_ad("/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 < env_count; i++) { + if (!env[i]) + break; + + dm = strchr(env[i], '='); + if (!dm) + continue; + + *dm = '\0'; + + py_envname = PyUnicode_DecodeFSDefault(env[i]); + if (!py_envname) + goto error; + + py_envval = PyUnicode_DecodeFSDefault(dm + 1); + if (!py_envname) + goto error; + + if (PyDict_SetItem(py_retdict, py_envname, py_envval) < 0) + goto error; + + Py_CLEAR(py_envname); + Py_CLEAR(py_envval); + } + + psutil_free_cstrings_array(env, env_count); + return py_retdict; + +error: + if (env && env_count >= 0) + psutil_free_cstrings_array(env, env_count); + + Py_XDECREF(py_envname); + Py_XDECREF(py_envval); + Py_XDECREF(py_retdict); + return NULL; +} + + +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; + 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() + 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; + + str_format(path, sizeof(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) { + psutil_oserror(); + goto error; + } + if (nbytes != sizeof(header)) { + psutil_runtime_error("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) { + psutil_oserror(); + goto error; + } + if (nbytes != size) { + psutil_runtime_error("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; + 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( + "iiiiii", + info.pr_ruid, + info.pr_euid, + info.pr_suid, + info.pr_rgid, + info.pr_egid, + info.pr_sgid + ); +} + + +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; + 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); +} + + +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. + * + * 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; + str_format(path, sizeof(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; + 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( + "dd", PSUTIL_TV2DOUBLE(info.pr_utime), PSUTIL_TV2DOUBLE(info.pr_stime) + ); +} + + +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_path = NULL; + PyObject *py_retlist = PyList_New(0); + + if (py_retlist == NULL) + return NULL; + if (!PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + goto error; + + str_format(path, sizeof(path), "%s/%i/status", procfs_path, pid); + if (!psutil_file_to_struct(path, (void *)&status, sizeof(status))) + goto error; + + str_format(path, sizeof(path), "%s/%i/xmap", procfs_path, pid); + if (stat(path, &st) == -1) { + psutil_oserror(); + goto error; + } + + size = st.st_size; + + fd = open(path, O_RDONLY); + if (fd == -1) { + psutil_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 + str_format( + perms, + sizeof(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; + 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); + + // increment pointer + p += 1; + } + + close(fd); + free(xmap); + return py_retlist; + +error: + if (fd != -1) + close(fd); + Py_XDECREF(py_path); + Py_DECREF(py_retlist); + if (xmap != NULL) + free(xmap); + return NULL; +} diff --git a/psutil/arch/sunos/sys.c b/psutil/arch/sunos/sys.c new file mode 100644 index 0000000000..d3afb12418 --- /dev/null +++ b/psutil/arch/sunos/sys.c @@ -0,0 +1,35 @@ +/* + * 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; + + UTXENT_MUTEX_LOCK(); + setutxent(); + while (NULL != (ut = getutxent())) { + if (ut->ut_type == BOOT_TIME) { + boot_time = (float)ut->ut_tv.tv_sec; + break; + } + } + endutxent(); + UTXENT_MUTEX_UNLOCK(); + if (fabs(boot_time) < 0.000001) { + // 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/windows/cpu.c b/psutil/arch/windows/cpu.c new file mode 100644 index 0000000000..9727882cc6 --- /dev/null +++ b/psutil/arch/windows/cpu.c @@ -0,0 +1,395 @@ +/* + * 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" + + +// 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 (GetActiveProcessorCount != NULL) { + ncpus = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); + if ((ncpus == 0) && (fail_on_err == 1)) { + psutil_oserror(); + } + } + else { + psutil_debug( + "GetActiveProcessorCount() not available; " + "using GetSystemInfo()" + ); + ncpus = (unsigned int)PSUTIL_SYSTEM_INFO.dwNumberOfProcessors; + if ((ncpus <= 0) && (fail_on_err == 1)) { + psutil_runtime_error("GetSystemInfo failed to retrieve CPU count"); + } + } + return ncpus; +} + + +// 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; + FILETIME idle_time, kernel_time, user_time; + + if (!GetSystemTimes(&idle_time, &kernel_time, &user_time)) { + psutil_oserror(); + 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)); + + // 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 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_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 information + status = 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++) { + 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; + if (!pylist_append_fmt( + py_retlist, "(ddddd)", user, systemt, idle, interrupt, dpc + )) + goto error; + } + + free(sppi); + return py_retlist; + +error: + 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; // mimic os.cpu_count() +} + + +// Return the number of CPU cores (non hyper-threading). +PyObject * +psutil_cpu_count_cores(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 (GetLogicalProcessorInformationEx == NULL) { + psutil_debug("Win < 7; cpu_count_cores() forced to None"); + Py_RETURN_NONE; + } + + while (1) { + 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); + if (NULL == buffer) { + PyErr_NoMemory(); + return NULL; + } + } + else { + psutil_debug( + "GetLogicalProcessorInformationEx() returned %u", + 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; // mimic os.cpu_count() + } + +return_none: + if (buffer != NULL) + free(buffer); + Py_RETURN_NONE; +} + + +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 = 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 = 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 = 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; +} + + +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) { + psutil_oserror(); + return NULL; + } + + // Syscall. + ret = CallNtPowerInformation(ProcessorInformation, NULL, 0, pBuffer, size); + if (ret != 0) { + psutil_runtime_error("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/disk.c b/psutil/arch/windows/disk.c new file mode 100644 index 0000000000..825aa373d3 --- /dev/null +++ b/psutil/arch/windows/disk.c @@ -0,0 +1,386 @@ +/* + * 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" + + +#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 "?"; + } +} + + +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; + } + + path = PyUnicode_AsWideCharString(py_path, NULL); + if (path == NULL) { + return NULL; + } + + Py_BEGIN_ALLOW_THREADS + retval = GetDiskFreeSpaceExW(path, &avail, &total, &free); + Py_END_ALLOW_THREADS + + PyMem_Free(path); + + if (retval == 0) { + return PyErr_SetExcFromWindowsErrWithFilenameObject( + PyExc_OSError, 0, py_path + ); + } + + used = total.QuadPart - free.QuadPart; + return Py_BuildValue("(KKK)", total.QuadPart, used, free.QuadPart); +} + + +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; + str_format(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. + psutil_oserror(); + goto error; + } + + str_format(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; +} + + +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[50]; + HANDLE mp_h; + 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); + + 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) { + psutil_oserror(); + goto error; + } + + while (*drive_letter != 0) { + 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, + &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"). + // Let's pretend it didn't happen as we already have + // the drive name and type ('removable'). + str_append(opts, sizeof(opts), ""); + SetLastError(0); + } + else { + if (pflags & FILE_READ_ONLY_VOLUME) + str_append(opts, sizeof(opts), "ro"); + else + str_append(opts, sizeof(opts), "rw"); + if (pflags & FILE_VOLUME_IS_COMPRESSED) + str_append(opts, sizeof(opts), ",compressed"); + if (pflags & FILE_READ_ONLY_VOLUME) + 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) + if (pflags & FILE_SUPPORTS_REPARSE_POINTS) { + 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 + str_copy( + mp_path, sizeof(mp_path), drive_letter + ); // initialize + str_append( + mp_path, sizeof(mp_path), mp_buf + ); // append mount point + + if (!pylist_append_fmt( + py_retlist, + "(ssss)", + drive_letter, + mp_path, + fs_type, // typically "NTFS" + opts + )) + { + FindVolumeMountPointClose(mp_h); + goto error; + } + + // Continue looking for more mount points + mp_flag = FindNextVolumeMountPoint( + mp_h, mp_buf, MAX_PATH + ); + } + FindVolumeMountPointClose(mp_h); + } + } + } + + if (strlen(opts) > 0) + str_append(opts, sizeof(opts), ","); + str_append(opts, sizeof(opts), psutil_get_drive_type(type)); + + 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; + } + goto next; + + next: + drive_letter = strchr(drive_letter, 0) + 1; + } + + SetErrorMode(old_mode); + return py_retlist; + +error: + SetErrorMode(old_mode); + 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_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 PyUnicode_FromString(szBuff); + } + } + d++; + } + return PyUnicode_FromString(""); +} 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/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/inet_ntop.c b/psutil/arch/windows/inet_ntop.c deleted file mode 100644 index 4b6c1dfec2..0000000000 --- a/psutil/arch/windows/inet_ntop.c +++ /dev/null @@ -1,46 +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(__in INT family, - __in PVOID pAddr, - __out_ecount(StringBufSize) PSTR pStringBuf, - __in size_t StringBufSize) { - 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 (WSAAddressToString((LPSOCKADDR) &srcaddr, - dwAddressLength, - 0, - pStringBuf, - (LPDWORD) &StringBufSize) != 0) { - PyErr_SetExcFromWindowsErr(PyExc_OSError, WSAGetLastError()); - return NULL; - } - return pStringBuf; -} diff --git a/psutil/arch/windows/inet_ntop.h b/psutil/arch/windows/inet_ntop.h deleted file mode 100644 index 70573a3685..0000000000 --- a/psutil/arch/windows/inet_ntop.h +++ /dev/null @@ -1,15 +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 - -PCSTR WSAAPI -inet_ntop( - __in INT Family, - __in PVOID pAddr, - __out_ecount(StringBufSize) PSTR pStringBuf, - __in size_t StringBufSize -); diff --git a/psutil/arch/windows/init.c b/psutil/arch/windows/init.c new file mode 100644 index 0000000000..544b9110e0 --- /dev/null +++ b/psutil/arch/windows/init.c @@ -0,0 +1,319 @@ +/* + * 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 "ntextapi.h" + + +// Needed to make these globally visible. +int PSUTIL_WINVER; +SYSTEM_INFO PSUTIL_SYSTEM_INFO; +CRITICAL_SECTION PSUTIL_CRITICAL_SECTION; + + +// ==================================================================== +// --- Backward compatibility with missing Python.h APIs +// ==================================================================== + +// PyPy on Windows. Missing APIs added in PyPy 7.3.14. +#if defined(PYPY_VERSION) +#if !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 // !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) +#endif // defined(PYPY_VERSION) + + +// ==================================================================== +// --- Utils +// ==================================================================== + +// 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(); + str_format(fullmsg, sizeof(fullmsg), "(originated from %s)", syscall); + return PyErr_SetFromWindowsErrWithFilename(err, fullmsg); +} + + +// A wrapper around GetModuleHandle and GetProcAddress. +PVOID +psutil_GetProcAddress(LPCSTR libname, LPCSTR apiname) { + HMODULE mod; + FARPROC addr; + + if ((mod = GetModuleHandleA(libname)) == NULL) { + psutil_debug( + "%s module not supported (needed for %s)", libname, apiname + ); + PyErr_SetFromWindowsErrWithFilename(0, libname); + return NULL; + } + if ((addr = GetProcAddress(mod, apiname)) == NULL) { + psutil_debug("%s -> %s API not supported", libname, apiname); + PyErr_SetFromWindowsErrWithFilename(0, apiname); + return NULL; + } + return addr; +} + + +// A wrapper around LoadLibrary and GetProcAddress. +PVOID +psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR apiname) { + HMODULE mod; + FARPROC addr; + + Py_BEGIN_ALLOW_THREADS + mod = LoadLibraryA(libname); + Py_END_ALLOW_THREADS + if (mod == NULL) { + psutil_debug("%s lib not supported (needed for %s)", libname, apiname); + PyErr_SetFromWindowsErrWithFilename(0, libname); + return NULL; + } + if ((addr = GetProcAddress(mod, apiname)) == NULL) { + psutil_debug("%s -> %s not supported", libname, apiname); + PyErr_SetFromWindowsErrWithFilename(0, apiname); + FreeLibrary(mod); + return NULL; + } + // Causes crash. + // FreeLibrary(mod); + return addr; +} + + +// 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); +} + + +// ==================================================================== +// --- Init / load libs +// ==================================================================== + + +static int +psutil_loadlibs() { + // --- Mandatory + NtQuerySystemInformation = psutil_GetProcAddressFromLib( + "ntdll.dll", "NtQuerySystemInformation" + ); + if (!NtQuerySystemInformation) + return -1; + NtQueryInformationProcess = psutil_GetProcAddress( + "ntdll.dll", "NtQueryInformationProcess" + ); + if (!NtQueryInformationProcess) + return -1; + NtSetInformationProcess = psutil_GetProcAddress( + "ntdll.dll", "NtSetInformationProcess" + ); + if (!NtSetInformationProcess) + 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; + GetTickCount64 = psutil_GetProcAddress("kernel32", "GetTickCount64"); + if (!GetTickCount64) + return -1; + RtlIpv6AddressToStringA = psutil_GetProcAddressFromLib( + "ntdll.dll", "RtlIpv6AddressToStringA" + ); + if (!RtlIpv6AddressToStringA) + return -1; + + // --- Optional + + // minimum requirement: Win 7 + QueryInterruptTime = psutil_GetProcAddressFromLib( + "kernelbase.dll", "QueryInterruptTime" + ); + // minimum requirement: Win 7 + GetActiveProcessorCount = psutil_GetProcAddress( + "kernel32", "GetActiveProcessorCount" + ); + // minimum 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; +} + + +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; +} + + +// Called on module import. +int +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); + return 0; +} diff --git a/psutil/arch/windows/init.h b/psutil/arch/windows/init.h new file mode 100644 index 0000000000..8727f6bddb --- /dev/null +++ b/psutil/arch/windows/init.h @@ -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 "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 + +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; + + +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); +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_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); +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 *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); +PyObject *psutil_proc_environ(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_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); +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_boot_time(PyObject *self, PyObject *args); +PyObject *psutil_users(PyObject *self, PyObject *args); +PyObject *psutil_GetPerformanceInfo(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.c b/psutil/arch/windows/mem.c new file mode 100644 index 0000000000..96ebeb0879 --- /dev/null +++ b/psutil/arch/windows/mem.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 "../../arch/all/init.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_GetPerformanceInfo(PyObject *self, PyObject *args) { + PERFORMANCE_INFORMATION info; + PyObject *dict = PyDict_New(); + + if (!dict) + return NULL; + if (!GetPerformanceInfo(&info, sizeof(PERFORMANCE_INFORMATION))) { + psutil_oserror(); + goto error; + } + + // 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; +} + + +// 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) { + psutil_runtime_error("PdhOpenQueryW failed"); + return NULL; + } + + s = PdhAddEnglishCounterW(hQuery, szCounterPath, 0, &hCounter); + if (s != ERROR_SUCCESS) { + PdhCloseQuery(hQuery); + psutil_runtime_error( + "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); + psutil_runtime_error("PdhGetFormattedCounterValue failed"); + return NULL; + } + percentUsage = counterValue.doubleValue; + } + + PdhRemoveCounter(hCounter); + PdhCloseQuery(hQuery); + return Py_BuildValue("d", percentUsage); +} diff --git a/psutil/arch/windows/net.c b/psutil/arch/windows/net.c new file mode 100644 index 0000000000..25da443502 --- /dev/null +++ b/psutil/arch/windows/net.c @@ -0,0 +1,461 @@ +/* + * 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 "../../arch/all/init.h" + + +static PIP_ADAPTER_ADDRESSES +psutil_get_nic_addresses(void) { + ULONG bufferLength = 0; + PIP_ADAPTER_ADDRESSES buffer; + + if (GetAdaptersAddresses(AF_UNSPEC, 0, NULL, NULL, &bufferLength) + != ERROR_BUFFER_OVERFLOW) + { + psutil_runtime_error("GetAdaptersAddresses() syscall failed."); + return NULL; + } + + 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); + psutil_runtime_error("GetAdaptersAddresses() syscall failed."); + return NULL; + } + + return buffer; +} + + +/* + * 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 ifRow; + 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_info = NULL; + py_nic_name = NULL; + + SecureZeroMemory(&ifRow, sizeof(ifRow)); + ifRow.InterfaceIndex = pCurrAddresses->IfIndex; + + dwRetVal = GetIfEntry2(&ifRow); + if (dwRetVal != NO_ERROR) { + psutil_runtime_error( + "GetIfEntry2() syscall failed for interface %lu", + (unsigned long)ifRow.InterfaceIndex + ); + goto error; + } + + py_nic_info = Py_BuildValue( + "(KKKKKKKK)", + 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, + wcsnlen(pCurrAddresses->FriendlyName, IF_MAX_STRING_SIZE) + ); + if (!py_nic_name) + goto error; + + if (PyDict_SetItem(py_retdict, py_nic_name, py_nic_info)) + goto error; + + Py_CLEAR(py_nic_info); + Py_CLEAR(py_nic_name); + + pCurrAddresses = pCurrAddresses->Next; + } + + free(pAddresses); + return py_retdict; + +error: + Py_XDECREF(py_nic_info); + Py_XDECREF(py_nic_name); + Py_DECREF(py_retdict); + if (pAddresses) + free(pAddresses); + 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; + int n; + size_t remaining; + 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_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) { + Py_CLEAR(py_nic_name); + Py_CLEAR(py_address); + Py_CLEAR(py_mac_address); + Py_CLEAR(py_netmask); + + pUnicast = pCurrAddresses->FirstUnicastAddress; + + netmaskIntRet = 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; + remaining = sizeof(buff_macaddr); + for (i = 0; i < pCurrAddresses->PhysicalAddressLength; i++) { + if (i == pCurrAddresses->PhysicalAddressLength - 1) { + n = str_format( + ptr, + remaining, + "%.2X", + (int)pCurrAddresses->PhysicalAddress[i] + ); + } + else { + n = str_format( + ptr, + remaining, + "%.2X-", + (int)pCurrAddresses->PhysicalAddress[i] + ); + } + if (n < 0) { // error or truncated + psutil_runtime_error("str_format() error"); + break; + } + ptr += n; + remaining -= n; + } + + py_mac_address = PyUnicode_FromString(buff_macaddr); + if (py_mac_address == NULL) + goto error; + + 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_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; + } + + Py_CLEAR(py_address); + Py_CLEAR(py_netmask); + + py_address = PyUnicode_FromString(buff_addr); + if (py_address == NULL) + goto error; + + if (netmaskIntRet != NULL) { + py_netmask = PyUnicode_FromString(buff_netmask); + } + else { + Py_INCREF(Py_None); + py_netmask = Py_None; + } + + 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_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_XDECREF(py_retlist); + Py_XDECREF(py_address); + Py_XDECREF(py_mac_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 = NULL; + 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) { + psutil_runtime_error("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; + Py_CLEAR(py_nic_name); + Py_CLEAR(py_ifc_info); + + pCurrAddresses = pAddresses; + while (pCurrAddresses) { + str_format(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; + } + + Py_CLEAR(py_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_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_XDECREF(py_retdict); + if (pIfTable) + free(pIfTable); + if (pAddresses) + free(pAddresses); + return NULL; +} diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index ea23ddb729..810ff4e5b9 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -2,12 +2,159 @@ * 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. */ + +// clang-format off #if !defined(__NTEXTAPI_H__) #define __NTEXTAPI_H__ #include +#include +typedef LONG NTSTATUS; +// 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) + +// WtsApi32.h +#define WTS_CURRENT_SERVER_HANDLE ((HANDLE)NULL) +#define WINSTATIONNAME_LENGTH 32 +#define DOMAIN_LENGTH 17 +#define USERNAME_LENGTH 20 + +// ================================================================ +// 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 +#undef SystemProcessIdInformation +#define SystemProcessIdInformation 88 +#undef SystemTimeOfDayInformation +#define SystemTimeOfDayInformation 3 + + +// process suspend() / resume() +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; + +// 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. +// ================================================================ + +// cpu_stats(), per_cpu_times() typedef struct { LARGE_INTEGER IdleTime; LARGE_INTEGER KernelTime; @@ -17,7 +164,7 @@ typedef struct { ULONG InterruptCount; } _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION; - +// cpu_stats() typedef struct { LARGE_INTEGER IdleProcessTime; LARGE_INTEGER IoReadTransferCount; @@ -93,10 +240,9 @@ typedef struct { ULONG FirstLevelTbFills; ULONG SecondLevelTbFills; ULONG SystemCalls; - } _SYSTEM_PERFORMANCE_INFORMATION; - +// cpu_stats() typedef struct { ULONG ContextSwitches; ULONG DpcCount; @@ -106,62 +252,22 @@ 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 = 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 -} 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; @@ -188,158 +294,440 @@ typedef struct _SYSTEM_THREAD_INFORMATION2 { #define SYSTEM_THREAD_INFORMATION SYSTEM_THREAD_INFORMATION2 #define PSYSTEM_THREAD_INFORMATION PSYSTEM_THREAD_INFORMATION2 -typedef struct _TEB *PTEB; - - -// private -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; - LARGE_INTEGER SpareLi1; - LARGE_INTEGER SpareLi2; - LARGE_INTEGER SpareLi3; - LARGE_INTEGER CreateTime; - LARGE_INTEGER UserTime; - LARGE_INTEGER KernelTime; - UNICODE_STRING ImageName; - LONG BasePriority; - HANDLE UniqueProcessId; - HANDLE InheritedFromUniqueProcessId; - ULONG HandleCount; - ULONG SessionId; - ULONG_PTR PageDirectoryBase; - 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 #define PSYSTEM_PROCESS_INFORMATION PSYSTEM_PROCESS_INFORMATION2 +// cpu_freq() +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 + +// PEB / cmdline(), cwd(), environ() +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_; + +// users() +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_cores() +#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 + +// memory_uss() +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; + +// 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; + SIZE_T NumberOfSharedPages; + 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; + +// boot_time() +typedef struct _SYSTEM_TIMEOFDAY_INFORMATION2 { + LARGE_INTEGER BootTime; + LARGE_INTEGER CurrentTime; + LARGE_INTEGER TimeZoneBias; + ULONG TimeZoneId; + ULONG Reserved; + ULONGLONG BootTimeBias; + ULONGLONG SleepTimeBias; +} SYSTEM_TIMEOFDAY_INFORMATION2, *PSYSTEM_TIMEOFDAY_INFORMATION2; + +#define SYSTEM_TIMEOFDAY_INFORMATION SYSTEM_TIMEOFDAY_INFORMATION2 +#define PSYSTEM_TIMEOFDAY_INFORMATION PSYSTEM_TIMEOFDAY_INFORMATION2 + +// ==================================================================== +// 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; -// ================================================ -// psutil.users() support -// ================================================ - -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 { + 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 BOOLEAN (WINAPI * PWINSTATIONQUERYINFORMATIONW) - (HANDLE,ULONG,WINSTATIONINFOCLASS,PVOID,ULONG,PULONG); +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; -/* - * 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 - */ -typedef LONG NTSTATUS; +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 NTSTATUS (NTAPI *_NtQueryInformationProcess)( +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. +// ================================================================ + +BOOL (WINAPI *_GetLogicalProcessorInformationEx) ( + LOGICAL_PROCESSOR_RELATIONSHIP relationship, + PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX Buffer, + PDWORD ReturnLength); + +#define GetLogicalProcessorInformationEx _GetLogicalProcessorInformationEx + +BOOLEAN (WINAPI * _WinStationQueryInformationW) ( + HANDLE ServerHandle, + ULONG SessionId, + WINSTATIONINFOCLASS WinStationInformationClass, + PVOID pWinStationInformation, + ULONG WinStationInformationLength, + PULONG pReturnLength); + +#define WinStationQueryInformationW _WinStationQueryInformationW + +NTSTATUS (NTAPI *_NtQueryInformationProcess) ( HANDLE ProcessHandle, DWORD ProcessInformationClass, PVOID ProcessInformation, DWORD ProcessInformationLength, - PDWORD ReturnLength -); + PDWORD ReturnLength); +#define NtQueryInformationProcess _NtQueryInformationProcess -typedef NTSTATUS (NTAPI *_NtSetInformationProcess)( +NTSTATUS (NTAPI *_NtQuerySystemInformation) ( + ULONG SystemInformationClass, + PVOID SystemInformation, + ULONG SystemInformationLength, + PULONG ReturnLength); + +#define NtQuerySystemInformation _NtQuerySystemInformation + +NTSTATUS (NTAPI *_NtSetInformationProcess) ( HANDLE ProcessHandle, DWORD ProcessInformationClass, PVOID ProcessInformation, - DWORD ProcessInformationLength + DWORD ProcessInformationLength); + +#define NtSetInformationProcess _NtSetInformationProcess + +PSTR (NTAPI * _RtlIpv4AddressToStringA) ( + struct in_addr *Addr, + PSTR S); + +#define RtlIpv4AddressToStringA _RtlIpv4AddressToStringA + +PSTR (NTAPI * _RtlIpv6AddressToStringA) ( + struct in6_addr *Addr, + PSTR P); + +#define RtlIpv6AddressToStringA _RtlIpv6AddressToStringA + +DWORD (WINAPI * _GetExtendedTcpTable) ( + PVOID pTcpTable, + PDWORD pdwSize, + BOOL bOrder, + ULONG ulAf, + TCP_TABLE_CLASS TableClass, + ULONG Reserved); + +#define GetExtendedTcpTable _GetExtendedTcpTable + +DWORD (WINAPI * _GetExtendedUdpTable) ( + PVOID pUdpTable, + PDWORD pdwSize, + BOOL bOrder, + ULONG ulAf, + UDP_TABLE_CLASS TableClass, + ULONG Reserved); + +#define GetExtendedUdpTable _GetExtendedUdpTable + +DWORD (CALLBACK *_GetActiveProcessorCount) ( + WORD GroupNumber); + +#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); + +#define GetTickCount64 _GetTickCount64 + +VOID(CALLBACK *_QueryInterruptTime) ( + PULONGLONG lpInterruptTime +); + +#define QueryInterruptTime _QueryInterruptTime + +NTSTATUS (NTAPI *_NtQueryObject) ( + HANDLE Handle, + OBJECT_INFORMATION_CLASS ObjectInformationClass, + PVOID ObjectInformation, + ULONG ObjectInformationLength, + PULONG ReturnLength); + +#define NtQueryObject _NtQueryObject + +NTSTATUS (WINAPI *_RtlGetVersion) ( + PRTL_OSVERSIONINFOW lpVersionInformation +); + +#define RtlGetVersion _RtlGetVersion + +NTSTATUS (WINAPI *_NtResumeProcess) ( + HANDLE hProcess +); + +#define NtResumeProcess _NtResumeProcess + +NTSTATUS (WINAPI *_NtSuspendProcess) ( + HANDLE hProcess ); +#define NtSuspendProcess _NtSuspendProcess + +NTSTATUS (NTAPI *_NtQueryVirtualMemory) ( + HANDLE ProcessHandle, + PVOID BaseAddress, + int MemoryInformationClass, + PVOID MemoryInformation, + SIZE_T MemoryInformationLength, + PSIZE_T ReturnLength +); + +#define NtQueryVirtualMemory _NtQueryVirtualMemory + +ULONG (WINAPI *_RtlNtStatusToDosErrorNoTeb) ( + NTSTATUS status +); -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 +#define RtlNtStatusToDosErrorNoTeb _RtlNtStatusToDosErrorNoTeb #endif // __NTEXTAPI_H__ +// clang-format on diff --git a/psutil/arch/windows/pids.c b/psutil/arch/windows/pids.c new file mode 100644 index 0000000000..8101aaeee8 --- /dev/null +++ b/psutil/arch/windows/pids.c @@ -0,0 +1,48 @@ +/* + * 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 "../../arch/all/init.h" + + +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); + psutil_oserror(); + 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 new file mode 100644 index 0000000000..e0367e02ac --- /dev/null +++ b/psutil/arch/windows/proc.c @@ -0,0 +1,1167 @@ +/* + * 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. + */ + +/* + * 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 + +#include +#include +#include // memory_info(), memory_maps() +#include +#include // threads(), PROCESSENTRY32 + +// Link with Iphlpapi.lib +#pragma comment(lib, "IPHLPAPI.lib") + +#include "../../arch/all/init.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); +} + + +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 psutil_oserror_ad("automatically set for PID 0"); + + hProcess = psutil_handle_from_pid(pid, access); + 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) { + psutil_oserror_wsyscall("TerminateProcess"); + return NULL; + } + } + + CloseHandle(hProcess); + Py_RETURN_NONE; +} + + +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 psutil_oserror_ad("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 { + psutil_oserror_wsyscall("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) { + psutil_oserror_wsyscall("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) { + psutil_oserror_wsyscall("GetExitCodeProcess"); + CloseHandle(hProcess); + return NULL; + } + + CloseHandle(hProcess); + + return PyLong_FromLong((long)ExitCode); +} + + +// Return (user_time, kernel_time, create_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 + psutil_oserror_nsp("GetProcessTimes -> ERROR_ACCESS_DENIED"); + } + else { + psutil_oserror(); + } + 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 psutil_oserror_ad("automatically set for PID 0"); + + // ...because NtQuerySystemInformation can succeed for terminated + // processes. + if (psutil_pid_is_running(pid) == 0) + return psutil_oserror_nsp("psutil_pid_is_running -> 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) + psutil_oserror_nsp("psutil_pid_is_running -> 0"); + else + psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); + return NULL; + } + + if (processIdInfo.ImageName.Buffer == NULL) { + // Happens for PID 4. + py_exe = PyUnicode_FromString(""); + } + else { + py_exe = PyUnicode_FromWideChar( + processIdInfo.ImageName.Buffer, processIdInfo.ImageName.Length / 2 + ); + } + FREE(buffer); + return py_exe; +} + + +PyObject * +psutil_proc_memory_info(PyObject *self, PyObject *args) { + HANDLE hProcess; + DWORD pid; + PROCESS_MEMORY_COUNTERS_EX cnt; + PyObject *dict = PyDict_New(); + + 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) + goto error; + + if (!GetProcessMemoryInfo( + hProcess, (PPROCESS_MEMORY_COUNTERS)&cnt, sizeof(cnt) + )) + { + psutil_oserror(); + CloseHandle(hProcess); + goto error; + } + CloseHandle(hProcess); + + // 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; +} + + +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) { + psutil_runtime_error("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) { + psutil_oserror_ad("NtQueryVirtualMemory -> STATUS_ACCESS_DENIED"); + } + else if (psutil_pid_is_running(pid) == 0) { + psutil_oserror_nsp("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; + DWORD access = PROCESS_SUSPEND_RESUME | PROCESS_QUERY_LIMITED_INFORMATION; + PyObject *suspend; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "O", &pid, &suspend)) + return NULL; + + hProcess = psutil_handle_from_pid(pid, access); + 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_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 + psutil_oserror_ad("forced for PID 0"); + goto error; + } + + pid_return = psutil_pid_is_running(pid); + if (pid_return == 0) { + psutil_oserror_nsp("psutil_pid_is_running -> 0"); + goto error; + } + if (pid_return == -1) + goto error; + + hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (hThreadSnap == INVALID_HANDLE_VALUE) { + psutil_oserror_wsyscall("CreateToolhelp32Snapshot"); + goto error; + } + + // Fill in the size of the structure before using it + te32.dwSize = sizeof(THREADENTRY32); + + if (!Thread32First(hThreadSnap, &te32)) { + psutil_oserror_wsyscall("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) { + 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) { + psutil_oserror_wsyscall("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 + */ + 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; + } + + CloseHandle(hThread); + } + } while (Thread32Next(hThreadSnap, &te32)); + + CloseHandle(hThreadSnap); + return py_retlist; + +error: + 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)) { + psutil_oserror_wsyscall("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); + userToken = NULL; + continue; + } + else { + psutil_oserror_wsyscall("GetTokenInformation"); + goto error; + } + } + break; + } + + CloseHandle(hProcess); + CloseHandle(hToken); + return userToken; + +error: + if (userToken != NULL) + free(userToken); + 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://learn.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. + psutil_oserror_ad("LookupAccountSidW -> ERROR_NONE_MAPPED"); + goto error; + } + else { + psutil_oserror_wsyscall("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) { + psutil_oserror(); + 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) { + psutil_oserror(); + CloseHandle(hProcess); + return NULL; + } + + CloseHandle(hProcess); + Py_RETURN_NONE; +} + + +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); +} + + +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; +} + + +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)) { + psutil_oserror(); + 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) { + psutil_oserror(); + CloseHandle(hProcess); + return NULL; + } + + CloseHandle(hProcess); + return Py_BuildValue("K", (unsigned long long)proc_mask); +} + + +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; + + if (!PyArg_ParseTuple(args, _Py_PARSE_PID "K", &pid, &mask)) + return NULL; + hProcess = psutil_handle_from_pid(pid, access); + if (hProcess == NULL) + return NULL; + + if (SetProcessAffinityMask(hProcess, mask) == 0) { + psutil_oserror(); + CloseHandle(hProcess); + return NULL; + } + + CloseHandle(hProcess); + Py_RETURN_NONE; +} + + +// 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. +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) != 0) + 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; +} + + +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)) { + psutil_oserror(); + 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 "?"; + } +} + + +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_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) + )) + { + if (baseAddress > maxAddr) + break; + if (GetMappedFileNameW( + hProcess, baseAddress, mappedFileName, sizeof(mappedFileName) + )) + { + py_str = PyUnicode_FromWideChar( + mappedFileName, wcslen(mappedFileName) + ); + if (py_str == NULL) + goto error; + 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_str); + } + baseAddress = (PCHAR)baseAddress + basicInfo.RegionSize; + } + + CloseHandle(hProcess); + return py_retlist; + +error: + 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) { + psutil_oserror(); + 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_handles.c b/psutil/arch/windows/proc_handles.c new file mode 100644 index 0000000000..e18c741f1b --- /dev/null +++ b/psutil/arch/windows/proc_handles.c @@ -0,0 +1,293 @@ +/* + * 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 +#include + +#include "../../arch/all/init.h" + + +#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); + if (buffer == NULL) { + PyErr_NoMemory(); + return -1; + } + + 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) { + psutil_runtime_error( + "SystemExtendedHandleInformation buffer too big" + ); + return -1; + } + + buffer = MALLOC_ZERO(bufferSize); + if (buffer == NULL) { + PyErr_NoMemory(); + return -1; + } + } + + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); + FREE(buffer); + return -1; + } + + *handles = (PSYSTEM_HANDLE_INFORMATION_EX)buffer; + return 0; +} + + +static int +psutil_get_filename(LPVOID lpvParam) { + HANDLE hFile = *((HANDLE *)lpvParam); + NTSTATUS status; + ULONG bufferSize; + ULONG attempts = 8; + + 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; + } + + // A loop is needed because the I/O subsystem likes to give us the + // wrong return lengths... + do { + status = NtQueryObject( + hFile, + ObjectNameInformation, + globalFileName, + bufferSize, + &bufferSize + ); + if (status == STATUS_BUFFER_OVERFLOW + || status == STATUS_INFO_LENGTH_MISMATCH + || status == STATUS_BUFFER_TOO_SMALL) + { + FREE(globalFileName); + globalFileName = MALLOC_ZERO(bufferSize); + if (globalFileName == NULL) { + PyErr_NoMemory(); + goto error; + } + } + else { + break; + } + } while (--attempts); + + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); + FREE(globalFileName); + globalFileName = NULL; + return 1; + } + + return 0; + +error: + if (globalFileName != NULL) { + FREE(globalFileName); + globalFileName = NULL; + } + return 1; +} + + +static DWORD +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) { + psutil_oserror_wsyscall("CreateThread"); + return -1; + } + + // 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 handle name thread timed out after %i ms", THREAD_TIMEOUT + ); + if (TerminateThread(hThread, 0) == 0) { + psutil_oserror_wsyscall("TerminateThread"); + CloseHandle(hThread); + return -1; + } + CloseHandle(hThread); + return 0; + } + + if (dwWait == WAIT_FAILED) { + psutil_debug("WaitForSingleObject -> WAIT_FAILED"); + if (TerminateThread(hThread, 0) == 0) { + psutil_oserror_wsyscall( + "WaitForSingleObject -> WAIT_FAILED -> TerminateThread" + ); + CloseHandle(hThread); + return -1; + } + psutil_oserror_wsyscall("WaitForSingleObject"); + CloseHandle(hThread); + return -1; + } + + if (GetExitCodeThread(hThread, &threadRetValue) == 0) { + if (TerminateThread(hThread, 0) == 0) { + psutil_oserror_wsyscall( + "GetExitCodeThread (failed) -> TerminateThread" + ); + CloseHandle(hThread); + return -1; + } + + CloseHandle(hThread); + psutil_oserror_wsyscall("GetExitCodeThread"); + return -1; + } + CloseHandle(hThread); + return threadRetValue; +} + + +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_retlist = PyList_New(0); + ; + + if (!py_retlist) + return NULL; + + // Due to the use of global variables, ensure only 1 call + // to psutil_get_open_files() is running. + EnterCriticalSection(&PSUTIL_CRITICAL_SECTION); + + if (psutil_enum_handles(&handlesList) != 0) + goto error; + + for (i = 0; i < handlesList->NumberOfHandles; i++) { + hHandle = &handlesList->Handles[i]; + if ((ULONG_PTR)hHandle->UniqueProcessId != dwPid) + continue; + if (!DuplicateHandle( + hProcess, + hHandle->HandleValue, + GetCurrentProcess(), + &hFile, + 0, + TRUE, + DUPLICATE_SAME_ACCESS + )) + { + // Will fail if not a regular file; just skip it. + continue; + } + + // This will set *globalFileName* global variable. + if (psutil_threaded_get_filename(hFile) != 0) + goto error; + + if ((globalFileName != NULL) && (globalFileName->Length > 0)) { + if (!pylist_append_obj( + py_retlist, + PyUnicode_FromWideChar( + globalFileName->Buffer, wcslen(globalFileName->Buffer) + ) + )) + goto error; + } + + // Loop cleanup section. + if (globalFileName != NULL) { + FREE(globalFileName); + globalFileName = NULL; + } + CloseHandle(hFile); + hFile = NULL; + } + + goto exit; + +error: + Py_XDECREF(py_retlist); + errorOccurred = TRUE; + goto exit; + +exit: + if (hFile != NULL) + CloseHandle(hFile); + if (globalFileName != NULL) { + FREE(globalFileName); + globalFileName = NULL; + } + if (handlesList != NULL) + FREE(handlesList); + + LeaveCriticalSection(&PSUTIL_CRITICAL_SECTION); + if (errorOccurred == TRUE) + return NULL; + return py_retlist; +} diff --git a/psutil/arch/windows/proc_info.c b/psutil/arch/windows/proc_info.c new file mode 100644 index 0000000000..c5906f4550 --- /dev/null +++ b/psutil/arch/windows/proc_info.c @@ -0,0 +1,829 @@ +/* + * 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. + +#include +#include + +#include "../../arch/all/init.h" + + +#ifndef _WIN64 +typedef NTSTATUS(NTAPI *__NtQueryInformationProcess)( + HANDLE ProcessHandle, + DWORD ProcessInformationClass, + PVOID ProcessInformation, + DWORD ProcessInformationLength, + 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 data +// can be read from it. +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))) { + psutil_oserror_wsyscall("VirtualQueryEx"); + return -1; + } + + *psize = info.RegionSize - ((char *)src - (char *)info.BaseAddress); + return 0; +} + + +enum psutil_process_data_kind { + KIND_CMDLINE, + KIND_CWD, + KIND_ENVIRON, +}; + + +static void +psutil_convert_winerr(ULONG err, char *syscall) { + char fullmsg[8192]; + + if (err == ERROR_NOACCESS) { + str_format(fullmsg, sizeof(fullmsg), "%s -> ERROR_NOACCESS", syscall); + psutil_debug(fullmsg); + psutil_oserror_ad(fullmsg); + } + else { + psutil_oserror_wsyscall(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); +} + + +static void +psutil_giveup_with_ad(NTSTATUS status, char *syscall) { + ULONG err; + char fullmsg[2048]; + + if (NT_NTWIN32(status)) + err = WIN32_FROM_NTSTATUS(status); + else + err = RtlNtStatusToDosErrorNoTeb(status); + str_format( + fullmsg, sizeof(fullmsg), "%s -> %lu (%s)", syscall, err, strerror(err) + ); + psutil_debug(fullmsg); + psutil_oserror_ad(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 +// 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 +) { + /* This function is quite complex because there are several cases to be + considered: + + Two cases are really simple: we (i.e. the python interpreter) and the + target process are both 32 bit or both 64 bit. In that case the memory + layout of the structures matches up and all is well. + + When we are 64 bit and the target process is 32 bit we need to use + custom 32 bit versions of the structures. + + When we are 32 bit and the target process is 64 bit we need to use + custom 64 bit version of the structures. Also we need to use separate + Wow64 functions to get the information. + + A few helper structs are defined above so that the compiler can handle + calculating the correct offsets. + + Additional help also came from the following sources: + + https://github.com/kohsuke/winp and + http://wj32.org/wp/2009/01/24/howto-get-the-command-line-of-processes/ + http://stackoverflow.com/a/14012919 + http://www.drdobbs.com/embracing-64-bit-windows/184401966 + */ + SIZE_T size = 0; + 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; +#endif + DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; + NTSTATUS status; + + hProcess = psutil_handle_from_pid(pid, access); + if (hProcess == NULL) + return -1; + +#ifdef _WIN64 + // 64 bit case. Check if the target is a 32 bit process running in + // WoW64 mode. + status = NtQueryInformationProcess( + hProcess, ProcessWow64Information, &ppeb32, sizeof(LPVOID), NULL + ); + + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQueryInformationProcess(ProcessWow64Information)" + ); + goto error; + } + + if (ppeb32 != NULL) { + // We are 64 bit. Target process is 32 bit running in WoW64 mode. + PEB32 peb32; + RTL_USER_PROCESS_PARAMETERS32 procParameters32; + + // read PEB + 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"); + goto error; + } + + // read process parameters + if (!ReadProcessMemory( + hProcess, + UlongToPtr(peb32.ProcessParameters), + &procParameters32, + sizeof(procParameters32), + NULL + )) + { + // May fail with ERROR_PARTIAL_COPY, see: + // https://github.com/giampaolo/psutil/issues/875 + psutil_convert_winerr(GetLastError(), "ReadProcessMemory"); + goto error; + } + + switch (kind) { + case KIND_CMDLINE: + src = UlongToPtr(procParameters32.CommandLine.Buffer), + size = procParameters32.CommandLine.Length; + break; + case KIND_CWD: + src = UlongToPtr(procParameters32.CurrentDirectoryPath.Buffer); + size = procParameters32.CurrentDirectoryPath.Length; + break; + case KIND_ENVIRON: + src = UlongToPtr(procParameters32.env); + break; + } + } + 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 + // (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)) + { + psutil_oserror_wsyscall("IsWow64Process"); + goto error; + } + + if (weAreWow64 && !theyAreWow64) { + // We are 32 bit running in WoW64 mode. Target process is 64 bit. + PROCESS_BASIC_INFORMATION64 pbi64; + PEB64 peb64; + RTL_USER_PROCESS_PARAMETERS64 procParameters64; + + if (NtWow64QueryInformationProcess64 == NULL) { + NtWow64QueryInformationProcess64 = psutil_GetProcAddressFromLib( + "ntdll.dll", "NtWow64QueryInformationProcess64" + ); + if (NtWow64QueryInformationProcess64 == NULL) { + PyErr_Clear(); + psutil_oserror_ad( + "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) { + PyErr_Clear(); + psutil_oserror_ad( + "can't query 64-bit process in 32-bit-WoW mode" + ); + goto error; + } + } + + status = NtWow64QueryInformationProcess64( + hProcess, ProcessBasicInformation, &pbi64, sizeof(pbi64), NULL + ); + if (!NT_SUCCESS(status)) { + // psutil_convert_ntstatus_err( + // status, + // "NtWow64QueryInformationProcess64(ProcessBasicInformation)"); + psutil_giveup_with_ad( + status, + "NtWow64QueryInformationProcess64(ProcessBasicInformation)" + ); + goto error; + } + + // read peb + status = NtWow64ReadVirtualMemory64( + 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)" + ); + goto error; + } + + // read process parameters + status = NtWow64ReadVirtualMemory64( + 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)" + ); + goto error; + } + + switch (kind) { + case KIND_CMDLINE: + src64 = procParameters64.CommandLine.Buffer; + size = procParameters64.CommandLine.Length; + break; + case KIND_CWD: + src64 = procParameters64.CurrentDirectoryPath.Buffer, + size = procParameters64.CurrentDirectoryPath.Length; + break; + case KIND_ENVIRON: + src64 = procParameters64.env; + break; + } + } + else +#endif + // Target process is of the same bitness as us. + { + PROCESS_BASIC_INFORMATION pbi; + PEB_ peb; + RTL_USER_PROCESS_PARAMETERS_ procParameters; + + status = 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, &peb, sizeof(peb), NULL + )) + { + // May fail with ERROR_PARTIAL_COPY, see: + // https://github.com/giampaolo/psutil/issues/875 + psutil_convert_winerr(GetLastError(), "ReadProcessMemory"); + goto error; + } + + // read process parameters + if (!ReadProcessMemory( + hProcess, + peb.ProcessParameters, + &procParameters, + sizeof(procParameters), + NULL + )) + { + // May fail with ERROR_PARTIAL_COPY, see: + // https://github.com/giampaolo/psutil/issues/875 + psutil_convert_winerr(GetLastError(), "ReadProcessMemory"); + goto error; + } + + switch (kind) { + case KIND_CMDLINE: + src = procParameters.CommandLine.Buffer; + size = procParameters.CommandLine.Length; + break; + case KIND_CWD: + src = procParameters.CurrentDirectoryPath.Buffer; + size = procParameters.CurrentDirectoryPath.Length; + break; + case KIND_ENVIRON: + src = procParameters.env; + break; + } + } + + if (kind == KIND_ENVIRON) { +#ifndef _WIN64 + if (weAreWow64 && !theyAreWow64) { + psutil_oserror_ad("can't query 64-bit process in 32-bit-WoW mode"); + goto error; + } + else +#endif + if (psutil_get_process_region_size(hProcess, src, &size) != 0) + goto error; + } + + buffer = calloc(size + 2, 1); + if (buffer == NULL) { + PyErr_NoMemory(); + goto error; + } + +#ifndef _WIN64 + if (weAreWow64 && !theyAreWow64) { + status = NtWow64ReadVirtualMemory64( + hProcess, src64, buffer, size, NULL + ); + if (!NT_SUCCESS(status)) { + // psutil_convert_ntstatus_err(status, + // "NtWow64ReadVirtualMemory64"); + psutil_giveup_with_ad(status, "NtWow64ReadVirtualMemory64"); + goto error; + } + } + else +#endif + 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"); + goto error; + } + + CloseHandle(hProcess); + + *pdata = buffer; + *psize = size; + + return 0; + +error: + if (hProcess != NULL) + CloseHandle(hProcess); + if (buffer != NULL) + free(buffer); + return -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; + ULONG bufLen = 0; + NTSTATUS status; + char *buffer = NULL; + WCHAR *bufWchar = NULL; + PUNICODE_STRING tmp = NULL; + size_t size; + int ProcessCommandLineInformation = 60; + + if (PSUTIL_WINVER < PSUTIL_WINDOWS_8_1) { + psutil_runtime_error("requires Windows 8.1+"); + goto error; + } + + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); + if (hProcess == NULL) + goto error; + + // get the right buf size + status = NtQueryInformationProcess( + hProcess, ProcessCommandLineInformation, NULL, 0, &bufLen + ); + + // https://github.com/giampaolo/psutil/issues/1501 + if (status == STATUS_NOT_FOUND) { + psutil_oserror_ad( + "NtQueryInformationProcess(ProcessBasicInformation) -> " + "STATUS_NOT_FOUND" + ); + goto error; + } + + if (status != STATUS_BUFFER_OVERFLOW && status != STATUS_BUFFER_TOO_SMALL + && status != STATUS_INFO_LENGTH_MISMATCH) + { + psutil_SetFromNTStatusErr( + status, "NtQueryInformationProcess(ProcessBasicInformation)" + ); + goto error; + } + + // allocate memory + buffer = calloc(bufLen, 1); + if (buffer == NULL) { + PyErr_NoMemory(); + goto error; + } + + // get the cmdline + status = NtQueryInformationProcess( + hProcess, ProcessCommandLineInformation, buffer, bufLen, &bufLen + ); + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQueryInformationProcess(ProcessCommandLineInformation)" + ); + goto error; + } + + // build the string + tmp = (PUNICODE_STRING)buffer; + size = wcslen(tmp->Buffer) + 1; + bufWchar = (WCHAR *)calloc(size, sizeof(WCHAR)); + if (bufWchar == NULL) { + PyErr_NoMemory(); + goto error; + } + wcscpy_s(bufWchar, size, tmp->Buffer); + *pdata = bufWchar; + *psize = size * sizeof(WCHAR); + free(buffer); + CloseHandle(hProcess); + return 0; + +error: + if (buffer != NULL) + free(buffer); + if (bufWchar != NULL) + free(bufWchar); + if (hProcess != NULL) + CloseHandle(hProcess); + return -1; +} + + +PyObject * +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; + 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 psutil_oserror_nsp("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 + // 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 + func_ret = psutil_cmdline_query_proc(pid, &data, &size); + if (func_ret != 0) + goto error; + + // attempt to parse the command line using Win32 API + szArglist = CommandLineToArgvW(data, &nArgs); + if (szArglist == NULL) { + psutil_oserror_wsyscall("CommandLineToArgvW"); + 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 error; + for (i = 0; i < nArgs; i++) { + py_unicode = PyUnicode_FromWideChar( + szArglist[i], wcslen(szArglist[i]) + ); + if (py_unicode == NULL) + goto error; + PyList_SetItem(py_retlist, i, py_unicode); + py_unicode = NULL; + } + + 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 NULL; +} + + +PyObject * +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 psutil_oserror_nsp("psutil_pid_is_running -> 0"); + if (pid_return == -1) + return NULL; + + if (psutil_get_process_data(pid, KIND_CWD, &data, &size) != 0) + goto out; + + // convert wchar array to a Python unicode string + ret = PyUnicode_FromWideChar(data, wcslen(data)); + +out: + if (data != NULL) + free(data); + + return ret; +} + + +PyObject * +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 PyUnicode_FromString(""); + + pid_return = psutil_pid_is_running(pid); + if (pid_return == 0) + return psutil_oserror_nsp("psutil_pid_is_running -> 0"); + if (pid_return == -1) + return NULL; + + if (psutil_get_process_data(pid, KIND_ENVIRON, &data, &size) != 0) + goto out; + + // convert wchar array to a Python unicode string + ret = PyUnicode_FromWideChar(data, size / 2); + +out: + if (data != NULL) + free(data); + return ret; +} + + +// 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 +) { + static ULONG initialBufferSize = 0x4000; + NTSTATUS status; + PVOID buffer; + ULONG bufferSize; + PSYSTEM_PROCESS_INFORMATION process; + + bufferSize = initialBufferSize; + buffer = malloc(bufferSize); + if (buffer == NULL) { + PyErr_NoMemory(); + goto error; + } + + while (TRUE) { + status = NtQuerySystemInformation( + SystemProcessInformation, buffer, bufferSize, &bufferSize + ); + if (status == STATUS_BUFFER_TOO_SMALL + || status == STATUS_INFO_LENGTH_MISMATCH) + { + free(buffer); + buffer = malloc(bufferSize); + if (buffer == NULL) { + PyErr_NoMemory(); + goto error; + } + } + else { + break; + } + } + + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemProcessInformation)" + ); + goto error; + } + + if (bufferSize <= 0x20000) + initialBufferSize = bufferSize; + + process = PSUTIL_FIRST_PROCESS(buffer); + do { + if ((ULONG_PTR)process->UniqueProcessId == pid) { + *retProcess = process; + *retBuffer = buffer; + return 0; + } + } while ((process = PSUTIL_NEXT_PROCESS(process))); + + psutil_oserror_nsp("NtQuerySystemInformation (no PID found)"); + goto error; + +error: + if (buffer != NULL) + free(buffer); + return -1; +} + + +// 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; + PSYSTEM_PROCESS_INFORMATION proc; + PVOID buffer = NULL; + ULONG i; + ULONG ctx_switches = 0; + double user_time; + double kernel_time; + double create_time; + PyObject *dict = PyDict_New(); + + 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; + + 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 + // 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 = psutil_LargeIntegerToUnixTime(proc->CreateTime); + } + + // 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; + 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); + return dict; + +error: + if (buffer != NULL) + free(buffer); + Py_DECREF(dict); + return NULL; +} diff --git a/psutil/arch/windows/proc_utils.c b/psutil/arch/windows/proc_utils.c new file mode 100644 index 0000000000..43ad7b6531 --- /dev/null +++ b/psutil/arch/windows/proc_utils.c @@ -0,0 +1,146 @@ +/* + * 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 // EnumProcesses + +#include "../../arch/all/init.h" + + +// Return 1 if PID exists, 0 if not, -1 on error. +int +psutil_pid_in_pids(DWORD pid) { + DWORD *pids_array = NULL; + int pids_count = 0; + int i; + + if (_psutil_pids(&pids_array, &pids_count) != 0) + return -1; + + for (i = 0; i < pids_count; i++) { + if (pids_array[i] == pid) { + free(pids_array); + return 1; + } + } + + free(pids_array); + return 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, 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". + psutil_oserror_nsp("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"); + psutil_oserror_ad("OpenProcess -> ERROR_SUCCESS"); + } + else { + psutil_debug("OpenProcess -> ERROR_SUCCESS turned into NSP"); + psutil_oserror_nsp("OpenProcess -> ERROR_SUCCESS"); + } + return NULL; + } + psutil_oserror_wsyscall("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 + if (exitCode == STILL_ACTIVE) { + return hProcess; + } + if (psutil_pid_in_pids(pid) == 1) { + return hProcess; + } + CloseHandle(hProcess); + psutil_oserror_nsp("GetExitCodeProcess != STILL_ACTIVE"); + return NULL; + } + + if (GetLastError() == ERROR_ACCESS_DENIED) { + psutil_debug("GetExitCodeProcess -> ERROR_ACCESS_DENIED (ignored)"); + SetLastError(0); + return hProcess; + } + psutil_oserror_wsyscall("GetExitCodeProcess"); + CloseHandle(hProcess); + return 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; + + if (pid == 0) { + // otherwise we'd get NoSuchProcess + return psutil_oserror_ad("automatically set for PID 0"); + } + + hProcess = OpenProcess(access, FALSE, pid); + + if ((hProcess == NULL) && (GetLastError() == ERROR_ACCESS_DENIED)) { + psutil_oserror_wsyscall("OpenProcess"); + return NULL; + } + + hProcess = psutil_check_phandle(hProcess, pid, 1); + return hProcess; +} + + +// Check for PID existence. Return 1 if pid exists, 0 if not, -1 on error. +int +psutil_pid_is_running(DWORD pid) { + HANDLE hProcess; + + // 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 (hProcess != NULL) { + hProcess = psutil_check_phandle(hProcess, pid, 1); + if (hProcess != NULL) { + CloseHandle(hProcess); + return 1; + } + CloseHandle(hProcess); + } + + PyErr_Clear(); + return psutil_pid_in_pids(pid); +} diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c deleted file mode 100644 index 356e236867..0000000000 --- a/psutil/arch/windows/process_handles.c +++ /dev/null @@ -1,523 +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 "process_handles.h" -#include "../../_psutil_common.h" - -static _NtQuerySystemInformation __NtQuerySystemInformation = NULL; -static _NtQueryObject __NtQueryObject = NULL; - -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; - - -PVOID -GetLibraryProcAddress(PSTR LibraryName, PSTR ProcName) { - return GetProcAddress(GetModuleHandleA(LibraryName), ProcName); -} - - -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); -} - - -VOID -psutil_get_open_files_init(BOOL threaded) { - if (g_initialized == TRUE) - return; - - // Resolve the Windows API calls - __NtQuerySystemInformation = - GetLibraryProcAddress("ntdll.dll", "NtQuerySystemInformation"); - __NtQueryObject = GetLibraryProcAddress("ntdll.dll", "NtQueryObject"); - - // 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); - } - - g_initialized = TRUE; -} - - -PyObject * -psutil_get_open_files_ntqueryobject(long 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; - DWORD i = 0; - BOOLEAN error = FALSE; - DWORD dwWait = 0; - PyObject* py_retlist = NULL; - PyObject* py_path = NULL; - - if (g_initialized == FALSE) - psutil_get_open_files_init(TRUE); - - // Due to the use of global variables, ensure only 1 call - // to psutil_get_open_files() is running - EnterCriticalSection(&g_cs); - - if (__NtQuerySystemInformation == NULL || - __NtQueryObject == NULL || - 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) { - HeapFree(GetProcessHeap(), 0, 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); - - 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)) { - PyErr_SetFromWindowsErr(HRESULT_FROM_NT(status)); - 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 (hHandle->UniqueProcessId != (HANDLE)dwPid) - goto loop_cleanup; - - if (!DuplicateHandle(hProcess, - hHandle->HandleValue, - GetCurrentProcess(), - &g_hFile, - 0, - TRUE, - DUPLICATE_SAME_ACCESS)) - { - /* - printf("[%d] DuplicateHandle (%#x): %#x \n", - dwPid, - hHandle->HandleValue, - GetLastError()); - */ - goto loop_cleanup; - } - - // Guess buffer size is MAX_PATH + 1 - g_dwLength = (MAX_PATH+1) * sizeof(WCHAR); - - do { - // Release any previously allocated buffer - if (g_pNameBuffer != NULL) { - HeapFree(GetProcessHeap(), 0, 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 = HeapAlloc(GetProcessHeap(), - HEAP_ZERO_MEMORY, - g_dwSize); - - if (g_pNameBuffer == NULL) - goto loop_cleanup; - } - - dwWait = psutil_NtQueryObject(); - - // 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) { - /* - 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); - 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; - } - } - -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; - } - -cleanup: - 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 (pHandleInfo != NULL) - HeapFree(GetProcessHeap(), 0, pHandleInfo); - pHandleInfo = NULL; - - if (error) { - Py_XDECREF(py_retlist); - py_retlist = NULL; - } - - LeaveCriticalSection(&g_cs); - - return py_retlist; -} - - -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 * -psutil_get_open_files_getmappedfilename(long 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; - TCHAR pszFilename[MAX_PATH+1]; - - 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) { - error = TRUE; - goto cleanup; - } - - do { - if (pHandleInfo != NULL) { - HeapFree(GetProcessHeap(), 0, 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); - - 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)) { - PyErr_SetFromWindowsErr(HRESULT_FROM_NT(status)); - 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 (hHandle->UniqueProcessId != (HANDLE)dwPid) - goto loop_cleanup; - - if (!DuplicateHandle(hProcess, - hHandle->HandleValue, - GetCurrentProcess(), - &hFile, - 0, - 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, 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; - } - -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) - HeapFree(GetProcessHeap(), 0, pHandleInfo); - pHandleInfo = NULL; - - if (error) { - Py_XDECREF(py_retlist); - py_retlist = NULL; - } - - return py_retlist; -} diff --git a/psutil/arch/windows/process_handles.h b/psutil/arch/windows/process_handles.h deleted file mode 100644 index 4a022c1c12..0000000000 --- a/psutil/arch/windows/process_handles.h +++ /dev/null @@ -1,111 +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. - */ - -#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 deleted file mode 100644 index ffd3c80ef4..0000000000 --- a/psutil/arch/windows/process_info.c +++ /dev/null @@ -1,970 +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_windows module methods. - */ - -#include -#include -#include -#include - -#include "security.h" -#include "process_info.h" -#include "ntextapi.h" -#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. -// ==================================================================== - -// 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 { - 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)( - IN HANDLE ProcessHandle, - IN PVOID64 BaseAddress, - OUT PVOID Buffer, - IN ULONG64 Size, - OUT PULONG64 NumberOfBytesRead); - -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; - 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 - - -#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) - -const int STATUS_INFO_LENGTH_MISMATCH = 0xC0000004; -const int STATUS_BUFFER_TOO_SMALL = 0xC0000023L; - - -// ==================================================================== -// Process and PIDs utiilties. -// ==================================================================== - - -/* - * 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) - return PyErr_SetFromWindowsErr(0); - else // -2 - 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_waccess(DWORD pid, DWORD dwDesiredAccess) { - HANDLE hProcess; - - if (pid == 0) { - // otherwise we'd get NoSuchProcess - return AccessDenied(""); - } - - hProcess = OpenProcess(dwDesiredAccess, FALSE, pid); - return psutil_check_phandle(hProcess, pid); -} - - -/* - * 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 - // 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; - 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_INFORMATION | PROCESS_VM_READ, - 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_SetFromWindowsErr(err); - 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_SetFromWindowsErr(0); - return -1; - } - } -} - - -/* 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; - - if (!VirtualQueryEx(hProcess, src, &info, sizeof(info))) { - PyErr_SetFromWindowsErr(0); - return -1; - } - - *psize = info.RegionSize - ((char*)src - (char*)info.BaseAddress); - return 0; -} - - -#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 = - (_NtWow64QueryVirtualMemory64)GetProcAddress( - GetModuleHandleA("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, - 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. */ -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: - - Two cases are really simple: we (i.e. the python interpreter) and the - target process are both 32 bit or both 64 bit. In that case the memory - layout of the structures matches up and all is well. - - When we are 64 bit and the target process is 32 bit we need to use - custom 32 bit versions of the structures. - - When we are 32 bit and the target process is 64 bit we need to use - custom 64 bit version of the structures. Also we need to use separate - Wow64 functions to get the information. - - A few helper structs are defined above so that the compiler can handle - calculating the correct offsets. - - Additional help also came from the following sources: - - https://github.com/kohsuke/winp and - http://wj32.org/wp/2009/01/24/howto-get-the-command-line-of-processes/ - http://stackoverflow.com/a/14012919 - http://www.drdobbs.com/embracing-64-bit-windows/184401966 - */ - static _NtQueryInformationProcess NtQueryInformationProcess = NULL; -#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; -#else - PVOID64 src64; - BOOL weAreWow64; - BOOL theyAreWow64; -#endif - - hProcess = psutil_handle_from_pid(pid); - if (hProcess == NULL) - return -1; - - if (NtQueryInformationProcess == NULL) { - NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress( - GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess"); - } - -#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))) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - if (ppeb32 != NULL) { - /* We are 64 bit. Target process is 32 bit running in WoW64 mode. */ - PEB32 peb32; - RTL_USER_PROCESS_PARAMETERS32 procParameters32; - - // read PEB - if (!ReadProcessMemory(hProcess, ppeb32, &peb32, sizeof(peb32), NULL)) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - // read process parameters - if (!ReadProcessMemory(hProcess, - UlongToPtr(peb32.ProcessParameters), - &procParameters32, - sizeof(procParameters32), - NULL)) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - switch (kind) { - case KIND_CMDLINE: - src = UlongToPtr(procParameters32.CommandLine.Buffer), - size = procParameters32.CommandLine.Length; - break; - case KIND_CWD: - src = UlongToPtr(procParameters32.CurrentDirectoryPath.Buffer); - size = procParameters32.CurrentDirectoryPath.Length; - break; - case KIND_ENVIRON: - src = UlongToPtr(procParameters32.env); - break; - } - } else -#else - /* 32 bit case. Check if the target is also 32 bit. */ - if (!IsWow64Process(GetCurrentProcess(), &weAreWow64) || - !IsWow64Process(hProcess, &theyAreWow64)) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - if (weAreWow64 && !theyAreWow64) { - /* We are 32 bit running in WoW64 mode. Target process is 64 bit. */ - PROCESS_BASIC_INFORMATION64 pbi64; - PEB64 peb64; - RTL_USER_PROCESS_PARAMETERS64 procParameters64; - - if (NtWow64QueryInformationProcess64 == NULL) { - NtWow64QueryInformationProcess64 = - (_NtQueryInformationProcess)GetProcAddress( - GetModuleHandleA("ntdll.dll"), - "NtWow64QueryInformationProcess64"); - - if (NtWow64QueryInformationProcess64 == NULL) { - PyErr_SetString(PyExc_NotImplementedError, - "NtWow64QueryInformationProcess64 missing"); - goto error; - } - } - - if (!NT_SUCCESS(NtWow64QueryInformationProcess64( - hProcess, - ProcessBasicInformation, - &pbi64, - sizeof(pbi64), - NULL))) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - // read peb - if (NtWow64ReadVirtualMemory64 == NULL) { - NtWow64ReadVirtualMemory64 = - (_NtWow64ReadVirtualMemory64)GetProcAddress( - GetModuleHandleA("ntdll.dll"), - "NtWow64ReadVirtualMemory64"); - - if (NtWow64ReadVirtualMemory64 == NULL) { - PyErr_SetString(PyExc_NotImplementedError, - "NtWow64ReadVirtualMemory64 missing"); - goto error; - } - } - - 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))) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - switch (kind) { - case KIND_CMDLINE: - src64 = procParameters64.CommandLine.Buffer; - size = procParameters64.CommandLine.Length; - break; - case KIND_CWD: - src64 = procParameters64.CurrentDirectoryPath.Buffer, - size = procParameters64.CurrentDirectoryPath.Length; - break; - case KIND_ENVIRON: - src64 = procParameters64.env; - break; - } - } 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))) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - // read peb - if (!ReadProcessMemory(hProcess, - pbi.PebBaseAddress, - &peb, - sizeof(peb), - NULL)) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - // read process parameters - if (!ReadProcessMemory(hProcess, - peb.ProcessParameters, - &procParameters, - sizeof(procParameters), - NULL)) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - switch (kind) { - case KIND_CMDLINE: - src = procParameters.CommandLine.Buffer; - size = procParameters.CommandLine.Length; - break; - case KIND_CWD: - src = procParameters.CurrentDirectoryPath.Buffer; - size = procParameters.CurrentDirectoryPath.Length; - break; - case KIND_ENVIRON: - src = procParameters.env; - break; - } - } - - 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; - } - else -#endif - if (psutil_get_process_region_size(hProcess, src, &size) != 0) - goto error; - } - - buffer = calloc(size + 2, 1); - - if (buffer == NULL) { - PyErr_NoMemory(); - goto error; - } - -#ifndef _WIN64 - if (weAreWow64 && !theyAreWow64) { - if (!NT_SUCCESS(NtWow64ReadVirtualMemory64(hProcess, - src64, - buffer, - size, - NULL))) { - PyErr_SetFromWindowsErr(0); - goto error; - } - } else -#endif - if (!ReadProcessMemory(hProcess, src, buffer, size, NULL)) { - PyErr_SetFromWindowsErr(0); - goto error; - } - - CloseHandle(hProcess); - - *pdata = buffer; - *psize = size; - - return 0; - -error: - if (hProcess != NULL) - CloseHandle(hProcess); - if (buffer != NULL) - free(buffer); - return -1; -} - -/* - * 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; - WCHAR *data = NULL; - SIZE_T size; - PyObject *py_retlist = NULL; - PyObject *py_unicode = NULL; - LPWSTR *szArglist = NULL; - int nArgs, i; - - if (psutil_get_process_data(pid, KIND_CMDLINE, &data, &size) != 0) - goto out; - - // attempt to parse the command line using Win32 API - szArglist = CommandLineToArgvW(data, &nArgs); - if (szArglist == NULL) { - PyErr_SetFromWindowsErr(0); - goto out; - } - - // 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; - for (i = 0; i < nArgs; i++) { - py_unicode = PyUnicode_FromWideChar(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); - free(data); - Py_XDECREF(py_unicode); - Py_XDECREF(py_retlist); - - return ret; -} - -PyObject *psutil_get_cwd(long pid) { - PyObject *ret = NULL; - WCHAR *data = NULL; - SIZE_T size; - - if (psutil_get_process_data(pid, KIND_CWD, &data, &size) != 0) - goto out; - - // convert wchar array to a Python unicode string - ret = PyUnicode_FromWideChar(data, wcslen(data)); - -out: - free(data); - - 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 *ret = NULL; - WCHAR *data = NULL; - SIZE_T size; - - if (psutil_get_process_data(pid, KIND_ENVIRON, &data, &size) != 0) - goto out; - - // convert wchar array to a Python unicode string - ret = PyUnicode_FromWideChar(data, size / 2); - -out: - free(data); - - return ret; -} - - -/* - * Given a process PID and a PSYSTEM_PROCESS_INFORMATION structure - * fills the structure 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. - * On success return 1, else 0 with Python exception already set. - */ -int -psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, - PVOID *retBuffer) { - static ULONG initialBufferSize = 0x4000; - NTSTATUS status; - 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"); - - bufferSize = initialBufferSize; - buffer = malloc(bufferSize); - if (buffer == NULL) { - PyErr_NoMemory(); - goto error; - } - - while (TRUE) { - status = NtQuerySystemInformation(SystemProcessInformation, buffer, - bufferSize, &bufferSize); - - if (status == STATUS_BUFFER_TOO_SMALL || - status == STATUS_INFO_LENGTH_MISMATCH) - { - free(buffer); - buffer = malloc(bufferSize); - if (buffer == NULL) { - PyErr_NoMemory(); - goto error; - } - } - else { - break; - } - } - - if (status != 0) { - PyErr_Format( - PyExc_RuntimeError, "NtQuerySystemInformation() syscall failed"); - goto error; - } - - if (bufferSize <= 0x20000) - initialBufferSize = bufferSize; - - process = PSUTIL_FIRST_PROCESS(buffer); - do { - if (process->UniqueProcessId == (HANDLE)pid) { - *retProcess = process; - *retBuffer = buffer; - return 1; - } - } while ( (process = PSUTIL_NEXT_PROCESS(process)) ); - - NoSuchProcess(""); - 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 deleted file mode 100644 index a2f70c2b95..0000000000 --- a/psutil/arch/windows/process_info.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. - */ - -#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)) - - -DWORD* psutil_get_pids(DWORD *numberOfReturnedPIDs); -HANDLE psutil_handle_from_pid(DWORD pid); -HANDLE psutil_handle_from_pid_waccess(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); -PyObject* psutil_get_cwd(long pid); -PyObject* psutil_get_environ(long pid); - -#endif diff --git a/psutil/arch/windows/security.c b/psutil/arch/windows/security.c index 331d96223c..019c41ad34 100644 --- a/psutil/arch/windows/security.c +++ b/psutil/arch/windows/security.c @@ -2,137 +2,48 @@ * 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. */ +// Security related functions for Windows platform (Set privileges such +// as SE DEBUG). + #include #include +#include "../../arch/all/init.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)) { + psutil_oserror_wsyscall("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; + if (!AdjustTokenPrivileges( + hToken, + FALSE, + &tp, + sizeof(TOKEN_PRIVILEGES), + &tpPrevious, + &cbPrevious + )) + { + psutil_oserror_wsyscall("AdjustTokenPrivileges"); + return -1; + } - // second pass. set privilege based on previous setting + // Second pass. Set privilege based on previous setting. tpPrevious.PrivilegeCount = 1; tpPrevious.Privileges[0].Luid = luid; @@ -142,84 +53,81 @@ 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 + )) + { + psutil_oserror_wsyscall("AdjustTokenPrivileges"); + return -1; + } - return TRUE; + return 0; } -int -psutil_set_se_debug() { - HANDLE hToken; - if (! OpenThreadToken(GetCurrentThread(), - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - FALSE, - &hToken) - ) { +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)) { - CloseHandle(hToken); - return 0; + psutil_oserror_wsyscall("ImpersonateSelf"); + return NULL; } - if (!OpenThreadToken(GetCurrentThread(), - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - FALSE, - &hToken) - ) { - RevertToSelf(); - CloseHandle(hToken); - return 0; + if (!OpenProcessToken( + me, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken + )) + { + psutil_oserror_wsyscall("OpenProcessToken"); + return NULL; } } + else { + psutil_oserror_wsyscall("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 (! OpenThreadToken(GetCurrentThread(), - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - FALSE, - &hToken) - ) { - if (GetLastError() == ERROR_NO_TOKEN) { - if (! ImpersonateSelf(SecurityImpersonation)) - return 0; - if (!OpenThreadToken(GetCurrentThread(), - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - FALSE, - &hToken)) - { - return 0; - } - } - } - // 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 deleted file mode 100644 index aa8a22ad1a..0000000000 --- a/psutil/arch/windows/security.h +++ /dev/null @@ -1,17 +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 - -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/psutil/arch/windows/sensors.c b/psutil/arch/windows/sensors.c new file mode 100644 index 0000000000..169324aa24 --- /dev/null +++ b/psutil/arch/windows/sensors.c @@ -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 +#include + +#include "../../arch/all/init.h" + + +// 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) { + psutil_oserror(); + return NULL; + } + return Py_BuildValue( + "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, + (double)sps.BatteryLifePercent, // percent + sps.BatteryLifeTime // remaining secs + ); +} diff --git a/psutil/arch/windows/services.c b/psutil/arch/windows/services.c index 62a12861f3..9944c13fdb 100644 --- a/psutil/arch/windows/services.c +++ b/psutil/arch/windows/services.c @@ -8,36 +8,70 @@ #include #include -#include "services.h" -#include "../../_psutil_common.h" +#include "../../arch/all/init.h" + // ================================================================== // utils // ================================================================== + SC_HANDLE -psutil_get_service_handler(char *service_name, DWORD scm_access, DWORD access) -{ - ENUM_SERVICE_STATUS_PROCESSW *lpService = NULL; +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) { - PyErr_SetFromWindowsErr(0); + psutil_oserror_wsyscall("OpenSCManagerW"); return NULL; } - hService = OpenService(sc, service_name, access); + + hService = OpenServiceW(sc, service_name, access); if (hService == NULL) { + psutil_oserror_wsyscall("OpenServiceW"); CloseServiceHandle(sc); - PyErr_SetFromWindowsErr(0); 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) { @@ -48,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"; } @@ -90,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; @@ -104,7 +134,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; @@ -113,7 +142,7 @@ psutil_winservice_enumerate(PyObject *self, PyObject *args) { sc = OpenSCManager(NULL, NULL, SC_MANAGER_ENUMERATE_SERVICE); if (sc == NULL) { - PyErr_SetFromWindowsErr(0); + psutil_oserror_wsyscall("OpenSCManager"); return NULL; } @@ -128,38 +157,37 @@ 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; // 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. @@ -170,7 +198,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); @@ -180,64 +207,65 @@ 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) { - char *service_name; + wchar_t *service_name = NULL; 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; 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. bytesNeeded = 0; QueryServiceConfigW(hService, NULL, 0, &bytesNeeded); if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - PyErr_SetFromWindowsErr(0); + psutil_oserror_wsyscall("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) { - PyErr_SetFromWindowsErr(0); + if (!ok) { + psutil_oserror_wsyscall("QueryServiceConfigW"); goto error; } // 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; @@ -258,6 +286,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: @@ -265,221 +294,238 @@ 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; } -/* - * Get service status information. Returns: - * - status - * - pid - */ +// Get service status information. Returns (status, pid) 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; - DWORD resumeHandle = 0; - DWORD dwBytes = 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. - 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. CloseServiceHandle(hService); - return Py_BuildValue("s", ""); + PyMem_Free(service_name); + return PyUnicode_FromString(""); } if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - PyErr_SetFromWindowsErr(0); + psutil_oserror_wsyscall("QueryServiceStatusEx"); goto error; } + ssp = (SERVICE_STATUS_PROCESS *)HeapAlloc( - GetProcessHeap(), 0, bytesNeeded); + GetProcessHeap(), 0, bytesNeeded + ); if (ssp == NULL) { PyErr_NoMemory(); goto error; } // Actual call. - ok = QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, (LPBYTE)ssp, - bytesNeeded, &bytesNeeded); - if (ok == 0) { - PyErr_SetFromWindowsErr(0); + ok = QueryServiceStatusEx( + hService, + SC_STATUS_PROCESS_INFO, + (LPBYTE)ssp, + bytesNeeded, + &bytesNeeded + ); + if (!ok) { + psutil_oserror_wsyscall("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; 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; - DWORD resumeHandle = 0; - DWORD dwBytes = 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); - if (GetLastError() == ERROR_MUI_FILE_NOT_FOUND) { - // Also services.msc fails in the same manner, so we return an + QueryServiceConfig2W( + hService, SERVICE_CONFIG_DESCRIPTION, NULL, 0, &bytesNeeded + ); + + 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", ""); + PyMem_Free(service_name); + return PyUnicode_FromString(""); } + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - PyErr_SetFromWindowsErr(0); + psutil_oserror_wsyscall("QueryServiceConfig2W"); goto error; } scd = (SERVICE_DESCRIPTIONW *)malloc(bytesNeeded); - ok = QueryServiceConfig2W(hService, SERVICE_CONFIG_DESCRIPTION, - (LPBYTE)scd, bytesNeeded, &bytesNeeded); - if (ok == 0) { - PyErr_SetFromWindowsErr(0); + if (scd == NULL) { + PyErr_NoMemory(); + goto error; + } + + ok = QueryServiceConfig2W( + hService, + SERVICE_CONFIG_DESCRIPTION, + (LPBYTE)scd, + bytesNeeded, + &bytesNeeded + ); + if (!ok) { + psutil_oserror_wsyscall("QueryServiceConfig2W"); goto error; } if (scd->lpDescription == NULL) { - py_retstr = Py_BuildValue("s", ""); + py_retstr = PyUnicode_FromString(""); } 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; } -/* - * 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) { - 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) { - PyErr_SetFromWindowsErr(0); + if (!ok) { + psutil_oserror_wsyscall("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; } -/* - * 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) { - 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) { - PyErr_SetFromWindowsErr(0); + if (!ok) { + psutil_oserror_wsyscall("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; } diff --git a/psutil/arch/windows/services.h b/psutil/arch/windows/services.h deleted file mode 100644 index 286ed232c9..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 new file mode 100644 index 0000000000..8ed25a2801 --- /dev/null +++ b/psutil/arch/windows/socks.c @@ -0,0 +1,459 @@ +/* + * 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 "../../arch/all/init.h" + + +#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 +// the data. Therefore we 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; + 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); + + table = malloc(size); + if (table == NULL) { + PyErr_NoMemory(); + return NULL; + } + + 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"); + return __GetExtendedTcpTable(family); + } + + psutil_runtime_error("GetExtendedTcpTable failed"); + return NULL; +} + + +static PVOID +__GetExtendedUdpTable(ULONG family) { + DWORD err; + PVOID table; + ULONG size = 0; + UDP_TABLE_CLASS class = UDP_TABLE_OWNER_PID; + + GetExtendedUdpTable(NULL, &size, FALSE, family, class, 0); + // reserve 25% more space + size = size + (size / 2 / 2); + + table = malloc(size); + if (table == NULL) { + PyErr_NoMemory(); + return NULL; + } + + 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"); + return __GetExtendedUdpTable(family); + } + + psutil_runtime_error("GetExtendedUdpTable failed"); + return NULL; +} + + +#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}; + DWORD pid; + int pid_return; + PVOID table = NULL; + 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 = 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); + + 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(); + 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 psutil_oserror_nsp("psutil_pid_is_running"); + } + 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_addr_tuple_local = NULL; + py_addr_tuple_remote = NULL; + + table = __GetExtendedTcpTable(AF_INET); + if (table == NULL) + 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; + 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; + 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 (!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_addr_tuple_local = NULL; + py_addr_tuple_remote = NULL; + } + + free(table); + table = NULL; + } + + // TCP IPv6 + if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) + && (PySequence_Contains(py_type_filter, _SOCK_STREAM) == 1) + && (RtlIpv6AddressToStringA != NULL)) + { + table = NULL; + py_addr_tuple_local = NULL; + py_addr_tuple_remote = NULL; + + table = __GetExtendedTcpTable(AF_INET6); + if (table == NULL) + 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); + 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); + 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 (!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_addr_tuple_local = NULL; + py_addr_tuple_remote = NULL; + } + + free(table); + table = NULL; + } + + // UDP IPv4 + + if ((PySequence_Contains(py_af_filter, _AF_INET) == 1) + && (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1)) + { + table = NULL; + py_addr_tuple_local = NULL; + table = __GetExtendedUdpTable(AF_INET); + if (table == NULL) + 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; + 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 (!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_addr_tuple_local = NULL; + } + + free(table); + table = NULL; + } + + // UDP IPv6 + + if ((PySequence_Contains(py_af_filter, _AF_INET6) == 1) + && (PySequence_Contains(py_type_filter, _SOCK_DGRAM) == 1) + && (RtlIpv6AddressToStringA != NULL)) + { + table = NULL; + py_addr_tuple_local = NULL; + table = __GetExtendedUdpTable(AF_INET6); + if (table == NULL) + 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); + 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; + + 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_addr_tuple_local = NULL; + } + + free(table); + table = NULL; + } + + psutil_conn_decref_objs(); + return py_retlist; + +error: + psutil_conn_decref_objs(); + 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/sys.c b/psutil/arch/windows/sys.c new file mode 100644 index 0000000000..1672b69e09 --- /dev/null +++ b/psutil/arch/windows/sys.c @@ -0,0 +1,231 @@ +/* + * 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 "../../arch/all/init.h" + + +// System boot time expressed in seconds since the UNIX epoch. +// +// Read boot time atomically from the kernel via +// NtQuerySystemInformation(SystemTimeOfDayInformation), so the value +// is identical across processes and has zero differences between calls +// (fixes issue #1007 for the second time, which was caused by sampling +// `time.time()` and `cext.uptime()` as two separate Python calls). +// +// This function is subject to system clock updates / NTP (it's by +// contract and documented). It also takes into account the time spent +// in suspend / hibernate mode, so that it returns the same result +// on wakeup. +PyObject * +psutil_boot_time(PyObject *self, PyObject *args) { + SYSTEM_TIMEOFDAY_INFORMATION info; + NTSTATUS status; + LARGE_INTEGER boot; + + status = NtQuerySystemInformation( + SystemTimeOfDayInformation, &info, sizeof(info), NULL + ); + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemTimeOfDayInformation)" + ); + return NULL; + } + // Both BootTime and SleepTimeBias are in 100-ns units. + boot.QuadPart = info.BootTime.QuadPart - (LONGLONG)info.SleepTimeBias; + return Py_BuildValue("d", psutil_LargeIntegerToUnixTime(boot)); +} + + +// Commented out in favor of psutil_boot_time() above. + +/* +// 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 / hibernate. +PyObject * +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); +} +*/ + + +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_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; + } + psutil_oserror_wsyscall("WTSEnumerateSessionsW"); + goto error; + } + + for (i = 0; i < count; i++) { + py_address = 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) + { + psutil_oserror_wsyscall("WTSQuerySessionInformationW"); + goto error; + } + if (bytes <= 2) + continue; + + // address + bytes = 0; + if (WTSQuerySessionInformationW( + hServer, sessionId, WTSClientAddress, &buffer_addr, &bytes + ) + == 0) + { + psutil_oserror_wsyscall("WTSQuerySessionInformationW"); + goto error; + } + + address = (PWTS_CLIENT_ADDRESS)buffer_addr; + if (address->AddressFamily == 2) { // AF_INET == 2 + str_format( + 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. + address->Address[2], + address->Address[3], + address->Address[4], + address->Address[5] + ); + py_address = PyUnicode_FromString(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) + { + psutil_oserror_wsyscall("WTSQuerySessionInformationW"); + goto error; + } + wts_info = (PWTSINFOW)buffer_info; + + py_username = PyUnicode_FromWideChar(buffer_user, wcslen(buffer_user)); + if (py_username == NULL) + goto error; + + 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); + } + + WTSFreeMemory(sessions); + WTSFreeMemory(buffer_user); + WTSFreeMemory(buffer_addr); + WTSFreeMemory(buffer_info); + return py_retlist; + +error: + Py_XDECREF(py_username); + 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/wmi.c b/psutil/arch/windows/wmi.c new file mode 100644 index 0000000000..043a367597 --- /dev/null +++ b/psutil/arch/windows/wmi.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. + */ + +// Functions related to the Windows Management Instrumentation API. + +#include +#include +#include + +#include "../../arch/all/init.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.9834714538216174894737477501 +#define LOADAVG_FACTOR_15F 0.9944598480048967508795473394 +// 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; + +// clang-format off +#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 +// clang-format on + + +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 + ); + // Skip updating the load if we can't get the value successfully + if (err != ERROR_SUCCESS) { + return; + } + 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); +} + + +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) { + psutil_runtime_error("PdhOpenQueryW failed"); + return NULL; + } + + s = PdhAddEnglishCounterW(hQuery, szCounterPath, 0, &hCounter); + if (s != ERROR_SUCCESS) { + psutil_runtime_error( + "PdhAddEnglishCounterW failed. Performance counters may be " + "disabled." + ); + return NULL; + } + + event = CreateEventW(NULL, FALSE, FALSE, L"LoadUpdateEvent"); + if (event == NULL) { + psutil_oserror_wsyscall("CreateEventW"); + return NULL; + } + + s = PdhCollectQueryDataEx(hQuery, SAMPLING_INTERVAL, event); + if (s != ERROR_SUCCESS) { + psutil_runtime_error("PdhCollectQueryDataEx failed"); + return NULL; + } + + ret = RegisterWaitForSingleObject( + &waitHandle, + event, + (WAITORTIMERCALLBACK)LoadAvgCallback, + (PVOID)hCounter, + INFINITE, + WT_EXECUTEDEFAULT + ); + + if (ret == 0) { + psutil_oserror_wsyscall("RegisterWaitForSingleObject"); + return NULL; + } + + Py_RETURN_NONE; +} + + +// 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); + 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 + ); +} diff --git a/psutil/tests/README.rst b/psutil/tests/README.rst deleted file mode 100644 index 515abf7729..0000000000 --- a/psutil/tests/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -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 whish to hack - on psutil:: - - make setup-dev-env # install test deps (+ other things) - make test - -* 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, OSX) 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 deleted file mode 100644 index dcdbd4fa82..0000000000 --- a/psutil/tests/__init__.py +++ /dev/null @@ -1,1202 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -""" -Test utilities. -""" - -from __future__ import print_function - -import atexit -import contextlib -import ctypes -import errno -import functools -import os -import random -import re -import select -import shutil -import socket -import stat -import subprocess -import sys -import tempfile -import textwrap -import threading -import time -import traceback -import warnings -from socket import AF_INET -from socket import AF_INET6 -from socket import SOCK_DGRAM -from socket import SOCK_STREAM - -import psutil -from psutil import OSX -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._common import supports_ipv6 -from psutil._compat import PY3 -from psutil._compat import u -from psutil._compat import unicode -from psutil._compat import which - -if sys.version_info < (2, 7): - import unittest2 as unittest # requires "pip install unittest2" -else: - import unittest - -try: - from unittest import mock # py3 -except ImportError: - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - import mock # NOQA - requires "pip install mock" - -if sys.version_info >= (3, 4): - import enum -else: - enum = None - - -__all__ = [ - # constants - 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'MEMORY_TOLERANCE', 'NO_RETRIES', - 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFILE_PREFIX', - 'TESTFN', 'TESTFN_UNICODE', 'TOX', 'TRAVIS', 'VALID_PROC_STATUSES', - '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", - "HAS_SENSORS_TEMPERATURES", "HAS_MEMORY_FULL_INFO", - # subprocesses - 'pyrun', 'reap_children', 'get_test_subprocess', 'create_zombie_proc', - 'create_proc_children_pair', - # threads - 'ThreadTask' - # test utils - 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', - 'retry_before_failing', 'run_test_module_by_name', 'get_suite', - 'run_suite', - # install utils - 'install_pip', 'install_test_deps', - # fs utils - 'chdir', 'safe_rmpath', 'create_exe', 'decode_path', 'encode_path', - 'unique_filename', - # os - 'get_winver', 'get_kernel_version', - # sync primitives - 'call_until', 'wait_for_pid', 'wait_for_file', - # network - 'check_connection_ntuple', 'check_net_address', - 'get_free_port', 'unix_socket_path', 'bind_socket', 'bind_unix_socket', - 'tcp_socketpair', 'unix_socketpair', 'create_sockets', - # compat - 'reload_module', 'import_module_by_path', - # others - 'warn', 'copyload_shared_lib', 'is_namedtuple', -] - - -# =================================================================== -# --- constants -# =================================================================== - -# --- platforms - -TOX = os.getenv('TOX') or '' in ('1', 'true') -PYPY = '__pypy__' in sys.builtin_module_names -WIN_VISTA = (6, 0, 0) if WINDOWS else None -# whether we're running this test suite on Travis (https://travis-ci.org/) -TRAVIS = bool(os.environ.get('TRAVIS')) -# whether we're running this test suite on Appveyor for Windows -# (http://www.appveyor.com/) -APPVEYOR = bool(os.environ.get('APPVEYOR')) - -# --- configurable defaults - -# how many times retry_before_failing() decorator will retry -NO_RETRIES = 10 -# bytes tolerance for system-wide memory related tests -MEMORY_TOLERANCE = 500 * 1024 # 500KB -# the timeout used in functions which have to wait -GLOBAL_TIMEOUT = 3 -# 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: - NO_RETRIES *= 3 - GLOBAL_TIMEOUT *= 3 - -# --- files - -TESTFILE_PREFIX = '$testfn' -TESTFN = os.path.join(os.path.realpath(os.getcwd()), TESTFILE_PREFIX) -_TESTFN = TESTFN + '-internal' -TESTFN_UNICODE = TESTFN + u("-ƒőő") -ASCII_FS = sys.getfilesystemencoding().lower() in ('ascii', 'us-ascii') - -# --- paths - -ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) -SCRIPTS_DIR = os.path.join(ROOT_DIR, 'scripts') -HERE = os.path.abspath(os.path.dirname(__file__)) - -# --- support - -HAS_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_PROC_CPU_NUM = hasattr(psutil.Process, "cpu_num") -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()) -HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") -HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") - -# --- misc - - -def _get_py_exe(): - def attempt(exe): - try: - subprocess.check_call( - [exe, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except Exception: - return None - else: - return exe - - if OSX: - exe = \ - attempt(sys.executable) or \ - attempt(os.path.realpath(sys.executable)) or \ - attempt(which("python%s.%s" % sys.version_info[:2])) or \ - attempt(psutil.Process().exe()) - if not exe: - raise ValueError("can't find python exe real abspath") - return exe - else: - exe = os.path.realpath(sys.executable) - assert os.path.exists(exe), exe - return exe - - -PYTHON_EXE = _get_py_exe() -DEVNULL = open(os.devnull, 'r+') -VALID_PROC_STATUSES = [getattr(psutil, x) for x in dir(psutil) - if x.startswith('STATUS_')] -AF_UNIX = getattr(socket, "AF_UNIX", object()) -SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) - -_subprocesses_started = set() -_pids_started = set() -_testfiles_created = set() - - -@atexit.register -def _cleanup_files(): - DEVNULL.close() - for name in os.listdir(u('.')): - if isinstance(name, unicode): - prefix = u(TESTFILE_PREFIX) - else: - prefix = TESTFILE_PREFIX - if name.startswith(prefix): - try: - safe_rmpath(name) - except Exception: - traceback.print_exc() - for path in _testfiles_created: - try: - safe_rmpath(path) - except Exception: - traceback.print_exc() - - -# this is executed first -@atexit.register -def _cleanup_procs(): - reap_children(recursive=True) - - -# =================================================================== -# --- threads -# =================================================================== - - -class ThreadTask(threading.Thread): - """A thread task which does nothing expect staying alive.""" - - def __init__(self): - threading.Thread.__init__(self) - self._running = False - self._interval = 0.001 - self._flag = threading.Event() - - def __repr__(self): - name = self.__class__.__name__ - return '<%s running=%s at %#x>' % (name, self._running, id(self)) - - def __enter__(self): - self.start() - return self - - def __exit__(self, *args, **kwargs): - self.stop() - - def start(self): - """Start thread and keep it running until an explicit - stop() request. Polls for shutdown every 'timeout' seconds. - """ - if self._running: - raise ValueError("already started") - threading.Thread.start(self) - self._flag.wait() - - def run(self): - self._running = True - self._flag.set() - while self._running: - time.sleep(self._interval) - - def stop(self): - """Stop thread execution and and waits until it is stopped.""" - if not self._running: - raise ValueError("already stopped") - self._running = False - self.join() - - -# =================================================================== -# --- subprocesses -# =================================================================== - - -def _reap_children_on_err(fun): - @functools.wraps(fun) - def wrapper(*args, **kwargs): - try: - return fun(*args, **kwargs) - except Exception: - reap_children() - raise - return wrapper - - -@_reap_children_on_err -def get_test_subprocess(cmd=None, **kwds): - """Creates a python subprocess which does nothing for 60 secs and - return it as subprocess.Popen instance. - If "cmd" is specified that is used instead of python. - By default stdin and stdout are redirected to /dev/null. - It also attemps to make sure the process is in a reasonably - initialized state. - The process is registered for cleanup on reap_children(). - """ - kwds.setdefault("stdin", DEVNULL) - kwds.setdefault("stdout", DEVNULL) - kwds.setdefault("cwd", os.getcwd()) - kwds.setdefault("env", os.environ) - if WINDOWS: - # Prevents the subprocess to open error dialogs. - kwds.setdefault("creationflags", 0x8000000) # CREATE_NO_WINDOW - if cmd is None: - safe_rmpath(_TESTFN) - pyline = "from time import sleep;" \ - "open(r'%s', 'w').close();" \ - "sleep(60);" % _TESTFN - cmd = [PYTHON_EXE, "-c", pyline] - sproc = subprocess.Popen(cmd, **kwds) - _subprocesses_started.add(sproc) - wait_for_file(_TESTFN, delete=True, empty=True) - else: - sproc = subprocess.Popen(cmd, **kwds) - _subprocesses_started.add(sproc) - wait_for_pid(sproc.pid) - return sproc - - -@_reap_children_on_err -def create_proc_children_pair(): - """Create a subprocess which creates another one as in: - A (us) -> B (child) -> C (grandchild). - Return a (child, grandchild) tuple. - The 2 processes are fully initialized and will live for 60 secs - and are registered for cleanup on reap_children(). - """ - _TESTFN2 = os.path.basename(_TESTFN) + '2' # need to be relative - s = textwrap.dedent("""\ - import subprocess, os, sys, time - s = "import os, time;" - s += "f = open('%s', 'w');" - s += "f.write(str(os.getpid()));" - s += "f.close();" - s += "time.sleep(60);" - subprocess.Popen(['%s', '-c', s]) - time.sleep(60) - """ % (_TESTFN2, PYTHON_EXE)) - # On Windows if we create a subprocess with CREATE_NO_WINDOW flag - # set (which is the default) a "conhost.exe" extra process will be - # spawned as a child. We don't want that. - if WINDOWS: - subp = pyrun(s, creationflags=0) - else: - subp = pyrun(s) - child1 = psutil.Process(subp.pid) - data = wait_for_file(_TESTFN2, delete=False, empty=False) - os.remove(_TESTFN2) - child2_pid = int(data) - _pids_started.add(child2_pid) - child2 = psutil.Process(child2_pid) - return (child1, child2) - - -def create_zombie_proc(): - """Create a zombie process and return its PID.""" - assert psutil.POSIX - unix_file = tempfile.mktemp(prefix=TESTFILE_PREFIX) if OSX else TESTFN - src = textwrap.dedent("""\ - import os, sys, time, socket, contextlib - child_pid = os.fork() - if child_pid > 0: - time.sleep(3000) - else: - # this is the zombie process - s = socket.socket(socket.AF_UNIX) - with contextlib.closing(s): - s.connect('%s') - if sys.version_info < (3, ): - pid = str(os.getpid()) - else: - pid = bytes(str(os.getpid()), 'ascii') - s.sendall(pid) - """ % unix_file) - with contextlib.closing(socket.socket(socket.AF_UNIX)) as sock: - sock.settimeout(GLOBAL_TIMEOUT) - sock.bind(unix_file) - sock.listen(1) - pyrun(src) - conn, _ = sock.accept() - try: - select.select([conn.fileno()], [], [], GLOBAL_TIMEOUT) - zpid = int(conn.recv(1024)) - _pids_started.add(zpid) - zproc = psutil.Process(zpid) - call_until(lambda: zproc.status(), "ret == psutil.STATUS_ZOMBIE") - return zpid - finally: - conn.close() - - -@_reap_children_on_err -def pyrun(src, **kwds): - """Run python 'src' code string in a separate interpreter. - Returns a subprocess.Popen instance. - """ - kwds.setdefault("stdout", None) - kwds.setdefault("stderr", None) - with tempfile.NamedTemporaryFile( - prefix=TESTFILE_PREFIX, mode="wt", delete=False) as f: - _testfiles_created.add(f.name) - f.write(src) - f.flush() - subp = get_test_subprocess([PYTHON_EXE, f.name], **kwds) - wait_for_pid(subp.pid) - return subp - - -@_reap_children_on_err -def sh(cmd, **kwds): - """run cmd in a subprocess and return its output. - raises RuntimeError on error. - """ - shell = True if isinstance(cmd, (str, unicode)) else False - # Prevents subprocess to open error dialogs in case of error. - flags = 0x8000000 if WINDOWS and shell else 0 - kwds.setdefault("shell", shell) - kwds.setdefault("stdout", subprocess.PIPE) - kwds.setdefault("stderr", subprocess.PIPE) - kwds.setdefault("universal_newlines", True) - kwds.setdefault("creationflags", flags) - p = subprocess.Popen(cmd, **kwds) - _subprocesses_started.add(p) - stdout, stderr = p.communicate() - if p.returncode != 0: - raise RuntimeError(stderr) - if stderr: - warn(stderr) - if stdout.endswith('\n'): - stdout = stdout[:-1] - return stdout - - -def reap_children(recursive=False): - """Terminate and wait() any subprocess started by this test suite - and ensure that no zombies stick around to hog resources and - create problems when looking for refleaks. - - If resursive is True it also tries to terminate and wait() - all grandchildren started by this process. - """ - # This is here to make sure wait_procs() behaves properly and - # investigate: - # https://ci.appveyor.com/project/giampaolo/psutil/build/job/ - # jiq2cgd6stsbtn60 - def assert_gone(pid): - assert not psutil.pid_exists(pid), pid - assert pid not in psutil.pids(), pid - try: - p = psutil.Process(pid) - assert not p.is_running(), pid - except psutil.NoSuchProcess: - pass - else: - assert 0, "pid %s is not gone" % pid - - # Get the children here, before terminating the children sub - # processes as we don't want to lose the intermediate reference - # in case of grandchildren. - if recursive: - children = set(psutil.Process().children(recursive=True)) - else: - children = set() - - # Terminate subprocess.Popen instances "cleanly" by closing their - # fds and wiat()ing for them in order to avoid zombies. - while _subprocesses_started: - subp = _subprocesses_started.pop() - _pids_started.add(subp.pid) - try: - subp.terminate() - except OSError as err: - if WINDOWS and err.errno == 6: # "invalid handle" - pass - elif err.errno != errno.ESRCH: - raise - if subp.stdout: - subp.stdout.close() - if subp.stderr: - subp.stderr.close() - try: - # Flushing a BufferedWriter may raise an error. - if subp.stdin: - subp.stdin.close() - finally: - # Wait for the process to terminate, to avoid zombies. - try: - subp.wait() - except OSError as err: - if err.errno != errno.ECHILD: - raise - - # Terminate started pids. - while _pids_started: - pid = _pids_started.pop() - try: - p = psutil.Process(pid) - except psutil.NoSuchProcess: - assert_gone(pid) - else: - children.add(p) - - # Terminate children. - if children: - for p in children: - try: - p.terminate() - except psutil.NoSuchProcess: - pass - gone, alive = psutil.wait_procs(children, timeout=GLOBAL_TIMEOUT) - for p in alive: - warn("couldn't terminate process %r; attempting kill()" % p) - try: - p.kill() - except psutil.NoSuchProcess: - pass - gone, alive = psutil.wait_procs(alive, timeout=GLOBAL_TIMEOUT) - if alive: - for p in alive: - warn("process %r survived kill()" % p) - - for p in children: - assert_gone(p.pid) - - -# =================================================================== -# --- OS -# =================================================================== - - -def get_kernel_version(): - """Return a tuple such as (2, 6, 36).""" - if not POSIX: - raise NotImplementedError("not POSIX") - s = "" - uname = os.uname()[2] - for c in uname: - if c.isdigit() or c == '.': - s += c - else: - break - if not s: - raise ValueError("can't parse %r" % uname) - minor = 0 - micro = 0 - nums = s.split('.') - major = int(nums[0]) - if len(nums) >= 2: - minor = int(nums[1]) - if len(nums) >= 3: - micro = int(nums[2]) - return (major, minor, micro) - - -def get_winver(): - if not WINDOWS: - raise NotImplementedError("not WINDOWS") - wv = sys.getwindowsversion() - if hasattr(wv, 'service_pack_major'): # python >= 2.7 - sp = wv.service_pack_major or 0 - else: - r = re.search(r"\s\d$", wv[4]) - if r: - sp = int(r.group(0)) - else: - sp = 0 - return (wv[0], wv[1], sp) - - -# =================================================================== -# --- sync primitives -# =================================================================== - - -class retry(object): - """A retry decorator.""" - - def __init__(self, - exception=Exception, - timeout=None, - retries=None, - interval=0.001, - logfun=lambda s: print(s, file=sys.stderr), - ): - if timeout and retries: - raise ValueError("timeout and retries args are mutually exclusive") - self.exception = exception - self.timeout = timeout - self.retries = retries - self.interval = interval - self.logfun = logfun - - def __iter__(self): - if self.timeout: - stop_at = time.time() + self.timeout - while time.time() < stop_at: - yield - elif self.retries: - for _ in range(self.retries): - yield - else: - while True: - yield - - def sleep(self): - if self.interval is not None: - time.sleep(self.interval) - - def __call__(self, fun): - @functools.wraps(fun) - def wrapper(*args, **kwargs): - exc = None - for _ in self: - try: - return fun(*args, **kwargs) - except self.exception as _: - exc = _ - if self.logfun is not None: - self.logfun(exc) - self.sleep() - continue - if PY3: - raise exc - else: - raise - - # This way the user of the decorated function can change config - # parameters. - wrapper.decorator = self - return wrapper - - -@retry(exception=psutil.NoSuchProcess, logfun=None, timeout=GLOBAL_TIMEOUT, - interval=0.001) -def wait_for_pid(pid): - """Wait for pid to show up in the process list then return. - Used in the test suite to give time the sub process to initialize. - """ - psutil.Process(pid) - if WINDOWS: - # give it some more time to allow better initialization - time.sleep(0.01) - - -@retry(exception=(EnvironmentError, AssertionError), logfun=None, - timeout=GLOBAL_TIMEOUT, interval=0.001) -def wait_for_file(fname, delete=True, empty=False): - """Wait for a file to be written on disk with some content.""" - with open(fname, "rb") as f: - data = f.read() - if not empty: - assert data - if delete: - os.remove(fname) - return data - - -@retry(exception=AssertionError, logfun=None, timeout=GLOBAL_TIMEOUT, - interval=0.001) -def call_until(fun, expr): - """Keep calling function for timeout secs and exit if eval() - expression is True. - """ - ret = fun() - assert eval(expr) - return ret - - -# =================================================================== -# --- fs -# =================================================================== - - -def safe_rmpath(path): - "Convenience function for removing temporary test files or dirs" - try: - st = os.stat(path) - if stat.S_ISDIR(st.st_mode): - os.rmdir(path) - else: - os.remove(path) - except OSError as err: - if err.errno != errno.ENOENT: - raise - - -def safe_mkdir(dir): - "Convenience function for creating a directory" - try: - os.mkdir(dir) - except OSError as err: - if err.errno != errno.EEXIST: - raise - - -@contextlib.contextmanager -def chdir(dirname): - "Context manager which temporarily changes the current directory." - curdir = os.getcwd() - try: - os.chdir(dirname) - yield - finally: - os.chdir(curdir) - - -def create_exe(outpath, c_code=None): - """Creates an executable file in the given location.""" - assert not os.path.exists(outpath), outpath - if c_code: - if not which("gcc"): - raise ValueError("gcc is not installed") - if isinstance(c_code, bool): # c_code is True - c_code = textwrap.dedent( - """ - #include - int main() { - pause(); - return 1; - } - """) - assert isinstance(c_code, str), c_code - with tempfile.NamedTemporaryFile( - suffix='.c', delete=False, mode='wt') as f: - f.write(c_code) - try: - subprocess.check_call(["gcc", f.name, "-o", outpath]) - finally: - safe_rmpath(f.name) - else: - # copy python executable - shutil.copyfile(PYTHON_EXE, outpath) - if POSIX: - st = os.stat(outpath) - os.chmod(outpath, st.st_mode | stat.S_IEXEC) - - -def unique_filename(prefix=TESTFILE_PREFIX, suffix=""): - return tempfile.mktemp(prefix=prefix, suffix=suffix) - - -# =================================================================== -# --- testing -# =================================================================== - - -class TestCase(unittest.TestCase): - - # Print a full path representation of the single unit tests - # being run. - def __str__(self): - return "%s.%s.%s" % ( - self.__class__.__module__, self.__class__.__name__, - self._testMethodName) - - # assertRaisesRegexp renamed to assertRaisesRegex in 3.3; - # add support for the new name. - if not hasattr(unittest.TestCase, 'assertRaisesRegex'): - assertRaisesRegex = unittest.TestCase.assertRaisesRegexp - - -# override default unittest.TestCase -unittest.TestCase = TestCase - - -def _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. - """ - return retry(exception=AssertionError, timeout=None, retries=retries) - - -def skip_on_access_denied(only_if=None): - """Decorator to Ignore AccessDenied exceptions.""" - def decorator(fun): - @functools.wraps(fun) - def wrapper(*args, **kwargs): - try: - return fun(*args, **kwargs) - except psutil.AccessDenied: - if only_if is not None: - if not only_if: - raise - raise unittest.SkipTest("raises AccessDenied") - return wrapper - return decorator - - -def skip_on_not_implemented(only_if=None): - """Decorator to Ignore NotImplementedError exceptions.""" - def decorator(fun): - @functools.wraps(fun) - def wrapper(*args, **kwargs): - try: - return fun(*args, **kwargs) - except NotImplementedError: - if only_if is not None: - if not only_if: - raise - msg = "%r was skipped because it raised NotImplementedError" \ - % fun.__name__ - raise unittest.SkipTest(msg) - return wrapper - return decorator - - -# =================================================================== -# --- network -# =================================================================== - - -def get_free_port(host='127.0.0.1'): - """Return an unused TCP port.""" - with contextlib.closing(socket.socket()) as sock: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((host, 0)) - return sock.getsockname()[1] - - -@contextlib.contextmanager -def unix_socket_path(suffix=""): - """A context manager which returns a non-existent file name - and tries to delete it on exit. - """ - assert psutil.POSIX - path = unique_filename(suffix=suffix) - try: - yield path - finally: - try: - os.unlink(path) - except OSError: - pass - - -def bind_socket(family=AF_INET, type=SOCK_STREAM, addr=None): - """Binds a generic socket.""" - if addr is None and family in (AF_INET, AF_INET6): - addr = ("", 0) - sock = socket.socket(family, type) - try: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind(addr) - if type == socket.SOCK_STREAM: - sock.listen(10) - return sock - except Exception: - sock.close() - raise - - -def bind_unix_socket(name, type=socket.SOCK_STREAM): - """Bind a UNIX socket.""" - assert psutil.POSIX - assert not os.path.exists(name), name - sock = socket.socket(socket.AF_UNIX, type) - try: - sock.bind(name) - if type == socket.SOCK_STREAM: - sock.listen(10) - except Exception: - sock.close() - raise - return sock - - -def tcp_socketpair(family, addr=("", 0)): - """Build a pair of TCP sockets connected to each other. - Return a (server, client) tuple. - """ - with contextlib.closing(socket.socket(family, SOCK_STREAM)) as ll: - ll.bind(addr) - ll.listen(10) - addr = ll.getsockname() - c = socket.socket(family, SOCK_STREAM) - try: - c.connect(addr) - caddr = c.getsockname() - while True: - a, addr = ll.accept() - # check that we've got the correct client - if addr == caddr: - return (a, c) - a.close() - except OSError: - c.close() - raise - - -def unix_socketpair(name): - """Build a pair of UNIX sockets connected to each other through - the same UNIX file name. - Return a (server, client) tuple. - """ - assert psutil.POSIX - server = client = None - try: - server = bind_unix_socket(name, type=socket.SOCK_STREAM) - server.setblocking(0) - client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - client.setblocking(0) - client.connect(name) - # new = server.accept() - except Exception: - if server is not None: - server.close() - if client is not None: - client.close() - raise - return (server, client) - - -@contextlib.contextmanager -def create_sockets(): - """Open as many socket families / types as possible.""" - socks = [] - fname1 = fname2 = None - try: - socks.append(bind_socket(socket.AF_INET, socket.SOCK_STREAM)) - socks.append(bind_socket(socket.AF_INET, socket.SOCK_DGRAM)) - if supports_ipv6(): - socks.append(bind_socket(socket.AF_INET6, socket.SOCK_STREAM)) - socks.append(bind_socket(socket.AF_INET6, socket.SOCK_DGRAM)) - if POSIX and HAS_CONNECTIONS_UNIX: - fname1 = unix_socket_path().__enter__() - fname2 = unix_socket_path().__enter__() - s1, s2 = unix_socketpair(fname1) - s3 = bind_unix_socket(fname2, type=socket.SOCK_DGRAM) - # self.addCleanup(safe_rmpath, fname1) - # self.addCleanup(safe_rmpath, fname2) - for s in (s1, s2, s3): - socks.append(s) - yield socks - finally: - for s in socks: - s.close() - if fname1 is not None: - safe_rmpath(fname1) - if fname2 is not None: - safe_rmpath(fname2) - - -def check_net_address(addr, family): - """Check a net address validity. Supported families are IPv4, - IPv6 and MAC addresses. - """ - import ipaddress # python >= 3.3 / requires "pip install ipaddress" - if enum and PY3: - assert isinstance(family, enum.IntEnum), family - if family == socket.AF_INET: - octs = [int(x) for x in addr.split('.')] - assert len(octs) == 4, addr - for num in octs: - assert 0 <= num <= 255, addr - if not PY3: - addr = unicode(addr) - ipaddress.IPv4Address(addr) - elif family == socket.AF_INET6: - assert isinstance(addr, str), addr - if not PY3: - addr = unicode(addr) - ipaddress.IPv6Address(addr) - elif family == psutil.AF_LINK: - assert re.match(r'([a-fA-F0-9]{2}[:|\-]?){6}', addr) is not None, addr - else: - raise ValueError("unknown family %r", family) - - -def check_connection_ntuple(conn): - """Check validity of a connection namedtuple.""" - # check ntuple - assert len(conn) in (6, 7), conn - has_pid = len(conn) == 7 - has_fd = getattr(conn, 'fd', -1) != -1 - assert conn[0] == conn.fd - assert conn[1] == conn.family - assert conn[2] == conn.type - assert conn[3] == conn.laddr - assert conn[4] == conn.raddr - assert conn[5] == conn.status - if has_pid: - assert conn[6] == conn.pid - - # check fd - if has_fd: - assert conn.fd >= 0, conn - if hasattr(socket, 'fromfd') and not WINDOWS: - try: - dupsock = socket.fromfd(conn.fd, conn.family, conn.type) - except (socket.error, OSError) as err: - if err.args[0] != errno.EBADF: - raise - else: - with contextlib.closing(dupsock): - assert dupsock.family == conn.family - assert dupsock.type == conn.type - - # check family - assert conn.family in (AF_INET, AF_INET6, AF_UNIX), repr(conn.family) - if conn.family in (AF_INET, AF_INET6): - # actually try to bind the local socket; ignore IPv6 - # sockets as their address might be represented as - # an IPv4-mapped-address (e.g. "::127.0.0.1") - # and that's rejected by bind() - if conn.family == AF_INET: - s = socket.socket(conn.family, conn.type) - with contextlib.closing(s): - try: - s.bind((conn.laddr[0], 0)) - except socket.error as err: - if err.errno != errno.EADDRNOTAVAIL: - raise - elif conn.family == AF_UNIX: - assert conn.status == psutil.CONN_NONE, conn.status - - # check type (SOCK_SEQPACKET may happen in case of AF_UNIX socks) - assert conn.type in (SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET), \ - repr(conn.type) - if conn.type == SOCK_DGRAM: - assert conn.status == psutil.CONN_NONE, conn.status - - # check laddr (IP address and port sanity) - for addr in (conn.laddr, conn.raddr): - if conn.family in (AF_INET, AF_INET6): - assert isinstance(addr, tuple), addr - if not addr: - continue - assert isinstance(addr.port, int), addr.port - assert 0 <= addr.port <= 65535, addr.port - check_net_address(addr.ip, conn.family) - elif conn.family == AF_UNIX: - assert isinstance(addr, str), addr - - # check status - assert isinstance(conn.status, str), conn - valids = [getattr(psutil, x) for x in dir(psutil) if x.startswith('CONN_')] - assert conn.status in valids, conn - - -# =================================================================== -# --- compatibility -# =================================================================== - - -def reload_module(module): - """Backport of importlib.reload of Python 3.3+.""" - try: - import importlib - if not hasattr(importlib, 'reload'): # python <=3.3 - raise ImportError - except ImportError: - import imp - return imp.reload(module) - else: - return importlib.reload(module) - - -def import_module_by_path(path): - name = os.path.splitext(os.path.basename(path))[0] - if sys.version_info[0] == 2: - import imp - return imp.load_source(name, path) - elif sys.version_info[:2] <= (3, 4): - from importlib.machinery import SourceFileLoader - return SourceFileLoader(name, path).load_module() - else: - import importlib.util - spec = importlib.util.spec_from_file_location(name, path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -# =================================================================== -# --- others -# =================================================================== - - -def warn(msg): - """Raise a warning msg.""" - warnings.warn(msg, UserWarning) - - -def is_namedtuple(x): - """Check if object is an instance of namedtuple.""" - t = type(x) - b = t.__bases__ - if len(b) != 1 or b[0] != tuple: - return False - f = getattr(t, '_fields', None) - if not isinstance(f, tuple): - return False - return all(type(n) == str for n in f) - - -if POSIX: - @contextlib.contextmanager - def copyload_shared_lib(dst_prefix=TESTFILE_PREFIX): - """Ctx manager which picks up a random shared CO lib used - by this process, copies it in another location and loads it - in memory via ctypes. Return the new absolutized path. - """ - 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()] - src = random.choice(libs) - shutil.copyfile(src, dst) - try: - ctypes.CDLL(dst) - yield dst - finally: - safe_rmpath(dst) -else: - @contextlib.contextmanager - def copyload_shared_lib(dst_prefix=TESTFILE_PREFIX): - """Ctx manager which picks up a random shared DLL lib used - by this process, copies it in another location and loads it - in memory via ctypes. - Return the new absolutized, normcased path. - """ - from ctypes import wintypes - from ctypes import WinError - ext = ".dll" - dst = tempfile.mktemp(prefix=dst_prefix, suffix=ext) - libs = [x.path for x in psutil.Process().memory_maps() if - os.path.splitext(x.path)[1].lower() == ext and - 'python' in os.path.basename(x.path).lower() and - 'wow64' not in x.path.lower()] - src = random.choice(libs) - shutil.copyfile(src, dst) - cfile = None - try: - cfile = ctypes.WinDLL(dst) - yield dst - finally: - # Work around OverflowError: - # - https://ci.appveyor.com/project/giampaolo/psutil/build/1207/ - # job/o53330pbnri9bcw7 - # - http://bugs.python.org/issue30286 - # - http://stackoverflow.com/questions/23522055 - if cfile is not None: - FreeLibrary = ctypes.windll.kernel32.FreeLibrary - FreeLibrary.argtypes = [wintypes.HMODULE] - ret = FreeLibrary(cfile._handle) - if ret == 0: - WinError() - safe_rmpath(dst) diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py deleted file mode 100755 index 62fe074287..0000000000 --- a/psutil/tests/__main__.py +++ /dev/null @@ -1,94 +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. - -""" -Run unit tests. This is invoked by: - -$ python -m psutil.tests -""" - -import contextlib -import optparse -import os -import ssl -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 import run_suite - - -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: - 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_suite() - - -main() diff --git a/psutil/tests/test_aix.py b/psutil/tests/test_aix.py deleted file mode 100755 index 7a8a4c3342..0000000000 --- a/psutil/tests/test_aix.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola' -# Copyright (c) 2017, Arnon Yaari -# All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""AIX specific tests.""" - -import re - -from psutil import AIX -from psutil.tests import run_test_module_by_name -from psutil.tests import sh -from psutil.tests import unittest -import psutil - - -@unittest.skipIf(not AIX, "AIX only") -class AIXSpecificTestCase(unittest.TestCase): - - def test_virtual_memory(self): - out = sh('/usr/bin/svmon -O unit=KB') - re_pattern = "memory\s*" - for field in ("size inuse free pin virtual available mmode").split(): - re_pattern += "(?P<%s>\S+)\s+" % (field,) - matchobj = re.search(re_pattern, out) - - self.assertIsNotNone( - matchobj, "svmon command returned unexpected output") - - KB = 1024 - total = int(matchobj.group("size")) * KB - available = int(matchobj.group("available")) * KB - used = int(matchobj.group("inuse")) * KB - free = int(matchobj.group("free")) * KB - - psutil_result = psutil.virtual_memory() - - # MEMORY_TOLERANCE from psutil.tests is not enough. For some reason - # we're seeing differences of ~1.2 MB. 2 MB is still a good tolerance - # when compared to GBs. - MEMORY_TOLERANCE = 2 * KB * KB # 2 MB - self.assertEqual(psutil_result.total, total) - self.assertAlmostEqual( - psutil_result.used, used, delta=MEMORY_TOLERANCE) - self.assertAlmostEqual( - psutil_result.available, available, delta=MEMORY_TOLERANCE) - self.assertAlmostEqual( - psutil_result.free, free, delta=MEMORY_TOLERANCE) - - def test_swap_memory(self): - out = sh('/usr/sbin/lsps -a') - # From the man page, "The size is given in megabytes" so we assume - # we'll always have 'MB' in the result - # TODO maybe try to use "swap -l" to check "used" too, but its units - # are not guaranteed to be "MB" so parsing may not be consistent - matchobj = re.search("(?P\S+)\s+" - "(?P\S+)\s+" - "(?P\S+)\s+" - "(?P\d+)MB", out) - - self.assertIsNotNone( - matchobj, "lsps command returned unexpected output") - - total_mb = int(matchobj.group("size")) - MB = 1024 ** 2 - psutil_result = psutil.swap_memory() - # we divide our result by MB instead of multiplying the lsps value by - # MB because lsps may round down, so we round down too - self.assertEqual(int(psutil_result.total / MB), total_mb) - - def test_cpu_stats(self): - out = sh('/usr/bin/mpstat -a') - - re_pattern = "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,) - matchobj = re.search(re_pattern, out) - - self.assertIsNotNone( - matchobj, "mpstat command returned unexpected output") - - # numbers are usually in the millions so 1000 is ok for tolerance - CPU_STATS_TOLERANCE = 1000 - psutil_result = psutil.cpu_stats() - self.assertAlmostEqual( - psutil_result.ctx_switches, - int(matchobj.group("cs")), - delta=CPU_STATS_TOLERANCE) - self.assertAlmostEqual( - psutil_result.syscalls, - int(matchobj.group("sysc")), - delta=CPU_STATS_TOLERANCE) - self.assertAlmostEqual( - psutil_result.interrupts, - int(matchobj.group("dev")), - delta=CPU_STATS_TOLERANCE) - self.assertAlmostEqual( - psutil_result.soft_interrupts, - int(matchobj.group("soft")), - delta=CPU_STATS_TOLERANCE) - - def test_cpu_count_logical(self): - out = sh('/usr/bin/mpstat -a') - mpstat_lcpu = int(re.search("lcpu=(\d+)", out).group(1)) - psutil_lcpu = psutil.cpu_count(logical=True) - self.assertEqual(mpstat_lcpu, psutil_lcpu) - - def test_net_if_addrs_names(self): - out = sh('/etc/ifconfig -l') - ifconfig_names = set(out.split()) - psutil_names = set(psutil.net_if_addrs().keys()) - self.assertSetEqual(ifconfig_names, psutil_names) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py deleted file mode 100755 index 7846c1ca25..0000000000 --- a/psutil/tests/test_bsd.py +++ /dev/null @@ -1,519 +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. - -# TODO: (FreeBSD) add test for comparing connections with 'sockstat' cmd. - - -"""Tests specific to all BSD platforms.""" - - -import datetime -import os -import re -import time - -import psutil -from psutil import BSD -from psutil import FREEBSD -from psutil import NETBSD -from psutil import OPENBSD -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_BATTERY -from psutil.tests import MEMORY_TOLERANCE -from psutil.tests import reap_children -from psutil.tests import retry_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 - - -if BSD: - PAGESIZE = os.sysconf("SC_PAGE_SIZE") - if os.getuid() == 0: # muse requires root privileges - MUSE_AVAILABLE = which('muse') - else: - MUSE_AVAILABLE = False -else: - MUSE_AVAILABLE = False - - -def sysctl(cmdline): - """Expects a sysctl command with an argument and parse the result - returning only the value of interest. - """ - result = sh("sysctl " + cmdline) - if FREEBSD: - result = result[result.find(": ") + 2:] - elif OPENBSD or NETBSD: - result = result[result.find("=") + 1:] - try: - return int(result) - except ValueError: - return result - - -def muse(field): - """Thin wrapper around 'muse' cmdline utility.""" - out = sh('muse') - for line in out.split('\n'): - if line.startswith(field): - break - else: - raise ValueError("line not found") - return int(line.split()[1]) - - -# ===================================================================== -# --- All BSD* -# ===================================================================== - - -@unittest.skipIf(not BSD, "BSD only") -class BSDSpecificTestCase(unittest.TestCase): - """Generic tests common to all BSD variants.""" - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - - @unittest.skipIf(NETBSD, "-o lstart doesn't work on NETBSD") - def test_process_create_time(self): - output = sh("ps -o lstart -p %s" % self.pid) - start_ps = output.replace('STARTED', '').strip() - start_psutil = psutil.Process(self.pid).create_time() - start_psutil = time.strftime("%a %b %e %H:%M:%S %Y", - time.localtime(start_psutil)) - self.assertEqual(start_ps, start_psutil) - - def test_disks(self): - # test psutil.disk_usage() and psutil.disk_partitions() - # against "df -a" - def df(path): - out = sh('df -k "%s"' % path).strip() - lines = out.split('\n') - lines.pop(0) - line = lines.pop(0) - dev, total, used, free = line.split()[:4] - if dev == 'none': - dev = '' - total = int(total) * 1024 - used = int(used) * 1024 - free = int(free) * 1024 - return dev, total, used, free - - for part in psutil.disk_partitions(all=False): - usage = psutil.disk_usage(part.mountpoint) - dev, total, used, free = df(part.mountpoint) - self.assertEqual(part.device, dev) - self.assertEqual(usage.total, total) - # 10 MB tollerance - if abs(usage.free - free) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % (usage.free, free)) - if abs(usage.used - used) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % (usage.used, used)) - - @unittest.skipIf(not which('sysctl'), "sysctl cmd not available") - def test_cpu_count_logical(self): - syst = sysctl("hw.ncpu") - self.assertEqual(psutil.cpu_count(logical=True), syst) - - @unittest.skipIf(not which('sysctl'), "sysctl cmd not available") - def test_virtual_memory_total(self): - num = sysctl('hw.physmem') - self.assertEqual(num, psutil.virtual_memory().total) - - def test_net_if_stats(self): - for name, stats in psutil.net_if_stats().items(): - try: - out = sh("ifconfig %s" % name) - except RuntimeError: - pass - else: - self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) - if "mtu" in out: - self.assertEqual(stats.mtu, - int(re.findall(r'mtu (\d+)', out)[0])) - - -# ===================================================================== -# --- FreeBSD -# ===================================================================== - - -@unittest.skipIf(not FREEBSD, "FREEBSD only") -class FreeBSDSpecificTestCase(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - - @staticmethod - def parse_swapinfo(): - # the last line is always the total - output = sh("swapinfo -k").splitlines()[-1] - parts = re.split(r'\s+', output) - - if not parts: - raise ValueError("Can't parse swapinfo: %s" % output) - - # the size is in 1k units, so multiply by 1024 - total, used, free = (int(p) * 1024 for p in parts[1:4]) - return total, used, free - - @retry_before_failing() - def test_proc_memory_maps(self): - out = sh('procstat -v %s' % self.pid) - maps = psutil.Process(self.pid).memory_maps(grouped=False) - lines = out.split('\n')[1:] - while lines: - line = lines.pop() - fields = line.split() - _, start, stop, perms, res = fields[:5] - map = maps.pop() - self.assertEqual("%s-%s" % (start, stop), map.addr) - self.assertEqual(int(res), map.rss) - if not map.path.startswith('['): - self.assertEqual(fields[10], map.path) - - def test_proc_exe(self): - out = sh('procstat -b %s' % self.pid) - self.assertEqual(psutil.Process(self.pid).exe(), - out.split('\n')[1].split()[-1]) - - def test_proc_cmdline(self): - out = sh('procstat -c %s' % self.pid) - self.assertEqual(' '.join(psutil.Process(self.pid).cmdline()), - ' '.join(out.split('\n')[1].split()[2:])) - - def test_proc_uids_gids(self): - out = sh('procstat -s %s' % self.pid) - euid, ruid, suid, egid, rgid, sgid = out.split('\n')[1].split()[2:8] - p = psutil.Process(self.pid) - uids = p.uids() - gids = p.gids() - self.assertEqual(uids.real, int(ruid)) - self.assertEqual(uids.effective, int(euid)) - self.assertEqual(uids.saved, int(suid)) - self.assertEqual(gids.real, int(rgid)) - self.assertEqual(gids.effective, int(egid)) - self.assertEqual(gids.saved, int(sgid)) - - @retry_before_failing() - def test_proc_ctx_switches(self): - tested = [] - out = sh('procstat -r %s' % self.pid) - p = psutil.Process(self.pid) - for line in out.split('\n'): - line = line.lower().strip() - if ' voluntary context' in line: - pstat_value = int(line.split()[-1]) - psutil_value = p.num_ctx_switches().voluntary - self.assertEqual(pstat_value, psutil_value) - tested.append(None) - elif ' involuntary context' in line: - pstat_value = int(line.split()[-1]) - psutil_value = p.num_ctx_switches().involuntary - self.assertEqual(pstat_value, psutil_value) - tested.append(None) - if len(tested) != 2: - raise RuntimeError("couldn't find lines match in procstat out") - - @retry_before_failing() - def test_proc_cpu_times(self): - tested = [] - out = sh('procstat -r %s' % self.pid) - p = psutil.Process(self.pid) - for line in out.split('\n'): - line = line.lower().strip() - if 'user time' in line: - pstat_value = float('0.' + line.split()[-1].split('.')[-1]) - psutil_value = p.cpu_times().user - self.assertEqual(pstat_value, psutil_value) - tested.append(None) - elif 'system time' in line: - pstat_value = float('0.' + line.split()[-1].split('.')[-1]) - psutil_value = p.cpu_times().system - self.assertEqual(pstat_value, psutil_value) - tested.append(None) - if len(tested) != 2: - raise RuntimeError("couldn't find lines match in procstat out") - - # --- virtual_memory(); tests against sysctl - - @retry_before_failing() - 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() - 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() - 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() - 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() - 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() - def test_vmem_buffers(self): - syst = sysctl("vfs.bufspace") - self.assertAlmostEqual(psutil.virtual_memory().buffers, syst, - delta=MEMORY_TOLERANCE) - - # --- virtual_memory(); tests against muse - - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") - def test_muse_vmem_total(self): - num = muse('Total') - self.assertEqual(psutil.virtual_memory().total, num) - - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") - @retry_before_failing() - 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() - 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() - 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() - 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() - 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() - def test_muse_vmem_buffers(self): - num = muse('Buffer') - self.assertAlmostEqual(psutil.virtual_memory().buffers, num, - delta=MEMORY_TOLERANCE) - - def test_cpu_stats_ctx_switches(self): - self.assertAlmostEqual(psutil.cpu_stats().ctx_switches, - sysctl('vm.stats.sys.v_swtch'), delta=1000) - - def test_cpu_stats_interrupts(self): - self.assertAlmostEqual(psutil.cpu_stats().interrupts, - sysctl('vm.stats.sys.v_intr'), delta=1000) - - def test_cpu_stats_soft_interrupts(self): - self.assertAlmostEqual(psutil.cpu_stats().soft_interrupts, - sysctl('vm.stats.sys.v_soft'), delta=1000) - - def test_cpu_stats_syscalls(self): - self.assertAlmostEqual(psutil.cpu_stats().syscalls, - sysctl('vm.stats.sys.v_syscall'), delta=1000) - - # def test_cpu_stats_traps(self): - # self.assertAlmostEqual(psutil.cpu_stats().traps, - # sysctl('vm.stats.sys.v_trap'), delta=1000) - - # --- swap memory - - def test_swapmem_free(self): - total, used, free = self.parse_swapinfo() - self.assertAlmostEqual( - psutil.swap_memory().free, free, delta=MEMORY_TOLERANCE) - - def test_swapmem_used(self): - total, used, free = self.parse_swapinfo() - self.assertAlmostEqual( - psutil.swap_memory().used, used, delta=MEMORY_TOLERANCE) - - def test_swapmem_total(self): - total, used, free = self.parse_swapinfo() - self.assertAlmostEqual( - psutil.swap_memory().total, total, delta=MEMORY_TOLERANCE) - - # --- others - - def test_boot_time(self): - s = sysctl('sysctl kern.boottime') - s = s[s.find(" sec = ") + 7:] - s = s[:s.find(',')] - btime = int(s) - self.assertEqual(btime, psutil.boot_time()) - - # --- sensors_battery - - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_sensors_battery(self): - def secs2hours(secs): - m, s = divmod(secs, 60) - h, m = divmod(m, 60) - return "%d:%02d" % (h, m) - - out = sh("acpiconf -i 0") - fields = dict([(x.split('\t')[0], x.split('\t')[-1]) - for x in out.split("\n")]) - metrics = psutil.sensors_battery() - percent = int(fields['Remaining capacity:'].replace('%', '')) - remaining_time = fields['Remaining time:'] - self.assertEqual(metrics.percent, percent) - if remaining_time == 'unknown': - self.assertEqual(metrics.secsleft, psutil.POWER_TIME_UNLIMITED) - else: - self.assertEqual(secs2hours(metrics.secsleft), remaining_time) - - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_sensors_battery_against_sysctl(self): - self.assertEqual(psutil.sensors_battery().percent, - sysctl("hw.acpi.battery.life")) - self.assertEqual(psutil.sensors_battery().power_plugged, - sysctl("hw.acpi.acline") == 1) - secsleft = psutil.sensors_battery().secsleft - if secsleft < 0: - self.assertEqual(sysctl("hw.acpi.battery.time"), -1) - else: - self.assertEqual(secsleft, sysctl("hw.acpi.battery.time") * 60) - - @unittest.skipIf(HAS_BATTERY, "has battery") - def test_sensors_battery_no_battery(self): - # If no battery is present one of these calls is supposed - # to fail, see: - # https://github.com/giampaolo/psutil/issues/1074 - with self.assertRaises(RuntimeError): - sysctl("hw.acpi.battery.life") - sysctl("hw.acpi.battery.time") - sysctl("hw.acpi.acline") - self.assertIsNone(psutil.sensors_battery()) - - -# ===================================================================== -# --- OpenBSD -# ===================================================================== - - -@unittest.skipIf(not OPENBSD, "OPENBSD only") -class OpenBSDSpecificTestCase(unittest.TestCase): - - def test_boot_time(self): - s = sysctl('kern.boottime') - sys_bt = datetime.datetime.strptime(s, "%a %b %d %H:%M:%S %Y") - psutil_bt = datetime.datetime.fromtimestamp(psutil.boot_time()) - self.assertEqual(sys_bt, psutil_bt) - - -# ===================================================================== -# --- NetBSD -# ===================================================================== - - -@unittest.skipIf(not NETBSD, "NETBSD only") -class NetBSDSpecificTestCase(unittest.TestCase): - - @staticmethod - def parse_meminfo(look_for): - with open('/proc/meminfo', 'rb') as f: - for line in f: - if line.startswith(look_for): - return int(line.split()[1]) * 1024 - raise ValueError("can't find %s" % look_for) - - def test_vmem_total(self): - self.assertEqual( - psutil.virtual_memory().total, self.parse_meminfo("MemTotal:")) - - def test_vmem_free(self): - self.assertAlmostEqual( - psutil.virtual_memory().free, self.parse_meminfo("MemFree:"), - delta=MEMORY_TOLERANCE) - - def test_vmem_buffers(self): - self.assertAlmostEqual( - psutil.virtual_memory().buffers, self.parse_meminfo("Buffers:"), - delta=MEMORY_TOLERANCE) - - def test_vmem_shared(self): - self.assertAlmostEqual( - psutil.virtual_memory().shared, self.parse_meminfo("MemShared:"), - delta=MEMORY_TOLERANCE) - - def test_swapmem_total(self): - self.assertAlmostEqual( - psutil.swap_memory().total, self.parse_meminfo("SwapTotal:"), - delta=MEMORY_TOLERANCE) - - def test_swapmem_free(self): - self.assertAlmostEqual( - psutil.swap_memory().free, self.parse_meminfo("SwapFree:"), - delta=MEMORY_TOLERANCE) - - def test_swapmem_used(self): - smem = psutil.swap_memory() - self.assertEqual(smem.used, smem.total - smem.free) - - def test_cpu_stats_interrupts(self): - with open('/proc/stat', 'rb') as f: - for line in f: - if line.startswith(b'intr'): - interrupts = int(line.split()[1]) - break - else: - raise ValueError("couldn't find line") - self.assertAlmostEqual( - psutil.cpu_stats().interrupts, interrupts, delta=1000) - - def test_cpu_stats_ctx_switches(self): - with open('/proc/stat', 'rb') as f: - for line in f: - if line.startswith(b'ctxt'): - ctx_switches = int(line.split()[1]) - break - else: - raise ValueError("couldn't find line") - self.assertAlmostEqual( - psutil.cpu_stats().ctx_switches, ctx_switches, delta=1000) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py deleted file mode 100755 index 176e266481..0000000000 --- a/psutil/tests/test_connections.py +++ /dev/null @@ -1,525 +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. - -"""Tests for net_connections() and Process.connections() APIs.""" - -import os -import socket -import textwrap -from contextlib import closing -from socket import AF_INET -from socket import AF_INET6 -from socket import SOCK_DGRAM -from socket import SOCK_STREAM - -import psutil -from psutil import FREEBSD -from psutil import LINUX -from psutil import NETBSD -from psutil import OPENBSD -from psutil import OSX -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._common import supports_ipv6 -from psutil._compat import PY3 -from psutil.tests import AF_UNIX -from psutil.tests import bind_socket -from psutil.tests import bind_unix_socket -from psutil.tests import check_connection_ntuple -from psutil.tests import create_sockets -from psutil.tests import get_free_port -from psutil.tests import HAS_CONNECTIONS_UNIX -from psutil.tests import pyrun -from psutil.tests import reap_children -from psutil.tests import 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 -from psutil.tests import TESTFN -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import unix_socket_path -from psutil.tests import unix_socketpair -from psutil.tests import wait_for_file - - -thisproc = psutil.Process() - - -class Base(object): - - def setUp(self): - if not NETBSD: - # NetBSD opens a UNIX socket to /var/log/run. - cons = thisproc.connections(kind='all') - assert not cons, cons - - def tearDown(self): - safe_rmpath(TESTFN) - reap_children() - if not NETBSD: - # Make sure we closed all resources. - # NetBSD opens a UNIX socket to /var/log/run. - cons = thisproc.connections(kind='all') - assert not cons, cons - - def get_conn_from_sock(self, sock): - cons = thisproc.connections(kind='all') - smap = dict([(c.fd, c) for c in cons]) - if NETBSD: - # NetBSD opens a UNIX socket to /var/log/run - # so there may be more connections. - return smap[sock.fileno()] - else: - self.assertEqual(len(cons), 1) - if cons[0].fd != -1: - self.assertEqual(smap[sock.fileno()].fd, sock.fileno()) - return cons[0] - - def check_socket(self, sock, conn=None): - """Given a socket, makes sure it matches the one obtained - via psutil. It assumes this process created one connection - only (the one supposed to be checked). - """ - if conn is None: - conn = self.get_conn_from_sock(sock) - check_connection_ntuple(conn) - - # fd, family, type - if conn.fd != -1: - self.assertEqual(conn.fd, sock.fileno()) - self.assertEqual(conn.family, sock.family) - # see: http://bugs.python.org/issue30204 - self.assertEqual( - conn.type, sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)) - - # local address - laddr = sock.getsockname() - if not laddr and PY3 and isinstance(laddr, bytes): - # See: http://bugs.python.org/issue30205 - laddr = laddr.decode() - if sock.family == AF_INET6: - laddr = laddr[:2] - if sock.family == AF_UNIX and OPENBSD: - # No addresses are set for UNIX sockets on OpenBSD. - pass - else: - self.assertEqual(conn.laddr, laddr) - - # XXX Solaris can't retrieve system-wide UNIX sockets - if sock.family == AF_UNIX and HAS_CONNECTIONS_UNIX: - cons = thisproc.connections(kind='all') - self.compare_procsys_connections(os.getpid(), cons) - return conn - - def compare_procsys_connections(self, pid, proc_cons, kind='all'): - """Given a process PID and its list of connections compare - those against system-wide connections retrieved via - psutil.net_connections. - """ - try: - sys_cons = psutil.net_connections(kind=kind) - except psutil.AccessDenied: - # On OSX, system-wide connections are retrieved by iterating - # over all processes - if OSX: - return - else: - raise - # Filter for this proc PID and exlucde PIDs from the tuple. - sys_cons = [c[:-1] for c in sys_cons if c.pid == pid] - sys_cons.sort() - proc_cons.sort() - self.assertEqual(proc_cons, sys_cons) - - -# ===================================================================== -# --- Test unconnected sockets -# ===================================================================== - - -class TestUnconnectedSockets(Base, unittest.TestCase): - """Tests sockets which are open but not connected to anything.""" - - def test_tcp_v4(self): - addr = ("127.0.0.1", get_free_port()) - with closing(bind_socket(AF_INET, SOCK_STREAM, addr=addr)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_LISTEN) - - @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") - def test_tcp_v6(self): - addr = ("::1", get_free_port()) - with closing(bind_socket(AF_INET6, SOCK_STREAM, addr=addr)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_LISTEN) - - def test_udp_v4(self): - addr = ("127.0.0.1", get_free_port()) - with closing(bind_socket(AF_INET, SOCK_DGRAM, addr=addr)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_NONE) - - @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") - def test_udp_v6(self): - addr = ("::1", get_free_port()) - with closing(bind_socket(AF_INET6, SOCK_DGRAM, addr=addr)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_NONE) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_unix_tcp(self): - with unix_socket_path() as name: - with closing(bind_unix_socket(name, type=SOCK_STREAM)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_NONE) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_unix_udp(self): - with unix_socket_path() as name: - with closing(bind_unix_socket(name, type=SOCK_STREAM)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_NONE) - - -# ===================================================================== -# --- Test connected sockets -# ===================================================================== - - -class TestConnectedSocketPairs(Base, unittest.TestCase): - """Test socket pairs which are are actually connected to - each other. - """ - - # On SunOS, even after we close() it, the server socket stays around - # in TIME_WAIT state. - @unittest.skipIf(SUNOS, "unreliable on SUONS") - def test_tcp(self): - addr = ("127.0.0.1", get_free_port()) - assert not thisproc.connections(kind='tcp4') - server, client = tcp_socketpair(AF_INET, addr=addr) - try: - cons = thisproc.connections(kind='tcp4') - self.assertEqual(len(cons), 2) - self.assertEqual(cons[0].status, psutil.CONN_ESTABLISHED) - self.assertEqual(cons[1].status, psutil.CONN_ESTABLISHED) - # May not be fast enough to change state so it stays - # commenteed. - # client.close() - # cons = thisproc.connections(kind='all') - # self.assertEqual(len(cons), 1) - # self.assertEqual(cons[0].status, psutil.CONN_CLOSE_WAIT) - finally: - server.close() - client.close() - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_unix(self): - with unix_socket_path() as name: - server, client = unix_socketpair(name) - try: - cons = thisproc.connections(kind='unix') - assert not (cons[0].laddr and cons[0].raddr) - assert not (cons[1].laddr and cons[1].raddr) - if NETBSD: - # On NetBSD creating a UNIX socket will cause - # a UNIX connection to /var/run/log. - cons = [c for c in cons if c.raddr != '/var/run/log'] - self.assertEqual(len(cons), 2) - if LINUX or FREEBSD or SUNOS: - # remote path is never set - self.assertEqual(cons[0].raddr, "") - self.assertEqual(cons[1].raddr, "") - # one local address should though - self.assertEqual(name, cons[0].laddr or cons[1].laddr) - elif OPENBSD: - # No addresses whatsoever here. - for addr in (cons[0].laddr, cons[0].raddr, - cons[1].laddr, cons[1].raddr): - self.assertEqual(addr, "") - else: - # On other systems either the laddr or raddr - # of both peers are set. - self.assertEqual(cons[0].laddr or cons[1].laddr, name) - self.assertEqual(cons[0].raddr or cons[1].raddr, name) - finally: - server.close() - client.close() - - @skip_on_access_denied(only_if=OSX) - def test_combos(self): - def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): - all_kinds = ("all", "inet", "inet4", "inet6", "tcp", "tcp4", - "tcp6", "udp", "udp4", "udp6") - check_connection_ntuple(conn) - self.assertEqual(conn.family, family) - self.assertEqual(conn.type, type) - self.assertEqual(conn.laddr, laddr) - self.assertEqual(conn.raddr, raddr) - self.assertEqual(conn.status, status) - for kind in all_kinds: - cons = proc.connections(kind=kind) - if kind in kinds: - assert cons - else: - assert not cons, cons - # compare against system-wide connections - # XXX Solaris can't retrieve system-wide UNIX - # sockets. - if HAS_CONNECTIONS_UNIX: - self.compare_procsys_connections(proc.pid, [conn]) - - tcp_template = textwrap.dedent(""" - import socket, time - s = socket.socket($family, socket.SOCK_STREAM) - s.bind(('$addr', 0)) - s.listen(1) - with open('$testfn', 'w') as f: - f.write(str(s.getsockname()[:2])) - time.sleep(60) - """) - - udp_template = textwrap.dedent(""" - import socket, time - s = socket.socket($family, socket.SOCK_DGRAM) - s.bind(('$addr', 0)) - with open('$testfn', 'w') as f: - f.write(str(s.getsockname()[:2])) - time.sleep(60) - """) - - from string import Template - testfile = os.path.basename(TESTFN) - tcp4_template = Template(tcp_template).substitute( - family=int(AF_INET), addr="127.0.0.1", testfn=testfile) - udp4_template = Template(udp_template).substitute( - family=int(AF_INET), addr="127.0.0.1", testfn=testfile) - tcp6_template = Template(tcp_template).substitute( - family=int(AF_INET6), addr="::1", testfn=testfile) - udp6_template = Template(udp_template).substitute( - family=int(AF_INET6), addr="::1", testfn=testfile) - - # launch various subprocess instantiating a socket of various - # families and types to enrich psutil results - tcp4_proc = pyrun(tcp4_template) - tcp4_addr = eval(wait_for_file(testfile)) - udp4_proc = pyrun(udp4_template) - udp4_addr = eval(wait_for_file(testfile)) - if supports_ipv6(): - tcp6_proc = pyrun(tcp6_template) - tcp6_addr = eval(wait_for_file(testfile)) - udp6_proc = pyrun(udp6_template) - udp6_addr = eval(wait_for_file(testfile)) - else: - tcp6_proc = None - udp6_proc = None - tcp6_addr = None - udp6_addr = None - - for p in thisproc.children(): - cons = p.connections() - self.assertEqual(len(cons), 1) - for conn in cons: - # TCP v4 - if p.pid == tcp4_proc.pid: - check_conn(p, conn, AF_INET, SOCK_STREAM, tcp4_addr, (), - psutil.CONN_LISTEN, - ("all", "inet", "inet4", "tcp", "tcp4")) - # UDP v4 - elif p.pid == udp4_proc.pid: - check_conn(p, conn, AF_INET, SOCK_DGRAM, udp4_addr, (), - psutil.CONN_NONE, - ("all", "inet", "inet4", "udp", "udp4")) - # TCP v6 - elif p.pid == getattr(tcp6_proc, "pid", None): - check_conn(p, conn, AF_INET6, SOCK_STREAM, tcp6_addr, (), - psutil.CONN_LISTEN, - ("all", "inet", "inet6", "tcp", "tcp6")) - # UDP v6 - elif p.pid == getattr(udp6_proc, "pid", None): - check_conn(p, conn, AF_INET6, SOCK_DGRAM, udp6_addr, (), - psutil.CONN_NONE, - ("all", "inet", "inet6", "udp", "udp6")) - - # err - self.assertRaises(ValueError, p.connections, kind='???') - - def test_multi_sockets_filtering(self): - with create_sockets() as socks: - cons = thisproc.connections(kind='all') - self.assertEqual(len(cons), len(socks)) - # tcp - cons = thisproc.connections(kind='tcp') - self.assertEqual(len(cons), 2 if supports_ipv6() else 1) - for conn in cons: - self.assertIn(conn.family, (AF_INET, AF_INET6)) - self.assertEqual(conn.type, SOCK_STREAM) - # tcp4 - cons = thisproc.connections(kind='tcp4') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET) - self.assertEqual(cons[0].type, SOCK_STREAM) - # tcp6 - if supports_ipv6(): - cons = thisproc.connections(kind='tcp6') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET6) - self.assertEqual(cons[0].type, SOCK_STREAM) - # udp - cons = thisproc.connections(kind='udp') - self.assertEqual(len(cons), 2 if supports_ipv6() else 1) - for conn in cons: - self.assertIn(conn.family, (AF_INET, AF_INET6)) - self.assertEqual(conn.type, SOCK_DGRAM) - # udp4 - cons = thisproc.connections(kind='udp4') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET) - self.assertEqual(cons[0].type, SOCK_DGRAM) - # udp6 - if supports_ipv6(): - cons = thisproc.connections(kind='udp6') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET6) - self.assertEqual(cons[0].type, SOCK_DGRAM) - # inet - cons = thisproc.connections(kind='inet') - self.assertEqual(len(cons), 4 if supports_ipv6() else 2) - for conn in cons: - self.assertIn(conn.family, (AF_INET, AF_INET6)) - self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) - # inet6 - if supports_ipv6(): - cons = thisproc.connections(kind='inet6') - self.assertEqual(len(cons), 2) - for conn in cons: - self.assertEqual(conn.family, AF_INET6) - self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) - # unix - if HAS_CONNECTIONS_UNIX: - cons = thisproc.connections(kind='unix') - self.assertEqual(len(cons), 3) - for conn in cons: - self.assertEqual(conn.family, AF_UNIX) - self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) - - -# ===================================================================== -# --- Miscellaneous tests -# ===================================================================== - - -class TestSystemWideConnections(Base, unittest.TestCase): - """Tests for net_connections().""" - - @skip_on_access_denied() - def test_it(self): - def check(cons, families, types_): - AF_UNIX = getattr(socket, 'AF_UNIX', object()) - for conn in cons: - self.assertIn(conn.family, families, msg=conn) - if conn.family != AF_UNIX: - self.assertIn(conn.type, types_, msg=conn) - check_connection_ntuple(conn) - - with create_sockets(): - from psutil._common import conn_tmap - for kind, groups in conn_tmap.items(): - # XXX: SunOS does not retrieve UNIX sockets. - if kind == 'unix' and not HAS_CONNECTIONS_UNIX: - continue - families, types_ = groups - cons = psutil.net_connections(kind) - self.assertEqual(len(cons), len(set(cons))) - check(cons, families, types_) - - self.assertRaises(ValueError, psutil.net_connections, kind='???') - - @skip_on_access_denied() - def test_multi_socks(self): - with create_sockets() as socks: - cons = [x for x in psutil.net_connections(kind='all') - if x.pid == os.getpid()] - self.assertEqual(len(cons), len(socks)) - - @skip_on_access_denied() - # See: https://travis-ci.org/giampaolo/psutil/jobs/237566297 - @unittest.skipIf(OSX and TRAVIS, "unreliable on OSX + TRAVIS") - def test_multi_sockets_procs(self): - # Creates multiple sub processes, each creating different - # sockets. For each process check that proc.connections() - # and net_connections() return the same results. - # This is done mainly to check whether net_connections()'s - # pid is properly set, see: - # https://github.com/giampaolo/psutil/issues/1013 - with create_sockets() as socks: - expected = len(socks) - pids = [] - times = 10 - for i in range(times): - fname = os.path.realpath(TESTFN) + str(i) - src = textwrap.dedent("""\ - import time, os - from psutil.tests import create_sockets - with create_sockets(): - with open('%s', 'w') as f: - f.write(str(os.getpid())) - time.sleep(60) - """ % fname) - sproc = pyrun(src) - pids.append(sproc.pid) - self.addCleanup(safe_rmpath, fname) - - # sync - for i in range(times): - fname = TESTFN + str(i) - wait_for_file(fname) - - syscons = [x for x in psutil.net_connections(kind='all') if x.pid - in pids] - for pid in pids: - self.assertEqual(len([x for x in syscons if x.pid == pid]), - expected) - p = psutil.Process(pid) - self.assertEqual(len(p.connections('all')), expected) - - -# ===================================================================== -# --- Miscellaneous tests -# ===================================================================== - - -class TestMisc(unittest.TestCase): - - def test_connection_constants(self): - ints = [] - strs = [] - for name in dir(psutil): - if name.startswith('CONN_'): - num = getattr(psutil, name) - str_ = str(num) - assert str_.isupper(), str_ - self.assertNotIn(str, strs) - self.assertNotIn(num, ints) - ints.append(num) - strs.append(str_) - if SUNOS: - psutil.CONN_IDLE - psutil.CONN_BOUND - if WINDOWS: - psutil.CONN_DELETE_TCB - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py deleted file mode 100755 index 0dfb3e2a82..0000000000 --- a/psutil/tests/test_contracts.py +++ /dev/null @@ -1,650 +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. - -"""Contracts tests. These tests mainly check API sanity in terms of -returned types and APIs availability. -Some of these are duplicates of tests test_system.py and test_process.py -""" - -import errno -import os -import stat -import time -import traceback -import warnings -from contextlib import closing - -from psutil import AIX -from psutil import BSD -from psutil import FREEBSD -from psutil import LINUX -from psutil import NETBSD -from psutil import OPENBSD -from psutil import OSX -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._compat import long -from psutil.tests import bind_unix_socket -from psutil.tests import check_connection_ntuple -from psutil.tests import get_kernel_version -from psutil.tests import HAS_CONNECTIONS_UNIX -from psutil.tests import HAS_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 run_test_module_by_name -from psutil.tests import safe_rmpath -from psutil.tests import skip_on_access_denied -from psutil.tests import TESTFN -from psutil.tests import unittest -from psutil.tests import unix_socket_path -from psutil.tests import VALID_PROC_STATUSES -from psutil.tests import warn -import psutil - - -# =================================================================== -# --- APIs availability -# =================================================================== - - -class TestAvailability(unittest.TestCase): - """Make sure code reflects what doc promises in terms of APIs - availability. - """ - - def test_cpu_affinity(self): - hasit = LINUX or WINDOWS or FREEBSD - self.assertEqual(hasattr(psutil.Process, "cpu_affinity"), hasit) - - def test_win_service(self): - self.assertEqual(hasattr(psutil, "win_service_iter"), WINDOWS) - self.assertEqual(hasattr(psutil, "win_service_get"), WINDOWS) - - def test_PROCFS_PATH(self): - self.assertEqual(hasattr(psutil, "PROCFS_PATH"), - LINUX or SUNOS or AIX) - - def test_win_priority(self): - ae = self.assertEqual - ae(hasattr(psutil, "ABOVE_NORMAL_PRIORITY_CLASS"), WINDOWS) - ae(hasattr(psutil, "BELOW_NORMAL_PRIORITY_CLASS"), WINDOWS) - ae(hasattr(psutil, "HIGH_PRIORITY_CLASS"), WINDOWS) - ae(hasattr(psutil, "IDLE_PRIORITY_CLASS"), WINDOWS) - ae(hasattr(psutil, "NORMAL_PRIORITY_CLASS"), WINDOWS) - ae(hasattr(psutil, "REALTIME_PRIORITY_CLASS"), WINDOWS) - - def test_linux_ioprio(self): - ae = self.assertEqual - ae(hasattr(psutil, "IOPRIO_CLASS_NONE"), LINUX) - ae(hasattr(psutil, "IOPRIO_CLASS_RT"), LINUX) - ae(hasattr(psutil, "IOPRIO_CLASS_BE"), LINUX) - ae(hasattr(psutil, "IOPRIO_CLASS_IDLE"), LINUX) - - def test_linux_rlimit(self): - ae = self.assertEqual - hasit = LINUX and get_kernel_version() >= (2, 6, 36) - ae(hasattr(psutil.Process, "rlimit"), hasit) - ae(hasattr(psutil, "RLIM_INFINITY"), hasit) - ae(hasattr(psutil, "RLIMIT_AS"), hasit) - ae(hasattr(psutil, "RLIMIT_CORE"), hasit) - ae(hasattr(psutil, "RLIMIT_CPU"), hasit) - ae(hasattr(psutil, "RLIMIT_DATA"), hasit) - ae(hasattr(psutil, "RLIMIT_FSIZE"), hasit) - ae(hasattr(psutil, "RLIMIT_LOCKS"), hasit) - ae(hasattr(psutil, "RLIMIT_MEMLOCK"), hasit) - ae(hasattr(psutil, "RLIMIT_NOFILE"), hasit) - ae(hasattr(psutil, "RLIMIT_NPROC"), hasit) - ae(hasattr(psutil, "RLIMIT_RSS"), hasit) - ae(hasattr(psutil, "RLIMIT_STACK"), hasit) - - hasit = LINUX and get_kernel_version() >= (3, 0) - ae(hasattr(psutil, "RLIMIT_MSGQUEUE"), hasit) - ae(hasattr(psutil, "RLIMIT_NICE"), hasit) - ae(hasattr(psutil, "RLIMIT_RTPRIO"), hasit) - ae(hasattr(psutil, "RLIMIT_RTTIME"), hasit) - ae(hasattr(psutil, "RLIMIT_SIGPENDING"), hasit) - - def test_cpu_freq(self): - linux = (LINUX and - (os.path.exists("/sys/devices/system/cpu/cpufreq") or - os.path.exists("/sys/devices/system/cpu/cpu0/cpufreq"))) - self.assertEqual(hasattr(psutil, "cpu_freq"), linux or OSX or WINDOWS) - - def test_sensors_temperatures(self): - self.assertEqual(hasattr(psutil, "sensors_temperatures"), LINUX) - - 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 OSX) - - def test_proc_environ(self): - self.assertEqual(hasattr(psutil.Process, "environ"), - LINUX or OSX or WINDOWS) - - def test_proc_uids(self): - self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) - - def test_proc_gids(self): - self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) - - def test_proc_terminal(self): - self.assertEqual(hasattr(psutil.Process, "terminal"), POSIX) - - def test_proc_ionice(self): - self.assertEqual(hasattr(psutil.Process, "ionice"), LINUX or WINDOWS) - - def test_proc_rlimit(self): - self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX) - - def test_proc_io_counters(self): - hasit = hasattr(psutil.Process, "io_counters") - self.assertEqual(hasit, False if OSX or SUNOS else True) - - def test_proc_num_fds(self): - self.assertEqual(hasattr(psutil.Process, "num_fds"), POSIX) - - def test_proc_num_handles(self): - self.assertEqual(hasattr(psutil.Process, "num_handles"), WINDOWS) - - def test_proc_cpu_affinity(self): - self.assertEqual(hasattr(psutil.Process, "cpu_affinity"), - LINUX or WINDOWS or FREEBSD) - - def test_proc_cpu_num(self): - self.assertEqual(hasattr(psutil.Process, "cpu_num"), - LINUX or FREEBSD or SUNOS) - - def test_proc_memory_maps(self): - hasit = hasattr(psutil.Process, "memory_maps") - self.assertEqual(hasit, False if OPENBSD or NETBSD or AIX 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(), FutureWarning) - self.assertIn("memory_info_ex() is deprecated", str(w.message)) - self.assertIn("use memory_info() instead", str(w.message)) - - -# =================================================================== -# --- System API types -# =================================================================== - - -class TestSystem(unittest.TestCase): - """Check the return types of system related APIs. - Mainly we want to test we never return unicode on Python 2, see: - https://github.com/giampaolo/psutil/issues/1039 - """ - - @classmethod - def setUpClass(cls): - cls.proc = psutil.Process() - - def tearDown(self): - safe_rmpath(TESTFN) - - def test_cpu_times(self): - # Duplicate of test_system.py. Keep it anyway. - ret = psutil.cpu_times() - assert is_namedtuple(ret) - for n in ret: - self.assertIsInstance(n, float) - self.assertGreaterEqual(n, 0) - - def test_io_counters(self): - # Duplicate of test_system.py. Keep it anyway. - for k in psutil.disk_io_counters(perdisk=True): - self.assertIsInstance(k, str) - - def test_disk_partitions(self): - # Duplicate of test_system.py. Keep it anyway. - for disk in psutil.disk_partitions(): - self.assertIsInstance(disk.device, str) - self.assertIsInstance(disk.mountpoint, str) - self.assertIsInstance(disk.fstype, str) - self.assertIsInstance(disk.opts, str) - - @unittest.skipIf(not POSIX, 'POSIX only') - @unittest.skipIf(not HAS_CONNECTIONS_UNIX, "can't list UNIX sockets") - @skip_on_access_denied(only_if=OSX) - def test_net_connections(self): - with unix_socket_path() as name: - with closing(bind_unix_socket(name)): - cons = psutil.net_connections(kind='unix') - assert cons - for conn in cons: - self.assertIsInstance(conn.laddr, str) - - def test_net_if_addrs(self): - # Duplicate of test_system.py. Keep it anyway. - for ifname, addrs in psutil.net_if_addrs().items(): - self.assertIsInstance(ifname, str) - for addr in addrs: - self.assertIsInstance(addr.address, str) - self.assertIsInstance(addr.netmask, (str, type(None))) - self.assertIsInstance(addr.broadcast, (str, type(None))) - - def test_net_if_stats(self): - # Duplicate of test_system.py. Keep it anyway. - for ifname, _ in psutil.net_if_stats().items(): - self.assertIsInstance(ifname, str) - - def test_net_io_counters(self): - # Duplicate of test_system.py. Keep it anyway. - for ifname, _ in psutil.net_io_counters(pernic=True).items(): - self.assertIsInstance(ifname, str) - - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") - def test_sensors_fans(self): - # Duplicate of test_system.py. Keep it anyway. - for name, units in psutil.sensors_fans().items(): - self.assertIsInstance(name, str) - for unit in units: - self.assertIsInstance(unit.label, str) - - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") - def test_sensors_temperatures(self): - # Duplicate of test_system.py. Keep it anyway. - for name, units in psutil.sensors_temperatures().items(): - self.assertIsInstance(name, str) - for unit in units: - self.assertIsInstance(unit.label, str) - - def test_users(self): - # Duplicate of test_system.py. Keep it anyway. - for user in psutil.users(): - self.assertIsInstance(user.name, str) - self.assertIsInstance(user.terminal, (str, type(None))) - self.assertIsInstance(user.host, (str, type(None))) - self.assertIsInstance(user.pid, (int, type(None))) - - -# =================================================================== -# --- Featch all processes test -# =================================================================== - - -class TestFetchAllProcesses(unittest.TestCase): - """Test which iterates over all running processes and performs - some sanity checks against Process API's returned values. - """ - - def 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([ - 'send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', - 'as_dict', 'parent', '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) - - default = object() - failures = [] - 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 as err: - 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) - - if failures: - self.fail(''.join(failures)) - - # we should always have a non-empty list, not including PID 0 etc. - # special cases. - assert valid_procs - - def cmdline(self, ret, proc): - self.assertIsInstance(ret, list) - for part in ret: - self.assertIsInstance(part, str) - - def exe(self, ret, proc): - self.assertIsInstance(ret, (str, type(None))) - if not ret: - self.assertEqual(ret, '') - else: - assert os.path.isabs(ret), ret - # Note: os.stat() may return False even if the file is there - # hence we skip the test, see: - # http://stackoverflow.com/questions/3112546/os-path-exists-lies - if POSIX and os.path.isfile(ret): - if hasattr(os, 'access') and hasattr(os, "X_OK"): - # XXX may fail on OSX - assert os.access(ret, os.X_OK) - - def pid(self, ret, proc): - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 0) - - def ppid(self, ret, proc): - self.assertIsInstance(ret, (int, long)) - self.assertGreaterEqual(ret, 0) - - def name(self, ret, proc): - self.assertIsInstance(ret, str) - # on AIX, "" processes don't have names - if not AIX: - assert ret - - def create_time(self, ret, proc): - self.assertIsInstance(ret, float) - try: - self.assertGreaterEqual(ret, 0) - except AssertionError: - # XXX - if OPENBSD and proc.status() == psutil.STATUS_ZOMBIE: - pass - else: - raise - # this can't be taken for granted on all platforms - # self.assertGreaterEqual(ret, psutil.boot_time()) - # make sure returned value can be pretty printed - # with strftime - time.strftime("%Y %m %d %H:%M:%S", time.localtime(ret)) - - def uids(self, ret, proc): - assert is_namedtuple(ret) - for uid in ret: - self.assertIsInstance(uid, int) - self.assertGreaterEqual(uid, 0) - self.assertIn(uid, self.all_uids) - - def gids(self, ret, proc): - assert is_namedtuple(ret) - # note: testing all gids as above seems not to be reliable for - # gid == 30 (nodoby); not sure why. - for gid in ret: - self.assertIsInstance(gid, int) - if not OSX 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) - assert ret - self.assertNotEqual(ret, '?') # XXX - self.assertIn(ret, VALID_PROC_STATUSES) - - def io_counters(self, ret, proc): - assert is_namedtuple(ret) - for field in ret: - self.assertIsInstance(field, (int, long)) - if field != -1: - self.assertGreaterEqual(field, 0) - - def ionice(self, ret, proc): - if POSIX: - assert is_namedtuple(ret) - for field in ret: - self.assertIsInstance(field, int) - if LINUX: - self.assertGreaterEqual(ret.ioclass, 0) - self.assertGreaterEqual(ret.value, 0) - else: - self.assertGreaterEqual(ret, 0) - self.assertIn(ret, (0, 1, 2)) - - def num_threads(self, ret, proc): - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 1) - - def threads(self, ret, proc): - self.assertIsInstance(ret, list) - for t in ret: - assert is_namedtuple(t) - self.assertGreaterEqual(t.id, 0) - self.assertGreaterEqual(t.user_time, 0) - self.assertGreaterEqual(t.system_time, 0) - for field in t: - self.assertIsInstance(field, (int, float)) - - def cpu_times(self, ret, proc): - assert is_namedtuple(ret) - for n in ret: - self.assertIsInstance(n, float) - self.assertGreaterEqual(n, 0) - # TODO: check ntuple fields - - def cpu_percent(self, ret, proc): - self.assertIsInstance(ret, float) - assert 0.0 <= ret <= 100.0, ret - - def cpu_num(self, ret, proc): - self.assertIsInstance(ret, int) - if FREEBSD and ret == -1: - return - self.assertGreaterEqual(ret, 0) - if psutil.cpu_count() == 1: - self.assertEqual(ret, 0) - self.assertIn(ret, list(range(psutil.cpu_count()))) - - def memory_info(self, ret, proc): - assert is_namedtuple(ret) - for value in ret: - self.assertIsInstance(value, (int, long)) - self.assertGreaterEqual(value, 0) - if POSIX and not AIX and ret.vms != 0: - # VMS is always supposed to be the highest - for name in ret._fields: - if name != 'vms': - value = getattr(ret, name) - self.assertGreater(ret.vms, value, msg=ret) - elif WINDOWS: - self.assertGreaterEqual(ret.peak_wset, ret.wset) - self.assertGreaterEqual(ret.peak_paged_pool, ret.paged_pool) - self.assertGreaterEqual(ret.peak_nonpaged_pool, ret.nonpaged_pool) - self.assertGreaterEqual(ret.peak_pagefile, ret.pagefile) - - def memory_full_info(self, ret, proc): - assert is_namedtuple(ret) - total = psutil.virtual_memory().total - for name in ret._fields: - value = getattr(ret, name) - self.assertIsInstance(value, (int, long)) - self.assertGreaterEqual(value, 0, msg=(name, value)) - self.assertLessEqual(value, total, msg=(name, value, total)) - - if LINUX: - self.assertGreaterEqual(ret.pss, ret.uss) - - def open_files(self, ret, proc): - self.assertIsInstance(ret, list) - for f in ret: - self.assertIsInstance(f.fd, int) - self.assertIsInstance(f.path, str) - if WINDOWS: - self.assertEqual(f.fd, -1) - elif LINUX: - self.assertIsInstance(f.position, int) - self.assertIsInstance(f.mode, str) - self.assertIsInstance(f.flags, int) - self.assertGreaterEqual(f.position, 0) - self.assertIn(f.mode, ('r', 'w', 'a', 'r+', 'a+')) - self.assertGreater(f.flags, 0) - elif BSD and not f.path: - # XXX see: https://github.com/giampaolo/psutil/issues/595 - continue - assert os.path.isabs(f.path), f - assert os.path.isfile(f.path), f - - def num_fds(self, ret, proc): - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 0) - - def connections(self, ret, proc): - self.assertEqual(len(ret), len(set(ret))) - for conn in ret: - check_connection_ntuple(conn) - - def cwd(self, ret, proc): - if ret: # 'ret' can be None or empty - self.assertIsInstance(ret, str) - assert os.path.isabs(ret), ret - try: - st = os.stat(ret) - except OSError as err: - if WINDOWS and err.errno in \ - psutil._psplatform.ACCESS_DENIED_SET: - pass - # directory has been removed in mean time - elif err.errno != errno.ENOENT: - raise - else: - assert stat.S_ISDIR(st.st_mode) - - def memory_percent(self, ret, proc): - self.assertIsInstance(ret, float) - assert 0 <= ret <= 100, ret - - def is_running(self, ret, proc): - self.assertIsInstance(ret, bool) - - def cpu_affinity(self, ret, proc): - self.assertIsInstance(ret, list) - assert ret != [], ret - cpus = range(psutil.cpu_count()) - for n in ret: - self.assertIsInstance(n, int) - self.assertIn(n, cpus) - - def terminal(self, ret, proc): - self.assertIsInstance(ret, (str, type(None))) - if ret is not None: - assert os.path.isabs(ret), ret - assert os.path.exists(ret), ret - - def memory_maps(self, ret, proc): - for nt in ret: - self.assertIsInstance(nt.addr, str) - self.assertIsInstance(nt.perms, str) - self.assertIsInstance(nt.path, str) - for fname in nt._fields: - value = getattr(nt, fname) - if fname == 'path': - if not value.startswith('['): - assert os.path.isabs(nt.path), nt.path - # commented as on Linux we might get - # '/foo/bar (deleted)' - # assert os.path.exists(nt.path), nt.path - elif fname in ('addr', 'perms'): - assert value - else: - self.assertIsInstance(value, (int, long)) - self.assertGreaterEqual(value, 0) - - def num_handles(self, ret, proc): - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 0) - - def nice(self, ret, proc): - self.assertIsInstance(ret, int) - if POSIX: - assert -20 <= ret <= 20, ret - else: - priorities = [getattr(psutil, x) for x in dir(psutil) - if x.endswith('_PRIORITY_CLASS')] - self.assertIn(ret, priorities) - - def num_ctx_switches(self, ret, proc): - assert is_namedtuple(ret) - for value in ret: - self.assertIsInstance(value, (int, long)) - self.assertGreaterEqual(value, 0) - - def rlimit(self, ret, proc): - self.assertIsInstance(ret, tuple) - self.assertEqual(len(ret), 2) - self.assertGreaterEqual(ret[0], -1) - self.assertGreaterEqual(ret[1], -1) - - def environ(self, ret, proc): - self.assertIsInstance(ret, dict) - for k, v in ret.items(): - self.assertIsInstance(k, str) - self.assertIsInstance(v, str) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py deleted file mode 100755 index d4eaf2a146..0000000000 --- a/psutil/tests/test_linux.py +++ /dev/null @@ -1,1983 +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. - -"""Linux specific tests.""" - -from __future__ import division -import collections -import contextlib -import errno -import glob -import io -import os -import re -import shutil -import socket -import struct -import tempfile -import textwrap -import time -import warnings - -import psutil -from psutil import LINUX -from psutil._compat import basestring -from psutil._compat import PY3 -from psutil._compat import u -from psutil.tests import call_until -from psutil.tests import HAS_BATTERY -from psutil.tests import HAS_CPU_FREQ -from psutil.tests import HAS_RLIMIT -from psutil.tests import MEMORY_TOLERANCE -from psutil.tests import mock -from psutil.tests import PYPY -from psutil.tests import pyrun -from psutil.tests import reap_children -from psutil.tests import reload_module -from psutil.tests import retry_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 -from psutil.tests import TESTFN -from psutil.tests import ThreadTask -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import which - - -HERE = os.path.abspath(os.path.dirname(__file__)) -SIOCGIFADDR = 0x8915 -SIOCGIFCONF = 0x8912 -SIOCGIFHWADDR = 0x8927 -if LINUX: - SECTOR_SIZE = 512 - - -# ===================================================================== -# --- utils -# ===================================================================== - - -def get_ipv4_address(ifname): - import fcntl - ifname = ifname[:15] - if PY3: - ifname = bytes(ifname, 'ascii') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - with contextlib.closing(s): - return socket.inet_ntoa( - fcntl.ioctl(s.fileno(), - SIOCGIFADDR, - struct.pack('256s', ifname))[20:24]) - - -def get_mac_address(ifname): - import fcntl - ifname = ifname[:15] - if PY3: - ifname = bytes(ifname, 'ascii') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - with contextlib.closing(s): - info = fcntl.ioctl( - s.fileno(), SIOCGIFHWADDR, struct.pack('256s', ifname)) - if PY3: - def ord(x): - return x - else: - import __builtin__ - ord = __builtin__.ord - return ''.join(['%02x:' % ord(char) for char in info[18:24]])[:-1] - - -def free_swap(): - """Parse 'free' cmd and return swap memory's s total, used and free - values. - """ - out = sh('free -b') - lines = out.split('\n') - for line in lines: - if line.startswith('Swap'): - _, total, used, free = line.split() - nt = collections.namedtuple('free', 'total used free') - return nt(int(total), int(used), int(free)) - raise ValueError( - "can't find 'Swap' in 'free' output:\n%s" % '\n'.join(lines)) - - -def free_physmem(): - """Parse 'free' cmd and return physical memory's total, used - and free values. - """ - # Note: free can have 2 different formats, invalidating 'shared' - # and 'cached' memory which may have different positions so we - # do not return them. - # https://github.com/giampaolo/psutil/issues/538#issuecomment-57059946 - out = sh('free -b') - lines = out.split('\n') - for line in lines: - if line.startswith('Mem'): - total, used, free, shared = \ - [int(x) for x in line.split()[1:5]] - nt = collections.namedtuple( - 'free', 'total used free shared output') - return nt(total, used, free, shared, out) - raise ValueError( - "can't find 'Mem' in 'free' output:\n%s" % '\n'.join(lines)) - - -def vmstat(stat): - out = sh("vmstat -s") - for line in out.split("\n"): - line = line.strip() - if stat in line: - return int(line.split(' ')[0]) - raise ValueError("can't find %r in 'vmstat' output" % stat) - - -def get_free_version_info(): - out = sh("free -V").strip() - return tuple(map(int, out.split()[-1].split('.'))) - - -@contextlib.contextmanager -def mock_open_content(for_path, content): - """Mock open() builtin and forces it to return a certain `content` - on read() if the path being opened matches `for_path`. - """ - def open_mock(name, *args, **kwargs): - if name == for_path: - if PY3: - if isinstance(content, basestring): - return io.StringIO(content) - else: - return io.BytesIO(content) - else: - return io.BytesIO(content) - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, create=True, side_effect=open_mock) as m: - yield m - - -@contextlib.contextmanager -def mock_open_exception(for_path, exc): - """Mock open() builtin and raises `exc` if the path being opened - matches `for_path`. - """ - def open_mock(name, *args, **kwargs): - if name == for_path: - raise exc - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, create=True, side_effect=open_mock) as m: - yield m - - -# ===================================================================== -# --- system virtual memory -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemVirtualMemory(unittest.TestCase): - - def test_total(self): - # free_value = free_physmem().total - # psutil_value = psutil.virtual_memory().total - # self.assertEqual(free_value, psutil_value) - vmstat_value = vmstat('total memory') * 1024 - psutil_value = psutil.virtual_memory().total - self.assertAlmostEqual(vmstat_value, psutil_value) - - # Older versions of procps used slab memory to calculate used memory. - # This got changed in: - # https://gitlab.com/procps-ng/procps/commit/ - # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e - @unittest.skipIf(LINUX and get_free_version_info() < (3, 3, 12), - "old free version") - @retry_before_failing() - def test_used(self): - free = free_physmem() - free_value = free.used - psutil_value = psutil.virtual_memory().used - self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE, - msg='%s %s \n%s' % (free_value, psutil_value, free.output)) - - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - @retry_before_failing() - 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() - def test_buffers(self): - vmstat_value = vmstat('buffer memory') * 1024 - psutil_value = psutil.virtual_memory().buffers - self.assertAlmostEqual( - vmstat_value, psutil_value, delta=MEMORY_TOLERANCE) - - # https://travis-ci.org/giampaolo/psutil/jobs/226719664 - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - @retry_before_failing() - def test_active(self): - vmstat_value = vmstat('active memory') * 1024 - psutil_value = psutil.virtual_memory().active - self.assertAlmostEqual( - vmstat_value, psutil_value, delta=MEMORY_TOLERANCE) - - # https://travis-ci.org/giampaolo/psutil/jobs/227242952 - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - @retry_before_failing() - 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() - def test_shared(self): - free = free_physmem() - free_value = free.shared - if free_value == 0: - raise unittest.SkipTest("free does not support 'shared' column") - psutil_value = psutil.virtual_memory().shared - self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE, - msg='%s %s \n%s' % (free_value, psutil_value, free.output)) - - @retry_before_failing() - def test_available(self): - # "free" output format has changed at some point: - # https://github.com/giampaolo/psutil/issues/538#issuecomment-147192098 - out = sh("free -b") - lines = out.split('\n') - if 'available' not in lines[0]: - raise unittest.SkipTest("free does not support 'available' column") - else: - free_value = int(lines[1].split()[-1]) - psutil_value = psutil.virtual_memory().available - self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE, - msg='%s %s \n%s' % (free_value, psutil_value, out)) - - def test_warnings_on_misses(self): - # Emulate a case where /proc/meminfo provides few info. - # psutil is supposed to set the missing fields to 0 and - # raise a warning. - with mock_open_content( - '/proc/meminfo', - textwrap.dedent("""\ - Active(anon): 6145416 kB - Active(file): 2950064 kB - Inactive(anon): 574764 kB - Inactive(file): 1567648 kB - MemAvailable: -1 kB - MemFree: 2057400 kB - MemTotal: 16325648 kB - SReclaimable: 346648 kB - """).encode()) as m: - with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter("always") - ret = psutil.virtual_memory() - assert m.called - self.assertEqual(len(ws), 1) - w = ws[0] - assert w.filename.endswith('psutil/_pslinux.py') - self.assertIn( - "memory stats couldn't be determined", str(w.message)) - self.assertIn("cached", str(w.message)) - self.assertIn("shared", str(w.message)) - self.assertIn("active", str(w.message)) - self.assertIn("inactive", str(w.message)) - self.assertIn("buffers", str(w.message)) - self.assertIn("available", str(w.message)) - self.assertEqual(ret.cached, 0) - self.assertEqual(ret.active, 0) - self.assertEqual(ret.inactive, 0) - self.assertEqual(ret.shared, 0) - self.assertEqual(ret.buffers, 0) - self.assertEqual(ret.available, 0) - self.assertEqual(ret.slab, 0) - - def test_avail_old_percent(self): - # Make sure that our calculation of avail mem for old kernels - # is off by max 10%. - from psutil._pslinux import calculate_avail_vmem - from psutil._pslinux import open_binary - - mems = {} - with open_binary('/proc/meminfo') as f: - for line in f: - fields = line.split() - mems[fields[0]] = int(fields[1]) * 1024 - - a = calculate_avail_vmem(mems) - if b'MemAvailable:' in mems: - b = mems[b'MemAvailable:'] - diff_percent = abs(a - b) / a * 100 - self.assertLess(diff_percent, 10) - - def test_avail_old_comes_from_kernel(self): - # Make sure "MemAvailable:" coluimn is used instead of relying - # on our internal algorithm to calculate avail mem. - with mock_open_content( - '/proc/meminfo', - textwrap.dedent("""\ - Active: 9444728 kB - Active(anon): 6145416 kB - Active(file): 2950064 kB - Buffers: 287952 kB - Cached: 4818144 kB - Inactive(file): 1578132 kB - Inactive(anon): 574764 kB - Inactive(file): 1567648 kB - MemAvailable: 6574984 kB - MemFree: 2057400 kB - MemTotal: 16325648 kB - Shmem: 577588 kB - SReclaimable: 346648 kB - """).encode()) as m: - with warnings.catch_warnings(record=True) as ws: - ret = psutil.virtual_memory() - assert m.called - self.assertEqual(ret.available, 6574984 * 1024) - w = ws[0] - self.assertIn( - "inactive memory stats couldn't be determined", str(w.message)) - - def test_avail_old_missing_fields(self): - # Remove Active(file), Inactive(file) and SReclaimable - # from /proc/meminfo and make sure the fallback is used - # (free + cached), - with mock_open_content( - "/proc/meminfo", - textwrap.dedent("""\ - Active: 9444728 kB - Active(anon): 6145416 kB - Buffers: 287952 kB - Cached: 4818144 kB - Inactive(file): 1578132 kB - Inactive(anon): 574764 kB - MemFree: 2057400 kB - MemTotal: 16325648 kB - Shmem: 577588 kB - """).encode()) as m: - with warnings.catch_warnings(record=True) as ws: - ret = psutil.virtual_memory() - assert m.called - self.assertEqual(ret.available, 2057400 * 1024 + 4818144 * 1024) - w = ws[0] - self.assertIn( - "inactive memory stats couldn't be determined", str(w.message)) - - def test_avail_old_missing_zoneinfo(self): - # Remove /proc/zoneinfo file. Make sure fallback is used - # (free + cached). - with mock_open_content( - "/proc/meminfo", - textwrap.dedent("""\ - Active: 9444728 kB - Active(anon): 6145416 kB - Active(file): 2950064 kB - Buffers: 287952 kB - Cached: 4818144 kB - Inactive(file): 1578132 kB - Inactive(anon): 574764 kB - Inactive(file): 1567648 kB - MemFree: 2057400 kB - MemTotal: 16325648 kB - Shmem: 577588 kB - SReclaimable: 346648 kB - """).encode()): - with mock_open_exception( - "/proc/zoneinfo", - IOError(errno.ENOENT, 'no such file or directory')): - with warnings.catch_warnings(record=True) as ws: - ret = psutil.virtual_memory() - self.assertEqual( - ret.available, 2057400 * 1024 + 4818144 * 1024) - w = ws[0] - self.assertIn( - "inactive memory stats couldn't be determined", - str(w.message)) - - def test_virtual_memory_mocked(self): - # Emulate /proc/meminfo because neither vmstat nor free return slab. - def open_mock(name, *args, **kwargs): - if name == '/proc/meminfo': - return io.BytesIO(textwrap.dedent("""\ - MemTotal: 100 kB - MemFree: 2 kB - MemAvailable: 3 kB - Buffers: 4 kB - Cached: 5 kB - SwapCached: 6 kB - Active: 7 kB - Inactive: 8 kB - Active(anon): 9 kB - Inactive(anon): 10 kB - Active(file): 11 kB - Inactive(file): 12 kB - Unevictable: 13 kB - Mlocked: 14 kB - SwapTotal: 15 kB - SwapFree: 16 kB - Dirty: 17 kB - Writeback: 18 kB - AnonPages: 19 kB - Mapped: 20 kB - Shmem: 21 kB - Slab: 22 kB - SReclaimable: 23 kB - SUnreclaim: 24 kB - KernelStack: 25 kB - PageTables: 26 kB - NFS_Unstable: 27 kB - Bounce: 28 kB - WritebackTmp: 29 kB - CommitLimit: 30 kB - Committed_AS: 31 kB - VmallocTotal: 32 kB - VmallocUsed: 33 kB - VmallocChunk: 34 kB - HardwareCorrupted: 35 kB - AnonHugePages: 36 kB - ShmemHugePages: 37 kB - ShmemPmdMapped: 38 kB - CmaTotal: 39 kB - CmaFree: 40 kB - HugePages_Total: 41 kB - HugePages_Free: 42 kB - HugePages_Rsvd: 43 kB - HugePages_Surp: 44 kB - Hugepagesize: 45 kB - DirectMap46k: 46 kB - DirectMap47M: 47 kB - DirectMap48G: 48 kB - """).encode()) - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, create=True, side_effect=open_mock) as m: - mem = psutil.virtual_memory() - assert m.called - self.assertEqual(mem.total, 100 * 1024) - self.assertEqual(mem.free, 2 * 1024) - self.assertEqual(mem.buffers, 4 * 1024) - # cached mem also includes reclaimable memory - self.assertEqual(mem.cached, (5 + 23) * 1024) - self.assertEqual(mem.shared, 21 * 1024) - self.assertEqual(mem.active, 7 * 1024) - self.assertEqual(mem.inactive, 8 * 1024) - self.assertEqual(mem.slab, 22 * 1024) - self.assertEqual(mem.available, 3 * 1024) - - -# ===================================================================== -# --- system swap memory -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemSwapMemory(unittest.TestCase): - - @staticmethod - def meminfo_has_swap_info(): - """Return True if /proc/meminfo provides swap metrics.""" - with open("/proc/meminfo") as f: - data = f.read() - return 'SwapTotal:' in data and 'SwapFree:' in data - - def test_total(self): - free_value = free_swap().total - psutil_value = psutil.swap_memory().total - return self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE) - - @retry_before_failing() - 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() - def test_free(self): - free_value = free_swap().free - psutil_value = psutil.swap_memory().free - return self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE) - - def test_missing_sin_sout(self): - with mock.patch('psutil._pslinux.open', create=True) as m: - with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter("always") - ret = psutil.swap_memory() - assert m.called - self.assertEqual(len(ws), 1) - w = ws[0] - assert w.filename.endswith('psutil/_pslinux.py') - self.assertIn( - "'sin' and 'sout' swap memory stats couldn't " - "be determined", str(w.message)) - self.assertEqual(ret.sin, 0) - self.assertEqual(ret.sout, 0) - - def test_no_vmstat_mocked(self): - # see https://github.com/giampaolo/psutil/issues/722 - with mock_open_exception( - "/proc/vmstat", - IOError(errno.ENOENT, 'no such file or directory')) as m: - with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter("always") - ret = psutil.swap_memory() - assert m.called - self.assertEqual(len(ws), 1) - w = ws[0] - assert w.filename.endswith('psutil/_pslinux.py') - self.assertIn( - "'sin' and 'sout' swap memory stats couldn't " - "be determined and were set to 0", - str(w.message)) - self.assertEqual(ret.sin, 0) - self.assertEqual(ret.sout, 0) - - def test_meminfo_against_sysinfo(self): - # Make sure the content of /proc/meminfo about swap memory - # matches sysinfo() syscall, see: - # https://github.com/giampaolo/psutil/issues/1015 - if not self.meminfo_has_swap_info(): - return unittest.skip("/proc/meminfo has no swap metrics") - with mock.patch('psutil._pslinux.cext.linux_sysinfo') as m: - swap = psutil.swap_memory() - assert not m.called - import psutil._psutil_linux as cext - _, _, _, _, total, free, unit_multiplier = cext.linux_sysinfo() - total *= unit_multiplier - free *= unit_multiplier - self.assertEqual(swap.total, total) - self.assertEqual(swap.free, free) - - def test_emulate_meminfo_has_no_metrics(self): - # Emulate a case where /proc/meminfo provides no swap metrics - # in which case sysinfo() syscall is supposed to be used - # as a fallback. - with mock_open_content("/proc/meminfo", b"") as m: - psutil.swap_memory() - assert m.called - - -# ===================================================================== -# --- system CPU -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPU(unittest.TestCase): - - @unittest.skipIf(TRAVIS, "unknown failure on travis") - def test_cpu_times(self): - fields = psutil.cpu_times()._fields - kernel_ver = re.findall(r'\d+\.\d+\.\d+', os.uname()[2])[0] - kernel_ver_info = tuple(map(int, kernel_ver.split('.'))) - if kernel_ver_info >= (2, 6, 11): - self.assertIn('steal', fields) - else: - self.assertNotIn('steal', fields) - if kernel_ver_info >= (2, 6, 24): - self.assertIn('guest', fields) - else: - self.assertNotIn('guest', fields) - if kernel_ver_info >= (3, 2, 0): - self.assertIn('guest_nice', fields) - else: - self.assertNotIn('guest_nice', fields) - - @unittest.skipIf(not 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): - with open("/sys/devices/system/cpu/online") as f: - value = f.read().strip() - if "-" in str(value): - value = int(value.split('-')[1]) + 1 - self.assertEqual(psutil.cpu_count(), value) - - @unittest.skipIf(not os.path.exists("/sys/devices/system/cpu"), - "/sys/devices/system/cpu does not exist") - def test_cpu_count_logical_w_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): - 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): - out = sh("lscpu -p") - num = len([x for x in out.split('\n') if not x.startswith('#')]) - self.assertEqual(psutil.cpu_count(logical=True), num) - - def test_cpu_count_logical_mocked(self): - import psutil._pslinux - original = psutil._pslinux.cpu_count_logical() - # Here we want to mock os.sysconf("SC_NPROCESSORS_ONLN") in - # order to cause the parsing of /proc/cpuinfo and /proc/stat. - with mock.patch( - 'psutil._pslinux.os.sysconf', side_effect=ValueError) as m: - self.assertEqual(psutil._pslinux.cpu_count_logical(), original) - assert m.called - - # Let's have open() return emtpy data and make sure None is - # returned ('cause we mimick os.cpu_count()). - with mock.patch('psutil._pslinux.open', create=True) as m: - self.assertIsNone(psutil._pslinux.cpu_count_logical()) - self.assertEqual(m.call_count, 2) - # /proc/stat should be the last one - self.assertEqual(m.call_args[0][0], '/proc/stat') - - # Let's push this a bit further and make sure /proc/cpuinfo - # parsing works as expected. - with open('/proc/cpuinfo', 'rb') as f: - cpuinfo_data = f.read() - fake_file = io.BytesIO(cpuinfo_data) - with mock.patch('psutil._pslinux.open', - return_value=fake_file, create=True) as m: - self.assertEqual(psutil._pslinux.cpu_count_logical(), original) - - # Finally, let's make /proc/cpuinfo return meaningless data; - # this way we'll fall back on relying on /proc/stat - with mock_open_content('/proc/cpuinfo', b"") as m: - self.assertEqual(psutil._pslinux.cpu_count_logical(), original) - m.called - - 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: - self.assertIsNone(psutil._pslinux.cpu_count_physical()) - assert m.called - - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq_no_result(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): - # https://github.com/giampaolo/psutil/issues/981 - def glob_mock(pattern): - if pattern.startswith("/sys/devices/system/cpu/cpufreq/policy"): - flags.append(None) - return [] - else: - flags.append(None) - return orig_glob(pattern) - - flags = [] - orig_glob = glob.glob - with mock.patch("psutil._pslinux.glob.glob", side_effect=glob_mock, - create=True): - assert psutil.cpu_freq() - self.assertEqual(len(flags), 2) - - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq_emulate_data(self): - def open_mock(name, *args, **kwargs): - if name.endswith('/scaling_cur_freq'): - return io.BytesIO(b"500000") - elif name.endswith('/scaling_min_freq'): - return io.BytesIO(b"600000") - elif name.endswith('/scaling_max_freq'): - return io.BytesIO(b"700000") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - with mock.patch( - 'glob.glob', - return_value=['/sys/devices/system/cpu/cpufreq/policy0']): - freq = psutil.cpu_freq() - self.assertEqual(freq.current, 500.0) - self.assertEqual(freq.min, 600.0) - self.assertEqual(freq.max, 700.0) - - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq_emulate_multi_cpu(self): - def open_mock(name, *args, **kwargs): - if name.endswith('/scaling_cur_freq'): - return io.BytesIO(b"100000") - elif name.endswith('/scaling_min_freq'): - return io.BytesIO(b"200000") - elif name.endswith('/scaling_max_freq'): - return io.BytesIO(b"300000") - 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) - - @unittest.skipIf(TRAVIS, "fails on Travis") - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq_no_scaling_cur_freq_file(self): - # See: https://github.com/giampaolo/psutil/issues/1071 - def open_mock(name, *args, **kwargs): - if name.endswith('/scaling_cur_freq'): - raise IOError(errno.ENOENT, "") - elif name.endswith('/cpuinfo_cur_freq'): - return io.BytesIO(b"200000") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - 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) - - # Also test that NotImplementedError is raised in case no - # current freq file is present. - - def open_mock(name, *args, **kwargs): - if name.endswith('/scaling_cur_freq'): - raise IOError(errno.ENOENT, "") - elif name.endswith('/cpuinfo_cur_freq'): - raise IOError(errno.ENOENT, "") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - with mock.patch('glob.glob', return_value=policies): - self.assertRaises(NotImplementedError, psutil.cpu_freq) - - -# ===================================================================== -# --- system CPU stats -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUStats(unittest.TestCase): - - @unittest.skipIf(TRAVIS, "fails on Travis") - def test_ctx_switches(self): - vmstat_value = vmstat("context switches") - psutil_value = psutil.cpu_stats().ctx_switches - self.assertAlmostEqual(vmstat_value, psutil_value, delta=500) - - @unittest.skipIf(TRAVIS, "fails on Travis") - def test_interrupts(self): - vmstat_value = vmstat("interrupts") - psutil_value = psutil.cpu_stats().interrupts - self.assertAlmostEqual(vmstat_value, psutil_value, delta=500) - - -# ===================================================================== -# --- system network -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemNetwork(unittest.TestCase): - - def test_net_if_addrs_ips(self): - for name, addrs in psutil.net_if_addrs().items(): - for addr in addrs: - if addr.family == psutil.AF_LINK: - self.assertEqual(addr.address, get_mac_address(name)) - elif addr.family == socket.AF_INET: - self.assertEqual(addr.address, get_ipv4_address(name)) - # TODO: test for AF_INET6 family - - def test_net_if_stats(self): - for name, stats in psutil.net_if_stats().items(): - try: - out = sh("ifconfig %s" % name) - except RuntimeError: - pass - else: - # Not always reliable. - # self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) - self.assertEqual(stats.mtu, - int(re.findall(r'(?i)MTU[: ](\d+)', out)[0])) - - @retry_before_failing() - def test_net_io_counters(self): - def ifconfig(nic): - ret = {} - out = sh("ifconfig %s" % name) - ret['packets_recv'] = int( - re.findall(r'RX packets[: ](\d+)', out)[0]) - ret['packets_sent'] = int( - re.findall(r'TX packets[: ](\d+)', out)[0]) - ret['errin'] = int(re.findall(r'errors[: ](\d+)', out)[0]) - ret['errout'] = int(re.findall(r'errors[: ](\d+)', out)[1]) - ret['dropin'] = int(re.findall(r'dropped[: ](\d+)', out)[0]) - ret['dropout'] = int(re.findall(r'dropped[: ](\d+)', out)[1]) - ret['bytes_recv'] = int( - re.findall(r'RX (?:packets \d+ +)?bytes[: ](\d+)', out)[0]) - ret['bytes_sent'] = int( - re.findall(r'TX (?:packets \d+ +)?bytes[: ](\d+)', out)[0]) - return ret - - nio = psutil.net_io_counters(pernic=True, nowrap=False) - for name, stats in nio.items(): - try: - ifconfig_ret = ifconfig(name) - except RuntimeError: - continue - self.assertAlmostEqual( - stats.bytes_recv, ifconfig_ret['bytes_recv'], delta=1024 * 5) - self.assertAlmostEqual( - stats.bytes_sent, ifconfig_ret['bytes_sent'], delta=1024 * 5) - self.assertAlmostEqual( - stats.packets_recv, ifconfig_ret['packets_recv'], delta=1024) - self.assertAlmostEqual( - stats.packets_sent, ifconfig_ret['packets_sent'], delta=1024) - self.assertAlmostEqual( - stats.errin, ifconfig_ret['errin'], delta=10) - self.assertAlmostEqual( - stats.errout, ifconfig_ret['errout'], delta=10) - self.assertAlmostEqual( - stats.dropin, ifconfig_ret['dropin'], delta=10) - self.assertAlmostEqual( - stats.dropout, ifconfig_ret['dropout'], delta=10) - - # 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)) - - @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): - # see: https://github.com/giampaolo/psutil/issues/623 - try: - s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - self.addCleanup(s.close) - s.bind(("::1", 0)) - except socket.error: - pass - psutil.net_connections(kind='inet6') - - def test_net_connections_mocked(self): - with mock_open_content( - '/proc/net/unix', - textwrap.dedent("""\ - 0: 00000003 000 000 0001 03 462170 @/tmp/dbus-Qw2hMPIU3n - 0: 00000003 000 000 0001 03 35010 @/tmp/dbus-tB2X8h69BQ - 0: 00000003 000 000 0001 03 34424 @/tmp/dbus-cHy80Y8O - 000000000000000000000000000000000000000000000000000000 - """)) as m: - psutil.net_connections(kind='unix') - assert m.called - - -# ===================================================================== -# --- system disk -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemDisks(unittest.TestCase): - - @unittest.skipIf(not hasattr(os, 'statvfs'), "os.statvfs() not available") - @skip_on_not_implemented() - def test_disk_partitions_and_usage(self): - # test psutil.disk_usage() and psutil.disk_partitions() - # against "df -a" - def df(path): - out = sh('df -P -B 1 "%s"' % path).strip() - lines = out.split('\n') - lines.pop(0) - line = lines.pop(0) - dev, total, used, free = line.split()[:4] - if dev == 'none': - dev = '' - total, used, free = int(total), int(used), int(free) - return dev, total, used, free - - for part in psutil.disk_partitions(all=False): - usage = psutil.disk_usage(part.mountpoint) - dev, total, used, free = df(part.mountpoint) - self.assertEqual(usage.total, total) - # 10 MB tollerance - if abs(usage.free - free) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % (usage.free, free)) - if abs(usage.used - used) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % (usage.used, used)) - - def test_disk_partitions_mocked(self): - # Test that ZFS partitions are returned. - with open("/proc/filesystems", "r") as f: - data = f.read() - if 'zfs' in data: - for part in psutil.disk_partitions(): - if part.fstype == 'zfs': - break - else: - self.fail("couldn't find any ZFS partition") - else: - # No ZFS partitions on this system. Let's fake one. - fake_file = io.StringIO(u("nodev\tzfs\n")) - with mock.patch('psutil._pslinux.open', - return_value=fake_file, create=True) as m1: - with mock.patch( - 'psutil._pslinux.cext.disk_partitions', - return_value=[('/dev/sdb3', '/', 'zfs', 'rw')]) as m2: - ret = psutil.disk_partitions() - assert m1.called - assert m2.called - assert ret - self.assertEqual(ret[0].fstype, 'zfs') - - def test_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"): - ret = psutil.disk_io_counters(nowrap=False) - self.assertEqual(ret.read_count, 1) - self.assertEqual(ret.read_merged_count, 2) - self.assertEqual(ret.read_bytes, 3 * SECTOR_SIZE) - self.assertEqual(ret.read_time, 4) - self.assertEqual(ret.write_count, 5) - self.assertEqual(ret.write_merged_count, 6) - self.assertEqual(ret.write_bytes, 7 * SECTOR_SIZE) - self.assertEqual(ret.write_time, 8) - self.assertEqual(ret.busy_time, 10) - - def test_disk_io_counters_kernel_2_6_full_mocked(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/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"): - ret = psutil.disk_io_counters(nowrap=False) - self.assertEqual(ret.read_count, 1) - self.assertEqual(ret.read_merged_count, 2) - self.assertEqual(ret.read_bytes, 3 * SECTOR_SIZE) - self.assertEqual(ret.read_time, 4) - self.assertEqual(ret.write_count, 5) - self.assertEqual(ret.write_merged_count, 6) - self.assertEqual(ret.write_bytes, 7 * SECTOR_SIZE) - self.assertEqual(ret.write_time, 8) - self.assertEqual(ret.busy_time, 10) - - def test_disk_io_counters_kernel_2_6_limited_mocked(self): - # Tests /proc/diskstats parsing format for 2.6 kernels, - # where one line of /proc/partitions return a limited - # amount of metrics when it bumps into a partition - # (instead of a disk). See: - # https://github.com/giampaolo/psutil/issues/767 - with mock_open_content( - '/proc/partitions', - textwrap.dedent("""\ - major minor #blocks name - - 8 0 488386584 hda - """)): - with mock_open_content( - '/proc/diskstats', - " 3 1 hda 1 2 3 4"): - ret = psutil.disk_io_counters(nowrap=False) - self.assertEqual(ret.read_count, 1) - self.assertEqual(ret.read_bytes, 2 * SECTOR_SIZE) - self.assertEqual(ret.write_count, 3) - self.assertEqual(ret.write_bytes, 4 * SECTOR_SIZE) - - self.assertEqual(ret.read_merged_count, 0) - self.assertEqual(ret.read_time, 0) - self.assertEqual(ret.write_merged_count, 0) - self.assertEqual(ret.write_time, 0) - self.assertEqual(ret.busy_time, 0) - - -# ===================================================================== -# --- misc -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestMisc(unittest.TestCase): - - def test_boot_time(self): - vmstat_value = vmstat('boot time') - psutil_value = psutil.boot_time() - self.assertEqual(int(vmstat_value), int(psutil_value)) - - @mock.patch('psutil.traceback.print_exc') - def test_no_procfs_on_import(self, tb): - my_procfs = tempfile.mkdtemp() - - with open(os.path.join(my_procfs, 'stat'), 'w') as f: - f.write('cpu 0 0 0 0 0 0 0 0 0 0\n') - f.write('cpu0 0 0 0 0 0 0 0 0 0 0\n') - f.write('cpu1 0 0 0 0 0 0 0 0 0 0\n') - - try: - orig_open = open - - def open_mock(name, *args, **kwargs): - if name.startswith('/proc'): - raise IOError(errno.ENOENT, 'rejecting access for test') - return orig_open(name, *args, **kwargs) - - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - reload_module(psutil) - assert tb.called - - self.assertRaises(IOError, psutil.cpu_times) - self.assertRaises(IOError, psutil.cpu_times, percpu=True) - self.assertRaises(IOError, psutil.cpu_percent) - self.assertRaises(IOError, psutil.cpu_percent, percpu=True) - self.assertRaises(IOError, psutil.cpu_times_percent) - self.assertRaises( - IOError, psutil.cpu_times_percent, percpu=True) - - psutil.PROCFS_PATH = my_procfs - - self.assertEqual(psutil.cpu_percent(), 0) - self.assertEqual(sum(psutil.cpu_times_percent()), 0) - - # since we don't know the number of CPUs at import time, - # we awkwardly say there are none until the second call - per_cpu_percent = psutil.cpu_percent(percpu=True) - self.assertEqual(sum(per_cpu_percent), 0) - - # ditto awkward length - per_cpu_times_percent = psutil.cpu_times_percent(percpu=True) - self.assertEqual(sum(map(sum, per_cpu_times_percent)), 0) - - # much user, very busy - with open(os.path.join(my_procfs, 'stat'), 'w') as f: - f.write('cpu 1 0 0 0 0 0 0 0 0 0\n') - f.write('cpu0 1 0 0 0 0 0 0 0 0 0\n') - f.write('cpu1 1 0 0 0 0 0 0 0 0 0\n') - - self.assertNotEqual(psutil.cpu_percent(), 0) - self.assertNotEqual( - sum(psutil.cpu_percent(percpu=True)), 0) - self.assertNotEqual(sum(psutil.cpu_times_percent()), 0) - self.assertNotEqual( - sum(map(sum, psutil.cpu_times_percent(percpu=True))), 0) - finally: - shutil.rmtree(my_procfs) - reload_module(psutil) - - self.assertEqual(psutil.PROCFS_PATH, '/proc') - - def test_cpu_steal_decrease(self): - # Test cumulative cpu stats decrease. We should ignore this. - # See issue #1210. - with mock_open_content( - "/proc/stat", - textwrap.dedent("""\ - cpu 0 0 0 0 0 0 0 1 0 0 - cpu0 0 0 0 0 0 0 0 1 0 0 - cpu1 0 0 0 0 0 0 0 1 0 0 - """).encode()) as m: - # first call to "percent" functions should read the new stat file - # and compare to the "real" file read at import time - so the - # values are meaningless - psutil.cpu_percent() - assert m.called - psutil.cpu_percent(percpu=True) - psutil.cpu_times_percent() - psutil.cpu_times_percent(percpu=True) - - with mock_open_content( - "/proc/stat", - textwrap.dedent("""\ - cpu 1 0 0 0 0 0 0 0 0 0 - cpu0 1 0 0 0 0 0 0 0 0 0 - cpu1 1 0 0 0 0 0 0 0 0 0 - """).encode()) as m: - # Increase "user" while steal goes "backwards" to zero. - cpu_percent = psutil.cpu_percent() - assert m.called - cpu_percent_percpu = psutil.cpu_percent(percpu=True) - cpu_times_percent = psutil.cpu_times_percent() - cpu_times_percent_percpu = psutil.cpu_times_percent(percpu=True) - self.assertNotEqual(cpu_percent, 0) - self.assertNotEqual(sum(cpu_percent_percpu), 0) - self.assertNotEqual(sum(cpu_times_percent), 0) - self.assertNotEqual(sum(cpu_times_percent), 100.0) - self.assertNotEqual(sum(map(sum, cpu_times_percent_percpu)), 0) - self.assertNotEqual(sum(map(sum, cpu_times_percent_percpu)), 100.0) - self.assertEqual(cpu_times_percent.steal, 0) - self.assertNotEqual(cpu_times_percent.user, 0) - - def test_boot_time_mocked(self): - with mock.patch('psutil._pslinux.open', create=True) as m: - self.assertRaises( - RuntimeError, - psutil._pslinux.boot_time) - assert m.called - - def test_users_mocked(self): - # Make sure ':0' and ':0.0' (returned by C ext) are converted - # to 'localhost'. - with mock.patch('psutil._pslinux.cext.users', - return_value=[('giampaolo', 'pts/2', ':0', - 1436573184.0, True, 2)]) as m: - self.assertEqual(psutil.users()[0].host, 'localhost') - assert m.called - with mock.patch('psutil._pslinux.cext.users', - return_value=[('giampaolo', 'pts/2', ':0.0', - 1436573184.0, True, 2)]) as m: - self.assertEqual(psutil.users()[0].host, 'localhost') - assert m.called - # ...otherwise it should be returned as-is - with mock.patch('psutil._pslinux.cext.users', - return_value=[('giampaolo', 'pts/2', 'foo', - 1436573184.0, True, 2)]) as m: - self.assertEqual(psutil.users()[0].host, 'foo') - assert m.called - - def test_procfs_path(self): - tdir = tempfile.mkdtemp() - try: - psutil.PROCFS_PATH = tdir - self.assertRaises(IOError, psutil.virtual_memory) - self.assertRaises(IOError, psutil.cpu_times) - self.assertRaises(IOError, psutil.cpu_times, percpu=True) - self.assertRaises(IOError, psutil.boot_time) - # self.assertRaises(IOError, psutil.pids) - self.assertRaises(IOError, psutil.net_connections) - self.assertRaises(IOError, psutil.net_io_counters) - self.assertRaises(IOError, psutil.net_if_stats) - self.assertRaises(IOError, psutil.disk_io_counters) - self.assertRaises(IOError, psutil.disk_partitions) - self.assertRaises(psutil.NoSuchProcess, psutil.Process) - finally: - psutil.PROCFS_PATH = "/proc" - os.rmdir(tdir) - - def test_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 - # - Process(tid) is supposed to work - # - pids() should not return the TID - # See: https://github.com/giampaolo/psutil/issues/687 - t = ThreadTask() - t.start() - try: - p = psutil.Process() - tid = p.threads()[1].id - assert not psutil.pid_exists(tid), tid - pt = psutil.Process(tid) - pt.as_dict() - self.assertNotIn(tid, psutil.pids()) - finally: - t.stop() - - def test_pid_exists_no_proc_status(self): - # Internally pid_exists relies on /proc/{pid}/status. - # Emulate a case where this file is empty in which case - # psutil is supposed to fall back on using pids(). - with mock_open_content("/proc/%s/status", "") as m: - assert psutil.pid_exists(os.getpid()) - assert m.called - - -# ===================================================================== -# --- sensors -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -@unittest.skipIf(not HAS_BATTERY, "no battery") -class TestSensorsBattery(unittest.TestCase): - - @unittest.skipIf(not which("acpi"), "acpi utility not available") - def test_percent(self): - out = sh("acpi -b") - acpi_value = int(out.split(",")[1].strip().replace('%', '')) - psutil_value = psutil.sensors_battery().percent - self.assertAlmostEqual(acpi_value, psutil_value, delta=1) - - @unittest.skipIf(not which("acpi"), "acpi utility not available") - def test_power_plugged(self): - out = sh("acpi -b") - if 'unknown' in out.lower(): - return unittest.skip("acpi output not reliable") - if 'discharging at zero rate' in out: - plugged = True - else: - plugged = "Charging" in out.split('\n')[0] - self.assertEqual(psutil.sensors_battery().power_plugged, plugged) - - def test_emulate_power_plugged(self): - # Pretend the AC power cable is connected. - def open_mock(name, *args, **kwargs): - if name.endswith("AC0/online") or name.endswith("AC/online"): - return io.BytesIO(b"1") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - self.assertEqual(psutil.sensors_battery().power_plugged, True) - self.assertEqual( - psutil.sensors_battery().secsleft, psutil.POWER_TIME_UNLIMITED) - assert m.called - - def test_emulate_power_plugged_2(self): - # Same as above but pretend /AC0/online does not exist in which - # case code relies on /status file. - def open_mock(name, *args, **kwargs): - if name.endswith("AC0/online") or name.endswith("AC/online"): - raise IOError(errno.ENOENT, "") - elif name.endswith("/status"): - return io.StringIO(u("charging")) - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - self.assertEqual(psutil.sensors_battery().power_plugged, True) - assert m.called - - def test_emulate_power_not_plugged(self): - # Pretend the AC power cable is not connected. - def open_mock(name, *args, **kwargs): - if name.endswith("AC0/online") or name.endswith("AC/online"): - return io.BytesIO(b"0") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - self.assertEqual(psutil.sensors_battery().power_plugged, False) - assert m.called - - def test_emulate_power_not_plugged_2(self): - # Same as above but pretend /AC0/online does not exist in which - # case code relies on /status file. - def open_mock(name, *args, **kwargs): - if name.endswith("AC0/online") or name.endswith("AC/online"): - raise IOError(errno.ENOENT, "") - elif name.endswith("/status"): - return io.StringIO(u("discharging")) - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - self.assertEqual(psutil.sensors_battery().power_plugged, False) - assert m.called - - def test_emulate_power_undetermined(self): - # Pretend we can't know whether the AC power cable not - # connected (assert fallback to False). - def open_mock(name, *args, **kwargs): - if name.startswith("/sys/class/power_supply/AC0/online") or \ - name.startswith("/sys/class/power_supply/AC/online"): - raise IOError(errno.ENOENT, "") - elif name.startswith("/sys/class/power_supply/BAT0/status"): - return io.BytesIO(b"???") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - self.assertIsNone(psutil.sensors_battery().power_plugged) - assert m.called - - def test_emulate_no_base_files(self): - # Emulate a case where base metrics files are not present, - # in which case we're supposed to get None. - with mock_open_exception( - "/sys/class/power_supply/BAT0/energy_now", - IOError(errno.ENOENT, "")): - with mock_open_exception( - "/sys/class/power_supply/BAT0/charge_now", - IOError(errno.ENOENT, "")): - self.assertIsNone(psutil.sensors_battery()) - - def test_emulate_energy_full_0(self): - # Emulate a case where energy_full files returns 0. - with mock_open_content( - "/sys/class/power_supply/BAT0/energy_full", b"0") as m: - self.assertEqual(psutil.sensors_battery().percent, 0) - assert m.called - - def test_emulate_energy_full_not_avail(self): - # Emulate a case where energy_full file does not exist. - # Expected fallback on /capacity. - with mock_open_exception( - "/sys/class/power_supply/BAT0/energy_full", - IOError(errno.ENOENT, "")): - with mock_open_exception( - "/sys/class/power_supply/BAT0/charge_full", - IOError(errno.ENOENT, "")): - with mock_open_content( - "/sys/class/power_supply/BAT0/capacity", b"88"): - self.assertEqual(psutil.sensors_battery().percent, 88) - - def test_emulate_no_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( - "/sys/class/power_supply/AC/online", - IOError(errno.ENOENT, "")): - with mock_open_exception( - "/sys/class/power_supply/AC0/online", - IOError(errno.ENOENT, "")): - with mock_open_exception( - "/sys/class/power_supply/BAT0/status", - IOError(errno.ENOENT, "")): - self.assertIsNone(psutil.sensors_battery().power_plugged) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSensorsTemperatures(unittest.TestCase): - - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - def test_emulate_eio_error(self): - def open_mock(name, *args, **kwargs): - if name.endswith("_input"): - raise OSError(errno.EIO, "") - 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_data(self): - def open_mock(name, *args, **kwargs): - if name.endswith('/name'): - return io.StringIO(u("name")) - elif name.endswith('/temp1_label'): - return io.StringIO(u("label")) - elif name.endswith('/temp1_input'): - return io.BytesIO(b"30000") - elif name.endswith('/temp1_max'): - return io.BytesIO(b"40000") - elif name.endswith('/temp1_crit'): - return io.BytesIO(b"50000") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - with mock.patch('glob.glob', - return_value=['/sys/class/hwmon/hwmon0/temp1']): - temp = psutil.sensors_temperatures()['name'][0] - self.assertEqual(temp.label, 'label') - self.assertEqual(temp.current, 30.0) - self.assertEqual(temp.high, 40.0) - self.assertEqual(temp.critical, 50.0) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSensorsFans(unittest.TestCase): - - def test_emulate_data(self): - def open_mock(name, *args, **kwargs): - if name.endswith('/name'): - return io.StringIO(u("name")) - elif name.endswith('/fan1_label'): - return io.StringIO(u("label")) - elif name.endswith('/fan1_input'): - return io.StringIO(u("2000")) - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - with mock.patch('glob.glob', - return_value=['/sys/class/hwmon/hwmon2/fan1']): - fan = psutil.sensors_fans()['name'][0] - self.assertEqual(fan.label, 'label') - self.assertEqual(fan.current, 2000) - - -# ===================================================================== -# --- test process -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestProcess(unittest.TestCase): - - def setUp(self): - safe_rmpath(TESTFN) - - tearDown = setUp - - def test_memory_full_info(self): - src = textwrap.dedent(""" - import time - with open("%s", "w") as f: - time.sleep(10) - """ % TESTFN) - sproc = pyrun(src) - self.addCleanup(reap_children) - call_until(lambda: os.listdir('.'), "'%s' not in ret" % TESTFN) - p = psutil.Process(sproc.pid) - time.sleep(.1) - mem = p.memory_full_info() - maps = p.memory_maps(grouped=False) - self.assertAlmostEqual( - mem.uss, sum([x.private_dirty + x.private_clean for x in maps]), - delta=4096) - self.assertAlmostEqual( - mem.pss, sum([x.pss for x in maps]), delta=4096) - self.assertAlmostEqual( - mem.swap, sum([x.swap for x in maps]), delta=4096) - - def test_memory_full_info_mocked(self): - # See: https://github.com/giampaolo/psutil/issues/1222 - with mock_open_content( - "/proc/%s/smaps" % os.getpid(), - textwrap.dedent("""\ - fffff0 r-xp 00000000 00:00 0 [vsyscall] - Size: 1 kB - Rss: 2 kB - Pss: 3 kB - Shared_Clean: 4 kB - Shared_Dirty: 5 kB - Private_Clean: 6 kB - Private_Dirty: 7 kB - Referenced: 8 kB - Anonymous: 9 kB - LazyFree: 10 kB - AnonHugePages: 11 kB - ShmemPmdMapped: 12 kB - Shared_Hugetlb: 13 kB - Private_Hugetlb: 14 kB - Swap: 15 kB - SwapPss: 16 kB - KernelPageSize: 17 kB - MMUPageSize: 18 kB - Locked: 19 kB - VmFlags: rd ex - """).encode()) as m: - p = psutil.Process() - mem = p.memory_full_info() - assert m.called - self.assertEqual(mem.uss, (6 + 7 + 14) * 1024) - self.assertEqual(mem.pss, 3 * 1024) - self.assertEqual(mem.swap, 15 * 1024) - - # On PYPY file descriptors are not closed fast enough. - @unittest.skipIf(PYPY, "unreliable on PYPY") - def test_open_files_mode(self): - def get_test_file(): - p = psutil.Process() - giveup_at = time.time() + 2 - while True: - for file in p.open_files(): - if file.path == os.path.abspath(TESTFN): - return file - elif time.time() > giveup_at: - break - raise RuntimeError("timeout looking for test file") - - # - with open(TESTFN, "w"): - self.assertEqual(get_test_file().mode, "w") - with open(TESTFN, "r"): - self.assertEqual(get_test_file().mode, "r") - with open(TESTFN, "a"): - self.assertEqual(get_test_file().mode, "a") - # - with open(TESTFN, "r+"): - self.assertEqual(get_test_file().mode, "r+") - with open(TESTFN, "w+"): - self.assertEqual(get_test_file().mode, "r+") - with open(TESTFN, "a+"): - self.assertEqual(get_test_file().mode, "a+") - # note: "x" bit is not supported - if PY3: - safe_rmpath(TESTFN) - with open(TESTFN, "x"): - self.assertEqual(get_test_file().mode, "w") - safe_rmpath(TESTFN) - with open(TESTFN, "x+"): - self.assertEqual(get_test_file().mode, "r+") - - def test_open_files_file_gone(self): - # simulates a file which gets deleted during open_files() - # execution - p = psutil.Process() - files = p.open_files() - with tempfile.NamedTemporaryFile(): - # give the kernel some time to see the new file - call_until(p.open_files, "len(ret) != %i" % len(files)) - with mock.patch('psutil._pslinux.os.readlink', - side_effect=OSError(errno.ENOENT, "")) as m: - files = p.open_files() - assert not files - assert m.called - # also simulate the case where os.readlink() returns EINVAL - # in which case psutil is supposed to 'continue' - with mock.patch('psutil._pslinux.os.readlink', - side_effect=OSError(errno.EINVAL, "")) as m: - self.assertEqual(p.open_files(), []) - assert m.called - - def test_open_files_fd_gone(self): - # Simulate a case where /proc/{pid}/fdinfo/{fd} disappears - # while iterating through fds. - # https://travis-ci.org/giampaolo/psutil/jobs/225694530 - p = psutil.Process() - files = p.open_files() - with tempfile.NamedTemporaryFile(): - # give the kernel some time to see the new file - call_until(p.open_files, "len(ret) != %i" % len(files)) - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, - side_effect=IOError(errno.ENOENT, "")) as m: - files = p.open_files() - assert not files - assert m.called - - # --- mocked tests - - def test_terminal_mocked(self): - with mock.patch('psutil._pslinux._psposix.get_terminal_map', - return_value={}) as m: - self.assertIsNone(psutil._pslinux.Process(os.getpid()).terminal()) - assert m.called - - # TODO: re-enable this test. - # def test_num_ctx_switches_mocked(self): - # with mock.patch('psutil._pslinux.open', create=True) as m: - # self.assertRaises( - # NotImplementedError, - # psutil._pslinux.Process(os.getpid()).num_ctx_switches) - # assert m.called - - def test_cmdline_mocked(self): - # see: https://github.com/giampaolo/psutil/issues/639 - p = psutil.Process() - fake_file = io.StringIO(u('foo\x00bar\x00')) - with mock.patch('psutil._pslinux.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', - return_value=fake_file, create=True) as m: - self.assertEqual(p.cmdline(), ['foo', 'bar', '']) - assert m.called - - def test_cmdline_spaces_mocked(self): - # see: https://github.com/giampaolo/psutil/issues/1179 - p = psutil.Process() - fake_file = io.StringIO(u('foo bar ')) - with mock.patch('psutil._pslinux.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', - return_value=fake_file, create=True) as m: - self.assertEqual(p.cmdline(), ['foo', 'bar', '']) - assert m.called - - def test_readlink_path_deleted_mocked(self): - with mock.patch('psutil._pslinux.os.readlink', - return_value='/home/foo (deleted)'): - self.assertEqual(psutil.Process().exe(), "/home/foo") - self.assertEqual(psutil.Process().cwd(), "/home/foo") - - def test_threads_mocked(self): - # Test the case where os.listdir() returns a file (thread) - # which no longer exists by the time we open() it (race - # condition). threads() is supposed to ignore that instead - # of raising NSP. - def open_mock(name, *args, **kwargs): - if name.startswith('/proc/%s/task' % os.getpid()): - raise IOError(errno.ENOENT, "") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - ret = psutil.Process().threads() - assert m.called - self.assertEqual(ret, []) - - # ...but if it bumps into something != ENOENT we want an - # exception. - def open_mock(name, *args, **kwargs): - if name.startswith('/proc/%s/task' % os.getpid()): - raise IOError(errno.EPERM, "") - else: - return orig_open(name, *args, **kwargs) - - with mock.patch(patch_point, side_effect=open_mock): - self.assertRaises(psutil.AccessDenied, psutil.Process().threads) - - def test_exe_mocked(self): - with mock.patch('psutil._pslinux.readlink', - side_effect=OSError(errno.ENOENT, "")) as m1: - with mock.patch('psutil.Process.cmdline', - side_effect=psutil.AccessDenied(0, "")) as m2: - # No such file error; might be raised also if /proc/pid/exe - # path actually exists for system processes with low pids - # (about 0-20). In this case psutil is supposed to return - # an empty string. - ret = psutil.Process().exe() - assert m1.called - assert m2.called - self.assertEqual(ret, "") - - # ...but if /proc/pid no longer exist we're supposed to treat - # it as an alias for zombie process - with mock.patch('psutil._pslinux.os.path.lexists', - return_value=False): - self.assertRaises( - psutil.ZombieProcess, psutil.Process().exe) - - def test_issue_1014(self): - # Emulates a case where smaps file does not exist. In this case - # wrap_exception decorator should not raise NoSuchProcess. - with mock_open_exception( - '/proc/%s/smaps' % os.getpid(), - IOError(errno.ENOENT, "")) as m: - p = psutil.Process() - with self.assertRaises(IOError) as err: - p.memory_maps() - self.assertEqual(err.exception.errno, errno.ENOENT) - assert m.called - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_zombie(self): - # Emulate a case where rlimit() raises ENOSYS, which may - # happen in case of zombie process: - # https://travis-ci.org/giampaolo/psutil/jobs/51368273 - with mock.patch("psutil._pslinux.cext.linux_prlimit", - side_effect=OSError(errno.ENOSYS, "")) as m: - p = psutil.Process() - p.name() - with self.assertRaises(psutil.ZombieProcess) as exc: - p.rlimit(psutil.RLIMIT_NOFILE) - assert m.called - self.assertEqual(exc.exception.pid, p.pid) - self.assertEqual(exc.exception.name, p.name()) - - def test_cwd_zombie(self): - with mock.patch("psutil._pslinux.os.readlink", - side_effect=OSError(errno.ENOENT, "")) as m: - p = psutil.Process() - p.name() - with self.assertRaises(psutil.ZombieProcess) as exc: - p.cwd() - assert m.called - self.assertEqual(exc.exception.pid, p.pid) - self.assertEqual(exc.exception.name, p.name()) - - def test_stat_file_parsing(self): - from psutil._pslinux import CLOCK_TICKS - - args = [ - "0", # pid - "(cat)", # name - "Z", # status - "1", # ppid - "0", # pgrp - "0", # session - "0", # tty - "0", # tpgid - "0", # flags - "0", # minflt - "0", # cminflt - "0", # majflt - "0", # cmajflt - "2", # utime - "3", # stime - "4", # cutime - "5", # cstime - "0", # priority - "0", # nice - "0", # num_threads - "0", # itrealvalue - "6", # starttime - "0", # vsize - "0", # rss - "0", # rsslim - "0", # startcode - "0", # endcode - "0", # startstack - "0", # kstkesp - "0", # kstkeip - "0", # signal - "0", # blocked - "0", # sigignore - "0", # sigcatch - "0", # wchan - "0", # nswap - "0", # cnswap - "0", # exit_signal - "6", # processor - ] - content = " ".join(args).encode() - with mock_open_content('/proc/%s/stat' % os.getpid(), content): - p = psutil.Process() - self.assertEqual(p.name(), 'cat') - self.assertEqual(p.status(), psutil.STATUS_ZOMBIE) - self.assertEqual(p.ppid(), 1) - self.assertEqual( - p.create_time(), 6 / CLOCK_TICKS + psutil.boot_time()) - cpu = p.cpu_times() - self.assertEqual(cpu.user, 2 / CLOCK_TICKS) - self.assertEqual(cpu.system, 3 / CLOCK_TICKS) - self.assertEqual(cpu.children_user, 4 / CLOCK_TICKS) - self.assertEqual(cpu.children_system, 5 / CLOCK_TICKS) - self.assertEqual(p.cpu_num(), 6) - - def test_status_file_parsing(self): - with mock_open_content( - '/proc/%s/status' % os.getpid(), - textwrap.dedent("""\ - Uid:\t1000\t1001\t1002\t1003 - Gid:\t1004\t1005\t1006\t1007 - Threads:\t66 - Cpus_allowed:\tf - Cpus_allowed_list:\t0-7 - voluntary_ctxt_switches:\t12 - nonvoluntary_ctxt_switches:\t13""").encode()): - p = psutil.Process() - self.assertEqual(p.num_ctx_switches().voluntary, 12) - self.assertEqual(p.num_ctx_switches().involuntary, 13) - self.assertEqual(p.num_threads(), 66) - uids = p.uids() - self.assertEqual(uids.real, 1000) - self.assertEqual(uids.effective, 1001) - self.assertEqual(uids.saved, 1002) - gids = p.gids() - self.assertEqual(gids.real, 1004) - self.assertEqual(gids.effective, 1005) - self.assertEqual(gids.saved, 1006) - self.assertEqual(p._proc._get_eligible_cpus(), list(range(0, 8))) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestProcessAgainstStatus(unittest.TestCase): - """/proc/pid/stat and /proc/pid/status have many values in common. - Whenever possible, psutil uses /proc/pid/stat (it's faster). - For all those cases we check that the value found in - /proc/pid/stat (by psutil) matches the one found in - /proc/pid/status. - """ - - @classmethod - def setUpClass(cls): - cls.proc = psutil.Process() - - def read_status_file(self, linestart): - with psutil._psplatform.open_text( - '/proc/%s/status' % self.proc.pid) as f: - for line in f: - line = line.strip() - if line.startswith(linestart): - value = line.partition('\t')[2] - try: - return int(value) - except ValueError: - return value - raise ValueError("can't find %r" % linestart) - - def test_name(self): - value = self.read_status_file("Name:") - self.assertEqual(self.proc.name(), value) - - def test_status(self): - value = self.read_status_file("State:") - value = value[value.find('(') + 1:value.rfind(')')] - value = value.replace(' ', '-') - self.assertEqual(self.proc.status(), value) - - def test_ppid(self): - value = self.read_status_file("PPid:") - self.assertEqual(self.proc.ppid(), value) - - def test_num_threads(self): - value = self.read_status_file("Threads:") - self.assertEqual(self.proc.num_threads(), value) - - def test_uids(self): - value = self.read_status_file("Uid:") - value = tuple(map(int, value.split()[1:4])) - self.assertEqual(self.proc.uids(), value) - - def test_gids(self): - value = self.read_status_file("Gid:") - value = tuple(map(int, value.split()[1:4])) - self.assertEqual(self.proc.gids(), value) - - @retry_before_failing() - def test_num_ctx_switches(self): - value = self.read_status_file("voluntary_ctxt_switches:") - self.assertEqual(self.proc.num_ctx_switches().voluntary, value) - value = self.read_status_file("nonvoluntary_ctxt_switches:") - self.assertEqual(self.proc.num_ctx_switches().involuntary, value) - - def test_cpu_affinity(self): - value = self.read_status_file("Cpus_allowed_list:") - if '-' in str(value): - min_, max_ = map(int, value.split('-')) - self.assertEqual( - self.proc.cpu_affinity(), list(range(min_, max_ + 1))) - - def test_cpu_affinity_eligible_cpus(self): - value = self.read_status_file("Cpus_allowed_list:") - with mock.patch("psutil._pslinux.per_cpu_times") as m: - self.proc._proc._get_eligible_cpus() - if '-' in str(value): - assert not m.called - else: - assert m.called - - -# ===================================================================== -# --- test utils -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestUtils(unittest.TestCase): - - def test_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") - assert m.called - - def test_cat(self): - fname = os.path.abspath(TESTFN) - with open(fname, "wt") as f: - f.write("foo ") - self.assertEqual(psutil._psplatform.cat(TESTFN, binary=False), "foo") - self.assertEqual(psutil._psplatform.cat(TESTFN, binary=True), b"foo") - self.assertEqual( - psutil._psplatform.cat(TESTFN + '??', fallback="bar"), "bar") - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py deleted file mode 100755 index 680fe78036..0000000000 --- a/psutil/tests/test_memory_leaks.py +++ /dev/null @@ -1,599 +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. - -""" -Tests for detecting function memory leaks (typically the ones -implemented in C). It does so by calling a function many times and -checking whether process memory usage keeps increasing between -calls or over time. -Note that this may produce false positives (especially on Windows -for some reason). -""" - -from __future__ import print_function -import errno -import functools -import gc -import os -import sys -import threading -import time - -import psutil -import psutil._common -from psutil import LINUX -from psutil import OPENBSD -from psutil import OSX -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._compat import xrange -from psutil.tests import create_sockets -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_CPU_AFFINITY -from psutil.tests import HAS_CPU_FREQ -from psutil.tests import HAS_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_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 run_test_module_by_name -from psutil.tests import safe_rmpath -from psutil.tests import skip_on_access_denied -from psutil.tests import TESTFN -from psutil.tests import TRAVIS -from psutil.tests import unittest - - -LOOPS = 1000 -MEMORY_TOLERANCE = 4096 -RETRY_FOR = 3 - -SKIP_PYTHON_IMPL = True if TRAVIS else False -cext = psutil._psplatform.cext -thisproc = psutil.Process() -SKIP_PYTHON_IMPL = True if TRAVIS else False - - -# =================================================================== -# utils -# =================================================================== - - -def skip_if_linux(): - return unittest.skipIf(LINUX and SKIP_PYTHON_IMPL, - "worthless on LINUX (pure python)") - - -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 - 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 OSX: - return thisproc.memory_full_info().uss - else: - return thisproc.memory_info().rss - - @staticmethod - def _call(fun, *args, **kwargs): - fun(*args, **kwargs) - - -# =================================================================== -# Process class -# =================================================================== - - -class TestProcessObjectLeaks(TestMemLeak): - """Test leaks of Process class methods.""" - - proc = thisproc - - def test_coverage(self): - skip = set(( - "pid", "as_dict", "children", "cpu_affinity", "cpu_percent", - "ionice", "is_running", "kill", "memory_info_ex", "memory_percent", - "nice", "oneshot", "parent", "rlimit", "send_signal", "suspend", - "terminate", "wait")) - for name in dir(psutil.Process): - if name.startswith('_'): - continue - if name in skip: - continue - self.assertTrue(hasattr(self, "test_" + name), msg=name) - - @skip_if_linux() - def test_name(self): - self.execute(self.proc.name) - - @skip_if_linux() - def test_cmdline(self): - self.execute(self.proc.cmdline) - - @skip_if_linux() - def test_exe(self): - self.execute(self.proc.exe) - - @skip_if_linux() - def test_ppid(self): - self.execute(self.proc.ppid) - - @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() - def test_uids(self): - self.execute(self.proc.uids) - - @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() - def test_gids(self): - self.execute(self.proc.gids) - - @skip_if_linux() - def test_status(self): - self.execute(self.proc.status) - - def test_nice_get(self): - self.execute(self.proc.nice) - - def test_nice_set(self): - niceness = thisproc.nice() - self.execute(self.proc.nice, niceness) - - @unittest.skipIf(not HAS_IONICE, "not supported") - def test_ionice_get(self): - self.execute(self.proc.ionice) - - @unittest.skipIf(not HAS_IONICE, "not supported") - def test_ionice_set(self): - if WINDOWS: - value = thisproc.ionice() - self.execute(self.proc.ionice, value) - else: - self.execute(self.proc.ionice, psutil.IOPRIO_CLASS_NONE) - fun = functools.partial(cext.proc_ioprio_set, os.getpid(), -1, 0) - self.execute_w_exc(OSError, fun) - - @unittest.skipIf(not HAS_PROC_IO_COUNTERS, "not supported") - @skip_if_linux() - def test_io_counters(self): - self.execute(self.proc.io_counters) - - @unittest.skipIf(POSIX, "worthless on POSIX") - def test_username(self): - self.execute(self.proc.username) - - @skip_if_linux() - def test_create_time(self): - self.execute(self.proc.create_time) - - @skip_if_linux() - @skip_on_access_denied(only_if=OPENBSD) - def test_num_threads(self): - self.execute(self.proc.num_threads) - - @unittest.skipIf(not WINDOWS, "WINDOWS only") - def test_num_handles(self): - self.execute(self.proc.num_handles) - - @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() - def test_num_fds(self): - self.execute(self.proc.num_fds) - - @skip_if_linux() - def test_num_ctx_switches(self): - self.execute(self.proc.num_ctx_switches) - - @skip_if_linux() - @skip_on_access_denied(only_if=OPENBSD) - def test_threads(self): - self.execute(self.proc.threads) - - @skip_if_linux() - def test_cpu_times(self): - self.execute(self.proc.cpu_times) - - @skip_if_linux() - @unittest.skipIf(not HAS_PROC_CPU_NUM, "not supported") - def test_cpu_num(self): - self.execute(self.proc.cpu_num) - - @skip_if_linux() - def test_memory_info(self): - self.execute(self.proc.memory_info) - - @skip_if_linux() - def test_memory_full_info(self): - self.execute(self.proc.memory_full_info) - - @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() - def test_terminal(self): - self.execute(self.proc.terminal) - - @unittest.skipIf(POSIX and SKIP_PYTHON_IMPL, - "worthless on POSIX (pure python)") - def test_resume(self): - self.execute(self.proc.resume) - - @skip_if_linux() - def test_cwd(self): - self.execute(self.proc.cwd) - - @unittest.skipIf(not HAS_CPU_AFFINITY, "not supported") - def test_cpu_affinity_get(self): - self.execute(self.proc.cpu_affinity) - - @unittest.skipIf(not HAS_CPU_AFFINITY, "not supported") - def test_cpu_affinity_set(self): - affinity = thisproc.cpu_affinity() - self.execute(self.proc.cpu_affinity, affinity) - if not TRAVIS: - self.execute_w_exc(ValueError, self.proc.cpu_affinity, [-1]) - - @skip_if_linux() - def test_open_files(self): - safe_rmpath(TESTFN) # needed after UNIX socket test has run - with open(TESTFN, 'w'): - self.execute(self.proc.open_files) - - # OSX implementation is unbelievably slow - @unittest.skipIf(OSX, "too slow on OSX") - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") - @skip_if_linux() - def test_memory_maps(self): - self.execute(self.proc.memory_maps) - - @unittest.skipIf(not LINUX, "LINUX only") - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_get(self): - self.execute(self.proc.rlimit, psutil.RLIMIT_NOFILE) - - @unittest.skipIf(not LINUX, "LINUX only") - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_set(self): - limit = thisproc.rlimit(psutil.RLIMIT_NOFILE) - self.execute(self.proc.rlimit, psutil.RLIMIT_NOFILE, limit) - self.execute_w_exc(OSError, self.proc.rlimit, -1) - - @skip_if_linux() - # Windows implementation is based on a single system-wide - # function (tested later). - @unittest.skipIf(WINDOWS, "worthless on WINDOWS") - def test_connections(self): - # TODO: UNIX sockets are temporarily implemented by parsing - # 'pfiles' cmd output; we don't want that part of the code to - # be executed. - with create_sockets(): - kind = 'inet' if SUNOS else 'all' - self.execute(self.proc.connections, kind) - - @unittest.skipIf(not HAS_ENVIRON, "not supported") - def test_environ(self): - self.execute(self.proc.environ) - - @unittest.skipIf(not WINDOWS, "WINDOWS only") - def test_proc_info(self): - self.execute(cext.proc_info, os.getpid()) - - -class TestTerminatedProcessLeaks(TestProcessObjectLeaks): - """Repeat the tests above looking for leaks occurring when dealing - with terminated processes raising NoSuchProcess exception. - The C functions are still invoked but will follow different code - paths. We'll check those code paths. - """ - - @classmethod - def setUpClass(cls): - super(TestTerminatedProcessLeaks, cls).setUpClass() - p = get_test_subprocess() - cls.proc = psutil.Process(p.pid) - cls.proc.kill() - cls.proc.wait() - - @classmethod - def tearDownClass(cls): - super(TestTerminatedProcessLeaks, cls).tearDownClass() - reap_children() - - def _call(self, fun, *args, **kwargs): - try: - fun(*args, **kwargs) - except psutil.NoSuchProcess: - pass - - if WINDOWS: - - def test_kill(self): - self.execute(self.proc.kill) - - def test_terminate(self): - self.execute(self.proc.terminate) - - def test_suspend(self): - self.execute(self.proc.suspend) - - def test_resume(self): - self.execute(self.proc.resume) - - def test_wait(self): - self.execute(self.proc.wait) - - def test_proc_info(self): - # test dual implementation - def call(): - try: - return cext.proc_info(self.proc.pid) - except OSError as err: - if err.errno != errno.ESRCH: - raise - - self.execute(call) - - -# =================================================================== -# system APIs -# =================================================================== - - -class TestModuleFunctionsLeaks(TestMemLeak): - """Test leaks of psutil module functions.""" - - def test_coverage(self): - skip = set(( - "version_info", "__version__", "process_iter", "wait_procs", - "cpu_percent", "cpu_times_percent", "cpu_count")) - for name in psutil.__all__: - if not name.islower(): - continue - if name in skip: - continue - self.assertTrue(hasattr(self, "test_" + name), msg=name) - - # --- cpu - - @skip_if_linux() - def test_cpu_count_logical(self): - self.execute(psutil.cpu_count, logical=True) - - @skip_if_linux() - def test_cpu_count_physical(self): - self.execute(psutil.cpu_count, logical=False) - - @skip_if_linux() - def test_cpu_times(self): - self.execute(psutil.cpu_times) - - @skip_if_linux() - def test_per_cpu_times(self): - self.execute(psutil.cpu_times, percpu=True) - - def test_cpu_stats(self): - self.execute(psutil.cpu_stats) - - @skip_if_linux() - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq(self): - self.execute(psutil.cpu_freq) - - # --- mem - - def test_virtual_memory(self): - self.execute(psutil.virtual_memory) - - # TODO: remove this skip when this gets fixed - @unittest.skipIf(SUNOS, - "worthless on SUNOS (uses a subprocess)") - def test_swap_memory(self): - self.execute(psutil.swap_memory) - - @unittest.skipIf(POSIX and SKIP_PYTHON_IMPL, - "worthless on POSIX (pure python)") - def test_pid_exists(self): - self.execute(psutil.pid_exists, os.getpid()) - - # --- disk - - @unittest.skipIf(POSIX and SKIP_PYTHON_IMPL, - "worthless on POSIX (pure python)") - def test_disk_usage(self): - self.execute(psutil.disk_usage, '.') - - def test_disk_partitions(self): - self.execute(psutil.disk_partitions) - - @unittest.skipIf(LINUX and not os.path.exists('/proc/diskstats'), - '/proc/diskstats not available on this Linux version') - @skip_if_linux() - def test_disk_io_counters(self): - self.execute(psutil.disk_io_counters, nowrap=False) - - # --- proc - - @skip_if_linux() - def test_pids(self): - self.execute(psutil.pids) - - # --- net - - @skip_if_linux() - def test_net_io_counters(self): - self.execute(psutil.net_io_counters, nowrap=False) - - @unittest.skipIf(LINUX, - "worthless on Linux (pure python)") - @unittest.skipIf(OSX and os.getuid() != 0, "need root access") - def test_net_connections(self): - with create_sockets(): - self.execute(psutil.net_connections) - - def test_net_if_addrs(self): - # Note: verified that on Windows this was a false positive. - self.execute(psutil.net_if_addrs, - tolerance_=80 * 1024 if WINDOWS else None) - - @unittest.skipIf(TRAVIS, "EPERM on travis") - def test_net_if_stats(self): - self.execute(psutil.net_if_stats) - - # --- sensors - - @skip_if_linux() - @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") - def test_sensors_battery(self): - self.execute(psutil.sensors_battery) - - @skip_if_linux() - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") - def test_sensors_temperatures(self): - self.execute(psutil.sensors_temperatures) - - @skip_if_linux() - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") - def test_sensors_fans(self): - self.execute(psutil.sensors_fans) - - # --- others - - @skip_if_linux() - def test_boot_time(self): - self.execute(psutil.boot_time) - - # XXX - on Windows this produces a false positive - @unittest.skipIf(WINDOWS, "XXX produces a false positive on Windows") - def test_users(self): - self.execute(psutil.users) - - if WINDOWS: - - # --- win services - - def test_win_service_iter(self): - self.execute(cext.winservice_enumerate) - - def test_win_service_get(self): - pass - - def test_win_service_get_config(self): - name = next(psutil.win_service_iter()).name() - self.execute(cext.winservice_query_config, name) - - def test_win_service_get_status(self): - name = next(psutil.win_service_iter()).name() - self.execute(cext.winservice_query_status, name) - - def test_win_service_get_description(self): - name = next(psutil.win_service_iter()).name() - self.execute(cext.winservice_query_descr, name) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py deleted file mode 100755 index 1d9067e77d..0000000000 --- a/psutil/tests/test_misc.py +++ /dev/null @@ -1,1046 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -""" -Miscellaneous tests. -""" - -import ast -import collections -import contextlib -import errno -import json -import os -import pickle -import socket -import stat - -from psutil import LINUX -from psutil import POSIX -from psutil import WINDOWS -from psutil._common import memoize -from psutil._common import memoize_when_activated -from psutil._common import supports_ipv6 -from psutil._common import wrap_numbers -from psutil._compat import PY3 -from psutil.tests import APPVEYOR -from psutil.tests import bind_socket -from psutil.tests import bind_unix_socket -from psutil.tests import call_until -from psutil.tests import chdir -from psutil.tests import create_proc_children_pair -from psutil.tests import create_sockets -from psutil.tests import create_zombie_proc -from psutil.tests import DEVNULL -from psutil.tests import get_free_port -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_BATTERY -from psutil.tests import HAS_CONNECTIONS_UNIX -from psutil.tests import HAS_MEMORY_FULL_INFO -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 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 run_test_module_by_name -from psutil.tests import safe_mkdir -from psutil.tests import safe_rmpath -from psutil.tests import SCRIPTS_DIR -from psutil.tests import sh -from psutil.tests import tcp_socketpair -from psutil.tests import TESTFN -from psutil.tests import TOX -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import unix_socket_path -from psutil.tests import unix_socketpair -from psutil.tests import wait_for_file -from psutil.tests import wait_for_pid -import psutil -import psutil.tests - - -# =================================================================== -# --- Misc / generic tests. -# =================================================================== - - -class TestMisc(unittest.TestCase): - - def test_process__repr__(self, func=repr): - p = psutil.Process() - r = func(p) - self.assertIn("psutil.Process", r) - self.assertIn("pid=%s" % p.pid, r) - self.assertIn("name=", r) - self.assertIn(p.name(), r) - with mock.patch.object(psutil.Process, "name", - side_effect=psutil.ZombieProcess(os.getpid())): - p = psutil.Process() - r = func(p) - self.assertIn("pid=%s" % p.pid, r) - self.assertIn("zombie", r) - self.assertNotIn("name=", r) - with mock.patch.object(psutil.Process, "name", - side_effect=psutil.NoSuchProcess(os.getpid())): - p = psutil.Process() - r = func(p) - self.assertIn("pid=%s" % p.pid, r) - self.assertIn("terminated", r) - self.assertNotIn("name=", r) - with mock.patch.object(psutil.Process, "name", - side_effect=psutil.AccessDenied(os.getpid())): - p = psutil.Process() - r = func(p) - self.assertIn("pid=%s" % p.pid, r) - self.assertNotIn("name=", r) - - def test_process__str__(self): - self.test_process__repr__(func=str) - - def test_no_such_process__repr__(self, func=repr): - self.assertEqual( - repr(psutil.NoSuchProcess(321)), - "psutil.NoSuchProcess process no longer exists (pid=321)") - self.assertEqual( - repr(psutil.NoSuchProcess(321, name='foo')), - "psutil.NoSuchProcess process no longer exists (pid=321, " - "name='foo')") - self.assertEqual( - repr(psutil.NoSuchProcess(321, msg='foo')), - "psutil.NoSuchProcess foo") - - def test_zombie_process__repr__(self, func=repr): - self.assertEqual( - repr(psutil.ZombieProcess(321)), - "psutil.ZombieProcess process still exists but it's a zombie " - "(pid=321)") - self.assertEqual( - repr(psutil.ZombieProcess(321, name='foo')), - "psutil.ZombieProcess process still exists but it's a zombie " - "(pid=321, name='foo')") - self.assertEqual( - repr(psutil.ZombieProcess(321, name='foo', ppid=1)), - "psutil.ZombieProcess process still exists but it's a zombie " - "(pid=321, name='foo', ppid=1)") - self.assertEqual( - repr(psutil.ZombieProcess(321, msg='foo')), - "psutil.ZombieProcess foo") - - def test_access_denied__repr__(self, func=repr): - self.assertEqual( - repr(psutil.AccessDenied(321)), - "psutil.AccessDenied (pid=321)") - self.assertEqual( - repr(psutil.AccessDenied(321, name='foo')), - "psutil.AccessDenied (pid=321, name='foo')") - self.assertEqual( - repr(psutil.AccessDenied(321, msg='foo')), - "psutil.AccessDenied foo") - - def test_timeout_expired__repr__(self, func=repr): - self.assertEqual( - repr(psutil.TimeoutExpired(321)), - "psutil.TimeoutExpired timeout after 321 seconds") - self.assertEqual( - repr(psutil.TimeoutExpired(321, pid=111)), - "psutil.TimeoutExpired timeout after 321 seconds (pid=111)") - self.assertEqual( - repr(psutil.TimeoutExpired(321, pid=111, name='foo')), - "psutil.TimeoutExpired timeout after 321 seconds " - "(pid=111, name='foo')") - - def test_process__eq__(self): - p1 = psutil.Process() - p2 = psutil.Process() - self.assertEqual(p1, p2) - p2._ident = (0, 0) - self.assertNotEqual(p1, p2) - self.assertNotEqual(p1, 'foo') - - def test_process__hash__(self): - s = set([psutil.Process(), psutil.Process()]) - self.assertEqual(len(s), 1) - - def test__all__(self): - dir_psutil = dir(psutil) - for name in dir_psutil: - if name in ('callable', 'error', 'namedtuple', 'tests', - 'long', 'test', 'NUM_CPUS', 'BOOT_TIME', - 'TOTAL_PHYMEM'): - continue - if not name.startswith('_'): - try: - __import__(name) - except ImportError: - if name not in psutil.__all__: - fun = getattr(psutil, name) - if fun is None: - continue - if (fun.__doc__ is not None and - 'deprecated' not in fun.__doc__.lower()): - self.fail('%r not in psutil.__all__' % name) - - # Import 'star' will break if __all__ is inconsistent, see: - # https://github.com/giampaolo/psutil/issues/656 - # Can't do `from psutil import *` as it won't work on python 3 - # so we simply iterate over __all__. - for name in psutil.__all__: - self.assertIn(name, dir_psutil) - - def test_version(self): - self.assertEqual('.'.join([str(x) for x in psutil.version_info]), - psutil.__version__) - - def test_process_as_dict_no_new_names(self): - # See https://github.com/giampaolo/psutil/issues/813 - p = psutil.Process() - p.foo = '1' - self.assertNotIn('foo', p.as_dict()) - - def test_memoize(self): - @memoize - def foo(*args, **kwargs): - "foo docstring" - calls.append(None) - return (args, kwargs) - - calls = [] - # no args - for x in range(2): - ret = foo() - expected = ((), {}) - self.assertEqual(ret, expected) - self.assertEqual(len(calls), 1) - # with args - for x in range(2): - ret = foo(1) - expected = ((1, ), {}) - self.assertEqual(ret, expected) - self.assertEqual(len(calls), 2) - # with args + kwargs - for x in range(2): - ret = foo(1, bar=2) - expected = ((1, ), {'bar': 2}) - self.assertEqual(ret, expected) - self.assertEqual(len(calls), 3) - # clear cache - foo.cache_clear() - ret = foo() - expected = ((), {}) - self.assertEqual(ret, expected) - self.assertEqual(len(calls), 4) - # docstring - self.assertEqual(foo.__doc__, "foo docstring") - - def test_memoize_when_activated(self): - class Foo: - - @memoize_when_activated - def foo(self): - calls.append(None) - - f = Foo() - calls = [] - f.foo() - f.foo() - self.assertEqual(len(calls), 2) - - # activate - calls = [] - f.foo.cache_activate() - f.foo() - f.foo() - self.assertEqual(len(calls), 1) - - # deactivate - calls = [] - f.foo.cache_deactivate() - f.foo() - f.foo() - self.assertEqual(len(calls), 2) - - def test_parse_environ_block(self): - from psutil._common import parse_environ_block - - def k(s): - return s.upper() if WINDOWS else s - - self.assertEqual(parse_environ_block("a=1\0"), - {k("a"): "1"}) - self.assertEqual(parse_environ_block("a=1\0b=2\0\0"), - {k("a"): "1", k("b"): "2"}) - self.assertEqual(parse_environ_block("a=1\0b=\0\0"), - {k("a"): "1", k("b"): ""}) - # ignore everything after \0\0 - self.assertEqual(parse_environ_block("a=1\0b=2\0\0c=3\0"), - {k("a"): "1", k("b"): "2"}) - # ignore everything that is not an assignment - self.assertEqual(parse_environ_block("xxx\0a=1\0"), {k("a"): "1"}) - self.assertEqual(parse_environ_block("a=1\0=b=2\0"), {k("a"): "1"}) - # do not fail if the block is incomplete - self.assertEqual(parse_environ_block("a=1\0b=2"), {k("a"): "1"}) - - def test_supports_ipv6(self): - self.addCleanup(supports_ipv6.cache_clear) - if supports_ipv6(): - with mock.patch('psutil._common.socket') as s: - s.has_ipv6 = False - supports_ipv6.cache_clear() - assert not supports_ipv6() - - supports_ipv6.cache_clear() - with mock.patch('psutil._common.socket.socket', - side_effect=socket.error) as s: - assert not supports_ipv6() - assert s.called - - supports_ipv6.cache_clear() - with mock.patch('psutil._common.socket.socket', - side_effect=socket.gaierror) as s: - assert not supports_ipv6() - supports_ipv6.cache_clear() - assert s.called - - supports_ipv6.cache_clear() - with mock.patch('psutil._common.socket.socket.bind', - side_effect=socket.gaierror) as s: - assert not supports_ipv6() - supports_ipv6.cache_clear() - assert s.called - else: - with self.assertRaises(Exception): - sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - sock.bind(("::1", 0)) - - def test_isfile_strict(self): - from psutil._common import isfile_strict - this_file = os.path.abspath(__file__) - assert isfile_strict(this_file) - assert not isfile_strict(os.path.dirname(this_file)) - with mock.patch('psutil._common.os.stat', - side_effect=OSError(errno.EPERM, "foo")): - self.assertRaises(OSError, isfile_strict, this_file) - with mock.patch('psutil._common.os.stat', - side_effect=OSError(errno.EACCES, "foo")): - self.assertRaises(OSError, isfile_strict, this_file) - with mock.patch('psutil._common.os.stat', - side_effect=OSError(errno.EINVAL, "foo")): - assert not isfile_strict(this_file) - with mock.patch('psutil._common.stat.S_ISREG', return_value=False): - assert not isfile_strict(this_file) - - def test_serialization(self): - def check(ret): - if json is not None: - json.loads(json.dumps(ret)) - a = pickle.dumps(ret) - b = pickle.loads(a) - self.assertEqual(ret, b) - - check(psutil.Process().as_dict()) - check(psutil.virtual_memory()) - check(psutil.swap_memory()) - check(psutil.cpu_times()) - check(psutil.cpu_times_percent(interval=0)) - check(psutil.net_io_counters()) - if LINUX and not os.path.exists('/proc/diskstats'): - pass - else: - if not APPVEYOR: - check(psutil.disk_io_counters()) - check(psutil.disk_partitions()) - check(psutil.disk_usage(os.getcwd())) - check(psutil.users()) - - def test_setup_script(self): - setup_py = os.path.join(ROOT_DIR, 'setup.py') - if TRAVIS and not os.path.exists(setup_py): - return self.skipTest("can't find setup.py") - module = import_module_by_path(setup_py) - self.assertRaises(SystemExit, module.setup) - self.assertEqual(module.get_version(), psutil.__version__) - - def test_ad_on_process_creation(self): - # We are supposed to be able to instantiate Process also in case - # of zombie processes or access denied. - with mock.patch.object(psutil.Process, 'create_time', - side_effect=psutil.AccessDenied) as meth: - psutil.Process() - assert meth.called - with mock.patch.object(psutil.Process, 'create_time', - side_effect=psutil.ZombieProcess(1)) as meth: - psutil.Process() - assert meth.called - with mock.patch.object(psutil.Process, 'create_time', - side_effect=ValueError) as meth: - with self.assertRaises(ValueError): - psutil.Process() - assert meth.called - - def test_sanity_version_check(self): - # see: https://github.com/giampaolo/psutil/issues/564 - with mock.patch( - "psutil._psplatform.cext.version", return_value="0.0.0"): - with self.assertRaises(ImportError) as cm: - reload_module(psutil) - self.assertIn("version conflict", str(cm.exception).lower()) - - -# =================================================================== -# --- Tests for wrap_numbers() function. -# =================================================================== - - -nt = collections.namedtuple('foo', 'a b c') - - -class TestWrapNumbers(unittest.TestCase): - - def setUp(self): - wrap_numbers.cache_clear() - - tearDown = setUp - - def test_first_call(self): - input = {'disk1': nt(5, 5, 5)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - - def test_input_hasnt_changed(self): - input = {'disk1': nt(5, 5, 5)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - - def test_increase_but_no_wrap(self): - input = {'disk1': nt(5, 5, 5)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - input = {'disk1': nt(10, 15, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - input = {'disk1': nt(20, 25, 30)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - input = {'disk1': nt(20, 25, 30)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - - def test_wrap(self): - # let's say 100 is the threshold - input = {'disk1': nt(100, 100, 100)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - # first wrap restarts from 10 - input = {'disk1': nt(100, 100, 10)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(100, 100, 110)}) - # then it remains the same - input = {'disk1': nt(100, 100, 10)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(100, 100, 110)}) - # then it goes up - input = {'disk1': nt(100, 100, 90)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(100, 100, 190)}) - # then it wraps again - input = {'disk1': nt(100, 100, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(100, 100, 210)}) - # and remains the same - input = {'disk1': nt(100, 100, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(100, 100, 210)}) - # now wrap another num - input = {'disk1': nt(50, 100, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(150, 100, 210)}) - # and again - input = {'disk1': nt(40, 100, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(190, 100, 210)}) - # keep it the same - input = {'disk1': nt(40, 100, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(190, 100, 210)}) - - def test_changing_keys(self): - # Emulate a case where the second call to disk_io() - # (or whatever) provides a new disk, then the new disk - # disappears on the third call. - input = {'disk1': nt(5, 5, 5)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - input = {'disk1': nt(5, 5, 5), - 'disk2': nt(7, 7, 7)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - input = {'disk1': nt(8, 8, 8)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - - def test_changing_keys_w_wrap(self): - input = {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 100)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - # disk 2 wraps - input = {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 10)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 110)}) - # disk 2 disappears - input = {'disk1': nt(50, 50, 50)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - - # then it appears again; the old wrap is supposed to be - # gone. - input = {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 100)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - # remains the same - input = {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 100)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - # and then wraps again - input = {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 10)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 110)}) - - def test_real_data(self): - d = {'nvme0n1': (300, 508, 640, 1571, 5970, 1987, 2049, 451751, 47048), - 'nvme0n1p1': (1171, 2, 5600256, 1024, 516, 0, 0, 0, 8), - 'nvme0n1p2': (54, 54, 2396160, 5165056, 4, 24, 30, 1207, 28), - 'nvme0n1p3': (2389, 4539, 5154, 150, 4828, 1844, 2019, 398, 348)} - self.assertEqual(wrap_numbers(d, 'disk_io'), d) - self.assertEqual(wrap_numbers(d, 'disk_io'), d) - # decrease this ↓ - d = {'nvme0n1': (100, 508, 640, 1571, 5970, 1987, 2049, 451751, 47048), - 'nvme0n1p1': (1171, 2, 5600256, 1024, 516, 0, 0, 0, 8), - 'nvme0n1p2': (54, 54, 2396160, 5165056, 4, 24, 30, 1207, 28), - 'nvme0n1p3': (2389, 4539, 5154, 150, 4828, 1844, 2019, 398, 348)} - out = wrap_numbers(d, 'disk_io') - self.assertEqual(out['nvme0n1'][0], 400) - - # --- cache tests - - def test_cache_first_call(self): - input = {'disk1': nt(5, 5, 5)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - self.assertEqual(cache[1], {'disk_io': {}}) - self.assertEqual(cache[2], {'disk_io': {}}) - - def test_cache_call_twice(self): - input = {'disk1': nt(5, 5, 5)} - wrap_numbers(input, 'disk_io') - input = {'disk1': nt(10, 10, 10)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - self.assertEqual( - cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0}}) - self.assertEqual(cache[2], {'disk_io': {}}) - - def test_cache_wrap(self): - # let's say 100 is the threshold - input = {'disk1': nt(100, 100, 100)} - wrap_numbers(input, 'disk_io') - - # first wrap restarts from 10 - input = {'disk1': nt(100, 100, 10)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - self.assertEqual( - cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 100}}) - self.assertEqual(cache[2], {'disk_io': {'disk1': set([('disk1', 2)])}}) - - def assert_(): - cache = wrap_numbers.cache_info() - self.assertEqual( - cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, - ('disk1', 2): 100}}) - self.assertEqual(cache[2], - {'disk_io': {'disk1': set([('disk1', 2)])}}) - - # then it remains the same - input = {'disk1': nt(100, 100, 10)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - assert_() - - # then it goes up - input = {'disk1': nt(100, 100, 90)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - assert_() - - # then it wraps again - input = {'disk1': nt(100, 100, 20)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - self.assertEqual( - cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 190}}) - self.assertEqual(cache[2], {'disk_io': {'disk1': set([('disk1', 2)])}}) - - def test_cache_changing_keys(self): - input = {'disk1': nt(5, 5, 5)} - wrap_numbers(input, 'disk_io') - input = {'disk1': nt(5, 5, 5), - 'disk2': nt(7, 7, 7)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - self.assertEqual( - cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0}}) - self.assertEqual(cache[2], {'disk_io': {}}) - - def test_cache_clear(self): - input = {'disk1': nt(5, 5, 5)} - wrap_numbers(input, 'disk_io') - wrap_numbers(input, 'disk_io') - wrap_numbers.cache_clear('disk_io') - self.assertEqual(wrap_numbers.cache_info(), ({}, {}, {})) - wrap_numbers.cache_clear('disk_io') - wrap_numbers.cache_clear('?!?') - - @unittest.skipIf( - not psutil.disk_io_counters() or not psutil.net_io_counters(), - "no disks or NICs available") - def test_cache_clear_public_apis(self): - psutil.disk_io_counters() - psutil.net_io_counters() - caches = wrap_numbers.cache_info() - for cache in caches: - self.assertIn('psutil.disk_io_counters', cache) - self.assertIn('psutil.net_io_counters', cache) - - psutil.disk_io_counters.cache_clear() - caches = wrap_numbers.cache_info() - for cache in caches: - self.assertIn('psutil.net_io_counters', cache) - self.assertNotIn('psutil.disk_io_counters', cache) - - psutil.net_io_counters.cache_clear() - caches = wrap_numbers.cache_info() - self.assertEqual(caches, ({}, {}, {})) - - -# =================================================================== -# --- Example script tests -# =================================================================== - - -@unittest.skipIf(TOX, "can't test on TOX") -# See: https://travis-ci.org/giampaolo/psutil/jobs/295224806 -@unittest.skipIf(TRAVIS and not os.path.exists(SCRIPTS_DIR), - "can't locate scripts directory") -class TestScripts(unittest.TestCase): - """Tests for scripts in the "scripts" directory.""" - - @staticmethod - def assert_stdout(exe, *args, **kwargs): - exe = '%s' % os.path.join(SCRIPTS_DIR, exe) - cmd = [PYTHON_EXE, exe] - for arg in args: - cmd.append(arg) - try: - out = sh(cmd, **kwargs).strip() - except RuntimeError as err: - if 'AccessDenied' in str(err): - return str(err) - else: - raise - assert out, out - return out - - @staticmethod - def assert_syntax(exe, args=None): - exe = os.path.join(SCRIPTS_DIR, exe) - if PY3: - f = open(exe, 'rt', encoding='utf8') - else: - f = open(exe, 'rt') - with f: - src = f.read() - ast.parse(src) - - def test_coverage(self): - # make sure all example scripts have a test method defined - meths = dir(self) - for name in os.listdir(SCRIPTS_DIR): - if name.endswith('.py'): - if 'test_' + os.path.splitext(name)[0] not in meths: - # self.assert_stdout(name) - self.fail('no test defined for %r script' - % os.path.join(SCRIPTS_DIR, name)) - - @unittest.skipIf(not POSIX, "POSIX only") - def test_executable(self): - for name in os.listdir(SCRIPTS_DIR): - if name.endswith('.py'): - path = os.path.join(SCRIPTS_DIR, name) - if not stat.S_IXUSR & os.stat(path)[stat.ST_MODE]: - self.fail('%r is not executable' % path) - - def test_disk_usage(self): - self.assert_stdout('disk_usage.py') - - def test_free(self): - self.assert_stdout('free.py') - - def test_meminfo(self): - self.assert_stdout('meminfo.py') - - def test_procinfo(self): - self.assert_stdout('procinfo.py', str(os.getpid())) - - # can't find users on APPVEYOR or TRAVIS - @unittest.skipIf(APPVEYOR or TRAVIS and not psutil.users(), - "unreliable on APPVEYOR or TRAVIS") - def test_who(self): - self.assert_stdout('who.py') - - def test_ps(self): - self.assert_stdout('ps.py') - - def test_pstree(self): - self.assert_stdout('pstree.py') - - def test_netstat(self): - self.assert_stdout('netstat.py') - - # permission denied on travis - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - def test_ifconfig(self): - self.assert_stdout('ifconfig.py') - - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") - def test_pmap(self): - self.assert_stdout('pmap.py', str(os.getpid())) - - @unittest.skipIf(not HAS_MEMORY_FULL_INFO, "not supported") - def test_procsmem(self): - self.assert_stdout('procsmem.py', stderr=DEVNULL) - - def test_killall(self): - self.assert_syntax('killall.py') - - def test_nettop(self): - self.assert_syntax('nettop.py') - - def test_top(self): - self.assert_syntax('top.py') - - def test_iotop(self): - self.assert_syntax('iotop.py') - - def test_pidof(self): - output = self.assert_stdout('pidof.py', psutil.Process().name()) - self.assertIn(str(os.getpid()), output) - - @unittest.skipIf(not WINDOWS, "WINDOWS only") - def test_winservices(self): - self.assert_stdout('winservices.py') - - def test_cpu_distribution(self): - self.assert_syntax('cpu_distribution.py') - - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - def test_temperatures(self): - self.assert_stdout('temperatures.py') - - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - def test_fans(self): - self.assert_stdout('fans.py') - - @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_battery(self): - self.assert_stdout('battery.py') - - def test_sensors(self): - self.assert_stdout('sensors.py') - - -# =================================================================== -# --- Unit tests for test utilities. -# =================================================================== - - -class TestRetryDecorator(unittest.TestCase): - - @mock.patch('time.sleep') - def test_retry_success(self, sleep): - # Fail 3 times out of 5; make sure the decorated fun returns. - - @retry(retries=5, interval=1, logfun=None) - def foo(): - while queue: - queue.pop() - 1 / 0 - return 1 - - queue = list(range(3)) - self.assertEqual(foo(), 1) - self.assertEqual(sleep.call_count, 3) - - @mock.patch('time.sleep') - def test_retry_failure(self, sleep): - # Fail 6 times out of 5; th function is supposed to raise exc. - - @retry(retries=5, interval=1, logfun=None) - def foo(): - while queue: - queue.pop() - 1 / 0 - return 1 - - queue = list(range(6)) - self.assertRaises(ZeroDivisionError, foo) - self.assertEqual(sleep.call_count, 5) - - @mock.patch('time.sleep') - def test_exception_arg(self, sleep): - @retry(exception=ValueError, interval=1) - def foo(): - raise TypeError - - self.assertRaises(TypeError, foo) - self.assertEqual(sleep.call_count, 0) - - @mock.patch('time.sleep') - def test_no_interval_arg(self, sleep): - # if interval is not specified sleep is not supposed to be called - - @retry(retries=5, interval=None, logfun=None) - def foo(): - 1 / 0 - - self.assertRaises(ZeroDivisionError, foo) - self.assertEqual(sleep.call_count, 0) - - @mock.patch('time.sleep') - def test_retries_arg(self, sleep): - - @retry(retries=5, interval=1, logfun=None) - def foo(): - 1 / 0 - - self.assertRaises(ZeroDivisionError, foo) - self.assertEqual(sleep.call_count, 5) - - @mock.patch('time.sleep') - def test_retries_and_timeout_args(self, sleep): - self.assertRaises(ValueError, retry, retries=5, timeout=1) - - -class TestSyncTestUtils(unittest.TestCase): - - def tearDown(self): - safe_rmpath(TESTFN) - - def test_wait_for_pid(self): - wait_for_pid(os.getpid()) - nopid = max(psutil.pids()) + 99999 - with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): - self.assertRaises(psutil.NoSuchProcess, wait_for_pid, nopid) - - def test_wait_for_file(self): - with open(TESTFN, 'w') as f: - f.write('foo') - wait_for_file(TESTFN) - assert not os.path.exists(TESTFN) - - def test_wait_for_file_empty(self): - with open(TESTFN, 'w'): - pass - wait_for_file(TESTFN, empty=True) - assert not os.path.exists(TESTFN) - - def test_wait_for_file_no_file(self): - with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): - self.assertRaises(IOError, wait_for_file, TESTFN) - - def test_wait_for_file_no_delete(self): - with open(TESTFN, 'w') as f: - f.write('foo') - wait_for_file(TESTFN, delete=False) - assert os.path.exists(TESTFN) - - def test_call_until(self): - ret = call_until(lambda: 1, "ret == 1") - self.assertEqual(ret, 1) - - -class TestFSTestUtils(unittest.TestCase): - - def setUp(self): - safe_rmpath(TESTFN) - - tearDown = setUp - - def test_safe_mkdir(self): - safe_mkdir(TESTFN) - assert os.path.isdir(TESTFN) - safe_mkdir(TESTFN) - assert os.path.isdir(TESTFN) - - def test_safe_rmpath(self): - # test file is removed - open(TESTFN, 'w').close() - safe_rmpath(TESTFN) - assert not os.path.exists(TESTFN) - # test no exception if path does not exist - safe_rmpath(TESTFN) - # test dir is removed - os.mkdir(TESTFN) - safe_rmpath(TESTFN) - assert not os.path.exists(TESTFN) - # test other exceptions are raised - with mock.patch('psutil.tests.os.stat', - side_effect=OSError(errno.EINVAL, "")) as m: - with self.assertRaises(OSError): - safe_rmpath(TESTFN) - assert m.called - - def test_chdir(self): - base = os.getcwd() - os.mkdir(TESTFN) - with chdir(TESTFN): - self.assertEqual(os.getcwd(), os.path.join(base, TESTFN)) - self.assertEqual(os.getcwd(), base) - - -class TestProcessUtils(unittest.TestCase): - - def test_reap_children(self): - subp = get_test_subprocess() - p = psutil.Process(subp.pid) - assert p.is_running() - reap_children() - assert not p.is_running() - assert not psutil.tests._pids_started - assert not psutil.tests._subprocesses_started - - def test_create_proc_children_pair(self): - p1, p2 = create_proc_children_pair() - self.assertNotEqual(p1.pid, p2.pid) - assert p1.is_running() - assert p2.is_running() - children = psutil.Process().children(recursive=True) - self.assertEqual(len(children), 2) - self.assertIn(p1, children) - self.assertIn(p2, children) - self.assertEqual(p1.ppid(), os.getpid()) - self.assertEqual(p2.ppid(), p1.pid) - - # make sure both of them are cleaned up - reap_children() - assert not p1.is_running() - assert not p2.is_running() - assert not psutil.tests._pids_started - assert not psutil.tests._subprocesses_started - - @unittest.skipIf(not POSIX, "POSIX only") - def test_create_zombie_proc(self): - zpid = create_zombie_proc() - self.addCleanup(reap_children, recursive=True) - p = psutil.Process(zpid) - self.assertEqual(p.status(), psutil.STATUS_ZOMBIE) - - -class TestNetUtils(unittest.TestCase): - - def bind_socket(self): - port = get_free_port() - with contextlib.closing(bind_socket(addr=('', port))) as s: - self.assertEqual(s.getsockname()[1], port) - - @unittest.skipIf(not POSIX, "POSIX only") - def test_bind_unix_socket(self): - with unix_socket_path() as name: - sock = bind_unix_socket(name) - with contextlib.closing(sock): - self.assertEqual(sock.family, socket.AF_UNIX) - self.assertEqual(sock.type, socket.SOCK_STREAM) - self.assertEqual(sock.getsockname(), name) - assert os.path.exists(name) - assert stat.S_ISSOCK(os.stat(name).st_mode) - # UDP - with unix_socket_path() as name: - sock = bind_unix_socket(name, type=socket.SOCK_DGRAM) - with contextlib.closing(sock): - self.assertEqual(sock.type, socket.SOCK_DGRAM) - - def tcp_tcp_socketpair(self): - addr = ("127.0.0.1", get_free_port()) - server, client = tcp_socketpair(socket.AF_INET, addr=addr) - with contextlib.closing(server): - with contextlib.closing(client): - # Ensure they are connected and the positions are - # correct. - self.assertEqual(server.getsockname(), addr) - self.assertEqual(client.getpeername(), addr) - self.assertNotEqual(client.getsockname(), addr) - - @unittest.skipIf(not POSIX, "POSIX only") - def test_unix_socketpair(self): - p = psutil.Process() - num_fds = p.num_fds() - assert not p.connections(kind='unix') - with unix_socket_path() as name: - server, client = unix_socketpair(name) - try: - assert os.path.exists(name) - assert stat.S_ISSOCK(os.stat(name).st_mode) - self.assertEqual(p.num_fds() - num_fds, 2) - self.assertEqual(len(p.connections(kind='unix')), 2) - self.assertEqual(server.getsockname(), name) - self.assertEqual(client.getpeername(), name) - finally: - client.close() - server.close() - - def test_create_sockets(self): - with create_sockets() as socks: - fams = collections.defaultdict(int) - types = collections.defaultdict(int) - for s in socks: - fams[s.family] += 1 - # work around http://bugs.python.org/issue30204 - types[s.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)] += 1 - self.assertGreaterEqual(fams[socket.AF_INET], 2) - if supports_ipv6(): - self.assertGreaterEqual(fams[socket.AF_INET6], 2) - if POSIX and HAS_CONNECTIONS_UNIX: - self.assertGreaterEqual(fams[socket.AF_UNIX], 2) - self.assertGreaterEqual(types[socket.SOCK_STREAM], 2) - self.assertGreaterEqual(types[socket.SOCK_DGRAM], 2) - - -class TestOtherUtils(unittest.TestCase): - - def test_is_namedtuple(self): - assert is_namedtuple(collections.namedtuple('foo', 'a b c')(1, 2, 3)) - assert not is_namedtuple(tuple()) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_osx.py b/psutil/tests/test_osx.py deleted file mode 100755 index bcb2ba4e1a..0000000000 --- a/psutil/tests/test_osx.py +++ /dev/null @@ -1,303 +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. - -"""OSX specific tests.""" - -import os -import re -import time - -import psutil -from psutil import OSX -from psutil.tests import create_zombie_proc -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_BATTERY -from psutil.tests import MEMORY_TOLERANCE -from psutil.tests import reap_children -from psutil.tests import retry_before_failing -from psutil.tests import run_test_module_by_name -from psutil.tests import sh -from psutil.tests import unittest - - -PAGESIZE = os.sysconf("SC_PAGE_SIZE") if OSX else None - - -def sysctl(cmdline): - """Expects a sysctl command with an argument and parse the result - returning only the value of interest. - """ - out = sh(cmdline) - result = out.split()[1] - try: - return int(result) - except ValueError: - return result - - -def vm_stat(field): - """Wrapper around 'vm_stat' cmdline utility.""" - out = sh('vm_stat') - for line in out.split('\n'): - if field in line: - break - else: - raise ValueError("line not found") - return int(re.search(r'\d+', line).group(0)) * PAGESIZE - - -# http://code.activestate.com/recipes/578019/ -def human2bytes(s): - SYMBOLS = { - 'customary': ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'), - } - init = s - num = "" - while s and s[0:1].isdigit() or s[0:1] == '.': - num += s[0] - s = s[1:] - num = float(num) - letter = s.strip() - for name, sset in SYMBOLS.items(): - if letter in sset: - break - else: - if letter == 'k': - sset = SYMBOLS['customary'] - letter = letter.upper() - else: - raise ValueError("can't interpret %r" % init) - prefix = {sset[0]: 1} - for i, s in enumerate(sset[1:]): - prefix[s] = 1 << (i + 1) * 10 - return int(num * prefix[letter]) - - -@unittest.skipIf(not OSX, "OSX only") -class TestProcess(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - - def test_process_create_time(self): - output = sh("ps -o lstart -p %s" % self.pid) - start_ps = output.replace('STARTED', '').strip() - hhmmss = start_ps.split(' ')[-2] - year = start_ps.split(' ')[-1] - start_psutil = psutil.Process(self.pid).create_time() - self.assertEqual( - hhmmss, - time.strftime("%H:%M:%S", time.localtime(start_psutil))) - self.assertEqual( - year, - time.strftime("%Y", time.localtime(start_psutil))) - - -@unittest.skipIf(not OSX, "OSX only") -class TestZombieProcessAPIs(unittest.TestCase): - - @classmethod - def setUpClass(cls): - zpid = create_zombie_proc() - cls.p = psutil.Process(zpid) - - @classmethod - def tearDownClass(cls): - reap_children(recursive=True) - - def test_pidtask_info(self): - self.assertEqual(self.p.status(), psutil.STATUS_ZOMBIE) - self.p.ppid() - self.p.uids() - self.p.gids() - self.p.terminal() - self.p.create_time() - - def test_exe(self): - self.assertRaises(psutil.ZombieProcess, self.p.exe) - - def test_cmdline(self): - self.assertRaises(psutil.ZombieProcess, self.p.cmdline) - - def test_environ(self): - self.assertRaises(psutil.ZombieProcess, self.p.environ) - - def test_cwd(self): - self.assertRaises(psutil.ZombieProcess, self.p.cwd) - - def test_memory_full_info(self): - self.assertRaises(psutil.ZombieProcess, self.p.memory_full_info) - - def test_cpu_times(self): - self.assertRaises(psutil.ZombieProcess, self.p.cpu_times) - - def test_num_ctx_switches(self): - self.assertRaises(psutil.ZombieProcess, self.p.num_ctx_switches) - - def test_num_threads(self): - self.assertRaises(psutil.ZombieProcess, self.p.num_threads) - - def test_open_files(self): - self.assertRaises(psutil.ZombieProcess, self.p.open_files) - - def test_connections(self): - self.assertRaises(psutil.ZombieProcess, self.p.connections) - - def test_num_fds(self): - self.assertRaises(psutil.ZombieProcess, self.p.num_fds) - - def test_threads(self): - self.assertRaises((psutil.ZombieProcess, psutil.AccessDenied), - self.p.threads) - - def test_memory_maps(self): - self.assertRaises(psutil.ZombieProcess, self.p.memory_maps) - - -@unittest.skipIf(not OSX, "OSX only") -class TestSystemAPIs(unittest.TestCase): - - # --- disk - - def test_disks(self): - # test psutil.disk_usage() and psutil.disk_partitions() - # against "df -a" - def df(path): - out = sh('df -k "%s"' % path).strip() - lines = out.split('\n') - lines.pop(0) - line = lines.pop(0) - dev, total, used, free = line.split()[:4] - if dev == 'none': - dev = '' - total = int(total) * 1024 - used = int(used) * 1024 - free = int(free) * 1024 - return dev, total, used, free - - for part in psutil.disk_partitions(all=False): - usage = psutil.disk_usage(part.mountpoint) - dev, total, used, free = df(part.mountpoint) - self.assertEqual(part.device, dev) - self.assertEqual(usage.total, total) - # 10 MB tollerance - if abs(usage.free - free) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % usage.free, free) - if abs(usage.used - used) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % usage.used, used) - - # --- cpu - - def test_cpu_count_logical(self): - num = sysctl("sysctl hw.logicalcpu") - self.assertEqual(num, psutil.cpu_count(logical=True)) - - def test_cpu_count_physical(self): - num = sysctl("sysctl hw.physicalcpu") - self.assertEqual(num, psutil.cpu_count(logical=False)) - - def test_cpu_freq(self): - freq = psutil.cpu_freq() - self.assertEqual( - freq.current * 1000 * 1000, sysctl("sysctl hw.cpufrequency")) - self.assertEqual( - freq.min * 1000 * 1000, sysctl("sysctl hw.cpufrequency_min")) - self.assertEqual( - freq.max * 1000 * 1000, sysctl("sysctl hw.cpufrequency_max")) - - # --- virtual mem - - def test_vmem_total(self): - sysctl_hwphymem = sysctl('sysctl hw.memsize') - self.assertEqual(sysctl_hwphymem, psutil.virtual_memory().total) - - @retry_before_failing() - 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() - 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") - psutil_val = psutil.virtual_memory().active - self.assertAlmostEqual(psutil_val, vmstat_val, delta=MEMORY_TOLERANCE) - - @retry_before_failing() - 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() - def test_vmem_wired(self): - vmstat_val = vm_stat("wired") - psutil_val = psutil.virtual_memory().wired - self.assertAlmostEqual(psutil_val, vmstat_val, delta=MEMORY_TOLERANCE) - - # --- swap mem - - @retry_before_failing() - 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() - def test_swapmem_sout(self): - vmstat_val = vm_stat("Pageout") - psutil_val = psutil.swap_memory().sout - self.assertEqual(psutil_val, vmstat_val) - - # Not very reliable. - # def test_swapmem_total(self): - # out = sh('sysctl vm.swapusage') - # out = out.replace('vm.swapusage: ', '') - # total, used, free = re.findall('\d+.\d+\w', out) - # psutil_smem = psutil.swap_memory() - # self.assertEqual(psutil_smem.total, human2bytes(total)) - # self.assertEqual(psutil_smem.used, human2bytes(used)) - # self.assertEqual(psutil_smem.free, human2bytes(free)) - - # --- network - - def test_net_if_stats(self): - for name, stats in psutil.net_if_stats().items(): - try: - out = sh("ifconfig %s" % name) - except RuntimeError: - pass - else: - self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) - self.assertEqual(stats.mtu, - int(re.findall(r'mtu (\d+)', out)[0])) - - # --- sensors_battery - - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_sensors_battery(self): - out = sh("pmset -g batt") - percent = re.search("(\d+)%", out).group(1) - drawing_from = re.search("Now drawing from '([^']+)'", out).group(1) - power_plugged = drawing_from == "AC Power" - psutil_result = psutil.sensors_battery() - self.assertEqual(psutil_result.power_plugged, power_plugged) - self.assertEqual(psutil_result.percent, int(percent)) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py deleted file mode 100755 index e9a6f5f63b..0000000000 --- a/psutil/tests/test_posix.py +++ /dev/null @@ -1,418 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""POSIX specific tests.""" - -import datetime -import errno -import os -import re -import subprocess -import sys -import time - -import psutil -from psutil import AIX -from psutil import BSD -from psutil import LINUX -from psutil import OPENBSD -from psutil import OSX -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 -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 run_test_module_by_name -from psutil.tests import sh -from psutil.tests import skip_on_access_denied -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import wait_for_pid -from psutil.tests import which - - -def ps(cmd): - """Expects a ps command with a -o argument and parse the result - returning only the value of interest. - """ - if not LINUX: - cmd = cmd.replace(" --no-headers ", " ") - if SUNOS: - cmd = cmd.replace("-o start", "-o stime") - if AIX: - cmd = cmd.replace("-o rss", "-o rssize") - output = sh(cmd) - if not LINUX: - output = output.split('\n')[1].strip() - try: - return int(output) - except ValueError: - return output - -# ps "-o" field names differ wildly between platforms. -# "comm" means "only executable name" but is not available on BSD platforms. -# "args" means "command with all its arguments", and is also not available -# on BSD platforms. -# "command" is like "args" on most platforms, but like "comm" on AIX, -# and not available on SUNOS. -# so for the executable name we can use "comm" on Solaris and split "command" -# on other platforms. -# to get the cmdline (with args) we have to use "args" on AIX and -# Solaris, and can use "command" on all others. - - -def ps_name(pid): - field = "command" - if SUNOS: - field = "comm" - return ps("ps --no-headers -o %s -p %s" % (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)) - - -@unittest.skipIf(not POSIX, "POSIX only") -class TestProcess(unittest.TestCase): - """Compare psutil results against 'ps' command line utility (mainly).""" - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess([PYTHON_EXE, "-E", "-O"], - stdin=subprocess.PIPE).pid - wait_for_pid(cls.pid) - - @classmethod - def tearDownClass(cls): - reap_children() - - def test_ppid(self): - ppid_ps = ps("ps --no-headers -o ppid -p %s" % 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_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_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_psutil = psutil.Process(self.pid).username() - self.assertEqual(username_ps, username_psutil) - - def test_username_no_resolution(self): - # Emulate a case where the system can't resolve the uid to - # a username in which case psutil is supposed to return - # the stringified uid. - p = psutil.Process() - with mock.patch("psutil.pwd.getpwuid", side_effect=KeyError) as fun: - self.assertEqual(p.username(), str(p.uids().real)) - assert fun.called - - @skip_on_access_denied() - @retry_before_failing() - 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_psutil = psutil.Process(self.pid).memory_info()[0] / 1024 - self.assertEqual(rss_ps, rss_psutil) - - @skip_on_access_denied() - @retry_before_failing() - 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_psutil = psutil.Process(self.pid).memory_info()[1] / 1024 - self.assertEqual(vsz_ps, vsz_psutil) - - def test_name(self): - name_ps = ps_name(self.pid) - # remove path if there is any, from the command - name_ps = os.path.basename(name_ps).lower() - name_psutil = psutil.Process(self.pid).name().lower() - # ...because of how we calculate PYTHON_EXE; on OSX this may - # be "pythonX.Y". - name_ps = re.sub(r"\d.\d", "", name_ps) - name_psutil = re.sub(r"\d.\d", "", name_psutil) - self.assertEqual(name_ps, name_psutil) - - def test_name_long(self): - # On UNIX the kernel truncates the name to the first 15 - # characters. In such a case psutil tries to determine the - # full name from the cmdline. - name = "long-program-name" - cmdline = ["long-program-name-extended", "foo", "bar"] - with mock.patch("psutil._psplatform.Process.name", - return_value=name): - with mock.patch("psutil._psplatform.Process.cmdline", - return_value=cmdline): - p = psutil.Process() - self.assertEqual(p.name(), "long-program-name-extended") - - def test_name_long_cmdline_ad_exc(self): - # Same as above but emulates a case where cmdline() raises - # AccessDenied in which case psutil is supposed to return - # the truncated name instead of crashing. - name = "long-program-name" - with mock.patch("psutil._psplatform.Process.name", - return_value=name): - with mock.patch("psutil._psplatform.Process.cmdline", - side_effect=psutil.AccessDenied(0, "")): - p = psutil.Process() - self.assertEqual(p.name(), "long-program-name") - - def test_name_long_cmdline_nsp_exc(self): - # Same as above but emulates a case where cmdline() raises NSP - # which is supposed to propagate. - name = "long-program-name" - with mock.patch("psutil._psplatform.Process.name", - return_value=name): - with mock.patch("psutil._psplatform.Process.cmdline", - side_effect=psutil.NoSuchProcess(0, "")): - p = psutil.Process() - self.assertRaises(psutil.NoSuchProcess, p.name) - - @unittest.skipIf(OSX 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() - time_psutil_tstamp = datetime.datetime.fromtimestamp( - time_psutil).strftime("%H:%M:%S") - # sometimes ps shows the time rounded up instead of down, so we check - # for both possible values - round_time_psutil = round(time_psutil) - round_time_psutil_tstamp = datetime.datetime.fromtimestamp( - round_time_psutil).strftime("%H:%M:%S") - self.assertIn(time_ps, [time_psutil_tstamp, round_time_psutil_tstamp]) - - def test_exe(self): - ps_pathname = ps_name(self.pid) - psutil_pathname = psutil.Process(self.pid).exe() - try: - self.assertEqual(ps_pathname, psutil_pathname) - except AssertionError: - # certain platforms such as BSD are more accurate returning: - # "/usr/local/bin/python2.7" - # ...instead of: - # "/usr/local/bin/python" - # We do not want to consider this difference in accuracy - # an error. - adjusted_ps_pathname = ps_pathname[:len(ps_pathname)] - self.assertEqual(ps_pathname, adjusted_ps_pathname) - - def test_cmdline(self): - ps_cmdline = ps_args(self.pid) - psutil_cmdline = " ".join(psutil.Process(self.pid).cmdline()) - self.assertEqual(ps_cmdline, psutil_cmdline) - - # On SUNOS "ps" reads niceness /proc/pid/psinfo which returns an - # incorrect value (20); the real deal is getpriority(2) which - # returns 0; psutil relies on it, see: - # https://github.com/giampaolo/psutil/issues/1082 - # AIX has the same issue - @unittest.skipIf(SUNOS, "not reliable on SUNOS") - @unittest.skipIf(AIX, "not reliable on AIX") - def test_nice(self): - ps_nice = ps("ps --no-headers -o nice -p %s" % self.pid) - psutil_nice = psutil.Process().nice() - self.assertEqual(ps_nice, psutil_nice) - - def test_num_fds(self): - # Note: this fails from time to time; I'm keen on thinking - # it doesn't mean something is broken - def call(p, attr): - args = () - attr = getattr(p, name, None) - if attr is not None and callable(attr): - if name == 'rlimit': - args = (psutil.RLIMIT_NOFILE,) - attr(*args) - else: - attr - - p = psutil.Process(os.getpid()) - failures = [] - ignored_names = ['terminate', 'kill', 'suspend', 'resume', 'nice', - 'send_signal', 'wait', 'children', 'as_dict', - 'memory_info_ex'] - if LINUX and get_kernel_version() < (2, 6, 36): - ignored_names.append('rlimit') - if LINUX and get_kernel_version() < (2, 6, 23): - ignored_names.append('num_ctx_switches') - for name in dir(psutil.Process): - if (name.startswith('_') or name in ignored_names): - continue - else: - try: - num1 = p.num_fds() - for x in range(2): - call(p, name) - num2 = p.num_fds() - except psutil.AccessDenied: - pass - else: - if abs(num2 - num1) > 1: - fail = "failure while processing Process.%s method " \ - "(before=%s, after=%s)" % (name, num1, num2) - failures.append(fail) - if failures: - self.fail('\n' + '\n'.join(failures)) - - -@unittest.skipIf(not POSIX, "POSIX only") -class TestSystemAPIs(unittest.TestCase): - """Test some system APIs.""" - - @retry_before_failing() - 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_psutil = psutil.pids() - 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: - pids_ps.insert(0, 0) - self.assertEqual(pids_ps, pids_psutil) - - # 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") - def test_nic_names(self): - output = sh("ifconfig -a") - for nic in psutil.net_io_counters(pernic=True).keys(): - for line in output.split(): - if line.startswith(nic): - break - else: - self.fail( - "couldn't find %s nic in 'ifconfig -a' output\n%s" % ( - nic, output)) - - # can't find users on APPVEYOR or TRAVIS - @unittest.skipIf(APPVEYOR or TRAVIS and not psutil.users(), - "unreliable on APPVEYOR or TRAVIS") - @retry_before_failing() - def test_users(self): - out = sh("who") - lines = out.split('\n') - users = [x.split()[0] for x in lines] - terminals = [x.split()[1] for x in lines] - self.assertEqual(len(users), len(psutil.users())) - for u in psutil.users(): - self.assertIn(u.name, users) - self.assertIn(u.terminal, terminals) - - def test_pid_exists_let_raise(self): - # According to "man 2 kill" possible error values for kill - # are (EINVAL, EPERM, ESRCH). Test that any other errno - # results in an exception. - with mock.patch("psutil._psposix.os.kill", - side_effect=OSError(errno.EBADF, "")) as m: - self.assertRaises(OSError, psutil._psposix.pid_exists, os.getpid()) - assert m.called - - def test_os_waitpid_let_raise(self): - # os.waitpid() is supposed to catch EINTR and ECHILD only. - # Test that any other errno results in an exception. - with mock.patch("psutil._psposix.os.waitpid", - side_effect=OSError(errno.EBADF, "")) as m: - self.assertRaises(OSError, psutil._psposix.wait_pid, os.getpid()) - assert m.called - - def test_os_waitpid_eintr(self): - # os.waitpid() is supposed to "retry" on EINTR. - with mock.patch("psutil._psposix.os.waitpid", - side_effect=OSError(errno.EINTR, "")) as m: - self.assertRaises( - psutil._psposix.TimeoutExpired, - psutil._psposix.wait_pid, os.getpid(), timeout=0.01) - assert m.called - - def test_os_waitpid_bad_ret_status(self): - # Simulate os.waitpid() returning a bad status. - with mock.patch("psutil._psposix.os.waitpid", - return_value=(1, -1)) as m: - self.assertRaises(ValueError, - psutil._psposix.wait_pid, os.getpid()) - assert m.called - - # AIX can return '-' in df output instead of numbers, e.g. for /proc - @unittest.skipIf(AIX, "unreliable on AIX") - def test_disk_usage(self): - def df(device): - out = sh("df -k %s" % device).strip() - line = out.split('\n')[1] - fields = line.split() - total = int(fields[1]) * 1024 - used = int(fields[2]) * 1024 - free = int(fields[3]) * 1024 - percent = float(fields[4].replace('%', '')) - return (total, used, free, percent) - - tolerance = 4 * 1024 * 1024 # 4MB - for part in psutil.disk_partitions(all=False): - usage = psutil.disk_usage(part.mountpoint) - try: - total, used, free, percent = df(part.device) - except RuntimeError as err: - # see: - # https://travis-ci.org/giampaolo/psutil/jobs/138338464 - # https://travis-ci.org/giampaolo/psutil/jobs/138343361 - err = str(err).lower() - if "no such file or directory" in err or \ - "raw devices not supported" in err or \ - "permission denied" in err: - continue - else: - raise - else: - self.assertAlmostEqual(usage.total, total, delta=tolerance) - self.assertAlmostEqual(usage.used, used, delta=tolerance) - self.assertAlmostEqual(usage.free, free, delta=tolerance) - self.assertAlmostEqual(usage.percent, percent, delta=1) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py deleted file mode 100755 index 3411114aff..0000000000 --- a/psutil/tests/test_process.py +++ /dev/null @@ -1,1567 +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. - -"""Tests for psutil.Process class.""" - -import collections -import errno -import getpass -import itertools -import os -import signal -import socket -import subprocess -import sys -import tempfile -import textwrap -import time -import types - -import psutil - -from psutil import AIX -from psutil import BSD -from psutil import LINUX -from psutil import NETBSD -from psutil import OPENBSD -from psutil import OSX -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._compat import long -from psutil._compat import PY3 -from psutil.tests import APPVEYOR -from psutil.tests import call_until -from psutil.tests import copyload_shared_lib -from psutil.tests import create_exe -from psutil.tests import create_proc_children_pair -from psutil.tests import create_zombie_proc -from psutil.tests import enum -from psutil.tests import get_test_subprocess -from psutil.tests import get_winver -from psutil.tests import HAS_CPU_AFFINITY -from psutil.tests import HAS_ENVIRON -from psutil.tests import HAS_IONICE -from psutil.tests import HAS_MEMORY_MAPS -from psutil.tests import HAS_PROC_CPU_NUM -from psutil.tests import HAS_PROC_IO_COUNTERS -from psutil.tests import HAS_RLIMIT -from psutil.tests import HAS_THREADS -from psutil.tests import mock -from psutil.tests import PYPY -from psutil.tests import PYTHON_EXE -from psutil.tests import reap_children -from psutil.tests import retry_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 -from psutil.tests import skip_on_not_implemented -from psutil.tests import TESTFILE_PREFIX -from psutil.tests import TESTFN -from psutil.tests import ThreadTask -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import wait_for_pid -from psutil.tests import WIN_VISTA - - -# =================================================================== -# --- psutil.Process class tests -# =================================================================== - -class TestProcess(unittest.TestCase): - """Tests for psutil.Process class.""" - - def setUp(self): - safe_rmpath(TESTFN) - - def tearDown(self): - reap_children() - - def test_pid(self): - p = psutil.Process() - self.assertEqual(p.pid, os.getpid()) - sproc = get_test_subprocess() - self.assertEqual(psutil.Process(sproc.pid).pid, sproc.pid) - with self.assertRaises(AttributeError): - p.pid = 33 - - def test_kill(self): - sproc = get_test_subprocess() - test_pid = sproc.pid - p = psutil.Process(test_pid) - p.kill() - sig = p.wait() - self.assertFalse(psutil.pid_exists(test_pid)) - if POSIX: - self.assertEqual(sig, -signal.SIGKILL) - - def test_terminate(self): - sproc = get_test_subprocess() - test_pid = sproc.pid - p = psutil.Process(test_pid) - p.terminate() - sig = p.wait() - self.assertFalse(psutil.pid_exists(test_pid)) - if POSIX: - self.assertEqual(sig, -signal.SIGTERM) - - def test_send_signal(self): - sig = signal.SIGKILL if POSIX else signal.SIGTERM - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.send_signal(sig) - exit_sig = p.wait() - self.assertFalse(psutil.pid_exists(p.pid)) - if POSIX: - self.assertEqual(exit_sig, -sig) - # - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.send_signal(sig) - with mock.patch('psutil.os.kill', - side_effect=OSError(errno.ESRCH, "")): - with self.assertRaises(psutil.NoSuchProcess): - p.send_signal(sig) - # - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.send_signal(sig) - with mock.patch('psutil.os.kill', - side_effect=OSError(errno.EPERM, "")): - with self.assertRaises(psutil.AccessDenied): - psutil.Process().send_signal(sig) - # Sending a signal to process with PID 0 is not allowed as - # it would affect every process in the process group of - # the calling process (os.getpid()) instead of PID 0"). - if 0 in psutil.pids(): - p = psutil.Process(0) - self.assertRaises(ValueError, p.send_signal, signal.SIGTERM) - - def test_wait(self): - # check exit code signal - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.kill() - code = p.wait() - if POSIX: - self.assertEqual(code, -signal.SIGKILL) - else: - self.assertEqual(code, signal.SIGTERM) - self.assertFalse(p.is_running()) - - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.terminate() - code = p.wait() - if POSIX: - self.assertEqual(code, -signal.SIGTERM) - else: - self.assertEqual(code, signal.SIGTERM) - self.assertFalse(p.is_running()) - - # check sys.exit() code - code = "import time, sys; time.sleep(0.01); sys.exit(5);" - sproc = get_test_subprocess([PYTHON_EXE, "-c", code]) - p = psutil.Process(sproc.pid) - self.assertEqual(p.wait(), 5) - self.assertFalse(p.is_running()) - - # Test wait() issued twice. - # It is not supposed to raise NSP when the process is gone. - # On UNIX this should return None, on Windows it should keep - # returning the exit code. - sproc = get_test_subprocess([PYTHON_EXE, "-c", code]) - p = psutil.Process(sproc.pid) - self.assertEqual(p.wait(), 5) - self.assertIn(p.wait(), (5, None)) - - # test timeout - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.name() - self.assertRaises(psutil.TimeoutExpired, p.wait, 0.01) - - # timeout < 0 not allowed - self.assertRaises(ValueError, p.wait, -1) - - def test_wait_non_children(self): - # Test wait() against a process which is not our direct - # child. - p1, p2 = create_proc_children_pair() - self.assertRaises(psutil.TimeoutExpired, p1.wait, 0.01) - self.assertRaises(psutil.TimeoutExpired, p2.wait, 0.01) - # We also terminate the direct child otherwise the - # grandchild will hang until the parent is gone. - p1.terminate() - p2.terminate() - ret1 = p1.wait() - ret2 = p2.wait() - if POSIX: - self.assertEqual(ret1, -signal.SIGTERM) - # For processes which are not our children we're supposed - # to get None. - self.assertEqual(ret2, None) - else: - self.assertEqual(ret1, signal.SIGTERM) - self.assertEqual(ret1, signal.SIGTERM) - - def test_wait_timeout_0(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - self.assertRaises(psutil.TimeoutExpired, p.wait, 0) - p.kill() - stop_at = time.time() + 2 - while True: - try: - code = p.wait(0) - except psutil.TimeoutExpired: - if time.time() >= stop_at: - raise - else: - break - if POSIX: - self.assertEqual(code, -signal.SIGKILL) - else: - self.assertEqual(code, signal.SIGTERM) - self.assertFalse(p.is_running()) - - def test_cpu_percent(self): - p = psutil.Process() - p.cpu_percent(interval=0.001) - p.cpu_percent(interval=0.001) - for x in range(100): - percent = p.cpu_percent(interval=None) - self.assertIsInstance(percent, float) - self.assertGreaterEqual(percent, 0.0) - if not POSIX: - self.assertLessEqual(percent, 100.0) - else: - self.assertGreaterEqual(percent, 0.0) - with self.assertRaises(ValueError): - p.cpu_percent(interval=-1) - - def test_cpu_percent_numcpus_none(self): - # See: https://github.com/giampaolo/psutil/issues/1087 - with mock.patch('psutil.cpu_count', return_value=None) as m: - psutil.Process().cpu_percent() - assert m.called - - def test_cpu_times(self): - times = psutil.Process().cpu_times() - assert (times.user > 0.0) or (times.system > 0.0), times - assert (times.children_user >= 0.0), times - assert (times.children_system >= 0.0), times - # make sure returned values can be pretty printed with strftime - for name in times._fields: - time.strftime("%H:%M:%S", time.localtime(getattr(times, name))) - - def test_cpu_times_2(self): - user_time, kernel_time = psutil.Process().cpu_times()[:2] - utime, ktime = os.times()[:2] - - # Use os.times()[:2] as base values to compare our results - # using a tolerance of +/- 0.1 seconds. - # It will fail if the difference between the values is > 0.1s. - if (max([user_time, utime]) - min([user_time, utime])) > 0.1: - self.fail("expected: %s, found: %s" % (utime, user_time)) - - if (max([kernel_time, ktime]) - min([kernel_time, ktime])) > 0.1: - self.fail("expected: %s, found: %s" % (ktime, kernel_time)) - - @unittest.skipIf(not HAS_PROC_CPU_NUM, "not supported") - def test_cpu_num(self): - p = psutil.Process() - num = p.cpu_num() - self.assertGreaterEqual(num, 0) - if psutil.cpu_count() == 1: - self.assertEqual(num, 0) - self.assertIn(p.cpu_num(), range(psutil.cpu_count())) - - def test_create_time(self): - sproc = get_test_subprocess() - now = time.time() - p = psutil.Process(sproc.pid) - create_time = p.create_time() - - # Use time.time() as base value to compare our result using a - # tolerance of +/- 1 second. - # It will fail if the difference between the values is > 2s. - difference = abs(create_time - now) - if difference > 2: - self.fail("expected: %s, found: %s, difference: %s" - % (now, create_time, difference)) - - # make sure returned value can be pretty printed with strftime - time.strftime("%Y %m %d %H:%M:%S", time.localtime(p.create_time())) - - @unittest.skipIf(not POSIX, 'POSIX only') - @unittest.skipIf(TRAVIS, 'not reliable on TRAVIS') - def test_terminal(self): - terminal = psutil.Process().terminal() - if sys.stdin.isatty() or sys.stdout.isatty(): - tty = os.path.realpath(sh('tty')) - self.assertEqual(terminal, tty) - else: - self.assertIsNone(terminal) - - @unittest.skipIf(not HAS_PROC_IO_COUNTERS, 'not supported') - @skip_on_not_implemented(only_if=LINUX) - def test_io_counters(self): - p = psutil.Process() - - # test reads - io1 = p.io_counters() - with open(PYTHON_EXE, 'rb') as f: - f.read() - io2 = p.io_counters() - if not BSD and not AIX: - self.assertGreater(io2.read_count, io1.read_count) - self.assertEqual(io2.write_count, io1.write_count) - if LINUX: - self.assertGreater(io2.read_chars, io1.read_chars) - self.assertEqual(io2.write_chars, io1.write_chars) - else: - self.assertGreaterEqual(io2.read_bytes, io1.read_bytes) - self.assertGreaterEqual(io2.write_bytes, io1.write_bytes) - - # test writes - io1 = p.io_counters() - with tempfile.TemporaryFile(prefix=TESTFILE_PREFIX) as f: - if PY3: - f.write(bytes("x" * 1000000, 'ascii')) - else: - f.write("x" * 1000000) - io2 = p.io_counters() - self.assertGreaterEqual(io2.write_count, io1.write_count) - self.assertGreaterEqual(io2.write_bytes, io1.write_bytes) - self.assertGreaterEqual(io2.read_count, io1.read_count) - self.assertGreaterEqual(io2.read_bytes, io1.read_bytes) - if LINUX: - self.assertGreater(io2.write_chars, io1.write_chars) - self.assertGreaterEqual(io2.read_chars, io1.read_chars) - - # sanity check - for i in range(len(io2)): - if BSD and i >= 2: - # On BSD read_bytes and write_bytes are always set to -1. - continue - self.assertGreaterEqual(io2[i], 0) - self.assertGreaterEqual(io2[i], 0) - - @unittest.skipIf(not HAS_IONICE, "not supported") - @unittest.skipIf(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 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) - if LINUX: - 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) - self.assertRaisesRegex( - ValueError, "can't specify value with IOPRIO_CLASS_IDLE", - p.ionice, psutil.IOPRIO_CLASS_IDLE, 1) - 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) - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_get(self): - import resource - p = psutil.Process(os.getpid()) - names = [x for x in dir(psutil) if x.startswith('RLIMIT')] - assert names, names - for name in names: - value = getattr(psutil, name) - self.assertGreaterEqual(value, 0) - if name in dir(resource): - self.assertEqual(value, getattr(resource, name)) - # XXX - On PyPy RLIMIT_INFINITY returned by - # resource.getrlimit() is reported as a very big long - # number instead of -1. It looks like a bug with PyPy. - if PYPY: - continue - self.assertEqual(p.rlimit(value), resource.getrlimit(value)) - else: - ret = p.rlimit(value) - self.assertEqual(len(ret), 2) - self.assertGreaterEqual(ret[0], -1) - self.assertGreaterEqual(ret[1], -1) - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_set(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) - self.assertEqual(p.rlimit(psutil.RLIMIT_NOFILE), (5, 5)) - # If pid is 0 prlimit() applies to the calling process and - # we don't want that. - with self.assertRaises(ValueError): - psutil._psplatform.Process(0).rlimit(0) - with self.assertRaises(ValueError): - p.rlimit(psutil.RLIMIT_NOFILE, (5, 5, 5)) - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit(self): - p = psutil.Process() - soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) - try: - p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) - with open(TESTFN, "wb") as f: - f.write(b"X" * 1024) - # write() or flush() doesn't always cause the exception - # but close() will. - with self.assertRaises(IOError) as exc: - with open(TESTFN, "wb") as f: - f.write(b"X" * 1025) - self.assertEqual(exc.exception.errno if PY3 else exc.exception[0], - errno.EFBIG) - finally: - p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) - self.assertEqual(p.rlimit(psutil.RLIMIT_FSIZE), (soft, hard)) - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_infinity(self): - # First set a limit, then re-set it by specifying INFINITY - # and assume we overridden the previous limit. - p = psutil.Process() - soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) - try: - p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) - p.rlimit(psutil.RLIMIT_FSIZE, (psutil.RLIM_INFINITY, hard)) - with open(TESTFN, "wb") as f: - f.write(b"X" * 2048) - finally: - p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) - self.assertEqual(p.rlimit(psutil.RLIMIT_FSIZE), (soft, hard)) - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_infinity_value(self): - # RLIMIT_FSIZE should be RLIM_INFINITY, which will be a really - # big number on a platform with large file support. On these - # platforms we need to test that the get/setrlimit functions - # properly convert the number to a C long long and that the - # conversion doesn't raise an error. - p = psutil.Process() - soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) - self.assertEqual(psutil.RLIM_INFINITY, hard) - p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) - - def test_num_threads(self): - # on certain platforms such as Linux we might test for exact - # thread number, since we always have with 1 thread per process, - # but this does not apply across all platforms (OSX, Windows) - p = psutil.Process() - if OPENBSD: - try: - step1 = p.num_threads() - except psutil.AccessDenied: - raise unittest.SkipTest("on OpenBSD this requires root access") - else: - step1 = p.num_threads() - - with ThreadTask(): - step2 = p.num_threads() - self.assertEqual(step2, step1 + 1) - - @unittest.skipIf(not WINDOWS, 'WINDOWS only') - def test_num_handles(self): - # a better test is done later into test/_windows.py - p = psutil.Process() - self.assertGreater(p.num_handles(), 0) - - @unittest.skipIf(not HAS_THREADS, 'not supported') - def test_threads(self): - p = psutil.Process() - if OPENBSD: - try: - step1 = p.threads() - except psutil.AccessDenied: - raise unittest.SkipTest("on OpenBSD this requires root access") - else: - step1 = p.threads() - - with ThreadTask(): - step2 = p.threads() - self.assertEqual(len(step2), len(step1) + 1) - # 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]) - self.assertEqual(athread.user_time, athread[1]) - self.assertEqual(athread.system_time, athread[2]) - - @retry_before_failing() - @skip_on_access_denied(only_if=OSX) - @unittest.skipIf(not HAS_THREADS, 'not supported') - def test_threads_2(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - if OPENBSD: - try: - p.threads() - except psutil.AccessDenied: - raise unittest.SkipTest( - "on OpenBSD this requires root access") - self.assertAlmostEqual( - p.cpu_times().user, - sum([x.user_time for x in p.threads()]), delta=0.1) - self.assertAlmostEqual( - p.cpu_times().system, - sum([x.system_time for x in p.threads()]), delta=0.1) - - def test_memory_info(self): - p = psutil.Process() - - # step 1 - get a base value to compare our results - rss1, vms1 = p.memory_info()[:2] - percent1 = p.memory_percent() - self.assertGreater(rss1, 0) - self.assertGreater(vms1, 0) - - # step 2 - allocate some memory - memarr = [None] * 1500000 - - rss2, vms2 = p.memory_info()[:2] - percent2 = p.memory_percent() - - # step 3 - make sure that the memory usage bumped up - self.assertGreater(rss2, rss1) - self.assertGreaterEqual(vms2, vms1) # vms might be equal - self.assertGreater(percent2, percent1) - del memarr - - if WINDOWS: - mem = p.memory_info() - self.assertEqual(mem.rss, mem.wset) - self.assertEqual(mem.vms, mem.pagefile) - - mem = p.memory_info() - for name in mem._fields: - self.assertGreaterEqual(getattr(mem, name), 0) - - def test_memory_full_info(self): - total = psutil.virtual_memory().total - mem = psutil.Process().memory_full_info() - for name in mem._fields: - value = getattr(mem, name) - self.assertGreaterEqual(value, 0, msg=(name, value)) - self.assertLessEqual(value, total, msg=(name, value, total)) - if LINUX or WINDOWS or OSX: - self.assertGreaterEqual(mem.uss, 0) - if LINUX: - self.assertGreaterEqual(mem.pss, 0) - self.assertGreaterEqual(mem.swap, 0) - - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") - def test_memory_maps(self): - p = psutil.Process() - maps = p.memory_maps() - paths = [x for x in maps] - self.assertEqual(len(paths), len(set(paths))) - ext_maps = p.memory_maps(grouped=False) - - for nt in maps: - if not nt.path.startswith('['): - assert os.path.isabs(nt.path), nt.path - if POSIX: - try: - assert os.path.exists(nt.path) or \ - os.path.islink(nt.path), nt.path - except AssertionError: - if not LINUX: - raise - else: - # https://github.com/giampaolo/psutil/issues/759 - with open('/proc/self/smaps') as f: - data = f.read() - if "%s (deleted)" % nt.path not in data: - raise - else: - # XXX - On Windows we have this strange behavior with - # 64 bit dlls: they are visible via explorer but cannot - # be accessed via os.stat() (wtf?). - if '64' not in os.path.basename(nt.path): - assert os.path.exists(nt.path), nt.path - for nt in ext_maps: - for fname in nt._fields: - value = getattr(nt, fname) - if fname == 'path': - continue - elif fname in ('addr', 'perms'): - assert value, value - else: - self.assertIsInstance(value, (int, long)) - assert value >= 0, value - - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") - def test_memory_maps_lists_lib(self): - # Make sure a newly loaded shared lib is listed. - with copyload_shared_lib() as path: - def normpath(p): - return os.path.realpath(os.path.normcase(p)) - libpaths = [normpath(x.path) - for x in psutil.Process().memory_maps()] - self.assertIn(normpath(path), libpaths) - - def test_memory_percent(self): - p = psutil.Process() - 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 - self.assertRaises(ValueError, p.memory_percent, memtype="?!?") - if LINUX or OSX or WINDOWS: - ret = p.memory_percent(memtype='uss') - assert 0 <= ret <= 100, ret - assert 0 <= ret <= 100, ret - - def test_is_running(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - assert p.is_running() - assert p.is_running() - p.kill() - p.wait() - assert not p.is_running() - assert not p.is_running() - - def test_exe(self): - sproc = get_test_subprocess() - exe = psutil.Process(sproc.pid).exe() - try: - self.assertEqual(exe, PYTHON_EXE) - except AssertionError: - if WINDOWS and len(exe) == len(PYTHON_EXE): - # on Windows we don't care about case sensitivity - normcase = os.path.normcase - self.assertEqual(normcase(exe), normcase(PYTHON_EXE)) - else: - # certain platforms such as BSD are more accurate returning: - # "/usr/local/bin/python2.7" - # ...instead of: - # "/usr/local/bin/python" - # We do not want to consider this difference in accuracy - # an error. - ver = "%s.%s" % (sys.version_info[0], sys.version_info[1]) - try: - self.assertEqual(exe.replace(ver, ''), - PYTHON_EXE.replace(ver, '')) - except AssertionError: - # Tipically OSX. Really not sure what to do here. - pass - - out = sh([exe, "-c", "import os; print('hey')"]) - self.assertEqual(out, 'hey') - - def test_cmdline(self): - cmdline = [PYTHON_EXE, "-c", "import time; time.sleep(60)"] - sproc = get_test_subprocess(cmdline) - try: - self.assertEqual(' '.join(psutil.Process(sproc.pid).cmdline()), - ' '.join(cmdline)) - except AssertionError: - # XXX - most of the times the underlying sysctl() call on Net - # and Open BSD returns a truncated string. - # Also /proc/pid/cmdline behaves the same so it looks - # like this is a kernel bug. - # XXX - AIX truncates long arguments in /proc/pid/cmdline - if NETBSD or OPENBSD or AIX: - self.assertEqual( - psutil.Process(sproc.pid).cmdline()[0], PYTHON_EXE) - else: - raise - - def test_name(self): - sproc = get_test_subprocess(PYTHON_EXE) - name = psutil.Process(sproc.pid).name().lower() - pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() - assert pyexe.startswith(name), (pyexe, name) - - # XXX - @unittest.skipIf(SUNOS, "broken on SUNOS") - @unittest.skipIf(AIX, "broken on AIX") - def test_prog_w_funky_name(self): - # Test that name(), exe() and cmdline() correctly handle programs - # with funky chars such as spaces and ")", see: - # https://github.com/giampaolo/psutil/issues/628 - - def rm(): - # Try to limit occasional failures on Appveyor: - # https://ci.appveyor.com/project/giampaolo/psutil/build/1350/ - # job/lbo3bkju55le850n - try: - safe_rmpath(funky_path) - except OSError: - pass - - funky_path = TESTFN + 'foo bar )' - create_exe(funky_path) - self.addCleanup(rm) - cmdline = [funky_path, "-c", - "import time; [time.sleep(0.01) for x in range(3000)];" - "arg1", "arg2", "", "arg3", ""] - sproc = get_test_subprocess(cmdline) - p = psutil.Process(sproc.pid) - # ...in order to try to prevent occasional failures on travis - if TRAVIS: - wait_for_pid(p.pid) - self.assertEqual(p.cmdline(), cmdline) - self.assertEqual(p.name(), os.path.basename(funky_path)) - self.assertEqual(os.path.normcase(p.exe()), - os.path.normcase(funky_path)) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_uids(self): - p = psutil.Process() - real, effective, saved = p.uids() - # os.getuid() refers to "real" uid - self.assertEqual(real, os.getuid()) - # os.geteuid() refers to "effective" uid - self.assertEqual(effective, os.geteuid()) - # No such thing as os.getsuid() ("saved" uid), but starting - # from python 2.7 we have os.getresuid() which returns all - # of them. - if hasattr(os, "getresuid"): - self.assertEqual(os.getresuid(), p.uids()) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_gids(self): - p = psutil.Process() - real, effective, saved = p.gids() - # os.getuid() refers to "real" uid - self.assertEqual(real, os.getgid()) - # os.geteuid() refers to "effective" uid - self.assertEqual(effective, os.getegid()) - # No such thing as os.getsgid() ("saved" gid), but starting - # from python 2.7 we have os.getresgid() which returns all - # of them. - if hasattr(os, "getresuid"): - self.assertEqual(os.getresgid(), p.gids()) - - def test_nice(self): - p = psutil.Process() - self.assertRaises(TypeError, p.nice, "str") - if WINDOWS: - try: - init = p.nice() - if sys.version_info > (3, 4): - self.assertIsInstance(init, enum.IntEnum) - else: - self.assertIsInstance(init, int) - self.assertEqual(init, psutil.NORMAL_PRIORITY_CLASS) - p.nice(psutil.HIGH_PRIORITY_CLASS) - self.assertEqual(p.nice(), psutil.HIGH_PRIORITY_CLASS) - p.nice(psutil.NORMAL_PRIORITY_CLASS) - self.assertEqual(p.nice(), psutil.NORMAL_PRIORITY_CLASS) - finally: - p.nice(psutil.NORMAL_PRIORITY_CLASS) - else: - first_nice = p.nice() - try: - if hasattr(os, "getpriority"): - self.assertEqual( - os.getpriority(os.PRIO_PROCESS, os.getpid()), p.nice()) - p.nice(1) - self.assertEqual(p.nice(), 1) - if hasattr(os, "getpriority"): - self.assertEqual( - os.getpriority(os.PRIO_PROCESS, os.getpid()), p.nice()) - # XXX - going back to previous nice value raises - # AccessDenied on OSX - if not OSX: - p.nice(0) - self.assertEqual(p.nice(), 0) - except psutil.AccessDenied: - pass - finally: - try: - p.nice(first_nice) - except psutil.AccessDenied: - pass - - def test_status(self): - p = psutil.Process() - self.assertEqual(p.status(), psutil.STATUS_RUNNING) - - def test_username(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - username = p.username() - if WINDOWS: - domain, username = username.split('\\') - self.assertEqual(username, getpass.getuser()) - if 'USERDOMAIN' in os.environ: - self.assertEqual(domain, os.environ['USERDOMAIN']) - else: - self.assertEqual(username, getpass.getuser()) - - def test_cwd(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - self.assertEqual(p.cwd(), os.getcwd()) - - def test_cwd_2(self): - cmd = [PYTHON_EXE, "-c", - "import os, time; os.chdir('..'); time.sleep(60)"] - sproc = get_test_subprocess(cmd) - p = psutil.Process(sproc.pid) - call_until(p.cwd, "ret == os.path.dirname(os.getcwd())") - - @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') - def test_cpu_affinity(self): - p = psutil.Process() - initial = p.cpu_affinity() - assert initial, initial - self.addCleanup(p.cpu_affinity, initial) - - if hasattr(os, "sched_getaffinity"): - self.assertEqual(initial, list(os.sched_getaffinity(p.pid))) - self.assertEqual(len(initial), len(set(initial))) - - all_cpus = list(range(len(psutil.cpu_percent(percpu=True)))) - # Work around travis failure: - # https://travis-ci.org/giampaolo/psutil/builds/284173194 - for n in all_cpus if not TRAVIS else initial: - p.cpu_affinity([n]) - self.assertEqual(p.cpu_affinity(), [n]) - if hasattr(os, "sched_getaffinity"): - self.assertEqual(p.cpu_affinity(), - list(os.sched_getaffinity(p.pid))) - # also test num_cpu() - if hasattr(p, "num_cpu"): - self.assertEqual(p.cpu_affinity()[0], p.num_cpu()) - - # [] is an alias for "all eligible CPUs"; on Linux this may - # not be equal to all available CPUs, see: - # https://github.com/giampaolo/psutil/issues/956 - p.cpu_affinity([]) - if LINUX: - self.assertEqual(p.cpu_affinity(), p._proc._get_eligible_cpus()) - else: - self.assertEqual(p.cpu_affinity(), all_cpus) - if hasattr(os, "sched_getaffinity"): - self.assertEqual(p.cpu_affinity(), - list(os.sched_getaffinity(p.pid))) - # - self.assertRaises(TypeError, p.cpu_affinity, 1) - p.cpu_affinity(initial) - # it should work with all iterables, not only lists - p.cpu_affinity(set(all_cpus)) - p.cpu_affinity(tuple(all_cpus)) - - @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') - def test_cpu_affinity_errs(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - invalid_cpu = [len(psutil.cpu_times(percpu=True)) + 10] - self.assertRaises(ValueError, p.cpu_affinity, invalid_cpu) - self.assertRaises(ValueError, p.cpu_affinity, range(10000, 11000)) - self.assertRaises(TypeError, p.cpu_affinity, [0, "1"]) - self.assertRaises(ValueError, p.cpu_affinity, [0, -1]) - - @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') - def test_cpu_affinity_all_combinations(self): - p = psutil.Process() - initial = p.cpu_affinity() - assert initial, initial - self.addCleanup(p.cpu_affinity, initial) - - # All possible CPU set combinations. - combos = [] - for l in range(0, len(initial) + 1): - for subset in itertools.combinations(initial, l): - if subset: - combos.append(list(subset)) - - for combo in combos: - p.cpu_affinity(combo) - self.assertEqual(p.cpu_affinity(), combo) - - # TODO: #595 - @unittest.skipIf(BSD, "broken on BSD") - # can't find any process file on Appveyor - @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") - def test_open_files(self): - # current process - p = psutil.Process() - files = p.open_files() - self.assertFalse(TESTFN in files) - with open(TESTFN, 'wb') as f: - f.write(b'x' * 1024) - f.flush() - # give the kernel some time to see the new file - files = call_until(p.open_files, "len(ret) != %i" % len(files)) - for file in files: - if file.path == TESTFN: - if LINUX: - self.assertEqual(file.position, 1024) - break - else: - self.fail("no file found; files=%s" % repr(files)) - for file in files: - assert os.path.isfile(file.path), file - - # another process - cmdline = "import time; f = open(r'%s', 'r'); time.sleep(60);" % TESTFN - sproc = get_test_subprocess([PYTHON_EXE, "-c", cmdline]) - p = psutil.Process(sproc.pid) - - for x in range(100): - filenames = [x.path for x in p.open_files()] - if TESTFN in filenames: - break - time.sleep(.01) - else: - self.assertIn(TESTFN, filenames) - for file in filenames: - assert os.path.isfile(file), file - - # TODO: #595 - @unittest.skipIf(BSD, "broken on BSD") - # can't find any process file on Appveyor - @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") - def test_open_files_2(self): - # test fd and path fields - with open(TESTFN, 'w') as fileobj: - p = psutil.Process() - for file in p.open_files(): - if file.path == fileobj.name or file.fd == fileobj.fileno(): - break - else: - self.fail("no file found; files=%s" % repr(p.open_files())) - self.assertEqual(file.path, fileobj.name) - if WINDOWS: - self.assertEqual(file.fd, -1) - else: - self.assertEqual(file.fd, fileobj.fileno()) - # test positions - ntuple = p.open_files()[0] - self.assertEqual(ntuple[0], ntuple.path) - self.assertEqual(ntuple[1], ntuple.fd) - # test file is gone - self.assertNotIn(fileobj.name, p.open_files()) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_num_fds(self): - p = psutil.Process() - start = p.num_fds() - file = open(TESTFN, 'w') - self.addCleanup(file.close) - self.assertEqual(p.num_fds(), start + 1) - sock = socket.socket() - self.addCleanup(sock.close) - self.assertEqual(p.num_fds(), start + 2) - file.close() - sock.close() - self.assertEqual(p.num_fds(), start) - - @skip_on_not_implemented(only_if=LINUX) - @unittest.skipIf(OPENBSD or NETBSD, "not reliable on OPENBSD & NETBSD") - def test_num_ctx_switches(self): - p = psutil.Process() - before = sum(p.num_ctx_switches()) - for x in range(500000): - after = sum(p.num_ctx_switches()) - if after > before: - return - self.fail("num ctx switches still the same after 50.000 iterations") - - def test_ppid(self): - if hasattr(os, 'getppid'): - self.assertEqual(psutil.Process().ppid(), os.getppid()) - this_parent = os.getpid() - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - self.assertEqual(p.ppid(), this_parent) - # no other process is supposed to have us as parent - reap_children(recursive=True) - if APPVEYOR: - # Occasional failures, see: - # https://ci.appveyor.com/project/giampaolo/psutil/build/ - # job/0hs623nenj7w4m33 - return - for p in psutil.process_iter(): - if p.pid == sproc.pid: - continue - # XXX: sometimes this fails on Windows; not sure why. - self.assertNotEqual(p.ppid(), this_parent, msg=p) - - def test_parent(self): - this_parent = os.getpid() - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - self.assertEqual(p.parent().pid, this_parent) - - def test_parent_disappeared(self): - # Emulate a case where the parent process disappeared. - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - with mock.patch("psutil.Process", - side_effect=psutil.NoSuchProcess(0, 'foo')): - self.assertIsNone(p.parent()) - - def test_children(self): - p = psutil.Process() - self.assertEqual(p.children(), []) - self.assertEqual(p.children(recursive=True), []) - # On Windows we set the flag to 0 in order to cancel out the - # CREATE_NO_WINDOW flag (enabled by default) which creates - # an extra "conhost.exe" child. - sproc = get_test_subprocess(creationflags=0) - children1 = p.children() - children2 = p.children(recursive=True) - for children in (children1, children2): - self.assertEqual(len(children), 1) - self.assertEqual(children[0].pid, sproc.pid) - self.assertEqual(children[0].ppid(), os.getpid()) - - def test_children_recursive(self): - # Test children() against two sub processes, p1 and p2, where - # p1 (our child) spawned p2 (our grandchild). - p1, p2 = create_proc_children_pair() - p = psutil.Process() - self.assertEqual(p.children(), [p1]) - self.assertEqual(p.children(recursive=True), [p1, p2]) - # If the intermediate process is gone there's no way for - # children() to recursively find it. - p1.terminate() - p1.wait() - self.assertEqual(p.children(recursive=True), []) - - def test_children_duplicates(self): - # find the process which has the highest number of children - table = collections.defaultdict(int) - for p in psutil.process_iter(): - try: - table[p.ppid()] += 1 - except psutil.Error: - pass - # this is the one, now let's make sure there are no duplicates - pid = sorted(table.items(), key=lambda x: x[1])[-1][0] - p = psutil.Process(pid) - try: - c = p.children(recursive=True) - except psutil.AccessDenied: # windows - pass - else: - self.assertEqual(len(c), len(set(c))) - - def test_suspend_resume(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.suspend() - for x in range(100): - if p.status() == psutil.STATUS_STOPPED: - break - time.sleep(0.01) - p.resume() - self.assertNotEqual(p.status(), psutil.STATUS_STOPPED) - - def test_invalid_pid(self): - self.assertRaises(TypeError, psutil.Process, "1") - self.assertRaises(ValueError, psutil.Process, -1) - - def test_as_dict(self): - p = psutil.Process() - d = p.as_dict(attrs=['exe', 'name']) - self.assertEqual(sorted(d.keys()), ['exe', 'name']) - - p = psutil.Process(min(psutil.pids())) - d = p.as_dict(attrs=['connections'], ad_value='foo') - if not isinstance(d['connections'], list): - self.assertEqual(d['connections'], 'foo') - - # Test ad_value is set on AccessDenied. - with mock.patch('psutil.Process.nice', create=True, - side_effect=psutil.AccessDenied): - self.assertEqual( - p.as_dict(attrs=["nice"], ad_value=1), {"nice": 1}) - - # Test that NoSuchProcess bubbles up. - with mock.patch('psutil.Process.nice', create=True, - side_effect=psutil.NoSuchProcess(p.pid, "name")): - self.assertRaises( - psutil.NoSuchProcess, p.as_dict, attrs=["nice"]) - - # Test that ZombieProcess is swallowed. - with mock.patch('psutil.Process.nice', create=True, - side_effect=psutil.ZombieProcess(p.pid, "name")): - self.assertEqual( - p.as_dict(attrs=["nice"], ad_value="foo"), {"nice": "foo"}) - - # By default APIs raising NotImplementedError are - # supposed to be skipped. - with mock.patch('psutil.Process.nice', create=True, - side_effect=NotImplementedError): - d = p.as_dict() - self.assertNotIn('nice', list(d.keys())) - # ...unless the user explicitly asked for some attr. - with self.assertRaises(NotImplementedError): - p.as_dict(attrs=["nice"]) - - # errors - with self.assertRaises(TypeError): - p.as_dict('name') - with self.assertRaises(ValueError): - p.as_dict(['foo']) - with self.assertRaises(ValueError): - p.as_dict(['foo', 'bar']) - - def test_oneshot(self): - with mock.patch("psutil._psplatform.Process.cpu_times") as m: - p = psutil.Process() - with p.oneshot(): - p.cpu_times() - p.cpu_times() - self.assertEqual(m.call_count, 1) - - with mock.patch("psutil._psplatform.Process.cpu_times") as m: - p.cpu_times() - p.cpu_times() - self.assertEqual(m.call_count, 2) - - def test_oneshot_twice(self): - # Test the case where the ctx manager is __enter__ed twice. - # The second __enter__ is supposed to resut in a NOOP. - with mock.patch("psutil._psplatform.Process.cpu_times") as m1: - with mock.patch("psutil._psplatform.Process.oneshot_enter") as m2: - p = psutil.Process() - with p.oneshot(): - p.cpu_times() - p.cpu_times() - with p.oneshot(): - p.cpu_times() - p.cpu_times() - self.assertEqual(m1.call_count, 1) - self.assertEqual(m2.call_count, 1) - - with mock.patch("psutil._psplatform.Process.cpu_times") as m: - p.cpu_times() - p.cpu_times() - self.assertEqual(m.call_count, 2) - - def test_halfway_terminated_process(self): - # Test that NoSuchProcess exception gets raised in case the - # process dies after we create the Process object. - # Example: - # >>> proc = Process(1234) - # >>> time.sleep(2) # time-consuming task, process dies in meantime - # >>> proc.name() - # Refers to Issue #15 - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.terminate() - p.wait() - if WINDOWS: - call_until(psutil.pids, "%s not in ret" % p.pid) - self.assertFalse(p.is_running()) - # self.assertFalse(p.pid in psutil.pids(), msg="retcode = %s" % - # retcode) - - excluded_names = ['pid', 'is_running', 'wait', 'create_time', - 'oneshot', 'memory_info_ex'] - if LINUX and not HAS_RLIMIT: - excluded_names.append('rlimit') - for name in dir(p): - if (name.startswith('_') or - name in excluded_names): - continue - try: - meth = getattr(p, name) - # get/set methods - if name == 'nice': - if POSIX: - ret = meth(1) - else: - ret = meth(psutil.NORMAL_PRIORITY_CLASS) - elif name == 'ionice': - ret = meth() - ret = meth(2) - elif name == 'rlimit': - ret = meth(psutil.RLIMIT_NOFILE) - ret = meth(psutil.RLIMIT_NOFILE, (5, 5)) - elif name == 'cpu_affinity': - ret = meth() - ret = meth([0]) - elif name == 'send_signal': - ret = meth(signal.SIGTERM) - else: - ret = meth() - except psutil.ZombieProcess: - self.fail("ZombieProcess for %r was not supposed to happen" % - name) - except psutil.NoSuchProcess: - pass - except psutil.AccessDenied: - if OPENBSD and name in ('threads', 'num_threads'): - pass - else: - raise - except NotImplementedError: - pass - else: - self.fail( - "NoSuchProcess exception not raised for %r, retval=%s" % ( - name, ret)) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_zombie_process(self): - def succeed_or_zombie_p_exc(fun, *args, **kwargs): - try: - return fun(*args, **kwargs) - except (psutil.ZombieProcess, psutil.AccessDenied): - pass - - zpid = create_zombie_proc() - self.addCleanup(reap_children, recursive=True) - # A zombie process should always be instantiable - zproc = psutil.Process(zpid) - # ...and at least its status always be querable - self.assertEqual(zproc.status(), psutil.STATUS_ZOMBIE) - # ...and it should be considered 'running' - self.assertTrue(zproc.is_running()) - # ...and as_dict() shouldn't crash - zproc.as_dict() - # if cmdline succeeds it should be an empty list - ret = succeed_or_zombie_p_exc(zproc.suspend) - if ret is not None: - self.assertEqual(ret, []) - - if hasattr(zproc, "rlimit"): - succeed_or_zombie_p_exc(zproc.rlimit, psutil.RLIMIT_NOFILE) - succeed_or_zombie_p_exc(zproc.rlimit, psutil.RLIMIT_NOFILE, - (5, 5)) - # set methods - succeed_or_zombie_p_exc(zproc.parent) - if hasattr(zproc, 'cpu_affinity'): - try: - succeed_or_zombie_p_exc(zproc.cpu_affinity, [0]) - except ValueError as err: - if TRAVIS and LINUX and "not eligible" in str(err): - # https://travis-ci.org/giampaolo/psutil/jobs/279890461 - pass - else: - raise - - succeed_or_zombie_p_exc(zproc.nice, 0) - if hasattr(zproc, 'ionice'): - if LINUX: - succeed_or_zombie_p_exc(zproc.ionice, 2, 0) - else: - succeed_or_zombie_p_exc(zproc.ionice, 0) # Windows - if hasattr(zproc, 'rlimit'): - succeed_or_zombie_p_exc(zproc.rlimit, - psutil.RLIMIT_NOFILE, (5, 5)) - succeed_or_zombie_p_exc(zproc.suspend) - succeed_or_zombie_p_exc(zproc.resume) - succeed_or_zombie_p_exc(zproc.terminate) - succeed_or_zombie_p_exc(zproc.kill) - - # ...its parent should 'see' it - # edit: not true on BSD and OSX - # descendants = [x.pid for x in psutil.Process().children( - # recursive=True)] - # self.assertIn(zpid, descendants) - # XXX should we also assume ppid be usable? Note: this - # would be an important use case as the only way to get - # rid of a zombie is to kill its parent. - # self.assertEqual(zpid.ppid(), os.getpid()) - # ...and all other APIs should be able to deal with it - self.assertTrue(psutil.pid_exists(zpid)) - if not TRAVIS and OSX: - # For some reason this started failing all of the sudden. - # Maybe they upgraded OSX version? - # https://travis-ci.org/giampaolo/psutil/jobs/310896404 - self.assertIn(zpid, psutil.pids()) - self.assertIn(zpid, [x.pid for x in psutil.process_iter()]) - psutil._pmap = {} - self.assertIn(zpid, [x.pid for x in psutil.process_iter()]) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_zombie_process_is_running_w_exc(self): - # Emulate a case where internally is_running() raises - # ZombieProcess. - p = psutil.Process() - with mock.patch("psutil.Process", - side_effect=psutil.ZombieProcess(0)) as m: - assert p.is_running() - assert m.called - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_zombie_process_status_w_exc(self): - # Emulate a case where internally status() raises - # ZombieProcess. - p = psutil.Process() - with mock.patch("psutil._psplatform.Process.status", - side_effect=psutil.ZombieProcess(0)) as m: - self.assertEqual(p.status(), psutil.STATUS_ZOMBIE) - assert m.called - - def test_pid_0(self): - # Process(0) is supposed to work on all platforms except Linux - if 0 not in psutil.pids(): - self.assertRaises(psutil.NoSuchProcess, psutil.Process, 0) - return - - # test all methods - p = psutil.Process(0) - for name in psutil._as_dict_attrnames: - if name == 'pid': - continue - meth = getattr(p, name) - try: - ret = meth() - except psutil.AccessDenied: - pass - else: - if name in ("uids", "gids"): - self.assertEqual(ret.real, 0) - elif name == "username": - if POSIX: - self.assertEqual(p.username(), 'root') - elif WINDOWS: - self.assertEqual(p.username(), 'NT AUTHORITY\\SYSTEM') - elif name == "name": - assert name, name - - if hasattr(p, 'rlimit'): - try: - p.rlimit(psutil.RLIMIT_FSIZE) - except psutil.AccessDenied: - pass - - p.as_dict() - - if not OPENBSD: - self.assertIn(0, psutil.pids()) - self.assertTrue(psutil.pid_exists(0)) - - @unittest.skipIf(not HAS_ENVIRON, "not supported") - def test_environ(self): - def clean_dict(d): - # Most of these are problematic on Travis. - d.pop("PSUTIL_TESTING", None) - d.pop("PLAT", None) - d.pop("HOME", None) - if OSX: - d.pop("__CF_USER_TEXT_ENCODING", None) - d.pop("VERSIONER_PYTHON_PREFER_32_BIT", None) - d.pop("VERSIONER_PYTHON_VERSION", None) - return dict( - [(k.replace("\r", "").replace("\n", ""), - v.replace("\r", "").replace("\n", "")) - for k, v in d.items()]) - - self.maxDiff = None - p = psutil.Process() - d1 = clean_dict(p.environ()) - d2 = clean_dict(os.environ.copy()) - self.assertEqual(d1, d2) - - @unittest.skipIf(not HAS_ENVIRON, "not supported") - @unittest.skipIf(not POSIX, "POSIX only") - def test_weird_environ(self): - # environment variables can contain values without an equals sign - code = textwrap.dedent(""" - #include - #include - char * const argv[] = {"cat", 0}; - char * const envp[] = {"A=1", "X", "C=3", 0}; - int main(void) { - /* Close stderr on exec so parent can wait for the execve to - * finish. */ - if (fcntl(2, F_SETFD, FD_CLOEXEC) != 0) - return 0; - return execve("/bin/cat", argv, envp); - } - """) - path = TESTFN - create_exe(path, c_code=code) - self.addCleanup(safe_rmpath, path) - sproc = get_test_subprocess([path], - stdin=subprocess.PIPE, - stderr=subprocess.PIPE) - p = psutil.Process(sproc.pid) - wait_for_pid(p.pid) - self.assertTrue(p.is_running()) - # Wait for process to exec or exit. - self.assertEqual(sproc.stderr.read(), b"") - self.assertEqual(p.environ(), {"A": "1", "C": "3"}) - sproc.communicate() - self.assertEqual(sproc.returncode, 0) - - -# =================================================================== -# --- Limited user tests -# =================================================================== - - -if POSIX and os.getuid() == 0: - class LimitedUserTestCase(TestProcess): - """Repeat the previous tests by using a limited user. - Executed only on UNIX and only if the user who run the test script - is root. - """ - # the uid/gid the test suite runs under - if hasattr(os, 'getuid'): - PROCESS_UID = os.getuid() - PROCESS_GID = os.getgid() - - def __init__(self, *args, **kwargs): - TestProcess.__init__(self, *args, **kwargs) - # re-define all existent test methods in order to - # ignore AccessDenied exceptions - for attr in [x for x in dir(self) if x.startswith('test')]: - meth = getattr(self, attr) - - def test_(self): - try: - meth() - except psutil.AccessDenied: - pass - setattr(self, attr, types.MethodType(test_, self)) - - def setUp(self): - safe_rmpath(TESTFN) - TestProcess.setUp(self) - os.setegid(1000) - os.seteuid(1000) - - def tearDown(self): - os.setegid(self.PROCESS_UID) - os.seteuid(self.PROCESS_GID) - TestProcess.tearDown(self) - - def test_nice(self): - try: - psutil.Process().nice(-1) - except psutil.AccessDenied: - pass - else: - self.fail("exception not raised") - - def test_zombie_process(self): - # causes problems if test test suite is run as root - pass - - -# =================================================================== -# --- psutil.Popen tests -# =================================================================== - - -class TestPopen(unittest.TestCase): - """Tests for psutil.Popen class.""" - - def tearDown(self): - reap_children() - - def test_misc(self): - # XXX this test causes a ResourceWarning on Python 3 because - # psutil.__subproc instance doesn't get propertly freed. - # Not sure what to do though. - cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"] - with psutil.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as proc: - proc.name() - proc.cpu_times() - proc.stdin - self.assertTrue(dir(proc)) - self.assertRaises(AttributeError, getattr, proc, 'foo') - proc.terminate() - - def test_ctx_manager(self): - with psutil.Popen([PYTHON_EXE, "-V"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE) as proc: - proc.communicate() - assert proc.stdout.closed - assert proc.stderr.closed - assert proc.stdin.closed - self.assertEqual(proc.returncode, 0) - - def test_kill_terminate(self): - # subprocess.Popen()'s terminate(), kill() and send_signal() do - # not raise exception after the process is gone. psutil.Popen - # diverges from that. - cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"] - with psutil.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as proc: - proc.terminate() - proc.wait() - self.assertRaises(psutil.NoSuchProcess, proc.terminate) - self.assertRaises(psutil.NoSuchProcess, proc.kill) - self.assertRaises(psutil.NoSuchProcess, proc.send_signal, - signal.SIGTERM) - if WINDOWS and sys.version_info >= (2, 7): - self.assertRaises(psutil.NoSuchProcess, proc.send_signal, - signal.CTRL_C_EVENT) - self.assertRaises(psutil.NoSuchProcess, proc.send_signal, - signal.CTRL_BREAK_EVENT) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_sunos.py b/psutil/tests/test_sunos.py deleted file mode 100755 index ea9afcde0e..0000000000 --- a/psutil/tests/test_sunos.py +++ /dev/null @@ -1,45 +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. - -"""Sun OS specific tests.""" - -import os - -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 - - -@unittest.skipIf(not SUNOS, "SUNOS only") -class SunOSSpecificTestCase(unittest.TestCase): - - def test_swap_memory(self): - out = sh('env PATH=/usr/sbin:/sbin:%s swap -l' % os.environ['PATH']) - lines = out.strip().split('\n')[1:] - if not lines: - raise ValueError('no swap device(s) configured') - total = free = 0 - for line in lines: - line = line.split() - t, f = line[-2:] - total += int(int(t) * 512) - free += int(int(f) * 512) - used = total - free - - psutil_swap = psutil.swap_memory() - self.assertEqual(psutil_swap.total, total) - self.assertEqual(psutil_swap.used, used) - self.assertEqual(psutil_swap.free, free) - - def test_cpu_count(self): - out = sh("/usr/sbin/psrinfo") - self.assertEqual(psutil.cpu_count(), len(out.split('\n'))) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py deleted file mode 100755 index 20b132a91d..0000000000 --- a/psutil/tests/test_system.py +++ /dev/null @@ -1,862 +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. - -"""Tests for system APIS.""" - -import contextlib -import datetime -import errno -import os -import pprint -import shutil -import signal -import socket -import sys -import tempfile -import time - -import psutil -from psutil import AIX -from psutil import BSD -from psutil import FREEBSD -from psutil import LINUX -from psutil import NETBSD -from psutil import OPENBSD -from psutil import OSX -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._compat import long -from psutil.tests import APPVEYOR -from psutil.tests import ASCII_FS -from psutil.tests import check_net_address -from psutil.tests import DEVNULL -from psutil.tests import enum -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_BATTERY -from psutil.tests import HAS_CPU_FREQ -from psutil.tests import HAS_SENSORS_BATTERY -from psutil.tests import HAS_SENSORS_FANS -from psutil.tests import HAS_SENSORS_TEMPERATURES -from psutil.tests import mock -from psutil.tests import reap_children -from psutil.tests import retry_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 -from psutil.tests import TRAVIS -from psutil.tests import unittest - - -# =================================================================== -# --- System-related API tests -# =================================================================== - - -class TestSystemAPIs(unittest.TestCase): - """Tests for system-related APIs.""" - - def setUp(self): - safe_rmpath(TESTFN) - - def tearDown(self): - reap_children() - - def test_process_iter(self): - self.assertIn(os.getpid(), [x.pid for x in psutil.process_iter()]) - sproc = get_test_subprocess() - self.assertIn(sproc.pid, [x.pid for x in psutil.process_iter()]) - p = psutil.Process(sproc.pid) - p.kill() - p.wait() - self.assertNotIn(sproc.pid, [x.pid for x in psutil.process_iter()]) - - with mock.patch('psutil.Process', - side_effect=psutil.NoSuchProcess(os.getpid())): - self.assertEqual(list(psutil.process_iter()), []) - with mock.patch('psutil.Process', - side_effect=psutil.AccessDenied(os.getpid())): - with self.assertRaises(psutil.AccessDenied): - list(psutil.process_iter()) - - def test_prcess_iter_w_params(self): - for p in psutil.process_iter(attrs=['pid']): - self.assertEqual(list(p.info.keys()), ['pid']) - with self.assertRaises(ValueError): - list(psutil.process_iter(attrs=['foo'])) - with mock.patch("psutil._psplatform.Process.cpu_times", - side_effect=psutil.AccessDenied(0, "")) as m: - for p in psutil.process_iter(attrs=["pid", "cpu_times"]): - self.assertIsNone(p.info['cpu_times']) - self.assertGreaterEqual(p.info['pid'], 0) - assert m.called - with mock.patch("psutil._psplatform.Process.cpu_times", - side_effect=psutil.AccessDenied(0, "")) as m: - flag = object() - for p in psutil.process_iter( - attrs=["pid", "cpu_times"], ad_value=flag): - self.assertIs(p.info['cpu_times'], flag) - self.assertGreaterEqual(p.info['pid'], 0) - assert m.called - - def test_wait_procs(self): - def callback(p): - pids.append(p.pid) - - pids = [] - sproc1 = get_test_subprocess() - sproc2 = get_test_subprocess() - sproc3 = get_test_subprocess() - procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] - self.assertRaises(ValueError, psutil.wait_procs, procs, timeout=-1) - self.assertRaises(TypeError, psutil.wait_procs, procs, callback=1) - t = time.time() - gone, alive = psutil.wait_procs(procs, timeout=0.01, callback=callback) - - self.assertLess(time.time() - t, 0.5) - self.assertEqual(gone, []) - self.assertEqual(len(alive), 3) - self.assertEqual(pids, []) - for p in alive: - self.assertFalse(hasattr(p, 'returncode')) - - @retry_before_failing(30) - def test(procs, callback): - gone, alive = psutil.wait_procs(procs, timeout=0.03, - callback=callback) - self.assertEqual(len(gone), 1) - self.assertEqual(len(alive), 2) - return gone, alive - - sproc3.terminate() - gone, alive = test(procs, callback) - self.assertIn(sproc3.pid, [x.pid for x in gone]) - if POSIX: - self.assertEqual(gone.pop().returncode, -signal.SIGTERM) - else: - self.assertEqual(gone.pop().returncode, 1) - self.assertEqual(pids, [sproc3.pid]) - for p in alive: - self.assertFalse(hasattr(p, 'returncode')) - - @retry_before_failing(30) - def test(procs, callback): - gone, alive = psutil.wait_procs(procs, timeout=0.03, - callback=callback) - self.assertEqual(len(gone), 3) - self.assertEqual(len(alive), 0) - return gone, alive - - sproc1.terminate() - sproc2.terminate() - gone, alive = test(procs, callback) - self.assertEqual(set(pids), set([sproc1.pid, sproc2.pid, sproc3.pid])) - for p in gone: - self.assertTrue(hasattr(p, 'returncode')) - - def test_wait_procs_no_timeout(self): - sproc1 = get_test_subprocess() - sproc2 = get_test_subprocess() - sproc3 = get_test_subprocess() - procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] - for p in procs: - p.terminate() - gone, alive = psutil.wait_procs(procs) - - def test_boot_time(self): - bt = psutil.boot_time() - self.assertIsInstance(bt, float) - self.assertGreater(bt, 0) - self.assertLess(bt, time.time()) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_PAGESIZE(self): - # pagesize is used internally to perform different calculations - # and it's determined by using SC_PAGE_SIZE; make sure - # getpagesize() returns the same value. - import resource - self.assertEqual(os.sysconf("SC_PAGE_SIZE"), resource.getpagesize()) - - def test_virtual_memory(self): - mem = psutil.virtual_memory() - assert mem.total > 0, mem - assert mem.available > 0, mem - assert 0 <= mem.percent <= 100, mem - assert mem.used > 0, mem - assert mem.free >= 0, mem - for name in mem._fields: - value = getattr(mem, name) - if name != 'percent': - self.assertIsInstance(value, (int, long)) - if name != 'total': - if not value >= 0: - self.fail("%r < 0 (%s)" % (name, value)) - if value > mem.total: - self.fail("%r > total (total=%s, %s=%s)" - % (name, mem.total, name, value)) - - def test_swap_memory(self): - mem = psutil.swap_memory() - self.assertEqual( - mem._fields, ('total', 'used', 'free', 'percent', 'sin', 'sout')) - - assert mem.total >= 0, mem - assert mem.used >= 0, mem - if mem.total > 0: - # likely a system with no swap partition - assert mem.free > 0, mem - else: - assert mem.free == 0, mem - assert 0 <= mem.percent <= 100, mem - assert mem.sin >= 0, mem - assert mem.sout >= 0, mem - - def test_pid_exists(self): - sproc = get_test_subprocess() - self.assertTrue(psutil.pid_exists(sproc.pid)) - p = psutil.Process(sproc.pid) - p.kill() - p.wait() - self.assertFalse(psutil.pid_exists(sproc.pid)) - self.assertFalse(psutil.pid_exists(-1)) - self.assertEqual(psutil.pid_exists(0), 0 in psutil.pids()) - - def test_pid_exists_2(self): - reap_children() - pids = psutil.pids() - for pid in pids: - try: - assert psutil.pid_exists(pid) - except AssertionError: - # in case the process disappeared in meantime fail only - # if it is no longer in psutil.pids() - time.sleep(.1) - if pid in psutil.pids(): - self.fail(pid) - pids = range(max(pids) + 5000, max(pids) + 6000) - for pid in pids: - self.assertFalse(psutil.pid_exists(pid), msg=pid) - - def test_pids(self): - plist = [x.pid for x in psutil.process_iter()] - pidlist = psutil.pids() - self.assertEqual(plist.sort(), pidlist.sort()) - # make sure every pid is unique - self.assertEqual(len(pidlist), len(set(pidlist))) - - def test_test(self): - # test for psutil.test() function - stdout = sys.stdout - sys.stdout = DEVNULL - try: - psutil.test() - finally: - sys.stdout = stdout - - def test_cpu_count(self): - logical = psutil.cpu_count() - self.assertEqual(logical, len(psutil.cpu_times(percpu=True))) - self.assertGreaterEqual(logical, 1) - # - if os.path.exists("/proc/cpuinfo"): - with open("/proc/cpuinfo") as fd: - cpuinfo_data = fd.read() - if "physical id" not in cpuinfo_data: - raise unittest.SkipTest("cpuinfo doesn't include physical id") - physical = psutil.cpu_count(logical=False) - self.assertGreaterEqual(physical, 1) - self.assertGreaterEqual(logical, physical) - - def test_cpu_count_none(self): - # https://github.com/giampaolo/psutil/issues/1085 - for val in (-1, 0, None): - with mock.patch('psutil._psplatform.cpu_count_logical', - return_value=val) as m: - self.assertIsNone(psutil.cpu_count()) - assert m.called - with mock.patch('psutil._psplatform.cpu_count_physical', - return_value=val) as m: - self.assertIsNone(psutil.cpu_count(logical=False)) - assert m.called - - def test_cpu_times(self): - # Check type, value >= 0, str(). - total = 0 - times = psutil.cpu_times() - sum(times) - for cp_time in times: - self.assertIsInstance(cp_time, float) - self.assertGreaterEqual(cp_time, 0.0) - total += cp_time - self.assertEqual(total, sum(times)) - str(times) - # CPU times are always supposed to increase over time - # or at least remain the same and that's because time - # cannot go backwards. - # Surprisingly sometimes this might not be the case (at - # least on Windows and Linux), see: - # https://github.com/giampaolo/psutil/issues/392 - # https://github.com/giampaolo/psutil/issues/645 - # if not WINDOWS: - # last = psutil.cpu_times() - # for x in range(100): - # new = psutil.cpu_times() - # for field in new._fields: - # new_t = getattr(new, field) - # last_t = getattr(last, field) - # self.assertGreaterEqual(new_t, last_t, - # msg="%s %s" % (new_t, last_t)) - # last = new - - def test_cpu_times_time_increases(self): - # Make sure time increases between calls. - t1 = sum(psutil.cpu_times()) - time.sleep(0.1) - t2 = sum(psutil.cpu_times()) - difference = t2 - t1 - if not difference >= 0.05: - self.fail("difference %s" % difference) - - def test_per_cpu_times(self): - # Check type, value >= 0, str(). - for times in psutil.cpu_times(percpu=True): - total = 0 - sum(times) - for cp_time in times: - self.assertIsInstance(cp_time, float) - self.assertGreaterEqual(cp_time, 0.0) - total += cp_time - self.assertEqual(total, sum(times)) - str(times) - self.assertEqual(len(psutil.cpu_times(percpu=True)[0]), - len(psutil.cpu_times(percpu=False))) - - # Note: in theory CPU times are always supposed to increase over - # time or remain the same but never go backwards. In practice - # sometimes this is not the case. - # This issue seemd to be afflict Windows: - # https://github.com/giampaolo/psutil/issues/392 - # ...but it turns out also Linux (rarely) behaves the same. - # last = psutil.cpu_times(percpu=True) - # for x in range(100): - # new = psutil.cpu_times(percpu=True) - # for index in range(len(new)): - # newcpu = new[index] - # lastcpu = last[index] - # for field in newcpu._fields: - # new_t = getattr(newcpu, field) - # last_t = getattr(lastcpu, field) - # self.assertGreaterEqual( - # new_t, last_t, msg="%s %s" % (lastcpu, newcpu)) - # last = new - - def test_per_cpu_times_2(self): - # Simulate some work load then make sure time have increased - # between calls. - tot1 = psutil.cpu_times(percpu=True) - stop_at = time.time() + 0.1 - while True: - if time.time() >= stop_at: - break - tot2 = psutil.cpu_times(percpu=True) - for t1, t2 in zip(tot1, tot2): - t1, t2 = sum(t1), sum(t2) - difference = t2 - t1 - if difference >= 0.05: - return - self.fail() - - def test_cpu_times_comparison(self): - # Make sure the sum of all per cpu times is almost equal to - # base "one cpu" times. - base = psutil.cpu_times() - per_cpu = psutil.cpu_times(percpu=True) - summed_values = base._make([sum(num) for num in zip(*per_cpu)]) - for field in base._fields: - self.assertAlmostEqual( - getattr(base, field), getattr(summed_values, field), delta=1) - - def _test_cpu_percent(self, percent, last_ret, new_ret): - try: - self.assertIsInstance(percent, float) - self.assertGreaterEqual(percent, 0.0) - self.assertIsNot(percent, -0.0) - self.assertLessEqual(percent, 100.0 * psutil.cpu_count()) - except AssertionError as err: - raise AssertionError("\n%s\nlast=%s\nnew=%s" % ( - err, pprint.pformat(last_ret), pprint.pformat(new_ret))) - - def test_cpu_percent(self): - last = psutil.cpu_percent(interval=0.001) - for x in range(100): - new = psutil.cpu_percent(interval=None) - self._test_cpu_percent(new, last, new) - last = new - with self.assertRaises(ValueError): - psutil.cpu_percent(interval=-1) - - def test_per_cpu_percent(self): - last = psutil.cpu_percent(interval=0.001, percpu=True) - self.assertEqual(len(last), psutil.cpu_count()) - for x in range(100): - new = psutil.cpu_percent(interval=None, percpu=True) - for percent in new: - self._test_cpu_percent(percent, last, new) - last = new - with self.assertRaises(ValueError): - psutil.cpu_percent(interval=-1, percpu=True) - - def test_cpu_times_percent(self): - last = psutil.cpu_times_percent(interval=0.001) - for x in range(100): - new = psutil.cpu_times_percent(interval=None) - for percent in new: - self._test_cpu_percent(percent, last, new) - self._test_cpu_percent(sum(new), last, new) - last = new - - def test_per_cpu_times_percent(self): - last = psutil.cpu_times_percent(interval=0.001, percpu=True) - self.assertEqual(len(last), psutil.cpu_count()) - for x in range(100): - new = psutil.cpu_times_percent(interval=None, percpu=True) - for cpu in new: - for percent in cpu: - self._test_cpu_percent(percent, last, new) - self._test_cpu_percent(sum(cpu), last, new) - last = new - - def test_per_cpu_times_percent_negative(self): - # see: https://github.com/giampaolo/psutil/issues/645 - psutil.cpu_times_percent(percpu=True) - zero_times = [x._make([0 for x in range(len(x._fields))]) - for x in psutil.cpu_times(percpu=True)] - with mock.patch('psutil.cpu_times', return_value=zero_times): - for cpu in psutil.cpu_times_percent(percpu=True): - for percent in cpu: - self._test_cpu_percent(percent, None, None) - - def test_disk_usage(self): - usage = psutil.disk_usage(os.getcwd()) - self.assertEqual(usage._fields, ('total', 'used', 'free', 'percent')) - - assert usage.total > 0, usage - assert usage.used > 0, usage - assert usage.free > 0, usage - assert usage.total > usage.used, usage - assert usage.total > usage.free, usage - assert 0 <= usage.percent <= 100, usage.percent - if hasattr(shutil, 'disk_usage'): - # py >= 3.3, see: http://bugs.python.org/issue12442 - shutil_usage = shutil.disk_usage(os.getcwd()) - tolerance = 5 * 1024 * 1024 # 5MB - self.assertEqual(usage.total, shutil_usage.total) - self.assertAlmostEqual(usage.free, shutil_usage.free, - delta=tolerance) - self.assertAlmostEqual(usage.used, shutil_usage.used, - delta=tolerance) - - # if path does not exist OSError ENOENT is expected across - # all platforms - fname = tempfile.mktemp() - with self.assertRaises(OSError) as exc: - psutil.disk_usage(fname) - self.assertEqual(exc.exception.errno, errno.ENOENT) - - def test_disk_usage_unicode(self): - # See: https://github.com/giampaolo/psutil/issues/416 - if ASCII_FS: - with self.assertRaises(UnicodeEncodeError): - psutil.disk_usage(TESTFN_UNICODE) - - def test_disk_usage_bytes(self): - psutil.disk_usage(b'.') - - def test_disk_partitions(self): - # all = False - ls = psutil.disk_partitions(all=False) - # on travis we get: - # self.assertEqual(p.cpu_affinity(), [n]) - # AssertionError: Lists differ: [0, 1, 2, 3, 4, 5, 6, 7,... != [0] - self.assertTrue(ls, msg=ls) - for disk in ls: - self.assertIsInstance(disk.device, str) - self.assertIsInstance(disk.mountpoint, str) - self.assertIsInstance(disk.fstype, str) - self.assertIsInstance(disk.opts, str) - if WINDOWS and 'cdrom' in disk.opts: - continue - if not POSIX: - assert os.path.exists(disk.device), disk - else: - # we cannot make any assumption about this, see: - # http://goo.gl/p9c43 - disk.device - if SUNOS or TRAVIS: - # on solaris apparently mount points can also be files - assert os.path.exists(disk.mountpoint), disk - else: - assert os.path.isdir(disk.mountpoint), disk - assert disk.fstype, disk - - # all = True - ls = psutil.disk_partitions(all=True) - self.assertTrue(ls, msg=ls) - for disk in psutil.disk_partitions(all=True): - if not WINDOWS: - try: - os.stat(disk.mountpoint) - except OSError as err: - if TRAVIS and OSX and err.errno == errno.EIO: - continue - # http://mail.python.org/pipermail/python-dev/ - # 2012-June/120787.html - if err.errno not in (errno.EPERM, errno.EACCES): - raise - else: - 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 - self.assertIsInstance(disk.fstype, str) - self.assertIsInstance(disk.opts, str) - - def find_mount_point(path): - path = os.path.abspath(path) - while not os.path.ismount(path): - path = os.path.dirname(path) - return path.lower() - - mount = find_mount_point(__file__) - mounts = [x.mountpoint.lower() for x in - psutil.disk_partitions(all=True)] - self.assertIn(mount, mounts) - psutil.disk_usage(mount) - - def test_net_io_counters(self): - def check_ntuple(nt): - self.assertEqual(nt[0], nt.bytes_sent) - self.assertEqual(nt[1], nt.bytes_recv) - self.assertEqual(nt[2], nt.packets_sent) - self.assertEqual(nt[3], nt.packets_recv) - self.assertEqual(nt[4], nt.errin) - self.assertEqual(nt[5], nt.errout) - self.assertEqual(nt[6], nt.dropin) - self.assertEqual(nt[7], nt.dropout) - assert nt.bytes_sent >= 0, nt - assert nt.bytes_recv >= 0, nt - assert nt.packets_sent >= 0, nt - assert nt.packets_recv >= 0, nt - assert nt.errin >= 0, nt - assert nt.errout >= 0, nt - assert nt.dropin >= 0, nt - assert nt.dropout >= 0, nt - - ret = psutil.net_io_counters(pernic=False) - check_ntuple(ret) - ret = psutil.net_io_counters(pernic=True) - self.assertNotEqual(ret, []) - for key in ret: - self.assertTrue(key) - self.assertIsInstance(key, str) - check_ntuple(ret[key]) - - def test_net_io_counters_no_nics(self): - # Emulate a case where no NICs are installed, see: - # https://github.com/giampaolo/psutil/issues/1062 - with mock.patch('psutil._psplatform.net_io_counters', - return_value={}) as m: - self.assertIsNone(psutil.net_io_counters(pernic=False)) - self.assertEqual(psutil.net_io_counters(pernic=True), {}) - assert m.called - - def test_net_if_addrs(self): - nics = psutil.net_if_addrs() - assert nics, nics - - nic_stats = psutil.net_if_stats() - - # Not reliable on all platforms (net_if_addrs() reports more - # interfaces). - # self.assertEqual(sorted(nics.keys()), - # sorted(psutil.net_io_counters(pernic=True).keys())) - - families = set([socket.AF_INET, socket.AF_INET6, psutil.AF_LINK]) - for nic, addrs in nics.items(): - self.assertIsInstance(nic, str) - self.assertEqual(len(set(addrs)), len(addrs)) - for addr in addrs: - self.assertIsInstance(addr.family, int) - self.assertIsInstance(addr.address, str) - self.assertIsInstance(addr.netmask, (str, type(None))) - self.assertIsInstance(addr.broadcast, (str, type(None))) - self.assertIn(addr.family, families) - if sys.version_info >= (3, 4): - self.assertIsInstance(addr.family, enum.IntEnum) - if nic_stats[nic].isup: - # Do not test binding to addresses of interfaces - # that are down - if addr.family == socket.AF_INET: - s = socket.socket(addr.family) - with contextlib.closing(s): - s.bind((addr.address, 0)) - elif addr.family == socket.AF_INET6: - info = socket.getaddrinfo( - addr.address, 0, socket.AF_INET6, - socket.SOCK_STREAM, 0, socket.AI_PASSIVE)[0] - af, socktype, proto, canonname, sa = info - s = socket.socket(af, socktype, proto) - with contextlib.closing(s): - s.bind(sa) - for ip in (addr.address, addr.netmask, addr.broadcast, - addr.ptp): - if ip is not None: - # TODO: skip AF_INET6 for now because I get: - # AddressValueError: Only hex digits permitted in - # u'c6f3%lxcbr0' in u'fe80::c8e0:fff:fe54:c6f3%lxcbr0' - if addr.family != socket.AF_INET6: - check_net_address(ip, addr.family) - # broadcast and ptp addresses are mutually exclusive - if addr.broadcast: - self.assertIsNone(addr.ptp) - elif addr.ptp: - self.assertIsNone(addr.broadcast) - - if BSD or OSX or SUNOS: - if hasattr(socket, "AF_LINK"): - self.assertEqual(psutil.AF_LINK, socket.AF_LINK) - elif LINUX: - self.assertEqual(psutil.AF_LINK, socket.AF_PACKET) - elif WINDOWS: - self.assertEqual(psutil.AF_LINK, -1) - - def test_net_if_addrs_mac_null_bytes(self): - # Simulate that the underlying C function returns an incomplete - # MAC address. psutil is supposed to fill it with null bytes. - # https://github.com/giampaolo/psutil/issues/786 - if POSIX: - ret = [('em1', psutil.AF_LINK, '06:3d:29', None, None, None)] - else: - ret = [('em1', -1, '06-3d-29', None, None, None)] - with mock.patch('psutil._psplatform.net_if_addrs', - return_value=ret) as m: - addr = psutil.net_if_addrs()['em1'][0] - assert m.called - if POSIX: - self.assertEqual(addr.address, '06:3d:29:00:00:00') - else: - self.assertEqual(addr.address, '06-3d-29-00-00-00') - - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") # raises EPERM - def test_net_if_stats(self): - nics = psutil.net_if_stats() - assert nics, nics - all_duplexes = (psutil.NIC_DUPLEX_FULL, - psutil.NIC_DUPLEX_HALF, - psutil.NIC_DUPLEX_UNKNOWN) - for name, stats in nics.items(): - self.assertIsInstance(name, str) - isup, duplex, speed, mtu = stats - self.assertIsInstance(isup, bool) - self.assertIn(duplex, all_duplexes) - self.assertIn(duplex, all_duplexes) - self.assertGreaterEqual(speed, 0) - self.assertGreaterEqual(mtu, 0) - - @unittest.skipIf(LINUX and not os.path.exists('/proc/diskstats'), - '/proc/diskstats not available on this linux version') - @unittest.skipIf(APPVEYOR and psutil.disk_io_counters() is None, - "unreliable on APPVEYOR") # no visible disks - def test_disk_io_counters(self): - def check_ntuple(nt): - self.assertEqual(nt[0], nt.read_count) - self.assertEqual(nt[1], nt.write_count) - self.assertEqual(nt[2], nt.read_bytes) - self.assertEqual(nt[3], nt.write_bytes) - if not (OPENBSD or NETBSD): - self.assertEqual(nt[4], nt.read_time) - self.assertEqual(nt[5], nt.write_time) - if LINUX: - self.assertEqual(nt[6], nt.read_merged_count) - self.assertEqual(nt[7], nt.write_merged_count) - self.assertEqual(nt[8], nt.busy_time) - elif FREEBSD: - self.assertEqual(nt[6], nt.busy_time) - for name in nt._fields: - assert getattr(nt, name) >= 0, nt - - ret = psutil.disk_io_counters(perdisk=False) - assert ret is not None, "no disks on this system?" - check_ntuple(ret) - ret = psutil.disk_io_counters(perdisk=True) - # make sure there are no duplicates - self.assertEqual(len(ret), len(set(ret))) - for key in ret: - assert key, key - check_ntuple(ret[key]) - 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: - # https://github.com/giampaolo/psutil/issues/1062 - with mock.patch('psutil._psplatform.disk_io_counters', - return_value={}) as m: - self.assertIsNone(psutil.disk_io_counters(perdisk=False)) - self.assertEqual(psutil.disk_io_counters(perdisk=True), {}) - assert m.called - - # can't find users on APPVEYOR or TRAVIS - @unittest.skipIf(APPVEYOR or TRAVIS and not psutil.users(), - "unreliable on APPVEYOR or TRAVIS") - def test_users(self): - users = psutil.users() - self.assertNotEqual(users, []) - for user in users: - assert user.name, user - self.assertIsInstance(user.name, str) - self.assertIsInstance(user.terminal, (str, type(None))) - if user.host is not None: - self.assertIsInstance(user.host, (str, type(None))) - user.terminal - user.host - assert user.started > 0.0, user - datetime.datetime.fromtimestamp(user.started) - if WINDOWS or OPENBSD: - self.assertIsNone(user.pid) - else: - psutil.Process(user.pid) - - def test_cpu_stats(self): - # Tested more extensively in per-platform test modules. - infos = psutil.cpu_stats() - self.assertEqual( - infos._fields, - ('ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls')) - for name in infos._fields: - value = getattr(infos, name) - self.assertGreaterEqual(value, 0) - # on AIX, ctx_switches is always 0 - if not AIX and name in ('ctx_switches', 'interrupts'): - self.assertGreater(value, 0) - - @unittest.skipIf(not HAS_CPU_FREQ, "not suported") - def test_cpu_freq(self): - def check_ls(ls): - for nt in ls: - self.assertEqual(nt._fields, ('current', 'min', 'max')) - self.assertLessEqual(nt.current, nt.max) - for name in nt._fields: - value = getattr(nt, name) - self.assertIsInstance(value, (int, long, float)) - self.assertGreaterEqual(value, 0) - - ls = psutil.cpu_freq(percpu=True) - if TRAVIS and not ls: - return - - assert ls, ls - check_ls([psutil.cpu_freq(percpu=False)]) - - if LINUX: - self.assertEqual(len(ls), psutil.cpu_count()) - - def test_os_constants(self): - names = ["POSIX", "WINDOWS", "LINUX", "OSX", "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.OSX - names.remove("OSX") - else: - assert psutil.WINDOWS - assert not psutil.POSIX - names.remove("WINDOWS") - - # assert all other constants are set to False - for name in names: - self.assertIs(getattr(psutil, name), False, msg=name) - - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") - def test_sensors_temperatures(self): - temps = psutil.sensors_temperatures() - for name, entries in temps.items(): - self.assertIsInstance(name, str) - for entry in entries: - self.assertIsInstance(entry.label, str) - if entry.current is not None: - self.assertGreaterEqual(entry.current, 0) - if entry.high is not None: - self.assertGreaterEqual(entry.high, 0) - if entry.critical is not None: - self.assertGreaterEqual(entry.critical, 0) - - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") - def test_sensors_temperatures_fahreneit(self): - d = {'coretemp': [('label', 50.0, 60.0, 70.0)]} - with mock.patch("psutil._psplatform.sensors_temperatures", - return_value=d) as m: - temps = psutil.sensors_temperatures( - fahrenheit=True)['coretemp'][0] - assert m.called - self.assertEqual(temps.current, 122.0) - self.assertEqual(temps.high, 140.0) - self.assertEqual(temps.critical, 158.0) - - @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_sensors_battery(self): - ret = psutil.sensors_battery() - self.assertGreaterEqual(ret.percent, 0) - self.assertLessEqual(ret.percent, 100) - if ret.secsleft not in (psutil.POWER_TIME_UNKNOWN, - psutil.POWER_TIME_UNLIMITED): - self.assertGreaterEqual(ret.secsleft, 0) - else: - if ret.secsleft == psutil.POWER_TIME_UNLIMITED: - self.assertTrue(ret.power_plugged) - self.assertIsInstance(ret.power_plugged, bool) - - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") - def test_sensors_fans(self): - fans = psutil.sensors_fans() - for name, entries in fans.items(): - self.assertIsInstance(name, str) - for entry in entries: - self.assertIsInstance(entry.label, str) - self.assertIsInstance(entry.current, (int, long)) - self.assertGreaterEqual(entry.current, 0) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py deleted file mode 100755 index 6383c9bec1..0000000000 --- a/psutil/tests/test_unicode.py +++ /dev/null @@ -1,366 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -""" -Notes about unicode handling in psutil -====================================== - -In psutil these are the APIs returning or dealing with a string -('not tested' means they are not tested to deal with non-ASCII strings): - -* Process.cmdline() -* Process.connections('unix') -* Process.cwd() -* Process.environ() -* Process.exe() -* Process.memory_maps() -* Process.name() -* Process.open_files() -* Process.username() (not tested) - -* disk_io_counters() (not tested) -* disk_partitions() (not tested) -* disk_usage(str) -* net_connections('unix') -* net_if_addrs() (not tested) -* net_if_stats() (not tested) -* net_io_counters() (not tested) -* sensors_fans() (not tested) -* sensors_temperatures() (not tested) -* users() (not tested) - -* WindowsService.binpath() (not tested) -* WindowsService.description() (not tested) -* WindowsService.display_name() (not tested) -* WindowsService.name() (not tested) -* WindowsService.status() (not tested) -* WindowsService.username() (not tested) - -In here we create a unicode path with a funky non-ASCII name and (where -possible) make psutil return it back (e.g. on name(), exe(), open_files(), -etc.) and make sure that: - -* psutil never crashes with UnicodeDecodeError -* the returned path matches - -For a detailed explanation of how psutil handles unicode see: -- https://github.com/giampaolo/psutil/issues/1040 -- http://psutil.readthedocs.io/#unicode -""" - -import os -import traceback -import warnings -from contextlib import closing - -from psutil import BSD -from psutil import OPENBSD -from psutil import OSX -from psutil import POSIX -from psutil import WINDOWS -from psutil._compat import PY3 -from psutil._compat import u -from psutil.tests import APPVEYOR -from psutil.tests import ASCII_FS -from psutil.tests import bind_unix_socket -from psutil.tests import chdir -from psutil.tests import copyload_shared_lib -from psutil.tests import create_exe -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_CONNECTIONS_UNIX -from psutil.tests import HAS_ENVIRON -from psutil.tests import HAS_MEMORY_MAPS -from psutil.tests import mock -from psutil.tests import 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 -from psutil.tests import TESTFILE_PREFIX -from psutil.tests import TESTFN -from psutil.tests import TESTFN_UNICODE -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import unix_socket_path -import psutil - - -def safe_rmpath(path): - if APPVEYOR: - # TODO - this is quite random and I'm not sure why it happens, - # nor I can reproduce it locally: - # https://ci.appveyor.com/project/giampaolo/psutil/build/job/ - # jiq2cgd6stsbtn60 - # safe_rmpath() happens after reap_children() so this is weird - # Perhaps wait_procs() on Windows is broken? Maybe because - # of STILL_ACTIVE? - # https://github.com/giampaolo/psutil/blob/ - # 68c7a70728a31d8b8b58f4be6c4c0baa2f449eda/psutil/arch/ - # windows/process_info.c#L146 - try: - return _safe_rmpath(path) - except WindowsError: - traceback.print_exc() - else: - return _safe_rmpath(path) - - -def subprocess_supports_unicode(name): - """Return True if both the fs and the subprocess module can - deal with a unicode file name. - """ - if PY3: - return True - try: - safe_rmpath(name) - create_exe(name) - get_test_subprocess(cmd=[name]) - except UnicodeEncodeError: - return False - else: - return True - finally: - reap_children() - - -# An invalid unicode string. -if PY3: - INVALID_NAME = (TESTFN.encode('utf8') + b"f\xc0\x80").decode( - 'utf8', 'surrogateescape') -else: - INVALID_NAME = TESTFN + "f\xc0\x80" - - -# =================================================================== -# FS APIs -# =================================================================== - - -class _BaseFSAPIsTests(object): - funky_name = None - - @classmethod - def setUpClass(cls): - safe_rmpath(cls.funky_name) - create_exe(cls.funky_name) - - @classmethod - def tearDownClass(cls): - reap_children() - safe_rmpath(cls.funky_name) - - def tearDown(self): - reap_children() - - def expect_exact_path_match(self): - raise NotImplementedError("must be implemented in subclass") - - def test_proc_exe(self): - subp = get_test_subprocess(cmd=[self.funky_name]) - p = psutil.Process(subp.pid) - exe = p.exe() - self.assertIsInstance(exe, str) - if self.expect_exact_path_match(): - self.assertEqual(exe, self.funky_name) - - def test_proc_name(self): - subp = get_test_subprocess(cmd=[self.funky_name]) - if WINDOWS: - # On Windows name() is determined from exe() first, because - # it's faster; we want to overcome the internal optimization - # and test name() instead of exe(). - with mock.patch("psutil._psplatform.cext.proc_exe", - side_effect=psutil.AccessDenied(os.getpid())) as m: - name = psutil.Process(subp.pid).name() - assert m.called - else: - name = psutil.Process(subp.pid).name() - self.assertIsInstance(name, str) - if self.expect_exact_path_match(): - self.assertEqual(name, os.path.basename(self.funky_name)) - - def test_proc_cmdline(self): - subp = get_test_subprocess(cmd=[self.funky_name]) - p = psutil.Process(subp.pid) - cmdline = p.cmdline() - for part in cmdline: - self.assertIsInstance(part, str) - if self.expect_exact_path_match(): - self.assertEqual(cmdline, [self.funky_name]) - - def test_proc_cwd(self): - dname = self.funky_name + "2" - self.addCleanup(safe_rmpath, dname) - safe_mkdir(dname) - with chdir(dname): - p = psutil.Process() - cwd = p.cwd() - self.assertIsInstance(p.cwd(), str) - if self.expect_exact_path_match(): - self.assertEqual(cwd, dname) - - def test_proc_open_files(self): - p = psutil.Process() - start = set(p.open_files()) - with open(self.funky_name, 'rb'): - new = set(p.open_files()) - path = (new - start).pop().path - self.assertIsInstance(path, str) - if BSD and not path: - # XXX - see https://github.com/giampaolo/psutil/issues/595 - return self.skipTest("open_files on BSD is broken") - if self.expect_exact_path_match(): - self.assertEqual(os.path.normcase(path), - os.path.normcase(self.funky_name)) - - @unittest.skipIf(not POSIX, "POSIX only") - def test_proc_connections(self): - suffix = os.path.basename(self.funky_name) - with unix_socket_path(suffix=suffix) as name: - try: - sock = bind_unix_socket(name) - except UnicodeEncodeError: - if PY3: - raise - else: - raise unittest.SkipTest("not supported") - with closing(sock): - conn = psutil.Process().connections('unix')[0] - self.assertIsInstance(conn.laddr, str) - # AF_UNIX addr not set on OpenBSD - if not OPENBSD: - self.assertEqual(conn.laddr, name) - - @unittest.skipIf(not POSIX, "POSIX only") - @unittest.skipIf(not HAS_CONNECTIONS_UNIX, "can't list UNIX sockets") - @skip_on_access_denied() - def test_net_connections(self): - def find_sock(cons): - for conn in cons: - if os.path.basename(conn.laddr).startswith(TESTFILE_PREFIX): - return conn - raise ValueError("connection not found") - - suffix = os.path.basename(self.funky_name) - with unix_socket_path(suffix=suffix) as name: - try: - sock = bind_unix_socket(name) - except UnicodeEncodeError: - if PY3: - raise - else: - raise unittest.SkipTest("not supported") - with closing(sock): - cons = psutil.net_connections(kind='unix') - # AF_UNIX addr not set on OpenBSD - if not OPENBSD: - conn = find_sock(cons) - self.assertIsInstance(conn.laddr, str) - self.assertEqual(conn.laddr, name) - - def test_disk_usage(self): - dname = self.funky_name + "2" - self.addCleanup(safe_rmpath, dname) - safe_mkdir(dname) - psutil.disk_usage(dname) - - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") - @unittest.skipIf(not PY3, "ctypes does not support unicode on PY2") - def test_memory_maps(self): - # XXX: on Python 2, using ctypes.CDLL with a unicode path - # opens a message box which blocks the test run. - with copyload_shared_lib(dst_prefix=self.funky_name) as funky_path: - def normpath(p): - return os.path.realpath(os.path.normcase(p)) - libpaths = [normpath(x.path) - for x in psutil.Process().memory_maps()] - # ...just to have a clearer msg in case of failure - libpaths = [x for x in libpaths if TESTFILE_PREFIX in x] - self.assertIn(normpath(funky_path), libpaths) - for path in libpaths: - self.assertIsInstance(path, str) - - -@unittest.skipIf(OSX and TRAVIS, "unreliable on TRAVIS") # TODO -@unittest.skipIf(ASCII_FS, "ASCII fs") -@unittest.skipIf(not subprocess_supports_unicode(TESTFN_UNICODE), - "subprocess can't deal with unicode") -class TestFSAPIs(_BaseFSAPIsTests, unittest.TestCase): - """Test FS APIs with a funky, valid, UTF8 path name.""" - funky_name = TESTFN_UNICODE - - @classmethod - def expect_exact_path_match(cls): - # Do not expect psutil to correctly handle unicode paths on - # Python 2 if os.listdir() is not able either. - if PY3: - return True - else: - here = '.' if isinstance(cls.funky_name, str) else u('.') - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - return cls.funky_name in os.listdir(here) - - -@unittest.skipIf(OSX and TRAVIS, "unreliable on TRAVIS") # TODO -@unittest.skipIf(not subprocess_supports_unicode(INVALID_NAME), - "subprocess can't deal with invalid unicode") -class TestFSAPIsWithInvalidPath(_BaseFSAPIsTests, unittest.TestCase): - """Test FS APIs with a funky, invalid path name.""" - funky_name = INVALID_NAME - - @classmethod - def expect_exact_path_match(cls): - # Invalid unicode names are supposed to work on Python 2. - return True - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestWinProcessName(unittest.TestCase): - - def test_name_type(self): - # On Windows name() is determined from exe() first, because - # it's faster; we want to overcome the internal optimization - # and test name() instead of exe(). - with mock.patch("psutil._psplatform.cext.proc_exe", - side_effect=psutil.AccessDenied(os.getpid())) as m: - self.assertIsInstance(psutil.Process().name(), str) - assert m.called - - -# =================================================================== -# Non fs APIs -# =================================================================== - - -class TestNonFSAPIS(unittest.TestCase): - """Unicode tests for non fs-related APIs.""" - - def tearDown(self): - reap_children() - - @unittest.skipIf(not HAS_ENVIRON, "not supported") - def test_proc_environ(self): - # Note: differently from others, this test does not deal - # with fs paths. On Python 2 subprocess module is broken as - # it's not able to handle with non-ASCII env vars, so - # we use "è", which is part of the extended ASCII table - # (unicode point <= 255). - env = os.environ.copy() - funky_str = TESTFN_UNICODE if PY3 else 'è' - env['FUNNY_ARG'] = funky_str - sproc = get_test_subprocess(env=env) - p = psutil.Process(sproc.pid) - env = p.environ() - for k, v in env.items(): - self.assertIsInstance(k, str) - self.assertIsInstance(v, str) - self.assertEqual(env['FUNNY_ARG'], funky_str) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py deleted file mode 100755 index 32c46f67a3..0000000000 --- a/psutil/tests/test_windows.py +++ /dev/null @@ -1,837 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -* - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Windows specific tests.""" - -import datetime -import errno -import glob -import os -import platform -import re -import signal -import subprocess -import sys -import time -import warnings - -import psutil -from psutil import WINDOWS -from psutil.tests import APPVEYOR -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_BATTERY -from psutil.tests import mock -from psutil.tests import 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 - -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - try: - import win32api # requires "pip install pypiwin32" - import win32con - import win32process - import wmi # requires "pip install wmi" / "make setup-dev-env" - except ImportError: - if os.name == 'nt': - raise - - -cext = psutil._psplatform.cext - -# are we a 64 bit process -IS_64_BIT = sys.maxsize > 2**32 - - -def wrap_exceptions(fun): - def wrapper(self, *args, **kwargs): - try: - return fun(self, *args, **kwargs) - except OSError as err: - from psutil._pswindows import ACCESS_DENIED_SET - if err.errno in ACCESS_DENIED_SET: - raise psutil.AccessDenied(None, None) - if err.errno == errno.ESRCH: - raise psutil.NoSuchProcess(None, None) - raise - return wrapper - - -# =================================================================== -# System APIs -# =================================================================== - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class 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) - - @unittest.skipIf('NUMBER_OF_PROCESSORS' not in os.environ, - 'NUMBER_OF_PROCESSORS env var is not available') - def test_cpu_count(self): - num_cpus = int(os.environ['NUMBER_OF_PROCESSORS']) - self.assertEqual(num_cpus, psutil.cpu_count()) - - def test_cpu_count_2(self): - sys_value = win32api.GetSystemInfo()[5] - psutil_value = psutil.cpu_count() - self.assertEqual(sys_value, psutil_value) - - 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) - - def test_total_phymem(self): - w = wmi.WMI().Win32_ComputerSystem()[0] - self.assertEqual(int(w.TotalPhysicalMemory), - psutil.virtual_memory().total) - - # @unittest.skipIf(wmi is None, "wmi module is not installed") - # def test__UPTIME(self): - # # _UPTIME constant is not public but it is used internally - # # as value to return for pid 0 creation time. - # # WMI behaves the same. - # w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - # p = psutil.Process(0) - # wmic_create = str(w.CreationDate.split('.')[0]) - # psutil_create = time.strftime("%Y%m%d%H%M%S", - # time.localtime(p.create_time())) - - # Note: this test is not very reliable - @unittest.skipIf(APPVEYOR, "test not relieable on appveyor") - @retry_before_failing() - def test_pids(self): - # Note: this test might fail if the OS is starting/killing - # other processes in the meantime - w = wmi.WMI().Win32_Process() - wmi_pids = set([x.ProcessId for x in w]) - psutil_pids = set(psutil.pids()) - self.assertEqual(wmi_pids, psutil_pids) - - @retry_before_failing() - def test_disks(self): - ps_parts = psutil.disk_partitions(all=True) - wmi_parts = wmi.WMI().Win32_LogicalDisk() - for ps_part in ps_parts: - for wmi_part in wmi_parts: - if ps_part.device.replace('\\', '') == wmi_part.DeviceID: - if not ps_part.mountpoint: - # this is usually a CD-ROM with no disk inserted - break - try: - usage = psutil.disk_usage(ps_part.mountpoint) - except OSError as err: - if err.errno == errno.ENOENT: - # usually this is the floppy - break - else: - raise - self.assertEqual(usage.total, int(wmi_part.Size)) - wmi_free = int(wmi_part.FreeSpace) - self.assertEqual(usage.free, wmi_free) - # 10 MB tollerance - if abs(usage.free - wmi_free) > 10 * 1024 * 1024: - self.fail("psutil=%s, wmi=%s" % ( - usage.free, wmi_free)) - break - else: - self.fail("can't find partition %s" % repr(ps_part)) - - def test_disk_usage(self): - for disk in psutil.disk_partitions(): - sys_value = win32api.GetDiskFreeSpaceEx(disk.mountpoint) - psutil_value = psutil.disk_usage(disk.mountpoint) - self.assertAlmostEqual(sys_value[0], psutil_value.free, - delta=1024 * 1024) - self.assertAlmostEqual(sys_value[1], psutil_value.total, - delta=1024 * 1024) - self.assertEqual(psutil_value.used, - psutil_value.total - psutil_value.free) - - def test_disk_partitions(self): - sys_value = [ - x + '\\' for x in win32api.GetLogicalDriveStrings().split("\\\x00") - if x and not x.startswith('A:')] - psutil_value = [x.mountpoint for x in psutil.disk_partitions(all=True)] - self.assertEqual(sys_value, psutil_value) - - def test_net_if_stats(self): - ps_names = set(cext.net_if_stats()) - wmi_adapters = wmi.WMI().Win32_NetworkAdapter() - wmi_names = set() - for wmi_adapter in wmi_adapters: - wmi_names.add(wmi_adapter.Name) - wmi_names.add(wmi_adapter.NetConnectionID) - self.assertTrue(ps_names & wmi_names, - "no common entries in %s, %s" % (ps_names, wmi_names)) - - def test_boot_time(self): - wmi_os = wmi.WMI().Win32_OperatingSystem() - wmi_btime_str = wmi_os[0].LastBootUpTime.split('.')[0] - wmi_btime_dt = datetime.datetime.strptime( - wmi_btime_str, "%Y%m%d%H%M%S") - psutil_dt = datetime.datetime.fromtimestamp(psutil.boot_time()) - diff = abs((wmi_btime_dt - psutil_dt).total_seconds()) - # Wmic time is 2-3 secs lower for some reason; that's OK. - self.assertLessEqual(diff, 3) - - def test_boot_time_fluctuation(self): - # https://github.com/giampaolo/psutil/issues/1007 - with mock.patch('psutil._pswindows.cext.boot_time', return_value=5): - self.assertEqual(psutil.boot_time(), 5) - with mock.patch('psutil._pswindows.cext.boot_time', return_value=4): - self.assertEqual(psutil.boot_time(), 5) - with mock.patch('psutil._pswindows.cext.boot_time', return_value=6): - self.assertEqual(psutil.boot_time(), 5) - with mock.patch('psutil._pswindows.cext.boot_time', return_value=333): - self.assertEqual(psutil.boot_time(), 333) - - -# =================================================================== -# sensors_battery() -# =================================================================== - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestSensorsBattery(unittest.TestCase): - - def test_has_battery(self): - if win32api.GetPwrCapabilities()['SystemBatteriesPresent']: - self.assertIsNotNone(psutil.sensors_battery()) - else: - self.assertIsNone(psutil.sensors_battery()) - - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_percent(self): - w = wmi.WMI() - battery_wmi = w.query('select * from Win32_Battery')[0] - battery_psutil = psutil.sensors_battery() - self.assertAlmostEqual( - battery_psutil.percent, battery_wmi.EstimatedChargeRemaining, - delta=1) - - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_power_plugged(self): - w = wmi.WMI() - battery_wmi = w.query('select * from Win32_Battery')[0] - battery_psutil = psutil.sensors_battery() - # Status codes: - # https://msdn.microsoft.com/en-us/library/aa394074(v=vs.85).aspx - self.assertEqual(battery_psutil.power_plugged, - battery_wmi.BatteryStatus == 2) - - def test_emulate_no_battery(self): - with mock.patch("psutil._pswindows.cext.sensors_battery", - return_value=(0, 128, 0, 0)) as m: - self.assertIsNone(psutil.sensors_battery()) - assert m.called - - def test_emulate_power_connected(self): - with mock.patch("psutil._pswindows.cext.sensors_battery", - return_value=(1, 0, 0, 0)) as m: - self.assertEqual(psutil.sensors_battery().secsleft, - psutil.POWER_TIME_UNLIMITED) - assert m.called - - def test_emulate_power_charging(self): - with mock.patch("psutil._pswindows.cext.sensors_battery", - return_value=(0, 8, 0, 0)) as m: - self.assertEqual(psutil.sensors_battery().secsleft, - psutil.POWER_TIME_UNLIMITED) - assert m.called - - def test_emulate_secs_left_unknown(self): - with mock.patch("psutil._pswindows.cext.sensors_battery", - return_value=(0, 0, 0, -1)) as m: - self.assertEqual(psutil.sensors_battery().secsleft, - psutil.POWER_TIME_UNKNOWN) - assert m.called - - -# =================================================================== -# Process APIs -# =================================================================== - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestProcess(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - - def test_issue_24(self): - p = psutil.Process(0) - self.assertRaises(psutil.AccessDenied, p.kill) - - def test_special_pid(self): - p = psutil.Process(4) - self.assertEqual(p.name(), 'System') - # use __str__ to access all common Process properties to check - # that nothing strange happens - str(p) - p.username() - self.assertTrue(p.create_time() >= 0.0) - try: - rss, vms = p.memory_info()[:2] - except psutil.AccessDenied: - # expected on Windows Vista and Windows 7 - if not platform.uname()[1] in ('vista', 'win-7', 'win7'): - raise - else: - self.assertTrue(rss > 0) - - def test_send_signal(self): - p = psutil.Process(self.pid) - self.assertRaises(ValueError, p.send_signal, signal.SIGINT) - - def test_exe(self): - for p in psutil.process_iter(): - try: - self.assertEqual(os.path.basename(p.exe()), p.name()) - except psutil.Error: - pass - - def test_num_handles_increment(self): - p = psutil.Process(os.getpid()) - before = p.num_handles() - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, os.getpid()) - after = p.num_handles() - self.assertEqual(after, before + 1) - win32api.CloseHandle(handle) - self.assertEqual(p.num_handles(), before) - - def test_handles_leak(self): - # Call all Process methods and make sure no handles are left - # open. This is here mainly to make sure functions using - # OpenProcess() always call CloseHandle(). - def call(p, attr): - attr = getattr(p, name, None) - if attr is not None and callable(attr): - attr() - else: - attr - - p = psutil.Process(self.pid) - failures = [] - for name in dir(psutil.Process): - if name.startswith('_') \ - or name in ('terminate', 'kill', 'suspend', 'resume', - 'nice', 'send_signal', 'wait', 'children', - 'as_dict', 'memory_info_ex'): - continue - else: - try: - call(p, name) - num1 = p.num_handles() - call(p, name) - num2 = p.num_handles() - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - else: - if num2 > num1: - fail = \ - "failure while processing Process.%s method " \ - "(before=%s, after=%s)" % (name, num1, num2) - failures.append(fail) - if failures: - self.fail('\n' + '\n'.join(failures)) - - 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): - p = psutil.Process(get_test_subprocess().pid) - p.send_signal(signal.CTRL_C_EVENT) - p.send_signal(signal.CTRL_BREAK_EVENT) - p.kill() - p.wait() - self.assertRaises(psutil.NoSuchProcess, - p.send_signal, signal.CTRL_C_EVENT) - self.assertRaises(psutil.NoSuchProcess, - p.send_signal, signal.CTRL_BREAK_EVENT) - - def test_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)) - - def test_cmdline(self): - sys_value = re.sub(' +', ' ', win32api.GetCommandLine()).strip() - psutil_value = ' '.join(psutil.Process().cmdline()) - self.assertEqual(sys_value, psutil_value) - - # XXX - occasional failures - - # def test_cpu_times(self): - # handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - # win32con.FALSE, os.getpid()) - # self.addCleanup(win32api.CloseHandle, handle) - # sys_value = win32process.GetProcessTimes(handle) - # psutil_value = psutil.Process().cpu_times() - # self.assertAlmostEqual( - # psutil_value.user, sys_value['UserTime'] / 10000000.0, - # delta=0.2) - # self.assertAlmostEqual( - # psutil_value.user, sys_value['KernelTime'] / 10000000.0, - # delta=0.2) - - def test_nice(self): - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, os.getpid()) - self.addCleanup(win32api.CloseHandle, handle) - sys_value = win32process.GetPriorityClass(handle) - psutil_value = psutil.Process().nice() - self.assertEqual(psutil_value, sys_value) - - def test_memory_info(self): - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, self.pid) - self.addCleanup(win32api.CloseHandle, handle) - sys_value = win32process.GetProcessMemoryInfo(handle) - psutil_value = psutil.Process(self.pid).memory_info() - self.assertEqual( - sys_value['PeakWorkingSetSize'], psutil_value.peak_wset) - self.assertEqual( - sys_value['WorkingSetSize'], psutil_value.wset) - self.assertEqual( - sys_value['QuotaPeakPagedPoolUsage'], psutil_value.peak_paged_pool) - self.assertEqual( - sys_value['QuotaPagedPoolUsage'], psutil_value.paged_pool) - self.assertEqual( - sys_value['QuotaPeakNonPagedPoolUsage'], - psutil_value.peak_nonpaged_pool) - self.assertEqual( - sys_value['QuotaNonPagedPoolUsage'], psutil_value.nonpaged_pool) - self.assertEqual( - sys_value['PagefileUsage'], psutil_value.pagefile) - self.assertEqual( - sys_value['PeakPagefileUsage'], psutil_value.peak_pagefile) - - self.assertEqual(psutil_value.rss, psutil_value.wset) - self.assertEqual(psutil_value.vms, psutil_value.pagefile) - - def test_wait(self): - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, self.pid) - self.addCleanup(win32api.CloseHandle, handle) - p = psutil.Process(self.pid) - p.terminate() - psutil_value = p.wait() - sys_value = win32process.GetExitCodeProcess(handle) - self.assertEqual(psutil_value, sys_value) - - def test_cpu_affinity(self): - def from_bitmask(x): - return [i for i in range(64) if (1 << i) & x] - - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, self.pid) - self.addCleanup(win32api.CloseHandle, handle) - sys_value = from_bitmask( - win32process.GetProcessAffinityMask(handle)[0]) - psutil_value = psutil.Process(self.pid).cpu_affinity() - self.assertEqual(psutil_value, sys_value) - - def test_io_counters(self): - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, os.getpid()) - self.addCleanup(win32api.CloseHandle, handle) - sys_value = win32process.GetProcessIoCounters(handle) - psutil_value = psutil.Process().io_counters() - self.assertEqual( - psutil_value.read_count, sys_value['ReadOperationCount']) - self.assertEqual( - psutil_value.write_count, sys_value['WriteOperationCount']) - self.assertEqual( - psutil_value.read_bytes, sys_value['ReadTransferCount']) - self.assertEqual( - psutil_value.write_bytes, sys_value['WriteTransferCount']) - self.assertEqual( - psutil_value.other_count, sys_value['OtherOperationCount']) - self.assertEqual( - psutil_value.other_bytes, sys_value['OtherTransferCount']) - - def test_num_handles(self): - import ctypes - import ctypes.wintypes - PROCESS_QUERY_INFORMATION = 0x400 - handle = ctypes.windll.kernel32.OpenProcess( - PROCESS_QUERY_INFORMATION, 0, os.getpid()) - 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) - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestProcessWMI(unittest.TestCase): - """Compare Process API results with WMI.""" - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - - def test_name(self): - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - self.assertEqual(p.name(), w.Caption) - - def test_exe(self): - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - # Note: wmi reports the exe as a lower case string. - # Being Windows paths case-insensitive we ignore that. - self.assertEqual(p.exe().lower(), w.ExecutablePath.lower()) - - def test_cmdline(self): - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - self.assertEqual(' '.join(p.cmdline()), - w.CommandLine.replace('"', '')) - - def test_username(self): - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - domain, _, username = w.GetOwner() - username = "%s\\%s" % (domain, username) - self.assertEqual(p.username(), username) - - def test_memory_rss(self): - time.sleep(0.1) - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - rss = p.memory_info().rss - self.assertEqual(rss, int(w.WorkingSetSize)) - - def test_memory_vms(self): - time.sleep(0.1) - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - vms = p.memory_info().vms - # http://msdn.microsoft.com/en-us/library/aa394372(VS.85).aspx - # ...claims that PageFileUsage is represented in Kilo - # bytes but funnily enough on certain platforms bytes are - # returned instead. - wmi_usage = int(w.PageFileUsage) - if (vms != wmi_usage) and (vms != wmi_usage * 1024): - self.fail("wmi=%s, psutil=%s" % (wmi_usage, vms)) - - def test_create_time(self): - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - wmic_create = str(w.CreationDate.split('.')[0]) - psutil_create = time.strftime("%Y%m%d%H%M%S", - time.localtime(p.create_time())) - self.assertEqual(wmic_create, psutil_create) - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestDualProcessImplementation(unittest.TestCase): - """ - Certain APIs on Windows have 2 internal implementations, one - based on documented Windows APIs, another one based - NtQuerySystemInformation() which gets called as fallback in - case the first fails because of limited permission error. - Here we test that the two methods return the exact same value, - see: - https://github.com/giampaolo/psutil/issues/304 - """ - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - # --- - # same tests as above but mimicks the AccessDenied failure of - # the first (fast) method failing with AD. - - def test_name(self): - name = psutil.Process(self.pid).name() - with mock.patch("psutil._psplatform.cext.proc_exe", - side_effect=psutil.AccessDenied(os.getpid())) as fun: - self.assertEqual(psutil.Process(self.pid).name(), name) - assert fun.called - - def test_memory_info(self): - mem_1 = psutil.Process(self.pid).memory_info() - with mock.patch("psutil._psplatform.cext.proc_memory_info", - side_effect=OSError(errno.EPERM, "msg")) as fun: - mem_2 = psutil.Process(self.pid).memory_info() - self.assertEqual(len(mem_1), len(mem_2)) - for i in range(len(mem_1)): - self.assertGreaterEqual(mem_1[i], 0) - self.assertGreaterEqual(mem_2[i], 0) - self.assertAlmostEqual(mem_1[i], mem_2[i], delta=512) - assert fun.called - - def test_create_time(self): - ctime = psutil.Process(self.pid).create_time() - with mock.patch("psutil._psplatform.cext.proc_create_time", - side_effect=OSError(errno.EPERM, "msg")) as fun: - self.assertEqual(psutil.Process(self.pid).create_time(), ctime) - assert fun.called - - def test_cpu_times(self): - cpu_times_1 = psutil.Process(self.pid).cpu_times() - with mock.patch("psutil._psplatform.cext.proc_cpu_times", - side_effect=OSError(errno.EPERM, "msg")) as fun: - cpu_times_2 = psutil.Process(self.pid).cpu_times() - assert fun.called - self.assertAlmostEqual( - cpu_times_1.user, cpu_times_2.user, delta=0.01) - self.assertAlmostEqual( - cpu_times_1.system, cpu_times_2.system, delta=0.01) - - def test_io_counters(self): - io_counters_1 = psutil.Process(self.pid).io_counters() - with mock.patch("psutil._psplatform.cext.proc_io_counters", - side_effect=OSError(errno.EPERM, "msg")) as fun: - io_counters_2 = psutil.Process(self.pid).io_counters() - for i in range(len(io_counters_1)): - self.assertAlmostEqual( - io_counters_1[i], io_counters_2[i], delta=5) - assert fun.called - - def test_num_handles(self): - num_handles = psutil.Process(self.pid).num_handles() - with mock.patch("psutil._psplatform.cext.proc_num_handles", - side_effect=OSError(errno.EPERM, "msg")) as fun: - self.assertEqual(psutil.Process(self.pid).num_handles(), - num_handles) - assert fun.called - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class RemoteProcessTestCase(unittest.TestCase): - """Certain functions require calling ReadProcessMemory. - This trivially works when called on the current process. - Check that this works on other processes, especially when they - have a different bitness. - """ - - @staticmethod - def find_other_interpreter(): - # find a python interpreter that is of the opposite bitness from us - code = "import sys; sys.stdout.write(str(sys.maxsize > 2**32))" - - # XXX: a different and probably more stable approach might be to access - # the registry but accessing 64 bit paths from a 32 bit process - for filename in glob.glob(r"C:\Python*\python.exe"): - proc = subprocess.Popen(args=[filename, "-c", code], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - output, _ = proc.communicate() - if output == str(not IS_64_BIT): - return filename - - @classmethod - def setUpClass(cls): - other_python = cls.find_other_interpreter() - - if other_python is None: - raise unittest.SkipTest( - "could not find interpreter with opposite bitness") - - if IS_64_BIT: - cls.python64 = sys.executable - cls.python32 = other_python - else: - cls.python64 = other_python - cls.python32 = sys.executable - - test_args = ["-c", "import sys; sys.stdin.read()"] - - def setUp(self): - env = os.environ.copy() - env["THINK_OF_A_NUMBER"] = str(os.getpid()) - self.proc32 = get_test_subprocess([self.python32] + self.test_args, - env=env, - stdin=subprocess.PIPE) - self.proc64 = get_test_subprocess([self.python64] + self.test_args, - env=env, - stdin=subprocess.PIPE) - - def tearDown(self): - self.proc32.communicate() - self.proc64.communicate() - reap_children() - - @classmethod - def tearDownClass(cls): - reap_children() - - def test_cmdline_32(self): - p = psutil.Process(self.proc32.pid) - self.assertEqual(len(p.cmdline()), 3) - self.assertEqual(p.cmdline()[1:], self.test_args) - - def test_cmdline_64(self): - p = psutil.Process(self.proc64.pid) - self.assertEqual(len(p.cmdline()), 3) - self.assertEqual(p.cmdline()[1:], self.test_args) - - def test_cwd_32(self): - p = psutil.Process(self.proc32.pid) - self.assertEqual(p.cwd(), os.getcwd()) - - def test_cwd_64(self): - p = psutil.Process(self.proc64.pid) - self.assertEqual(p.cwd(), os.getcwd()) - - def test_environ_32(self): - p = psutil.Process(self.proc32.pid) - e = p.environ() - self.assertIn("THINK_OF_A_NUMBER", e) - self.assertEquals(e["THINK_OF_A_NUMBER"], str(os.getpid())) - - def test_environ_64(self): - p = psutil.Process(self.proc64.pid) - e = p.environ() - self.assertIn("THINK_OF_A_NUMBER", e) - self.assertEquals(e["THINK_OF_A_NUMBER"], str(os.getpid())) - - -# =================================================================== -# Windows services -# =================================================================== - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestServices(unittest.TestCase): - - def test_win_service_iter(self): - valid_statuses = set([ - "running", - "paused", - "start", - "pause", - "continue", - "stop", - "stopped", - ]) - valid_start_types = set([ - "automatic", - "manual", - "disabled", - ]) - valid_statuses = set([ - "running", - "paused", - "start_pending", - "pause_pending", - "continue_pending", - "stop_pending", - "stopped" - ]) - for serv in psutil.win_service_iter(): - data = serv.as_dict() - self.assertIsInstance(data['name'], str) - self.assertNotEqual(data['name'].strip(), "") - self.assertIsInstance(data['display_name'], str) - self.assertIsInstance(data['username'], str) - self.assertIn(data['status'], valid_statuses) - if data['pid'] is not None: - psutil.Process(data['pid']) - self.assertIsInstance(data['binpath'], str) - self.assertIsInstance(data['username'], str) - self.assertIsInstance(data['start_type'], str) - self.assertIn(data['start_type'], valid_start_types) - self.assertIn(data['status'], valid_statuses) - self.assertIsInstance(data['description'], str) - pid = serv.pid() - if pid is not None: - p = psutil.Process(pid) - self.assertTrue(p.is_running()) - # win_service_get - s = psutil.win_service_get(serv.name()) - # test __eq__ - self.assertEqual(serv, s) - - def test_win_service_get(self): - 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( - 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) - with mock.patch("psutil._psplatform.cext.winservice_query_config", - side_effect=exc): - self.assertRaises(psutil.NoSuchProcess, service.username) - - # test AccessDenied - exc = WindowsError( - psutil._psplatform.cext.ERROR_ACCESS_DENIED, "") - with mock.patch("psutil._psplatform.cext.winservice_query_status", - side_effect=exc): - self.assertRaises(psutil.AccessDenied, service.status) - with mock.patch("psutil._psplatform.cext.winservice_query_config", - side_effect=exc): - self.assertRaises(psutil.AccessDenied, service.username) - - # test __str__ and __repr__ - self.assertIn(service.name(), str(service)) - self.assertIn(service.display_name(), str(service)) - self.assertIn(service.name(), repr(service)) - self.assertIn(service.display_name(), repr(service)) - - -if __name__ == '__main__': - run_test_module_by_name(__file__) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..b0818a2a0f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,248 @@ +[tool.black] +target-version = ["py37"] +line-length = 79 +skip-string-normalization = true +preview = true +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/ +target-version = "py37" +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", + "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 + "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 +] +ignore = [ + "A", # flake8-builtins (shadowing of builtins like all, any, ...) + "ANN", # flake8-annotations + "ARG001", # unused-function-argument + "ARG002", # unused-method-argument + "B007", # Loop control variable `x` not used within loop body + "C408", # Unnecessary dict() call + "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 + "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()` + "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 + "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 + "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) + "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 + "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 + "PT028", # pytest-parameter-with-default-argument + "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 + "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 + "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` + "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements + "SLF", # flake8-self + "TD", # all TODOs, XXXs, etc. + "TRY300", # Consider moving this statement to an `else` block + "TRY301", # Abstract `raise` to an inner function + "UP032", # [*] Use f-string instead of `format` call +] + +[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 +".github/workflows/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203"] +"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"] +"doc/internal/*" = ["B904", "EM101", "EM102", "EM103", "T201", "T203", "TRY003"] +"setup.py" = [ + "T201", + "T203", +] + +[tool.ruff.lint.isort] +# https://beta.ruff.rs/docs/settings/#isort +force-single-line = true # one import per line + +[tool.coverage.report] +exclude_lines = [ + "except ImportError:", + "globals().update", + "if BSD", + "if FREEBSD", + "if LINUX", + "if LITTLE_ENDIAN:", + "if MACOS", + "if NETBSD", + "if OPENBSD", + "if SUNOS", + "if WINDOWS", + "if _WINDOWS:", + "if __name__ == .__main__.:", + "if ppid_map is None:", + "if sys.platform.startswith", + "pragma: no cover", + "raise NotImplementedError", +] +omit = [ + "setup.py", + "tests/*", +] + +[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", +] + +[tool.vulture] +exclude = [ + "docs/conf.py", + "tests/", +] +ignore_decorators = [ + "@_common.deprecated_method", + "@atexit.register", + "@pytest.fixture", +] + +[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 = [ + "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" + +# 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* *-musllinux*" +test-extras = [] +test-command = "python -c \"import psutil; print(psutil.__version__)\"" + +[[tool.cibuildwheel.overrides]] +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.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/"] +python_files = ["test_*.py"] + +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools>=43"] diff --git a/scripts/battery.py b/scripts/battery.py index abbad8785a..d73b8cb8b6 100755 --- a/scripts/battery.py +++ b/scripts/battery.py @@ -1,20 +1,18 @@ -#!/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 # found in the LICENSE file. -""" -Show battery information. +"""Show battery information. -$ python scripts/battery.py +$ python3 scripts/battery.py charge: 74% left: 2:11:31 status: discharging plugged in: no """ -from __future__ import print_function import sys import psutil @@ -23,7 +21,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(): @@ -33,14 +31,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")) + print( + "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/cpu_distribution.py b/scripts/cpu_distribution.py index a9f76b4e37..b462033f9e 100755 --- a/scripts/cpu_distribution.py +++ b/scripts/cpu_distribution.py @@ -1,13 +1,12 @@ -#!/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 # found in the LICENSE file. -""" -Shows CPU workload split across different CPUs. +"""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 @@ -39,15 +38,14 @@ kwork """ -from __future__ import print_function import collections import os +import shutil import sys import time import psutil - if not hasattr(psutil.Process, "cpu_num"): sys.exit("platform not supported") @@ -60,33 +58,43 @@ def clean_screen(): 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): - print("CPU %-6i" % i, end="") + for i in range(num_cpus): + print(f"CPU {i:<6}", end="") + if cpus_hidden: + print(" (+ hidden)", end="") + print() - for percent in cpus_percent: - print("%-10s" % percent, end="") + for _ in range(num_cpus): + print(f"{cpus_percent.pop(0):<10}", end="") print() # processes procs = collections.defaultdict(list) - for p in psutil.process_iter(attrs=['name', 'cpu_num']): - procs[p.info['cpu_num']].append(p.info['name'][:5]) + for p in psutil.process_iter(['name', 'cpu_num']): + procs[p.cpu_num()].append(p.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(f"{pname[:10]:<10}", end="") print() - if procs.values() == end_marker: + curr_line += 1 + if curr_line >= shutil.get_terminal_size()[1]: break time.sleep(1) diff --git a/scripts/disk_usage.py b/scripts/disk_usage.py index 37f4da0c96..be391e28ab 100755 --- a/scripts/disk_usage.py +++ b/scripts/disk_usage.py @@ -1,62 +1,52 @@ -#!/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 # 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. -$ 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 /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 sys import os -import psutil - +import sys -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 +import psutil +from psutil._common import bytes2human 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 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. continue usage = psutil.disk_usage(part.mountpoint) - print(templ % ( + line = templ.format( 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/fans.py b/scripts/fans.py index 7a0ccf91d5..16f0266709 100755 --- a/scripts/fans.py +++ b/scripts/fans.py @@ -1,18 +1,16 @@ -#!/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 # found in the LICENSE file. -""" -Show fans information. +"""Show fans information. $ python fans.py asus cpu_fan 3200 RPM """ -from __future__ import print_function import sys import psutil @@ -24,11 +22,11 @@ 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: - print(" %-20s %s RPM" % (entry.label or name, entry.current)) + print(f" {entry.label or name:<20} {entry.current} RPM") print() diff --git a/scripts/free.py b/scripts/free.py index 82e962ffc9..c891b6fc92 100755 --- a/scripts/free.py +++ b/scripts/free.py @@ -1,13 +1,12 @@ -#!/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 # found in the LICENSE file. -""" -A clone of 'free' cmdline utility. +"""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 @@ -19,23 +18,30 @@ 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')) - print(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), 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.format( + '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 e2a9ce5362..2c3b7de5b4 100755 --- a/scripts/ifconfig.py +++ b/scripts/ifconfig.py @@ -1,13 +1,12 @@ -#!/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 # found in the LICENSE file. -""" -A clone of 'ifconfig' on UNIX. +"""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 @@ -43,11 +42,10 @@ broadcast : ff:ff:ff:ff:ff:ff """ -from __future__ import print_function import socket import psutil - +from psutil._common import bytes2human af_map = { socket.AF_INET: 'IPv4', @@ -62,55 +60,53 @@ } -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) 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='') - 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={}MB, duplex={}, mtu={}, up={}".format( + 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={}, pkts={}, errs={}, drops={}".format( + 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={}, pkts={}, errs={}, drops={}".format( + 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) + fam = " {:<4}".format(af_map.get(addr.family, addr.family)) + print(fam, end="") + 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("") + print(f" p2p : {addr.ptp}") + print() if __name__ == '__main__': diff --git a/scripts/internal/bench_oneshot.py b/scripts/internal/bench_oneshot.py index 639e9ad769..b89fe1ce7f 100755 --- a/scripts/internal/bench_oneshot.py +++ b/scripts/internal/bench_oneshot.py @@ -1,28 +1,28 @@ -#!/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 # 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 print_function, division +import argparse +import os import sys -import timeit 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', @@ -32,30 +32,31 @@ ] if psutil.POSIX: - names.append('uids') - names.append('username') + NAMES.extend(('uids', 'username')) if psutil.LINUX: - names += [ - # 'memory_full_info', + 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', - 'memory_full_info', + 'memory_footprint', 'memory_info', 'name', 'num_ctx_switches', @@ -65,12 +66,12 @@ 'uids', ] if psutil.FREEBSD: - names.append('cpu_num') + NAMES.append('cpu_num') elif psutil.SUNOS: - names += [ + NAMES += [ 'cmdline', 'gids', - 'memory_full_info', + 'memory_footprint', 'memory_info', 'name', 'num_threads', @@ -79,8 +80,8 @@ 'terminal', 'uids', ] -elif psutil.OSX: - names += [ +elif psutil.MACOS: + NAMES += [ 'cpu_times', 'create_time', 'gids', @@ -93,7 +94,7 @@ 'uids', ] elif psutil.WINDOWS: - names += [ + NAMES += [ 'num_ctx_switches', 'num_threads', # dual implementation, called in case of AccessDenied @@ -105,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): @@ -120,32 +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 main(): - 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) +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)) - # "normal" run - elapsed1 = timeit.timeit( - "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) - print("onshot: %.3f secs" % elapsed2) +def main(): + parse_cli() + print( + 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) + attr = getattr(psutil.Process, name, None) + if attr is None or not callable(attr): + raise ValueError(f"invalid name {name!r}") + + # regular run + setup = setup_code.format(PID) + elapsed1 = timeit.timeit("call_normal(funs)", setup=setup, number=TIMES) + print(f"\nregular: {elapsed1:.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("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 old mode 100644 new mode 100755 index a25d1806e4..2844cc0c5d --- a/scripts/internal/bench_oneshot_2.py +++ b/scripts/internal/bench_oneshot_2.py @@ -1,53 +1,51 @@ -#!/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 # 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. """ import sys -import perf # requires "pip install perf" +import pyperf # requires "pip install pyperf" import psutil -from bench_oneshot import names - 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 add_cmdline_args(cmd, args): - cmd.append(args.benchmark) - - def main(): - runner = perf.Runner() + from bench_oneshot import NAMES + + runner = pyperf.Runner() args = runner.parse_args() if not args.worker: - print("%s methods involved on platform %r (psutil %s):" % ( - len(names), sys.platform, psutil.__version__)) - for name in sorted(names): + print( + f"{len(NAMES)} methods involved on platform" + f" {sys.platform!r} (psutil {psutil.__version__}):" + ) + 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/convert_readme.py b/scripts/internal/convert_readme.py new file mode 100755 index 0000000000..cb5193052d --- /dev/null +++ b/scripts/internal/convert_readme.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. + +"""Strip raw HTML and other unsupported parts from README.rst +so it renders correctly on PyPI when uploading a new release. +""" + +import argparse +import re + +quick_links = """\ +Quick links +=========== + +- `Home page `_ +- `Documentation `_ +- `Who uses psutil `_ +- `Download `_ +- `Blog `_ +- `What's new `_ +""" + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('file', type=str) + args = parser.parse_args() + + lines = [] + + with open(args.file) as f: + 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 + ) + + print(text) + + +if __name__ == '__main__': + main() diff --git a/scripts/internal/download_exes.py b/scripts/internal/download_exes.py deleted file mode 100755 index 1b00442883..0000000000 --- a/scripts/internal/download_exes.py +++ /dev/null @@ -1,188 +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. - -""" -Script which downloads exe and wheel files hosted on AppVeyor: -https://ci.appveyor.com/project/giampaolo/psutil -Readapted from the original recipe of Ibarra Corretge' -: -http://code.saghul.net/index.php/2015/09/09/ -""" - -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 - - -BASE_URL = 'https://ci.appveyor.com/api' -PY_VERSIONS = ['2.7', '3.4', '3.5', '3.6'] -TIMEOUT = 30 -COLORS = True - - -def exit(msg=""): - if msg: - print(hilite(msg, ok=False), file=sys.stderr) - 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) - 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 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) - safe_makedirs('dist') - 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 - - -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) - data = data.json() - for item in data: - file_url = job_url + '/' + item['fileName'] - 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 - - -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 main(options): - safe_rmtree('dist') - urls = get_file_urls(options) - 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 as _: - exc = _ - print("error while downloading %s: %s" % (url, exc)) - 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 exit("expected %s files, got %s" % (expected, completed)) - if exc: - return exit() - rename_27_wheels() - - -if __name__ == '__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) diff --git a/scripts/internal/download_wheels.py b/scripts/internal/download_wheels.py new file mode 100755 index 0000000000..54ba4cf099 --- /dev/null +++ b/scripts/internal/download_wheels.py @@ -0,0 +1,107 @@ +#!/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 pathlib +import shutil +import sys +import zipfile + +import requests + +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_DIR / "psutil" / "_common.py") +bytes2human = _common.bytes2human + +USER = "giampaolo" +PROJECT = "psutil" +OUTFILE = "wheels-github.zip" +TOKEN = "" +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" + res = requests.get( + url=url, headers={"Authorization": f"token {TOKEN}"}, timeout=TIMEOUT + ) + 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": f"token {TOKEN}"}, timeout=TIMEOUT + ) + 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(f"got {OUTFILE}, size {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 TOKEN + parser = argparse.ArgumentParser(description='GitHub wheels downloader') + parser.add_argument('--token') + parser.add_argument('--tokenfile') + args = parser.parse_args() + + 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: + safe_rmpath(OUTFILE) + + +if __name__ == '__main__': + main() 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() diff --git a/scripts/internal/check_broken_links.py b/scripts/internal/find_broken_links.py similarity index 79% rename from scripts/internal/check_broken_links.py rename to scripts/internal/find_broken_links.py index 7cf1e4898d..4300482f41 100755 --- a/scripts/internal/check_broken_links.py +++ b/scripts/internal/find_broken_links.py @@ -1,11 +1,10 @@ -#!/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 # 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 @@ -25,7 +24,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 @@ -39,8 +38,7 @@ Author: Himanshu Shekhar (2017) """ -from __future__ import print_function - +import argparse import concurrent.futures import functools import os @@ -50,11 +48,10 @@ 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]))+') + 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 +61,7 @@ def memoize(fun): """A memoize decorator.""" + @functools.wraps(fun) def wrapper(*args, **kwargs): key = (args, frozenset(sorted(kwargs.items()))) @@ -91,20 +89,14 @@ 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): """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): @@ -119,7 +111,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 @@ -140,7 +132,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 @@ -150,7 +142,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 @@ -160,7 +152,7 @@ def parse_c(fname): def parse_generic(fname): - with open(fname) as f: + with open(fname, errors='ignore') as f: text = f.read() return find_urls(text) @@ -171,16 +163,16 @@ 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) as f: - if f.readline().strip().startswith('#!/usr/bin/env python'): + with open(fname, errors='ignore') as f: + if f.readline().strip().startswith('#!/usr/bin/env python3'): return parse_py(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. @@ -197,26 +189,27 @@ 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 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)) + sys.stdout.write(f"\r{current} / {total}") sys.stdout.flush() 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) + print(f"warn: error while validating {url}", file=sys.stderr) traceback.print_exc() else: if not ok: @@ -227,18 +220,19 @@ 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)) - for url in urls: - all_urls.append((fname, url)) + print(f"{len(urls):4} {fname}") + all_urls.extend((fname, url) for url in urls) fails = parallel_validator(all_urls) if not fails: @@ -246,9 +240,9 @@ def main(): else: for fail in fails: fname, url = fail - print("%-30s: %s " % (fname, url)) + print(f"{fname:<30}: {url} ") print('-' * 20) - print("total: %s fails!" % len(fails)) + print(f"total: {len(fails)} fails!") sys.exit(1) diff --git a/scripts/internal/generate_manifest.py b/scripts/internal/generate_manifest.py index 3511b74926..63c0d38cf9 100755 --- a/scripts/internal/generate_manifest.py +++ b/scripts/internal/generate_manifest.py @@ -1,35 +1,42 @@ -#!/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 # found in the LICENSE file. -""" -Generate MANIFEST.in file. -""" +"""Generate MANIFEST.in file.""" import os +import shlex import subprocess - -IGNORED_EXTS = ('.png', '.jpg', '.jpeg') -IGNORED_FILES = ('.travis.yml', 'appveyor.yml') +SKIP_EXTS = ('.png', '.jpg', '.jpeg') +SKIP_FILES = () +SKIP_PREFIXES = ('.ci/', '.github/') def sh(cmd): return subprocess.check_output( - cmd, shell=True, universal_newlines=True).strip() + shlex.split(cmd), universal_newlines=True + ).strip() 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: + 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 + files.add(file) + + for file in sorted(files): print("include " + file) + print("recursive-exclude docs/_static *") + if __name__ == '__main__': main() diff --git a/scripts/internal/git_pre_commit.py b/scripts/internal/git_pre_commit.py new file mode 100755 index 0000000000..b98eab768d --- /dev/null +++ b/scripts/internal/git_pre_commit.py @@ -0,0 +1,110 @@ +#!/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. +""" + +import os +import pathlib +import shlex +import subprocess +import sys + +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_DIR / "psutil" / "_common.py") +hilite = _common.hilite + +PYTHON = sys.executable + + +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): + log(msg + " Commit aborted.", color="red") + sys.exit(1) + + +def sh(cmd): + 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) + if stderr: + log(stderr) + return stdout.rstrip() + + +def git_commit_files(): + 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")] + new_rm_mv = sh( + ["git", "diff", "--name-only", "--diff-filter=ADR", "--cached"] + ).split() + return py, c, rst, toml, new_rm_mv + + +def lint_manifest(): + out = sh([PYTHON, "scripts/internal/generate_manifest.py"]) + with open("MANIFEST.in", encoding="utf8") as f: + if out.strip() != f.read().strip(): + exit_with( + "Some files were added, deleted or renamed. " + "Run 'make generate-manifest' and commit again." + ) + + +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.") + + +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() + + +if __name__ == "__main__": + main() diff --git a/scripts/internal/install-sysdeps.sh b/scripts/internal/install-sysdeps.sh new file mode 100755 index 0000000000..89fa90f7a0 --- /dev/null +++ b/scripts/internal/install-sysdeps.sh @@ -0,0 +1,75 @@ +#!/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) + if command -v apt > /dev/null 2>&1; then + HAS_APT=true # debian / ubuntu + elif command -v yum > /dev/null 2>&1; then + 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 + fi + ;; + FreeBSD) + FREEBSD=true + ;; + NetBSD) + NETBSD=true + ;; + OpenBSD) + OPENBSD=true + ;; + SunOS) + SUNOS=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 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 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 + elif [ $HAS_APK ]; then + $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 + $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 + $SUDO pkg install developer/gcc + else + echo "Unsupported platform '$UNAME_S'. Ignoring." + fi +} + +main diff --git a/scripts/internal/install_pip.py b/scripts/internal/install_pip.py new file mode 100755 index 0000000000..34bda5775d --- /dev/null +++ b/scripts/internal/install_pip.py @@ -0,0 +1,51 @@ +#!/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 +from urllib.request import urlopen + +URL = "https://bootstrap.pypa.io/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(f"downloading {URL} into {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( + f"{sys.executable} {f.name} --user --upgrade" + " --break-system-packages" + ) + + sys.exit(code) + + +if __name__ == "__main__": + main() diff --git a/scripts/internal/print_access_denied.py b/scripts/internal/print_access_denied.py new file mode 100755 index 0000000000..1633d6198c --- /dev/null +++ b/scripts/internal/print_access_denied.py @@ -0,0 +1,91 @@ +#!/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. + +"""Helper script iterates over all processes and . +It prints how many AccessDenied exceptions are raised in total and +for what Process method. + +$ make print-access-denied +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_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 +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 +""" + +import time +from collections import defaultdict + +import psutil +from psutil._common import print_color + + +def main(): + # collect + tot_procs = 0 + tot_ads = 0 + 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(): + tot_calls += 1 + if value is signaler: + tot_ads += 1 + d[methname] += 1 + else: + d[methname] += 0 + elapsed = time.time() - start + + # print + 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.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) + print( + f"Totals: access-denied={tot_ads} ({tot_perc}%%), calls={tot_calls}," + f" processes={tot_procs}, elapsed={round(elapsed, 2)}s" + ) + + +if __name__ == '__main__': + main() diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 1c2b9e1133..0239d465e7 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -1,28 +1,31 @@ -#!/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 # 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. + """ -import os +import pathlib import re +import subprocess +import sys -from psutil import __version__ as PRJ_VERSION - +from psutil import __version__ -HERE = os.path.abspath(os.path.dirname(__file__)) -HISTORY = os.path.abspath(os.path.join(HERE, '../../HISTORY.rst')) +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__ 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_WHATSNEW = \ - 'https://github.com/giampaolo/psutil/blob/master/HISTORY.rst' +PRJ_URL_DOWNLOAD = 'https://pypi.org/project/psutil/#files' +PRJ_URL_WHATSNEW = 'https://psutil.readthedocs.io/latest/changelog.html' template = """\ Hello all, @@ -39,9 +42,8 @@ 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. Supported Python versions are cPython 3.7+ and PyPy. What's new ========== @@ -56,51 +58,81 @@ - Documentation: {prj_urldoc} - What's new: {prj_urlwhatsnew} +Hashes +====== + +{hashes} + -- -Giampaolo - http://grodola.blogspot.com +Giampaolo - https://gmpy.dev/about """ +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 = [] # eliminate the part preceding the first block - for i, line in enumerate(lines): + while lines: line = lines.pop(0) - if line.startswith('===='): + if line.startswith('^^^^'): break - lines.pop(0) + else: + raise ValueError("something wrong") - for i, line in enumerate(lines): + lines.pop(0) + while lines: line = lines.pop(0) line = line.rstrip() - if re.match("^- \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('===='): + if line.startswith('^^^^'): break block.append(line) + else: + raise ValueError("something wrong") # eliminate bottom empty lines block.pop(-1) while not block[-1]: block.pop(-1) - return "\n".join(block) + text = "\n".join(block) + text = rst_to_text(text) + return text def main(): changes = get_changes() - print(template.format( + hashes = ( + subprocess.check_output([sys.executable, PRINT_HASHES_PY, 'dist/']) + .strip() + .decode() + ) + text = template.format( prj_name=PRJ_NAME, prj_version=PRJ_VERSION, prj_urlhome=PRJ_URL_HOME, @@ -108,7 +140,9 @@ def main(): prj_urldoc=PRJ_URL_DOC, 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 new file mode 100755 index 0000000000..4e5ec8a6ab --- /dev/null +++ b/scripts/internal/print_api_speed.py @@ -0,0 +1,208 @@ +#!/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. + +"""Benchmark all API calls and print them from fastest to slowest. + +$ make print_api_speed +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 +memory_info_ex 300 0.00701 +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_foorprint 300 0.07066 +memory_maps 300 0.74281 +""" + +import argparse +import inspect +import os +import sys +from timeit import default_timer as timer + +import psutil +from psutil._common import print_color + +TIMES = 300 +PID = os.getpid() +timings = [] +templ = "{:<25} {:>10} {:>10}" + + +def print_header(what): + s = templ.format(what, "NUM CALLS", "SECONDS") + print_color(s, color=None, bold=True) + print("-" * len(s)) + + +def print_timings(): + timings.sort(key=lambda x: (x[1], -x[2]), reverse=True) + i = 0 + while timings[:]: + title, times, elapsed = timings.pop(0) + s = templ.format(title, str(times), f"{elapsed:.5f}") + if i > len(timings) - 5: + print_color(s, color="red") + else: + print(s) + + +def timecall(title, fun, *args, **kw): + print(f"{title:<50}", end="") + sys.stdout.flush() + t = timer() + for n in range(TIMES): + 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)) + + +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 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: + prio_set = False + else: + prio_set = True + + # --- system + + public_apis = [] + 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): + if name not in ignore: + public_apis.append(name) + + print_header("SYSTEM APIS") + for name in public_apis: + fun = getattr(psutil, name) + args = () + if name == 'pid_exists': + args = (PID,) + elif name == 'disk_usage': + 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())) + print_timings() + + # --- process + print() + print_header("PROCESS APIS") + p = psutil.Process(PID) + for name in sorted(p.attrs): + fun = getattr(p, name) + if callable(fun): + timecall(name, fun) + + print_timings() + + if not prio_set: + msg = "\nWARN: couldn't set highest process priority " + msg += "(requires root)" + print_color(msg, "red") + + +if __name__ == '__main__': + main() diff --git a/scripts/internal/print_dist.py b/scripts/internal/print_dist.py new file mode 100755 index 0000000000..01d5c88ab8 --- /dev/null +++ b/scripts/internal/print_dist.py @@ -0,0 +1,147 @@ +#!/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. + +"""List and pretty print tarball & wheel files in the dist/ directory.""" + +import argparse +import collections +import os +import pathlib +import sys + +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_DIR / "psutil" / "_common.py") +bytes2human = _common.bytes2human +print_color = _common.print_color + + +class Wheel: + def __init__(self, path): + self._path = path + self._name = os.path.basename(path) + + def __repr__(self): + return "<{}(name={}, plat={}, arch={}, pyver={})>".format( + self.__class__.__name__, + 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: + return 'pypy_on_linux' + else: + return 'linux' + elif 'win' in plat: + if ispypy: + return 'pypy_on_windows' + else: + return 'windows' + elif 'macosx' in plat: + if ispypy: + return 'pypy_on_macos' + else: + return 'macos' + else: + raise ValueError(f"unknown platform {self.name!r}") + + def arch(self): + if self.name.endswith(('x86_64.whl', 'amd64.whl')): + return '64-bit' + if self.name.endswith(("i686.whl", "win32.whl")): + return '32-bit' + if self.name.endswith("arm64.whl"): + return 'arm64' + if self.name.endswith("aarch64.whl"): + return 'aarch64' + return '?' + + 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) + + +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", + help='directory containing tar.gz or wheel files', + ) + args = parser.parse_args() + + groups = collections.defaultdict(list) + 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(f"invalid package {path!r}") + groups[pkg.platform()].append(pkg) + + tot_files = 0 + tot_size = 0 + templ = "{:<120} {:>7} {:>8} {:>7}" + for platf, pkgs in groups.items(): + ppn = f"{platf} ({len(pkgs)})" + 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.format( + " " + 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( + f"\n\ntotals: files={tot_files}, size={bytes2human(tot_size)}", + bold=True, + ) + + +if __name__ == '__main__': + main() diff --git a/scripts/internal/print_downloads.py b/scripts/internal/print_downloads.py new file mode 100755 index 0000000000..933aae017f --- /dev/null +++ b/scripts/internal/print_downloads.py @@ -0,0 +1,169 @@ +#!/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/. +""" + +import functools +import json +import os +import shlex +import subprocess +import sys + +import pypinfo # noqa: F401 + +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" +) +LAST_UPDATE = None +bytes_billed = 0 + + +# --- get + + +@functools.lru_cache +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, + env=env, + ) + stdout, stderr = p.communicate() + if p.returncode != 0: + raise RuntimeError(stderr) + assert not stderr, stderr + return stdout.strip() + + +@functools.lru_cache +def query(cmd): + global bytes_billed + ret = json.loads(sh(cmd)) + bytes_billed += ret['query']['bytes_billed'] + return ret + + +def top_packages(): + global LAST_UPDATE + ret = query( + 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']] + + +def ranking(): + data = top_packages() + for i, (name, downloads) in enumerate(data, start=1): + if name == PKGNAME: + return i + raise ValueError(f"can't find {PKGNAME}") + + +def downloads(): + data = top_packages() + for name, downloads in data: + if name == PKGNAME: + return downloads + 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 = "| {:<30} | {:>15} |" + + +def print_row(left, right): + if isinstance(right, int): + right = f"{right:,}" + print(templ.format(left, right)) + + +def print_header(left, right="Downloads"): + print_row(left, right) + s = templ.format("-" * 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(): + downs = downloads() + + 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': downs}, + {'what': 'Per day', 'download_count': int(downs / 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(f"bytes billed: {bytes_billed}", file=sys.stderr) diff --git a/scripts/internal/print_hashes.py b/scripts/internal/print_hashes.py new file mode 100755 index 0000000000..1161268d46 --- /dev/null +++ b/scripts/internal/print_hashes.py @@ -0,0 +1,44 @@ +#!/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 argparse +import hashlib +import os + + +def csum(file, kind): + h = hashlib.new(kind) + with open(file, "rb") as f: + h.update(f.read()) + return h.hexdigest() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "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)): + file = os.path.join(args.dir, name) + if os.path.isfile(file): + md5 = csum(file, "md5") + sha256 = csum(file, "sha256") + print(f"{os.path.basename(file)}\nmd5: {md5}\nsha256: {sha256}\n") + else: + print(f"skipping {file!r} (not a file)") + + +if __name__ == "__main__": + main() diff --git a/scripts/internal/print_sysinfo.py b/scripts/internal/print_sysinfo.py new file mode 100755 index 0000000000..cce3dc894b --- /dev/null +++ b/scripts/internal/print_sysinfo.py @@ -0,0 +1,150 @@ +#!/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 datetime +import getpass +import locale +import os +import pathlib +import platform +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 + + +ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(ROOT_DIR)) +from _bootstrap import load_module # noqa: E402 + + +def sh(cmd): + if isinstance(cmd, str): + cmd = shlex.split(cmd) + return subprocess.check_output(cmd, universal_newlines=True).strip() + + +tests_init = ROOT_DIR / "tests" / "__init__.py" + +tests_init_mod = load_module(tests_init) + + +def main(): + info = {} + + # 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') + 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] + + # pip + 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() + 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( + 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) + for k, v in info.items(): + print("{:<17} {}".format(k + ":", v)) + print("=" * 70) + sys.stdout.flush() + + +if __name__ == "__main__": + main() diff --git a/scripts/internal/print_timeline.py b/scripts/internal/print_timeline.py deleted file mode 100644 index ffcb8fe85f..0000000000 --- a/scripts/internal/print_timeline.py +++ /dev/null @@ -1,53 +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. - -""" -Prints releases' timeline in RST format. -""" - -import subprocess - - -entry = """\ -- {date}: - `{ver} `__ - - `what's new `__ - - `diff `__""" # NOQA - - -def sh(cmd): - return subprocess.check_output( - cmd, shell=True, universal_newlines=True).strip() - - -def get_tag_date(tag): - out = sh(r"git log -1 --format=%ai {}".format(tag)) - return out.split(' ')[0] - - -def main(): - releases = [] - out = sh("git tags") - 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() diff --git a/scripts/internal/purge.py b/scripts/internal/purge.py deleted file mode 100755 index d930171955..0000000000 --- a/scripts/internal/purge.py +++ /dev/null @@ -1,42 +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. - -""" -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. -""" - -import os -import shutil -import site - - -PKGNAME = "psutil" - - -def rmpath(path): - if os.path.isdir(path): - print("rmdir " + path) - shutil.rmtree(path) - else: - print("rm " + path) - os.remove(path) - - -def main(): - locations = [site.getusersitepackages()] - locations.extend(site.getsitepackages()) - for root in locations: - if os.path.isdir(root): - for name in os.listdir(root): - if PKGNAME in name: - abspath = os.path.join(root, name) - rmpath(abspath) - - -main() diff --git a/scripts/internal/purge_installation.py b/scripts/internal/purge_installation.py new file mode 100755 index 0000000000..94c5f287d6 --- /dev/null +++ b/scripts/internal/purge_installation.py @@ -0,0 +1,79 @@ +#!/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. + +"""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. +""" + +import os +import shutil +import site + +PKGNAME = "psutil" + +locations = [site.getusersitepackages()] + site.getsitepackages() + + +def rmpath(path): + if os.path.isdir(path): + print("rmdir " + path) + shutil.rmtree(path) + else: + print("rm " + path) + os.remove(path) + + +def purge(): + for root in locations: + if os.path.isdir(root): + for name in os.listdir(root): + if PKGNAME in name: + abspath = os.path.join(root, name) + 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() diff --git a/scripts/internal/rst_unused_targets.py b/scripts/internal/rst_unused_targets.py new file mode 100755 index 0000000000..db4a1798d3 --- /dev/null +++ b/scripts/internal/rst_unused_targets.py @@ -0,0 +1,59 @@ +#!/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 URL hyperlink targets that are defined but +never referenced. + +Undefined references (backtick refs pointing at a missing target) are +already caught by Sphinx during the docs build, so this script only +covers the unused-target case. +""" + +import argparse +import re +import sys + +# .. _`Foo`: https://... or .. _foo: https://... +RE_URL_TARGET = re.compile(r"^\.\. _`?([^`\n:]+)`?:\s*https?://", re.MULTILINE) + +# `Foo Bar`_ but NOT `text `_ 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() diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py deleted file mode 100755 index 5754375088..0000000000 --- a/scripts/internal/winmake.py +++ /dev/null @@ -1,518 +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. - - -"""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! -""" - -from __future__ import print_function -import errno -import fnmatch -import functools -import os -import shutil -import site -import ssl -import subprocess -import sys -import tempfile - - -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 -HERE = os.path.abspath(os.path.dirname(__file__)) -ROOT_DIR = os.path.realpath(os.path.join(HERE, "..", "..")) -DEPS = [ - "coverage", - "flake8", - "ipaddress", - "mock", - "nose", - "pdbpp", - "perf", - "pip", - "pypiwin32", - "pyreadline", - "setuptools", - "unittest2", - "wheel", - "wmi", - "requests" -] -_cmds = {} -if PY3: - basestring = str - -# =================================================================== -# utils -# =================================================================== - - -def safe_print(text, file=sys.stdout, flush=False): - """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): - return print(text, file=file) - try: - file.write(text) - 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 sh(cmd, nolog=False): - if not nolog: - safe_print("cmd: " + cmd) - p = subprocess.Popen(cmd, shell=True, env=os.environ, cwd=os.getcwd()) - p.communicate() - if p.returncode != 0: - 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): - 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 - - 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) - else: - safe_remove(pattern) - return - - for root, subdirs, subfiles in os.walk('.'): - root = os.path.normpath(root) - if root.startswith('.git/'): - continue - found = fnmatch.filter(subdirs if directory else subfiles, pattern) - for name in found: - path = os.path.join(root, name) - if directory: - safe_print("rmdir -f %s" % path) - safe_rmtree(path) - else: - safe_print("rm %s" % path) - safe_remove(path) - - -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 - - existed = os.path.isdir(path) - shutil.rmtree(path, onerror=onerror) - if existed: - safe_print("rmdir -f %s" % path) - - -def recursive_rm(*patterns): - """Recursively remove a file or matching a list of patterns.""" - for root, subdirs, subfiles in os.walk(u'.'): - root = os.path.normpath(root) - if root.startswith('.git/'): - continue - for file in subfiles: - for pattern in patterns: - if fnmatch.fnmatch(file, pattern): - safe_remove(os.path.join(root, file)) - for dir in subdirs: - for pattern in patterns: - if fnmatch.fnmatch(dir, pattern): - 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 -# =================================================================== - - -@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' / - # edit mode). - sh('%s -c "import setuptools"' % PYTHON) - sh("%s setup.py build" % PYTHON) - # 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) - - -@cmd -def wheel(): - """Create wheel file.""" - build() - sh("%s setup.py bdist_wheel" % PYTHON) - - -@cmd -def install_pip(): - """Install pip""" - try: - import pip # NOQA - except ImportError: - 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) - - -@cmd -def install(): - """Install in develop / edit mode""" - install_git_hooks() - build() - sh("%s setup.py develop" % PYTHON) - - -@cmd -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 - except ImportError: - break - else: - sh("%s -m pip uninstall -y psutil" % PYTHON) - finally: - os.chdir(here) - - for dir in site.getsitepackages(): - for name in os.listdir(dir): - if name.startswith('psutil'): - rm(os.path.join(dir, name)) - - -@cmd -def clean(): - """Deletes dev files""" - recursive_rm( - "$testfn*", - "*.bak", - "*.core", - "*.egg-info", - "*.orig", - "*.pyc", - "*.pyd", - "*.pyo", - "*.rej", - "*.so", - "*.~", - "*__pycache__", - ".coverage", - ".tox", - ) - safe_rmtree("build") - safe_rmtree(".coverage") - safe_rmtree("dist") - safe_rmtree("docs/_build") - safe_rmtree("htmlcov") - safe_rmtree("tmp") - - -@cmd -def setup_dev_env(): - """Install useful deps""" - install_pip() - install_git_hooks() - 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") - 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) - - -@cmd -def test(): - """Run tests""" - install() - test_setup() - sh("%s %s" % (PYTHON, TSCRIPT)) - - -@cmd -def coverage(): - """Run coverage tests.""" - # Note: coverage options are controlled by .coveragerc file - install() - test_setup() - sh("%s -m coverage run %s" % (PYTHON, TSCRIPT)) - sh("%s -m coverage report" % PYTHON) - sh("%s -m coverage html" % PYTHON) - sh("%s -m webbrowser -t htmlcov/index.html" % PYTHON) - - -@cmd -def test_process(): - """Run process tests""" - install() - test_setup() - sh("%s -m unittest -v psutil.tests.test_process" % PYTHON) - - -@cmd -def test_system(): - """Run system tests""" - install() - test_setup() - sh("%s -m unittest -v psutil.tests.test_system" % PYTHON) - - -@cmd -def test_platform(): - """Run windows only tests""" - install() - test_setup() - sh("%s -m unittest -v psutil.tests.test_windows" % PYTHON) - - -@cmd -def test_misc(): - """Run misc tests""" - install() - test_setup() - sh("%s -m unittest -v psutil.tests.test_misc" % PYTHON) - - -@cmd -def test_unicode(): - """Run unicode tests""" - install() - test_setup() - sh("%s -m unittest -v psutil.tests.test_unicode" % PYTHON) - - -@cmd -def test_connections(): - """Run connections tests""" - install() - test_setup() - sh("%s -m unittest -v psutil.tests.test_connections" % PYTHON) - - -@cmd -def test_contracts(): - """Run contracts tests""" - install() - test_setup() - sh("%s -m unittest -v psutil.tests.test_contracts" % PYTHON) - - -@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') - install() - test_setup() - sh("%s -m unittest -v %s" % (PYTHON, name)) - - -@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() - test_setup() - sh("%s psutil\\tests\\test_memory_leaks.py" % PYTHON) - - -@cmd -def install_git_hooks(): - """Install GIT pre-commit hook.""" - if os.path.isdir('.git'): - src = os.path.join(ROOT_DIR, ".git-pre-commit") - 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: - d.write(s.read()) - - -@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) - - -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 = '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) - - -def main(): - parse_cmdline() - try: - cmd = sys.argv[1].replace('-', '_') - except IndexError: - return help() - if cmd in _cmds: - fun = getattr(sys.modules[__name__], cmd) - fun() - else: - help() - - -if __name__ == '__main__': - main() diff --git a/scripts/iotop.py b/scripts/iotop.py index 9f76eb1c95..056fecc6e2 100755 --- a/scripts/iotop.py +++ b/scripts/iotop.py @@ -1,20 +1,19 @@ -#!/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 # 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 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. 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,32 +29,22 @@ Author: Giampaolo Rodola' """ -import atexit -import time import sys +import time + try: import curses except ImportError: sys.exit('platform not supported') import psutil - - -# --- curses stuff -def tear_down(): - win.keypad(0) - curses.nocbreak() - curses.echo() - curses.endwin() - +from psutil._common import bytes2human 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: @@ -70,36 +59,17 @@ 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): - """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. """ # first get a list of all processes and disk io counters - procs = [p for p in psutil.process_iter()] - for p in procs[:]: + procs = list(psutil.process_iter()) + for p in procs.copy(): try: p._before = p.io_counters() except psutil.Error: @@ -111,7 +81,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() @@ -143,39 +113,66 @@ 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: %s | Total DISK WRITE: %s" \ - % (bytes2human(disks_read), bytes2human(disks_write)) - print_line(disks_tot) + disks_tot = "Total DISK READ: {} | Total DISK WRITE: {}".format( + bytes2human(disks_read), + bytes2human(disks_write), + ) + printl(disks_tot) - header = templ % ("PID", "USER", "DISK READ", "DISK WRITE", "COMMAND") - print_line(header, highlight=True) + 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), bytes2human(p._write_per_sec), - p._cmdline) + 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(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/killall.py b/scripts/killall.py index f9cc920185..532e8b15ce 100755 --- a/scripts/killall.py +++ b/scripts/killall.py @@ -1,31 +1,30 @@ -#!/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 # found in the LICENSE file. -""" -Kill a process by name. -""" +"""Kill a process by name.""" import os import sys + import psutil def main(): if len(sys.argv) != 2: - sys.exit('usage: %s name' % __file__) + sys.exit(f"usage: {__file__} name") 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(f"{name}: no process found") else: sys.exit(0) diff --git a/scripts/meminfo.py b/scripts/meminfo.py index 88c3a9378f..6bee96998a 100755 --- a/scripts/meminfo.py +++ b/scripts/meminfo.py @@ -1,13 +1,12 @@ -#!/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 # found in the LICENSE file. -""" -Print system memory information. +"""Print system memory information. -$ python scripts/meminfo.py +$ python3 scripts/meminfo.py MEMORY ------ Total : 9.7G @@ -31,23 +30,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): @@ -55,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/netstat.py b/scripts/netstat.py index 490b429f23..9f26ee0790 100755 --- a/scripts/netstat.py +++ b/scripts/netstat.py @@ -1,13 +1,12 @@ -#!/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 # found in the LICENSE file. -""" -A clone of 'netstat -antp' on Linux. +"""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 @@ -20,11 +19,12 @@ """ 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 - AD = "-" AF_INET6 = getattr(socket, 'AF_INET6', object()) proto_map = { @@ -36,26 +36,34 @@ def main(): - templ = "%-5s %-30s %-30s %-13s %-6s %s" - print(templ % ( - "Proto", "Local address", "Remote address", "Status", "PID", - "Program name")) + templ = "{:<5} {:<30} {:<30} {:<13} {:<6} {}" + header = templ.format( + "Proto", + "Local address", + "Remote address", + "Status", + "PID", + "Program name", + ) + print(header) proc_names = {} - for p in psutil.process_iter(attrs=['pid', 'name']): - proc_names[p.info['pid']] = p.info['name'] + for p in psutil.process_iter(['pid', 'name']): + proc_names[p.pid] = p.name() for c in psutil.net_connections(kind='inet'): - laddr = "%s:%s" % (c.laddr) + laddr = f"{c.laddr[0]}:{c.laddr[1]}" raddr = "" if c.raddr: - raddr = "%s:%s" % (c.raddr) - print(templ % ( + raddr = f"{c.raddr[0]}:{c.raddr[1]}" + name = proc_names.get(c.pid, '?') or '' + line = templ.format( proto_map[(c.family, c.type)], laddr, raddr or AD, c.status, c.pid or AD, - proc_names.get(c.pid, '?')[:15], - )) + name[:15], + ) + print(line) if __name__ == '__main__': diff --git a/scripts/nettop.py b/scripts/nettop.py index e13903c11f..de24bff335 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 $ # @@ -6,12 +6,11 @@ # 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' -$ 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,32 +30,22 @@ pkts-recv 1214470 0 """ -import atexit -import time import sys +import time + try: import curses except ImportError: sys.exit('platform not supported') import psutil +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 +win = curses.initscr() -def print_line(line, highlight=False): +def printl(line, highlight=False): """A thin wrapper around curses's addstr().""" global lineno try: @@ -71,25 +60,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): @@ -108,59 +78,82 @@ def refresh_window(tot_before, tot_after, pnic_before, pnic_after): global lineno # totals - print_line("total bytes: sent: %-10s received: %s" % ( - bytes2human(tot_after.bytes_sent), - bytes2human(tot_after.bytes_recv)) + printl( + "total bytes: sent: {:<10} received: {}".format( + bytes2human(tot_after.bytes_sent), + bytes2human(tot_after.bytes_recv), + ) ) - print_line("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 % ( + templ = "{:<15s} {:>15} {:>15}" + # fmt: off + 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', )) - print_line(templ % ( + printl(templ.format( "bytes-recv", bytes2human(stats_after.bytes_recv), bytes2human( stats_after.bytes_recv - stats_before.bytes_recv) + '/s', )) - print_line(templ % ( + printl(templ.format( "pkts-sent", stats_after.packets_sent, stats_after.packets_sent - stats_before.packets_sent, )) - print_line(templ % ( + printl(templ.format( "pkts-recv", stats_after.packets_recv, stats_after.packets_recv - stats_before.packets_recv, )) - print_line("") + printl("") + # fmt: on win.refresh() lineno = 0 +def setup(): + curses.start_color() + curses.use_default_colors() + for i in range(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/pidof.py b/scripts/pidof.py index bcb8a2e6da..2353126621 100755 --- a/scripts/pidof.py +++ b/scripts/pidof.py @@ -1,34 +1,34 @@ -#!/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 # 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 """ -from __future__ import print_function -import psutil import sys +import psutil + def pidof(pgname): - pids = [] - for proc in psutil.process_iter(attrs=['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.name() == pgname + or (proc.cmdline() and proc.cmdline()[0] == 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 16eebb6090..54d53bebd7 100755 --- a/scripts/pmap.py +++ b/scripts/pmap.py @@ -1,19 +1,17 @@ -#!/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 # found in the LICENSE file. -""" -A clone of 'pmap' utility on Linux, 'vmmap' on OSX 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. -$ python scripts/pmap.py 32402 -pid=32402, name=hg +$ 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 @@ -30,28 +28,40 @@ ... """ +import shutil import sys import psutil +from psutil._common import bytes2human + + +def safe_print(s): + s = s[: shutil.get_terminal_size()[0]] + try: + print(s) + except UnicodeEncodeError: + print(s.encode('ascii', 'ignore').decode()) 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" - 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 - print(templ % ( + line = templ.format( 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', '', '')) + m.path, + ) + safe_print(line) + print("-" * 31) + print(templ.format("Total", bytes2human(total_rss), "", "")) + safe_print(f"PID = {p.pid}, name = {p.name()}") if __name__ == '__main__': diff --git a/scripts/procinfo.py b/scripts/procinfo.py index 54205de36b..3f3afeeede 100755 --- a/scripts/procinfo.py +++ b/scripts/procinfo.py @@ -1,14 +1,14 @@ -#!/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 # found in the LICENSE file. -""" -Print detailed information about a process. +"""Print detailed information about a process. + Author: Giampaolo Rodola' -$ python scripts/procinfo.py +$ python3 scripts/procinfo.py pid 4600 name chrome parent 4554 (bash) @@ -91,7 +91,7 @@ import sys import psutil - +from psutil._common import bytes2human ACCESS_DENIED = '' NON_VERBOSE_ITERATIONS = 4 @@ -101,48 +101,39 @@ "RLIMIT_CPU": "cputime", "RLIMIT_DATA": "datasize", "RLIMIT_FSIZE": "filesize", - "RLIMIT_LOCKS": "locks", "RLIMIT_MEMLOCK": "memlock", "RLIMIT_MSGQUEUE": "msgqueue", "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", } -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) + fmt = "\x1b[1;32m{:<13}\x1b[0m {}".format(a, b) else: - fmt = '%-11s %s' % (a, b) + fmt = "{:<11} {}".format(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: - return ", ".join(["%s=%s" % (x, getattr(nt, x)) for x in nt._fields]) + if not convert_bytes: + return ", ".join([f"{x}={getattr(nt, x)}" for x in nt._fields]) else: - return ", ".join(["%s=%s" % (x, convert_bytes(getattr(nt, x))) - for x in nt._fields]) + return ", ".join( + [f"{x}={bytes2human(getattr(nt, x))}" for x in nt._fields] + ) def run(pid, verbose=False): @@ -156,10 +147,7 @@ def run(pid, verbose=False): with proc.oneshot(): try: parent = proc.parent() - if parent: - parent = '(%s)' % parent.name() - else: - parent = '' + parent = f"({parent.name()})" if parent else "" except psutil.Error: parent = '' try: @@ -168,24 +156,26 @@ 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 # 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'])) 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]) + 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'])) if hasattr(proc, "cpu_affinity"): @@ -193,7 +183,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: @@ -214,8 +204,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", + f"class={ionice.ioclass}, value={ionice.value}", + ) print_('num-threads', pinfo['num_threads']) if psutil.POSIX: @@ -224,17 +216,17 @@ 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']: - 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 @@ -248,11 +240,13 @@ def run(pid, verbose=False): else: print_('open-files', '') - if pinfo['connections']: - template = '%-5s %-25s %-25s %s' - print_('connections', - template % ('PROTO', 'LOCAL ADDR', 'REMOTE ADDR', 'STATUS')) - for conn in pinfo['connections']: + if pinfo['net_connections']: + template = "{:<5} {:<25} {:<25} {}" + print_( + 'connections', + template.format("PROTO", "LOCAL ADDR", "REMOTE ADDR", "STATUS"), + ) + for conn in pinfo['net_connections']: if conn.type == socket.SOCK_STREAM: type = 'TCP' elif conn.type == socket.SOCK_DGRAM: @@ -264,23 +258,25 @@ def run(pid, verbose=False): rip, rport = '*', '*' else: rip, rport = conn.raddr - print_('', template % ( + line = template.format( type, - "%s:%s" % (lip, lport), - "%s:%s" % (rip, rport), - conn.status)) + f"{lip}:{lport}", + f"{rip}:{rport}", + conn.status, + ) + print_('', line) else: 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_('', "total=%s" % len(pinfo['threads'])) + print_("", template.format(*thread)) + print_('', f"total={len(pinfo['threads'])}") else: print_('threads', '') @@ -295,42 +291,48 @@ 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" if hard == psutil.RLIM_INFINITY: hard = "infinity" - print_('', template % ( - RLIMITS_MAP.get(res_name, res_name), soft, hard)) + print_( + '', + 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 % (convert_bytes(region.rss), region.path)) + print_("", template.format(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") - parser.add_argument('--verbose', '-v', action='store_true', - help="print more info") + 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" + ) args = parser.parse_args() run(args.pid, args.verbose) diff --git a/scripts/procsmem.py b/scripts/procsmem.py index a28794b9d8..6ccb08b593 100755 --- a/scripts/procsmem.py +++ b/scripts/procsmem.py @@ -1,11 +1,10 @@ -#!/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 # 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 @@ -33,16 +32,15 @@ 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 import sys import psutil - -if not (psutil.LINUX or psutil.OSX 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): @@ -53,8 +51,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(): @@ -63,15 +61,16 @@ def main(): for p in psutil.process_iter(): with p.oneshot(): try: - mem = p.memory_full_info() - info = p.as_dict(attrs=["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 + p._vms = info["memory_info"].vms if not p._uss: continue p._pss = getattr(mem, "pss", "") @@ -80,23 +79,27 @@ 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")) - 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]: - line = templ % ( + cmd = " ".join(p._info["cmdline"])[:50] if p._info["cmdline"] else "" + line = templ.format( p.pid, - p._info["username"][:7], - " ".join(p._info["cmdline"])[:30], + 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), + convert_bytes(p._vms), + cmd, ) print(line) if ad_pids: - print("warning: access denied for %s pids" % (len(ad_pids)), - file=sys.stderr) + print(f"warning: access denied for {len(ad_pids)} pids") if __name__ == '__main__': diff --git a/scripts/ps.py b/scripts/ps.py index b85790d6df..8ba464d998 100755 --- a/scripts/ps.py +++ b/scripts/ps.py @@ -1,103 +1,115 @@ -#!/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 # found in the LICENSE file. -""" -A clone of 'ps -aux' on UNIX. +"""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 +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 -import os +import shutil import time import psutil - - -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" +from psutil._common import bytes2human def main(): today_day = datetime.date.today() - templ = "%-10s %5s %4s %4s %7s %7s %-13s %-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", - "STAT", "START", "TIME", "COMMAND")) - for p in psutil.process_iter(): - try: - pinfo = p.as_dict(attrs, ad_value='') - except psutil.NoSuchProcess: - pass - else: - 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") + # fmt: off + 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.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.create_time(): + ctime = datetime.datetime.fromtimestamp(p.create_time()) + if ctime.date() == today_day: + ctime = ctime.strftime("%H:%M") else: - ctime = '' - cputime = time.strftime("%M:%S", - time.localtime(sum(pinfo['cpu_times']))) + ctime = ctime.strftime("%b%d") + else: + ctime = '' + if p.cpu_times(): + cputime = time.strftime( + "%M:%S", time.localtime(sum(p.cpu_times())) + ) + else: + cputime = '' + + user = p.username() + if not user and psutil.POSIX: try: - user = p.username() - except KeyError: - if os.name == 'posix': - if pinfo['uids']: - user = str(pinfo['uids'].real) - else: - user = '' - else: - raise + user = p.uids()[0] 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 '?')) + pass + if user and psutil.WINDOWS and '\\' in user: + user = user.split('\\')[1] + if not user: + user = '' + user = user[:9] + vms = ( + bytes2human(p.memory_info().vms) + if p.memory_info() is not None + else '' + ) + rss = ( + bytes2human(p.memory_info().rss) + if p.memory_info() is not None + else '' + ) + memp = ( + round(p.memory_percent(), 1) + if p.memory_percent() is not None + else '' + ) + nice = int(p.nice()) if p.nice() else '' + if p.cmdline(): # noqa: SIM108 + cmdline = ' '.join(p.cmdline()) + else: + cmdline = p.name() + status = p.status()[:5] if p.status() else '' + + line = templ.format( + user, + p.pid, + memp, + vms, + rss, + nice, + status, + ctime, + cputime, + cmdline, + ) + print(line[: shutil.get_terminal_size()[0]]) if __name__ == '__main__': diff --git a/scripts/pstree.py b/scripts/pstree.py index 8e4c9f9572..694c815e72 100755 --- a/scripts/pstree.py +++ b/scripts/pstree.py @@ -1,14 +1,13 @@ -#!/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 # 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. -$ python scripts/pstree.py +$ python3 scripts/pstree.py 0 ? |- 1 init | |- 289 cgmanager @@ -28,7 +27,6 @@ ... """ -from __future__ import print_function import collections import sys diff --git a/scripts/sensors.py b/scripts/sensors.py index bbf3ac9088..726b53d52c 100755 --- a/scripts/sensors.py +++ b/scripts/sensors.py @@ -1,15 +1,13 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/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 clone of 'sensors' utility on Linux printing hardware temperatures, +"""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) @@ -29,15 +27,13 @@ plugged in: yes """ -from __future__ import print_function - import psutil 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(): @@ -45,10 +41,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: @@ -65,27 +58,37 @@ 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 = " {:<20} {}°C (high={}°C, critical={}°C)".format( + 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( + " {:<20} {} RPM".format( + entry.label or name, entry.current + ) + ) # 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" % ( - "charging" if battery.percent < 100 else "fully charged")) + print( + " status: {}".format( + "charging" if battery.percent < 100 else "fully charged" + ) + ) 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/temperatures.py b/scripts/temperatures.py index 15b9156b88..f3486b8817 100755 --- a/scripts/temperatures.py +++ b/scripts/temperatures.py @@ -1,14 +1,12 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/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 clone of 'sensors' utility on Linux printing hardware temperatures. +"""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) @@ -23,7 +21,6 @@ Core 3 54.0 °C (high = 100.0 °C, critical = 100.0 °C) """ -from __future__ import print_function import sys import psutil @@ -38,9 +35,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 = " {:<20} {} °C (high = {} °C, critical = %{} °C)".format( + entry.label or name, + entry.current, + entry.high, + entry.critical, + ) + print(line) print() diff --git a/scripts/top.py b/scripts/top.py index 70dbf6c9c3..25395870c0 100755 --- a/scripts/top.py +++ b/scripts/top.py @@ -1,76 +1,67 @@ -#!/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 # found in the LICENSE file. -""" -A clone of top / htop. +"""A clone of top / htop. 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 +$ python3 scripts/top.py + 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 ... """ -import atexit import datetime -import os import sys import time + try: import curses except ImportError: sys.exit('platform not supported') import psutil - - -# --- curses stuff - -def tear_down(): - win.keypad(0) - curses.nocbreak() - curses.echo() - curses.endwin() - +from psutil._common import bytes2human 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() @@ -78,25 +69,8 @@ def print_line(line, highlight=False): else: lineno += 1 -# --- /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 +# --- /curses stuff def poll(interval): @@ -106,9 +80,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: @@ -119,16 +100,26 @@ 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) +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.""" 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 @@ -136,63 +127,85 @@ 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{:<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" % ( - dashes, empty_dashes, + line = " Mem [{}{}] {:>5}% {:>6} / {}".format( + 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), ) - print_line(line) + printl(line, color=get_color(mem.percent)) # swap usage swap = psutil.swap_memory() dashes, empty_dashes = get_dashes(swap.percent) - line = " Swap [%s%s] %5s%% %6s / %s" % ( - dashes, empty_dashes, + line = " Swap [{}{}] {:>5}% {:>6} / {}".format( + 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), ) - print_line(line) + printl(line, color=get_color(swap.percent)) # processes number and status st = [] 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) - print_line(" Processes: %s (%s)" % (num_procs, ', '.join(st))) + st.append(f"{x}={y}") + st.sort(key=lambda x: x[:3] in {'run', 'sle'}, reverse=1) + printl(f" Processes: {num_procs} ({', '.join(st)})") # load average, uptime - uptime = datetime.datetime.now() - \ - datetime.datetime.fromtimestamp(psutil.boot_time()) - av1, av2, av3 = os.getloadavg() - line = " Load average: %.2f %.2f %.2f Uptime: %s" \ - % (av1, av2, av3, str(uptime).split('.')[0]) - print_line(line) + uptime = datetime.datetime.now() - datetime.datetime.fromtimestamp( + psutil.boot_time() + ) + av1, av2, av3 = psutil.getloadavg() + line = " Load average: {:.2f} {:.2f} {:.2f} Uptime: {}".format( + av1, + av2, + av3, + str(uptime).split('.')[0], + ) + printl(line) 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 = "{:<6} {:<8} {:>4} {:>6} {:>6} {:>5} {:>5} {:>9} {:>2}" win.erase() - header = templ % ("PID", "USER", "NI", "VIRT", "RES", "CPU%", "MEM%", - "TIME+", "NAME") + header = templ.format( + "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" 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 = "{}:{}.{}".format( + ctime.seconds // 60 % 60, + str(ctime.seconds % 60).zfill(2), + str(ctime.microseconds)[:2], + ) else: ctime = '' if p.dict['memory_percent'] is not None: @@ -201,36 +214,55 @@ 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 = "" - 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 '', - ) + username = p.dict['username'][:8] if p.dict['username'] else '' + line = templ.format( + 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: - print_line(line) + printl(line) except curses.error: break win.refresh() +def setup(): + curses.start_color() + curses.use_default_colors() + for i in range(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 748d936c94..d459547234 100755 --- a/scripts/who.py +++ b/scripts/who.py @@ -1,14 +1,13 @@ -#!/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 # 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. -$ 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 """ @@ -22,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 = "{:<12} {:<10} {:<10} {:<14} {}".format( user.name, user.terminal or '-', datetime.fromtimestamp(user.started).strftime("%Y-%m-%d %H:%M"), - "(%s)" % user.host if user.host else "", - proc_name - )) + f"({user.host or ''})", + proc_name, + ) + print(line) if __name__ == '__main__': diff --git a/scripts/winservices.py b/scripts/winservices.py index 1a65adcefc..bb58287506 100755 --- a/scripts/winservices.py +++ b/scripts/winservices.py @@ -1,13 +1,12 @@ -#!/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 # found in the LICENSE file. -""" -List all Windows services installed. +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 @@ -27,29 +26,36 @@ Appinfo (Application Information) status: stopped, start: manual, username: LocalSystem, pid: None binpath: C:\Windows\system32\svchost.exe -k netsvcs - ... """ - import os import sys import psutil - if os.name != 'nt': sys.exit("platform not supported (Windows only)") 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("%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'])) - print("binpath: %s" % info['binpath']) - print("") + print(f"{info['name']!r} ({info['display_name']!r})") + s = "status: {}, start: {}, username: {}, pid: {}".format( + info['status'], + info['start_type'], + info['username'], + info['pid'], + ) + print(s) + print(f"binpath: {info['binpath']}") + print() if __name__ == '__main__': diff --git a/setup.py b/setup.py index 61056f5f1d..1496c7ac09 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 @@ -7,10 +7,15 @@ """Cross-platform lib for process and system monitoring in Python.""" import contextlib +import glob import io import os -import platform +import pathlib +import shutil +import struct +import subprocess import sys +import sysconfig import tempfile import warnings @@ -18,93 +23,245 @@ warnings.simplefilter("ignore") try: import setuptools - from setuptools import setup, Extension + from setuptools import Extension + from setuptools import setup except ImportError: + if "CIBUILDWHEEL" in os.environ: + raise setuptools = None - from distutils.core import setup, Extension - -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 BSD # NOQA -from _common import FREEBSD # NOQA -from _common import LINUX # 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 - - + from distutils.core import Extension + from distutils.core import setup + + +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(ROOT_DIR / "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) +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") + +# Test deps, installable via `pip install .[test]` or +# `make install-pydeps-test`. +TEST_DEPS = [ + "psleak", + "pytest", + "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"', +] + +# Linter deps, installable via `pip install .[lint]` or +# `make install-pydeps-lint`. +LINT_DEPS = [ + "black", + "rstwrap", + "ruff", + "sphinx-lint", + "toml-sort", +] + +# Development deps, installable via `pip install .[dev]` or +# `make install-pydeps-dev`. +DEV_DEPS = [ + *TEST_DEPS, + *LINT_DEPS, + "abi3audit", + "check-manifest", + "coverage", + "packaging", + "pylint", # not enforced + "pyperf", + "pypinfo", + "pytest-cov", + "requests", + "sphinx", + "sphinx_rtd_theme", + "twine", + "validate-pyproject[all]", + "virtualenv", + "vulture", + "wheel", + 'colorama ; os_name == "nt"', + 'pyreadline3 ; os_name == "nt"', +] + +# The pre-processor macros that are passed to the C compiler when +# building the extension. macros = [] + if POSIX: macros.append(("PSUTIL_POSIX", 1)) -if WINDOWS: - macros.append(("PSUTIL_WINDOWS", 1)) if BSD: macros.append(("PSUTIL_BSD", 1)) -sources = ['psutil/_psutil_common.c'] -if POSIX: - sources.append('psutil/_psutil_posix.c') +# 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: + macros.append(('PSUTIL_SIZEOF_PID_T', '4')) # int +else: + macros.append(('PSUTIL_SIZEOF_PID_T', '8')) # long -def get_version(): - INIT = os.path.join(HERE, 'psutil/__init__.py') - with open(INIT, 'r') as f: - for line in f: - if line.startswith('__version__'): - ret = eval(line.strip().split(' = ')[1]) - assert ret.count('.') == 2, ret - for num in ret.split('.'): - assert num.isdigit(), ret - return ret - raise ValueError("couldn't find version string") +sources = glob.glob("psutil/arch/all/*.c") +if POSIX: + sources.extend(glob.glob("psutil/arch/posix/*.c")) VERSION = get_version() macros.append(('PSUTIL_VERSION', int(VERSION.replace('.', '')))) - -def get_description(): - README = os.path.join(HERE, 'README.rst') - with open(README, 'r') as f: - return f.read() +# Py_LIMITED_API lets us create a single wheel which works with multiple +# python versions, including unreleased ones. +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 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(): + script = ROOT_DIR / "scripts" / "internal" / "convert_readme.py" + readme = ROOT_DIR / 'README.rst' + 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) + return stdout @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) +def silenced_output(): + with contextlib.redirect_stdout(io.StringIO()): + with contextlib.redirect_stderr(io.StringIO()): + yield + + +def has_python_h(): + include_dir = sysconfig.get_path("include") + return os.path.exists(os.path.join(include_dir, "Python.h")) + + +def get_sysdeps(): + if LINUX: + pyimpl = "pypy" if PYPY else "python" + if shutil.which("dpkg"): + return "sudo apt-get install gcc {}3-dev".format(pyimpl) + elif shutil.which("rpm"): + return "sudo yum install gcc {}3-devel".format(pyimpl) + elif shutil.which("pacman"): + return "sudo pacman -S gcc python" + elif shutil.which("apk"): + return "sudo apk add gcc {}3-dev musl-dev linux-headers".format( + pyimpl + ) + elif MACOS: + return "xcode-select --install" + elif FREEBSD: + if shutil.which("pkg"): + return "pkg install gcc python3" + elif shutil.which("mport"): # MidnightBSD + return "mport install gcc python3" + elif OPENBSD: + return "pkg_add -v gcc python3" + elif NETBSD: + return "pkgin install gcc python3" + elif SUNOS: + 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 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: - setattr(sys, stream_name, DummyFile()) - yield + compiler = UnixCCompiler() + # https://github.com/giampaolo/psutil/pull/1568 + if os.getenv('CC'): + compiler.set_executable('compiler_so', os.getenv('CC')) + with silenced_output(): + compiler.compile([f.name], output_dir=tempdir) + compiler.compile([f.name], output_dir=tempdir) + except CompileError: + return False + else: + return True finally: - setattr(sys, stream_name, orig) + os.remove(f.name) + shutil.rmtree(tempdir) if WINDOWS: + 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 = "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([ # be nice to mingw, see: # http://www.mingw.org/wiki/Use_more_recent_defined_functions @@ -115,185 +272,204 @@ def get_winver(): ('PSAPI_VERSION', 1), ]) + if Py_GIL_DISABLED: + macros.append(('Py_GIL_DISABLED', 1)) + ext = Extension( 'psutil._psutil_windows', - sources=sources + [ - 'psutil/_psutil_windows.c', - 'psutil/arch/windows/process_info.c', - 'psutil/arch/windows/process_handles.c', - 'psutil/arch/windows/security.c', - 'psutil/arch/windows/inet_ntop.c', - 'psutil/arch/windows/services.c', - ], + sources=( + sources + + ["psutil/_psutil_windows.c"] + + glob.glob("psutil/arch/windows/*.c") + ), define_macros=macros, libraries=[ - "psapi", "kernel32", "advapi32", "shell32", "netapi32", - "iphlpapi", "wtsapi32", "ws2_32", "PowrProf", + "advapi32", + "kernel32", + "netapi32", + "pdh", + "PowrProf", + "psapi", + "shell32", + "ws2_32", ], - # extra_compile_args=["/Z7"], - # extra_link_args=["/DEBUG"] + # extra_compile_args=["/W 4"], + # extra_link_args=["/DEBUG"], + **py_limited_api, ) -elif OSX: - macros.append(("PSUTIL_OSX", 1)) +elif MACOS: + macros.extend([("PSUTIL_OSX", 1), ("PSUTIL_MACOS", 1)]) ext = Extension( 'psutil._psutil_osx', - sources=sources + [ - 'psutil/_psutil_osx.c', - 'psutil/arch/osx/process_info.c', - ], + sources=( + 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, + ) elif FREEBSD: macros.append(("PSUTIL_FREEBSD", 1)) + ext = Extension( 'psutil._psutil_bsd', - sources=sources + [ - 'psutil/_psutil_bsd.c', - 'psutil/arch/freebsd/specific.c', - 'psutil/arch/freebsd/sys_socks.c', - 'psutil/arch/freebsd/proc_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"]) + libraries=["devstat"], + **py_limited_api, + ) elif OPENBSD: macros.append(("PSUTIL_OPENBSD", 1)) + ext = Extension( 'psutil._psutil_bsd', - sources=sources + [ - 'psutil/_psutil_bsd.c', - 'psutil/arch/openbsd/specific.c', - ], + sources=( + sources + + ["psutil/_psutil_bsd.c"] + + glob.glob("psutil/arch/bsd/*.c") + + glob.glob("psutil/arch/openbsd/*.c") + ), define_macros=macros, - libraries=["kvm"]) + libraries=["kvm"], + **py_limited_api, + ) elif NETBSD: macros.append(("PSUTIL_NETBSD", 1)) + ext = Extension( 'psutil._psutil_bsd', - sources=sources + [ - 'psutil/_psutil_bsd.c', - 'psutil/arch/netbsd/specific.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"]) + libraries=["kvm", "jemalloc"], + **py_limited_api, + ) elif LINUX: - def get_ethtool_macro(): - # see: https://github.com/giampaolo/psutil/issues/659 - from distutils.unixccompiler import UnixCCompiler - from distutils.errors import CompileError - - with tempfile.NamedTemporaryFile( - suffix='.c', delete=False, mode="wt") as f: - f.write("#include ") - - try: - compiler = UnixCCompiler() - with silenced_output('stderr'): - with silenced_output('stdout'): - compiler.compile([f.name]) - except CompileError: - return ("PSUTIL_ETHTOOL_MISSING_TYPES", 1) - else: - return None - finally: - try: - os.remove(f.name) - except OSError: - pass + # 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'], - define_macros=macros) + sources=( + sources + + ["psutil/_psutil_linux.c"] + + glob.glob("psutil/arch/linux/*.c") + ), + define_macros=macros, + **py_limited_api, + ) elif SUNOS: 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") + ), define_macros=macros, - libraries=['kstat', 'nsl', 'socket']) -# AIX + libraries=["kstat", "nsl", "socket"], + **py_limited_api, + ) + elif AIX: 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'], - define_macros=macros) -else: - sys.exit('platform %s is not supported' % sys.platform) - - -if POSIX: - posix_extension = Extension( - 'psutil._psutil_posix', + sources=( + sources + + ["psutil/_psutil_aix.c"] + + glob.glob("psutil/arch/aix/*.c") + ), + libraries=["perfstat"], define_macros=macros, - sources=sources) - if SUNOS: - posix_extension.libraries.append('socket') - if platform.release() == '5.10': - posix_extension.sources.append('psutil/arch/solaris/v10/ifaddrs.c') - posix_extension.define_macros.append(('PSUTIL_SUNOS10', 1)) - elif AIX: - posix_extension.sources.append('psutil/arch/aix/ifaddrs.c') - - extensions = [ext, posix_extension] + **py_limited_api, + ) + else: - extensions = [ext] + sys.exit("platform {} is not supported".format(sys.platform)) def main(): kwargs = dict( name='psutil', version=VERSION, - description=__doc__ .replace('\n', ' ').strip() if __doc__ else '', - long_description=get_description(), + description="Cross-platform lib for process and system monitoring.", + 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', - 'ifconfig', 'taskset', 'who', 'pidof', 'pmap', 'smem', 'pstree', - 'monitoring', 'ulimit', 'prlimit', 'smem', + 'ps', 'top', 'kill', 'free', 'lsof', 'netstat', 'df', 'uptime', + 'taskmgr', 'process', 'monitoring', 'performance', 'metrics', + 'observability', ], + # fmt: on author='Giampaolo Rodola', author_email='g.rodola@gmail.com', url='https://github.com/giampaolo/psutil', platforms='Platform Independent', - license='BSD', - packages=['psutil', 'psutil.tests'], - ext_modules=extensions, - # see: python setup.py register --list-classifiers + license='BSD-3-Clause', + packages=['psutil'], + ext_modules=[ext], + options=options, + # https://docs.pypi.org/project_metadata/ + project_urls={ + 'Homepage': 'https://github.com/giampaolo/psutil', + 'Source': 'https://github.com/giampaolo/psutil', + 'Issues': 'https://github.com/giampaolo/psutil/issues', + 'Documentation': 'https://psutil.readthedocs.io/', + 'Changelog': ( + 'https://github.com/giampaolo/psutil/blob/master/HISTORY.rst' + ), + 'Funding': 'https://github.com/sponsors/giampaolo', + }, + # https://pypi.org/classifiers/ classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', - 'Environment :: Win32 (MS Windows)', '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 NT/2000', + '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', 'Operating System :: POSIX :: BSD :: FreeBSD', 'Operating System :: POSIX :: BSD :: NetBSD', 'Operating System :: POSIX :: BSD :: OpenBSD', @@ -302,13 +478,7 @@ def main(): 'Operating System :: POSIX :: SunOS/Solaris', '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 :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python', @@ -317,6 +487,7 @@ def main(): 'Topic :: System :: Benchmark', 'Topic :: System :: Hardware', 'Topic :: System :: Monitoring', + 'Topic :: System :: Networking :: Monitoring :: Hardware Watchdog', 'Topic :: System :: Networking :: Monitoring', 'Topic :: System :: Networking', 'Topic :: System :: Operating System', @@ -325,20 +496,30 @@ def main(): ], ) if setuptools is not None: + extras_require = { + "test": TEST_DEPS, + "lint": LINT_DEPS, + "dev": DEV_DEPS, + } 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"', - }, + python_requires=">=3.7", + extras_require=extras_require, zip_safe=False, ) - setup(**kwargs) + success = False + try: + setup(**kwargs) + 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") + ) + ): + print_install_instructions() if __name__ == '__main__': 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/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..1768fe1b71 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,1924 @@ +# 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.""" + +import atexit +import contextlib +import ctypes +import enum +import errno +import functools +import importlib +import ipaddress +import os +import pathlib +import platform +import random +import re +import select +import shlex +import shutil +import signal +import socket +import stat +import subprocess +import sys +import tempfile +import textwrap +import threading +import time +import traceback +import types +import typing +import unittest +import warnings +from socket import AF_INET +from socket import AF_INET6 +from socket import SOCK_STREAM + +try: + import pytest +except ImportError: + pytest = None + +import psutil +import psutil._ntuples as ntuples +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 POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil import _enums +from psutil._common import debug +from psutil._common import supports_ipv6 + +if POSIX: + from psutil._psposix import wait_pid + + +# fmt: off +__all__ = [ + # constants + 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', + '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", + "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", + "AARCH64", "PYTEST_PARALLEL", + # subprocesses + 'pyrun', 'terminate', 'reap_children', 'spawn_subproc', 'spawn_zombie', + 'spawn_children_pair', + # threads + 'ThreadTask', + # test utils + 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', + 'retry_on_failure', 'PsutilTestCase', 'process_namespace', + '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 + 'get_winver', 'kernel_version', + # sync primitives + 'call_until', 'wait_for_pid', 'wait_for_file', + # network + 'check_net_address', 'filter_proc_net_connections', + 'get_free_port', 'bind_socket', 'bind_unix_socket', 'tcp_socketpair', + 'unix_socketpair', 'create_sockets', + # compat + 'reload_module', 'import_module_by_path', + # others + 'warn', 'copyload_shared_lib', 'is_namedtuple' +] +# fmt: on + + +# =================================================================== +# --- constants +# =================================================================== + +# --- platforms + +PYPY = '__pypy__' in sys.builtin_module_names +# whether we're running this test suite on a Continuous Integration service +GITHUB_ACTIONS = 'GITHUB_ACTIONS' in os.environ or 'CIBUILDWHEEL' in os.environ +CI_TESTING = GITHUB_ACTIONS +COVERAGE = 'COVERAGE_RUN' in os.environ +PYTEST_PARALLEL = "PYTEST_XDIST_WORKER" in os.environ # `make test-parallel` +# are we a 64 bit process? +IS_64BIT = sys.maxsize > 2**32 +# apparently they're the same +AARCH64 = platform.machine().lower() in {"aarch64", "arm64"} +RISCV64 = platform.machine() == "riscv64" + + +@functools.lru_cache +def macos_version(): + version_str = platform.mac_ver()[0] + version = tuple(map(int, version_str.split(".")[:2])) + if version == (10, 16): + # When built against an older macOS SDK, Python will report + # macOS 10.16 instead of the real version. + version_str = subprocess.check_output( + [ + sys.executable, + "-sS", + "-c", + "import platform; print(platform.mac_ver()[0])", + ], + env={"SYSTEM_VERSION_COMPAT": "0"}, + universal_newlines=True, + ) + version = tuple(map(int, version_str.split(".")[:2])) + return version + + +if MACOS: + MACOS_11PLUS = macos_version() > (10, 15) + MACOS_12PLUS = macos_version() >= (12, 0) +else: + MACOS_11PLUS = False + MACOS_12PLUS = False + + +# --- configurable defaults + +# how many times retry_on_failure() decorator will retry +NO_RETRIES = 10 +# bytes tolerance for system-wide related tests +TOLERANCE_SYS_MEM = 5 * 1024 * 1024 # 5MB +TOLERANCE_DISK_USAGE = 10 * 1024 * 1024 # 10MB +# the timeout used in functions which have to wait +GLOBAL_TIMEOUT = 5 +# be more tolerant if we're on CI in order to avoid false positives +if CI_TESTING: + NO_RETRIES *= 3 + GLOBAL_TIMEOUT *= 3 + TOLERANCE_SYS_MEM *= 4 + TOLERANCE_DISK_USAGE *= 3 + +# --- file names + +# Disambiguate TESTFN 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') +ASCII_FS = sys.getfilesystemencoding().lower() in {"ascii", "us-ascii"} + +# --- paths + +ROOT_DIR = os.environ.get("PSUTIL_ROOT") or str( + pathlib.Path(__file__).resolve().parent.parent +) + +# --- support + +HAS_HEAP_INFO = hasattr(psutil, "heap_info") +HAS_NET_CONNECTIONS_UNIX = POSIX and not SUNOS +HAS_NET_IO_COUNTERS = hasattr(psutil, "net_io_counters") +HAS_SENSORS_BATTERY = hasattr(psutil, "sensors_battery") +HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") +HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") + +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_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") + +SKIP_SYSCONS = (MACOS or AIX) and os.getuid() != 0 + +try: + HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) +except Exception: # noqa: BLE001 + 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 + atexit.register(functools.partial(print, traceback.format_exc())) + HAS_CPU_FREQ = False + + +# --- misc + + +def _get_py_exe(): + def attempt(exe): + try: + subprocess.check_call( + [exe, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + except subprocess.CalledProcessError: + return None + else: + return exe + + env = os.environ.copy() + + # On Windows, starting with python 3.7, virtual environments use a + # venv launcher startup process. This does not play well when + # counting spawned processes, or when relying on the PID of the + # spawned process to do some checks, e.g. connections check per PID. + # Let's use the base python in this case. + base = getattr(sys, "_base_executable", None) + if WINDOWS and sys.version_info >= (3, 7) and base is not None: + # We need to set __PYVENV_LAUNCHER__ to sys.executable for the + # base python executable to know about the environment. + env["__PYVENV_LAUNCHER__"] = sys.executable + return base, env + elif GITHUB_ACTIONS: + return sys.executable, env + elif MACOS: + exe = ( + attempt(sys.executable) + or attempt(os.path.realpath(sys.executable)) + or attempt( + shutil.which("python{}.{}".format(*sys.version_info[:2])) + ) + or attempt(psutil.Process().exe()) + ) + if not exe: + raise ValueError("can't find python exe real abspath") + return exe, env + else: + exe = os.path.realpath(sys.executable) + assert os.path.exists(exe), exe + return exe, env + + +PYTHON_EXE, PYTHON_EXE_ENV = _get_py_exe() +DEVNULL = open(os.devnull, 'r+') # noqa: SIM115 +atexit.register(DEVNULL.close) + +VALID_PROC_STATUSES = [ + getattr(psutil, x) for x in dir(psutil) if x.startswith('STATUS_') +] +AF_UNIX = getattr(socket, "AF_UNIX", object()) + +_subprocesses_started = set() +_pids_started = set() + + +# =================================================================== +# --- threads +# =================================================================== + + +class ThreadTask(threading.Thread): + """A thread task which does nothing expect staying alive.""" + + def __init__(self): + super().__init__() + self._running = False + self._interval = 0.001 + self._flag = threading.Event() + + def __repr__(self): + name = self.__class__.__name__ + return f"<{name} running={self._running} at {id(self):#x}>" + + def __enter__(self): + self.start() + return self + + def __exit__(self, *args, **kwargs): + self.stop() + + def start(self): + """Start thread and keep it running until an explicit + stop() request. Polls for shutdown every 'timeout' seconds. + """ + if self._running: + raise ValueError("already started") + threading.Thread.start(self) + self._flag.wait() + + def run(self): + self._running = True + self._flag.set() + while self._running: + time.sleep(self._interval) + + def stop(self): + """Stop thread execution and and waits until it is stopped.""" + if not self._running: + raise ValueError("already stopped") + self._running = False + self.join() + + +# =================================================================== +# --- subprocesses +# =================================================================== + + +def _reap_children_on_err(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + try: + return fun(*args, **kwargs) + except Exception: + reap_children() + raise + + return wrapper + + +@_reap_children_on_err +def spawn_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. + By default stdin and stdout are redirected to /dev/null. + It also attempts to make sure the process is in a reasonably + initialized state. + The process is registered for cleanup on reap_children(). + """ + kwds.setdefault("stdin", DEVNULL) + kwds.setdefault("stdout", DEVNULL) + kwds.setdefault("cwd", os.getcwd()) + kwds.setdefault("env", PYTHON_EXE_ENV) + if WINDOWS: + # Prevents the subprocess to open error dialogs. This will also + # cause stderr to be suppressed, which is suboptimal in order + # to debug broken tests. + # CREATE_NO_WINDOW = 0x8000000 + # kwds.setdefault("creationflags", CREATE_NO_WINDOW) + + # 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: + safe_rmpath(testfn) + pyline = ( + "import time;" + 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) + _subprocesses_started.add(sproc) + wait_for_file(testfn, delete=True, empty=True) + finally: + safe_rmpath(testfn) + else: + sproc = subprocess.Popen(cmd, **kwds) + _subprocesses_started.add(sproc) + wait_for_pid(sproc.pid) + return sproc + + +@_reap_children_on_err +def spawn_children_pair(): + """Create a subprocess which creates another one as in: + A (us) -> B (child) -> C (grandchild). + Return a (child, grandchild) tuple. + The 2 processes are fully initialized and will live for 60 secs + and are registered for cleanup on reap_children(). + """ + tfile = None + testfn = get_testfn(dir=os.getcwd()) + try: + s = textwrap.dedent(f"""\ + import subprocess, os, sys, time + s = "import os, time;" + 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'{PYTHON_EXE}', '-c', s]) + p.wait() + """) + # On Windows if we create a subprocess with CREATE_NO_WINDOW flag + # set (which is the default) a "conhost.exe" extra process will be + # spawned as a child. We don't want that. + if WINDOWS: + subp, tfile = pyrun(s, creationflags=0) + else: + subp, tfile = pyrun(s) + child = psutil.Process(subp.pid) + grandchild_pid = int(wait_for_file(testfn, delete=True, empty=False)) + _pids_started.add(grandchild_pid) + grandchild = psutil.Process(grandchild_pid) + return (child, grandchild) + finally: + safe_rmpath(testfn) + if tfile is not None: + safe_rmpath(tfile) + + +def spawn_zombie(): + """Create a zombie process and return a (parent, zombie) process tuple. + In order to kill the zombie parent must be terminate()d first, then + zombie must be wait()ed on. + """ + assert psutil.POSIX + unix_file = get_testfn() + src = textwrap.dedent(f"""\ + import os, sys, time, socket, contextlib + child_pid = os.fork() + if child_pid > 0: + time.sleep(3000) + else: + # this is the zombie process + with socket.socket(socket.AF_UNIX) as s: + s.connect('{unix_file}') + pid = bytes(str(os.getpid()), 'ascii') + s.sendall(pid) + """) + tfile = None + sock = bind_unix_socket(unix_file) + try: + sock.settimeout(GLOBAL_TIMEOUT) + parent, tfile = pyrun(src) + conn, _ = sock.accept() + try: + select.select([conn.fileno()], [], [], GLOBAL_TIMEOUT) + zpid = int(conn.recv(1024)) + _pids_started.add(zpid) + zombie = psutil.Process(zpid) + call_until(lambda: zombie.status() == psutil.STATUS_ZOMBIE) + return (parent, zombie) + finally: + conn.close() + finally: + sock.close() + safe_rmpath(unix_file) + if tfile is not None: + safe_rmpath(tfile) + + +@_reap_children_on_err +def pyrun(src, **kwds): + """Run python 'src' code string in a separate interpreter. + Returns a subprocess.Popen instance and the test file where the source + code was written. + """ + kwds.setdefault("stdout", None) + kwds.setdefault("stderr", None) + srcfile = get_testfn() + try: + with open(srcfile, "w") as f: + f.write(src) + subp = spawn_subproc([PYTHON_EXE, f.name], **kwds) + wait_for_pid(subp.pid) + return (subp, srcfile) + except Exception: + safe_rmpath(srcfile) + raise + + +@_reap_children_on_err +def sh(cmd, **kwds): + """Run cmd in a subprocess and return its output. + raises RuntimeError on error. + """ + # Prevents subprocess to open error dialogs in case of error. + flags = 0x8000000 if WINDOWS else 0 + kwds.setdefault("stdout", subprocess.PIPE) + kwds.setdefault("stderr", subprocess.PIPE) + kwds.setdefault("universal_newlines", True) + kwds.setdefault("creationflags", flags) + if isinstance(cmd, str): + cmd = shlex.split(cmd) + p = subprocess.Popen(cmd, **kwds) + _subprocesses_started.add(p) + stdout, stderr = p.communicate(timeout=GLOBAL_TIMEOUT) + if p.returncode != 0: + raise RuntimeError(stdout + stderr) + if stderr: + warn(stderr) + if stdout.endswith('\n'): + stdout = stdout[:-1] + return stdout + + +def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT): + """Terminate a process and wait() for it. + Process can be a PID or an instance of psutil.Process(), + subprocess.Popen() or psutil.Popen(). + If it's a subprocess.Popen() or psutil.Popen() instance also closes + its stdin / stdout / stderr fds. + PID is wait()ed even if the process is already gone (kills zombies). + Does nothing if the process does not exist. + Return process exit status. + """ + + def wait(proc, timeout): + proc.wait(timeout) + if WINDOWS and isinstance(proc, subprocess.Popen): + # Otherwise PID may still hang around. + try: + return psutil.Process(proc.pid).wait(timeout) + except psutil.NoSuchProcess: + pass + + def sendsig(proc, sig): + # XXX: otherwise the build hangs for some reason. + if MACOS and GITHUB_ACTIONS: + sig = signal.SIGKILL + # If the process received SIGSTOP, SIGCONT is necessary first, + # otherwise SIGTERM won't work. + if POSIX and sig != signal.SIGKILL: + proc.send_signal(signal.SIGCONT) + proc.send_signal(sig) + + def term_subprocess_proc(proc, timeout): + try: + sendsig(proc, sig) + except ProcessLookupError: + pass + except OSError as err: + if WINDOWS and err.winerror == 6: # "invalid handle" + pass + raise + return wait(proc, timeout) + + def term_psutil_proc(proc, timeout): + try: + sendsig(proc, sig) + except psutil.NoSuchProcess: + pass + return wait(proc, timeout) + + def term_pid(pid, timeout): + try: + proc = psutil.Process(pid) + except psutil.NoSuchProcess: + # Needed to kill zombies. + if POSIX: + return wait_pid(pid, timeout) + else: + return term_psutil_proc(proc, timeout) + + def flush_popen(proc): + if proc.stdout: + proc.stdout.close() + if proc.stderr: + proc.stderr.close() + # Flushing a BufferedWriter may raise an error. + if proc.stdin: + proc.stdin.close() + + p = proc_or_pid + try: + if isinstance(p, int): + return term_pid(p, wait_timeout) + elif isinstance(p, (psutil.Process, psutil.Popen)): + return term_psutil_proc(p, wait_timeout) + elif isinstance(p, subprocess.Popen): + return term_subprocess_proc(p, wait_timeout) + else: + raise TypeError(f"wrong type {p!r}") + finally: + if isinstance(p, (subprocess.Popen, psutil.Popen)): + flush_popen(p) + pid = p if isinstance(p, int) else p.pid + assert not psutil.pid_exists(pid), pid + + +def reap_children(recursive=False): + """Terminate and wait() any subprocess started by this test suite + and any children currently running, ensuring that no processes stick + around to hog resources. + If recursive is True it also tries to terminate and wait() + all grandchildren started by this process. + """ + # Get the children here before terminating them, as in case of + # recursive=True we don't want to lose the intermediate reference + # pointing to the grandchildren. + children = psutil.Process().children(recursive=recursive) + + # Terminate subprocess.Popen. + while _subprocesses_started: + subp = _subprocesses_started.pop() + terminate(subp) + + # Collect started pids. + while _pids_started: + pid = _pids_started.pop() + terminate(pid) + + # Terminate children. + if children: + for p in children: + terminate(p, wait_timeout=None) + _, alive = psutil.wait_procs(children, timeout=GLOBAL_TIMEOUT) + for p in alive: + warn(f"couldn't terminate process {p!r}; attempting kill()") + terminate(p, sig=signal.SIGKILL) + + +# =================================================================== +# --- OS +# =================================================================== + + +def kernel_version(): + """Return a tuple such as (2, 6, 36).""" + if not POSIX: + raise NotImplementedError("not POSIX") + s = "" + uname = os.uname()[2] + for c in uname: + if c.isdigit() or c == '.': + s += c + else: + break + if not s: + raise ValueError(f"can't parse {uname!r}") + minor = 0 + micro = 0 + nums = s.split('.') + major = int(nums[0]) + if len(nums) >= 2: + minor = int(nums[1]) + if len(nums) >= 3: + micro = int(nums[2]) + return (major, minor, micro) + + +def get_winver(): + if not WINDOWS: + raise NotImplementedError("not WINDOWS") + wv = sys.getwindowsversion() + sp = wv.service_pack_major or 0 + return (wv[0], wv[1], sp) + + +# =================================================================== +# --- sync primitives +# =================================================================== + + +class retry: + """A retry decorator.""" + + def __init__( + self, + exception=Exception, + timeout=None, + retries=None, + interval=0.001, + logfun=None, + ): + if timeout and retries: + raise ValueError("timeout and retries args are mutually exclusive") + self.exception = exception + self.timeout = timeout + self.retries = retries + self.interval = interval + self.logfun = logfun + + def __iter__(self): + if self.timeout: + stop_at = time.time() + self.timeout + while time.time() < stop_at: + yield + elif self.retries: + for _ in range(self.retries): + yield + else: + while True: + yield + + def sleep(self): + if self.interval is not None: + time.sleep(self.interval) + + def __call__(self, fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + exc = None + for _ in self: + try: + return fun(*args, **kwargs) + except self.exception as _: + exc = _ + if self.logfun is not None: + self.logfun(exc) + self.sleep() + continue + + raise exc + + # This way the user of the decorated function can change config + # parameters. + wrapper.decorator = self + return wrapper + + +@retry( + exception=psutil.NoSuchProcess, + logfun=None, + timeout=GLOBAL_TIMEOUT, + interval=0.001, +) +def wait_for_pid(pid): + """Wait for pid to show up in the process list then return. + Used in the test suite to give time the sub process to initialize. + """ + if pid not in psutil.pids(): + raise psutil.NoSuchProcess(pid) + psutil.Process(pid) + + +@retry( + exception=(FileNotFoundError, AssertionError), + logfun=None, + timeout=GLOBAL_TIMEOUT, + interval=0.001, +) +def wait_for_file(fname, delete=True, empty=False): + """Wait for a file to be written on disk with some content.""" + with open(fname, "rb") as f: + data = f.read() + if not empty: + assert data + if delete: + safe_rmpath(fname) + return data + + +@retry( + exception=(AssertionError, pytest.fail.Exception), + logfun=None, + timeout=GLOBAL_TIMEOUT, + interval=0.001, +) +def call_until(fun): + """Keep calling function until it evaluates to True.""" + ret = fun() + assert ret + return ret + + +# =================================================================== +# --- fs +# =================================================================== + + +def safe_rmpath(path): + """Convenience function for removing temporary test files or dirs.""" + + def retry_fun(fun): + # On Windows it could happen that the file or directory has + # open handles or references preventing the delete operation + # to succeed immediately, so we retry for a while. See: + # https://bugs.python.org/issue33240 + stop_at = time.time() + GLOBAL_TIMEOUT + while time.time() < stop_at: + try: + return fun() + except FileNotFoundError: + pass + except OSError as _: + err = _ + warn(f"ignoring {err}") + time.sleep(0.01) + raise err + + try: + st = os.stat(path) + if stat.S_ISDIR(st.st_mode): + fun = functools.partial(shutil.rmtree, path) + else: + fun = functools.partial(os.remove, path) + if POSIX: + fun() + else: + retry_fun(fun) + except FileNotFoundError: + pass + + +def safe_mkdir(dir): + """Convenience function for creating a directory.""" + try: + os.mkdir(dir) + except FileExistsError: + pass + + +@contextlib.contextmanager +def chdir(dirname): + """Context manager which temporarily changes the current directory.""" + curdir = os.getcwd() + try: + os.chdir(dirname) + yield + finally: + os.chdir(curdir) + + +def create_py_exe(path): + """Create a Python executable file in the given location.""" + assert not os.path.exists(path), path + atexit.register(safe_rmpath, path) + shutil.copyfile(PYTHON_EXE, path) + if POSIX: + st = os.stat(path) + os.chmod(path, st.st_mode | stat.S_IEXEC) + return path + + +def create_c_exe(path, c_code=None): + """Create a compiled C executable in the given location.""" + assert not os.path.exists(path), path + if not shutil.which("gcc"): + return pytest.skip("gcc is not installed") + if c_code is None: + c_code = textwrap.dedent(""" + #include + int main() { + pause(); + return 1; + } + """) + else: + assert isinstance(c_code, str), c_code + + atexit.register(safe_rmpath, path) + with open(get_testfn(suffix='.c'), "w") as f: + f.write(c_code) + try: + subprocess.check_call(["gcc", f.name, "-o", path]) + finally: + safe_rmpath(f.name) + return path + + +def get_testfn(suffix="", dir=None): + """Return an absolute pathname of a file or dir that did not + exist at the time this call is made. Also schedule it for safe + deletion at interpreter exit. It's technically racy but probably + not really due to the time variant. + """ + 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 + + +# =================================================================== +# --- testing +# =================================================================== + + +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. + """ + + # 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 = '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) + return fname + + 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) + self.addCleanup(terminate, child1) # executed first + return (child1, child2) + + def spawn_zombie(self): + parent, zombie = spawn_zombie() + self.addCleanup(terminate, zombie) + self.addCleanup(terminate, parent) # executed first + return (parent, zombie) + + def pyrun(self, *args, **kwds): + sproc, srcfile = pyrun(*args, **kwds) + self.addCleanup(safe_rmpath, srcfile) + self.addCleanup(terminate, sproc) # executed first + return sproc + + def _check_proc_exc(self, proc, exc): + assert isinstance(exc, psutil.Error) + assert exc.pid == proc.pid + assert exc.name == proc._name + if exc.name: + assert exc.name + if isinstance(exc, psutil.ZombieProcess): + assert exc.ppid == proc._ppid + if exc.ppid is not None: + assert exc.ppid >= 0 + str(exc) + repr(exc) + + def assert_pid_gone(self, pid): + 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()] + + 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=str(proc), name=name): + try: + ret = fun() + except psutil.ZombieProcess: + raise + except psutil.NoSuchProcess as exc: + self._check_proc_exc(proc, exc) + else: + msg = ( + f"Process.{name}() didn't raise NSP and returned" + f" {ret!r}" + ) + raise AssertionError(msg) + 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 + # time, see: + # https://github.com/giampaolo/psutil/issues/2287 + # https://github.com/giampaolo/psutil/issues/2593 + assert proc == clone + if not (OPENBSD or NETBSD or SUNOS): + assert hash(proc) == hash(clone) + # Its status always be querable. + 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(). + assert_in_pids(proc) + # Call all methods. + ns = process_namespace(proc) + for fun, name in ns.iter(ns.all, clear_cache=True): + with self.subTest(proc=str(proc), name=name): + try: + fun() + except (psutil.ZombieProcess, psutil.AccessDenied) as exc: + self._check_proc_exc(proc, exc) + if LINUX: + # https://github.com/giampaolo/psutil/pull/2288 + with pytest.raises(psutil.ZombieProcess) as cm: + proc.cmdline() + self._check_proc_exc(proc, cm.value) + with pytest.raises(psutil.ZombieProcess) as cm: + proc.exe() + self._check_proc_exc(proc, cm.value) + with pytest.raises(psutil.ZombieProcess) as cm: + proc.memory_maps() + self._check_proc_exc(proc, cm.value) + # Zombie cannot be signaled or terminated. + proc.suspend() + proc.resume() + proc.terminate() + proc.kill() + assert proc.is_running() + assert psutil.pid_exists(proc.pid) + 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( + # recursive=True)] + # assert proc.pid in descendants + + # __eq__ can't be relied upon because creation time may not be + # querable. + # 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. + # assert proc == ppid(), os.getpid() + + def check_proc_memory(self, nt): + # Check the ntuple returned by Process.memory_*() methods. + check_ntuple_type_hints(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 + + +def is_win_secure_system_proc(pid): + # see: https://github.com/giampaolo/psutil/issues/2338 + @functools.lru_cache + def get_procs(): + ret = {} + out = sh("tasklist.exe /NH /FO csv") + for line in out.splitlines()[1:]: + bits = [x.replace('"', "") for x in line.split(",")] + name, pid = bits[0], int(bits[1]) + ret[pid] = name + return ret + + try: + return get_procs()[pid] == "Secure System" + except KeyError: + return False + + +def _get_eligible_cpu(): + p = psutil.Process() + if hasattr(p, "cpu_num"): + return p.cpu_num() + elif hasattr(p, "cpu_affinity"): + return random.choice(p.cpu_affinity()) + return 0 + + +class process_namespace: + """A container that lists all Process class method names + some + reasonable parameters to be called with. Utility methods (parent(), + children(), ...) are excluded. + + >>> ns = process_namespace(psutil.Process()) + >>> for fun, name in ns.iter(ns.getters): + ... fun() + """ + + utils = [('cpu_percent', (), {}), ('memory_percent', (), {})] + + ignored = [ + ('as_dict', (), {}), + ('attrs', (), {}), + ('children', (), {'recursive': True}), + ('connections', (), {}), # deprecated + ('info', (), {}), + ('is_running', (), {}), + ('memory_full_info', (), {}), # deprecated + ('oneshot', (), {}), + ('parent', (), {}), + ('parents', (), {}), + ('pid', (), {}), + ('wait', (0,), {}), + ] + + getters = [ + ('cmdline', (), {}), + ('cpu_times', (), {}), + ('create_time', (), {}), + ('cwd', (), {}), + ('exe', (), {}), + ('memory_info', (), {}), + ('memory_info_ex', (), {}), + ('name', (), {}), + ('net_connections', (), {'kind': 'all'}), + ('nice', (), {}), + ('num_ctx_switches', (), {}), + ('num_threads', (), {}), + ('open_files', (), {}), + ('page_faults', (), {}), + ('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_PROC_IONICE: + getters += [('ionice', (), {})] + if HAS_PROC_RLIMIT: + getters += [('rlimit', (psutil.RLIMIT_NOFILE,), {})] + if HAS_PROC_CPU_AFFINITY: + getters += [('cpu_affinity', (), {})] + if HAS_PROC_CPU_NUM: + getters += [('cpu_num', (), {})] + if HAS_PROC_ENVIRON: + getters += [('environ', (), {})] + if WINDOWS: + getters += [('num_handles', (), {})] + if HAS_PROC_MEMORY_FOOTPRINT: + getters += [('memory_footprint', (), {})] + if HAS_PROC_MEMORY_MAPS: + getters += [('memory_maps', (), {'grouped': True})] + getters += [('memory_maps', (), {'grouped': False})] + + setters = [] + if POSIX: + setters += [('nice', (0,), {})] + else: + setters += [('nice', (psutil.NORMAL_PRIORITY_CLASS,), {})] + if HAS_PROC_RLIMIT: + setters += [('rlimit', (psutil.RLIMIT_NOFILE, (1024, 4096)), {})] + if HAS_PROC_IONICE: + if LINUX: + setters += [('ionice', (psutil.IOPRIO_CLASS_NONE, 0), {})] + else: + setters += [('ionice', (psutil.IOPRIO_NORMAL,), {})] + if HAS_PROC_CPU_AFFINITY: + setters += [('cpu_affinity', ([_get_eligible_cpu()],), {})] + + killers = [ + ('send_signal', (signal.SIGTERM,), {}), + ('suspend', (), {}), + ('resume', (), {}), + ('terminate', (), {}), + ('kill', (), {}), + ] + if WINDOWS: + killers += [('send_signal', (signal.CTRL_C_EVENT,), {})] + killers += [('send_signal', (signal.CTRL_BREAK_EVENT,), {})] + + all = utils + getters + setters + killers + + def __init__(self, proc): + self._proc = proc + + def iter(self, ls, clear_cache=True): + """Given a list of tuples yields a set of (fun, fun_name) tuples + in random order. + """ + ls = list(ls) + random.shuffle(ls) + for fun_name, args, kwds in ls: + if clear_cache: + self.clear_cache() + fun = getattr(self._proc, fun_name) + fun = functools.partial(fun, *args, **kwds) + yield (fun, fun_name) + + def clear_cache(self): + """Clear the cache of a Process instance.""" + self._proc._init(self._proc.pid, _ignore_nsp=True) + + @classmethod + def test_class_coverage(cls, test_class, ls): + """Given a TestCase instance and a list of tuples checks that + the class defines the required test method names. + """ + for fun_name, _, _ in ls: + meth_name = 'test_' + fun_name + if not hasattr(test_class, meth_name): + msg = ( + f"{test_class.__class__.__name__!r} class should define a" + f" {meth_name!r} method" + ) + raise AttributeError(msg) + + @classmethod + def test(cls): + 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(f"uncovered Process class names: {leftout!r}") + + +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': False}), + ('disk_io_counters', (), {'perdisk': True}), + ('disk_partitions', (), {'all': False}), + ('disk_partitions', (), {'all': True}), + ('disk_usage', (os.getcwd(),), {}), + ('getloadavg', (), {}), + ('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', (), {}), + ('swap_memory', (), {}), + ('users', (), {}), + ('virtual_memory', (), {}), + ] + + if HAS_CPU_FREQ: + getters += [('cpu_freq', (), {'percpu': False})] + getters += [('cpu_freq', (), {'percpu': True})] + if HAS_SENSORS_TEMPERATURES: + getters += [('sensors_temperatures', (), {})] + if HAS_SENSORS_FANS: + 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',), {})] + + ignored = [ + ('process_iter', (), {}), + ('wait_procs', ([psutil.Process()],), {}), + ('cpu_percent', (), {}), + ('cpu_times_percent', (), {}), + ] + + all = getters + + @staticmethod + def iter(ls): + """Given a list of tuples yields a set of (fun, fun_name) tuples + in random order. + """ + ls = list(ls) + random.shuffle(ls) + for fun_name, args, kwds in ls: + fun = getattr(psutil, fun_name) + fun = functools.partial(fun, *args, **kwds) + yield (fun, fun_name) + + test_class_coverage = process_namespace.test_class_coverage + + +def retry_on_failure(retries=NO_RETRIES): + """Decorator which runs a test function and retries N times before + giving up and failing. + """ + + 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() + + raise err + + return wrapper + + assert retries > 1, retries + return decorator + + +def skip_on_access_denied(only_if=None): + """Decorator to Ignore AccessDenied exceptions.""" + + def decorator(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + try: + return fun(*args, **kwargs) + except psutil.AccessDenied: + if only_if is not None: + if not only_if: + raise + return pytest.skip("raises AccessDenied") + + return wrapper + + return decorator + + +def skip_on_not_implemented(only_if=None): + """Decorator to Ignore NotImplementedError exceptions.""" + + def decorator(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + try: + return fun(*args, **kwargs) + except NotImplementedError: + if only_if is not None: + if not only_if: + raise + msg = ( + f"{fun.__name__!r} was skipped because it raised" + " NotImplementedError" + ) + return pytest.skip(msg) + + return wrapper + + return decorator + + +# =================================================================== +# --- network +# =================================================================== + + +# XXX: no longer used +def get_free_port(host='127.0.0.1'): + """Return an unused TCP port. Subject to race conditions.""" + with socket.socket() as sock: + sock.bind((host, 0)) + return sock.getsockname()[1] + + +def bind_socket(family=AF_INET, type=SOCK_STREAM, addr=None): + """Binds a generic socket.""" + if addr is None and family in {AF_INET, AF_INET6}: + addr = ("", 0) + sock = socket.socket(family, type) + try: + if os.name not in {'nt', 'cygwin'}: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(addr) + if type == socket.SOCK_STREAM: + sock.listen(5) + return sock + except Exception: + sock.close() + raise + + +def bind_unix_socket(name, type=socket.SOCK_STREAM): + """Bind a UNIX socket.""" + assert psutil.POSIX + assert not os.path.exists(name), name + sock = socket.socket(socket.AF_UNIX, type) + try: + sock.bind(name) + if type == socket.SOCK_STREAM: + sock.listen(5) + except Exception: + sock.close() + raise + return sock + + +def tcp_socketpair(family, addr=("", 0)): + """Build a pair of TCP sockets connected to each other. + Return a (server, client) tuple. + """ + with socket.socket(family, SOCK_STREAM) as ll: + ll.bind(addr) + ll.listen(5) + addr = ll.getsockname() + c = socket.socket(family, SOCK_STREAM) + try: + c.connect(addr) + caddr = c.getsockname() + while True: + a, addr = ll.accept() + # check that we've got the correct client + if addr == caddr: + return (a, c) + a.close() + except OSError: + c.close() + raise + + +def unix_socketpair(name): + """Build a pair of UNIX sockets connected to each other through + the same UNIX file name. + Return a (server, client) tuple. + """ + assert psutil.POSIX + server = client = None + try: + server = bind_unix_socket(name, type=socket.SOCK_STREAM) + server.setblocking(0) + client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + client.setblocking(0) + client.connect(name) + # new = server.accept() + except Exception: + if server is not None: + server.close() + if client is not None: + client.close() + raise + return (server, client) + + +@contextlib.contextmanager +def create_sockets(): + """Open as many socket families / types as possible.""" + socks = [] + fname1 = fname2 = None + try: + socks.extend(( + bind_socket(socket.AF_INET, socket.SOCK_STREAM), + bind_socket(socket.AF_INET, socket.SOCK_DGRAM), + )) + if supports_ipv6(): + 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() + s1, s2 = unix_socketpair(fname1) + s3 = bind_unix_socket(fname2, type=socket.SOCK_DGRAM) + for s in (s1, s2, s3): + socks.append(s) + yield socks + finally: + for s in socks: + s.close() + for fname in (fname1, fname2): + if fname is not None: + safe_rmpath(fname) + + +def check_net_address(addr, family): + """Check a net address validity. Supported families are IPv4, + IPv6 and MAC addresses. + """ + 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 + ipaddress.IPv4Address(addr) + elif family == socket.AF_INET6: + assert isinstance(addr, str), addr + ipaddress.IPv6Address(addr) + elif family == psutil.AF_LINK: + assert re.match(r'([a-fA-F0-9]{2}[:|\-]?){6}', addr) is not None, addr + else: + raise ValueError(f"unknown family {family!r}") + + +def check_connection_ntuple(conn): + """Check validity of a connection named tuple.""" + + 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 + 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() + with socket.socket(conn.family, conn.type) as s: + try: + s.bind((conn.laddr[0], 0)) + except OSError 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 + assert isinstance(conn.type, enum.IntEnum), 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_type_hints(conn) + check_ntuple(conn) + check_family(conn) + check_type(conn) + check_addrs(conn) + check_status(conn) + + +def filter_proc_net_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(f"skipping {conn}") + continue + new.append(conn) + 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 +# =================================================================== + + +def reload_module(module): + return importlib.reload(module) + + +def import_module_by_path(path): + from _bootstrap import load_module + + return load_module(path) + + +# =================================================================== +# --- others +# =================================================================== + + +def warn(msg): + """Raise a warning msg.""" + warnings.warn(msg, UserWarning, stacklevel=2) + + +def is_namedtuple(x): + """Check if object is an instance of named tuple.""" + t = type(x) + if tuple not in t.__mro__: + return False + f = getattr(t, '_fields', None) + if not isinstance(f, tuple): + return False + return all(isinstance(n, str) for n in f) + + +if POSIX: + + @contextlib.contextmanager + def copyload_shared_lib(suffix=""): + """Ctx manager which picks up a random shared CO lib used + by this process, copies it in another location and loads it + in memory via ctypes. Return the new absolutized path. + """ + exe = 'pypy' if PYPY else 'python' + ext = ".so" + dst = get_testfn(suffix=suffix + ext) + libs = [ + x.path + for x in psutil.Process().memory_maps() + if os.path.splitext(x.path)[1] == ext and exe in x.path.lower() + ] + src = random.choice(libs) + shutil.copyfile(src, dst) + try: + ctypes.CDLL(dst) + yield dst + finally: + safe_rmpath(dst) + +else: + + @contextlib.contextmanager + def copyload_shared_lib(suffix=""): + """Ctx manager which picks up a random shared DLL lib used + by this process, copies it in another location and loads it + in memory via ctypes. + Return the new absolutized, normcased path. + """ + from ctypes import WinError + from ctypes import wintypes + + ext = ".dll" + dst = get_testfn(suffix=suffix + ext) + libs = [ + x.path + for x in psutil.Process().memory_maps() + if x.path.lower().endswith(ext) + and 'python' in os.path.basename(x.path).lower() + and 'wow64' not in x.path.lower() + ] + if PYPY and not libs: + libs = [ + x.path + for x in psutil.Process().memory_maps() + if 'pypy' in os.path.basename(x.path).lower() + ] + src = random.choice(libs) + shutil.copyfile(src, dst) + cfile = None + try: + cfile = ctypes.WinDLL(dst) + yield dst + finally: + # Work around OverflowError: + # - https://ci.appveyor.com/project/giampaolo/psutil/build/1207/ + # job/o53330pbnri9bcw7 + # - http://bugs.python.org/issue30286 + # - http://stackoverflow.com/questions/23522055 + if cfile is not None: + FreeLibrary = ctypes.windll.kernel32.FreeLibrary + FreeLibrary.argtypes = [wintypes.HMODULE] + ret = FreeLibrary(cfile._handle) + if ret == 0: + raise WinError() + safe_rmpath(dst) + + +# =================================================================== +# --- Exit funs (first is executed last) +# =================================================================== + + +# this is executed first +@atexit.register +def cleanup_test_procs(): + reap_children(recursive=True) + + +# atexit module does not execute exit functions in case of SIGTERM, which +# gets sent to test subprocesses, which is a problem if they import this +# module. With this it will. See: +# https://gmpy.dev/blog/2016/how-to-always-execute-exit-functions-in-python +if POSIX: + signal.signal(signal.SIGTERM, lambda sig, _: sys.exit(sig)) diff --git a/tests/test_aix.py b/tests/test_aix.py new file mode 100755 index 0000000000..fcc50146f7 --- /dev/null +++ b/tests/test_aix.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola' +# Copyright (c) 2017, Arnon Yaari +# All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""AIX specific tests.""" + +import re + +import psutil +from psutil import AIX + +from . import PsutilTestCase +from . import pytest +from . import sh + + +@pytest.mark.skipif(not AIX, reason="AIX only") +class AIXSpecificTestCase(PsutilTestCase): + def test_virtual_memory(self): + out = sh('/usr/bin/svmon -O unit=KB') + re_pattern = r"memory\s*" + for field in [ + "size", + "inuse", + "free", + "pin", + "virtual", + "available", + "mmode", + ]: + re_pattern += rf"(?P<{field}>\S+)\s+" + matchobj = re.search(re_pattern, out) + + assert matchobj is not None + + KB = 1024 + total = int(matchobj.group("size")) * KB + available = int(matchobj.group("available")) * KB + used = int(matchobj.group("inuse")) * KB + free = int(matchobj.group("free")) * KB + + psutil_result = psutil.virtual_memory() + + # TOLERANCE_SYS_MEM 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 + 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') + # From the man page, "The size is given in megabytes" so we assume + # we'll always have 'MB' in the result + # TODO maybe try to use "swap -l" to check "used" too, but its units + # are not guaranteed to be "MB" so parsing may not be consistent + matchobj = re.search( + r"(?P\S+)\s+" + r"(?P\S+)\s+" + r"(?P\S+)\s+" + r"(?P\d+)MB", + out, + ) + + 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 + assert int(psutil_result.total / MB) == total_mb + + def test_cpu_stats(self): + out = sh('/usr/bin/mpstat -a') + + re_pattern = r"ALL\s*" + for field in [ + "min", + "maj", + "mpcs", + "mpcr", + "dev", + "soft", + "dec", + "ph", + "cs", + "ics", + "bound", + "rq", + "push", + "S3pull", + "S3grd", + "S0rd", + "S1rd", + "S2rd", + "S3rd", + "S4rd", + "S5rd", + "sysc", + ]: + re_pattern += rf"(?P<{field}>\S+)\s+" + matchobj = re.search(re_pattern, out) + + 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() + assert ( + abs(psutil_result.ctx_switches - int(matchobj.group("cs"))) + < CPU_STATS_TOLERANCE + ) + assert ( + abs(psutil_result.syscalls - int(matchobj.group("sysc"))) + < CPU_STATS_TOLERANCE + ) + assert ( + abs(psutil_result.interrupts - int(matchobj.group("dev"))) + < 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) + 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()) + assert ifconfig_names == psutil_names diff --git a/tests/test_bsd.py b/tests/test_bsd.py new file mode 100755 index 0000000000..e443549376 --- /dev/null +++ b/tests/test_bsd.py @@ -0,0 +1,630 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# TODO: (FreeBSD) add test for comparing connections with 'sockstat' cmd. + + +"""Tests specific to all BSD platforms.""" + +import datetime +import os +import re +import shutil +import time + +import psutil +from psutil import BSD +from psutil import FREEBSD +from psutil import NETBSD +from psutil import OPENBSD + +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 + +PAGESIZE = psutil._psplatform.cext.getpagesize() if BSD else None + + +def sysctl(cmdline): + """Expects a sysctl command with an argument and parse the result + returning only the value of interest. + """ + result = sh("sysctl " + cmdline) + if FREEBSD: + result = result[result.find(": ") + 2 :] + elif OPENBSD or NETBSD: + result = result[result.find("=") + 1 :] + try: + return int(result) + except ValueError: + return result + + +# ===================================================================== +# --- All BSD* +# ===================================================================== + + +@pytest.mark.skipif(not BSD, reason="BSD only") +class TestSystemAPIs(PsutilTestCase): + """System tests common to all BSD variants.""" + + 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 + # 10 MB tolerance + if abs(usage.free - free) > 10 * 1024 * 1024: + return pytest.fail(f"psutil={usage.free}, df={free}") + if abs(usage.used - used) > 10 * 1024 * 1024: + return pytest.fail(f"psutil={usage.used}, df={used}") + + @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 shutil.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 + + @pytest.mark.skipif( + not shutil.which("ifconfig"), reason="ifconfig cmd not available" + ) + def test_net_if_stats(self): + for name, stats in psutil.net_if_stats().items(): + try: + out = sh(f"ifconfig {name}") + except RuntimeError: + pass + else: + assert stats.isup == ('RUNNING' in out) + if "mtu" in out: + 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 + + +@pytest.mark.skipif(not BSD, reason="BSD only") +class TestVmstat(PsutilTestCase): + + @staticmethod + def vmstat(labels): + out = sh(["vmstat", "-s"], env={"LANG": "C.UTF-8"}) + for line in out.split("\n"): + line = line.strip() + num, _, what = line.partition(" ") + for label in labels: + if label == what: + return int(num) + return pytest.skip(f"can't find {labels} in vmstat output") + + # --- virtual_memory() + + def test_vmem_free(self): + vmstat_value = self.vmstat(['pages free']) * PAGESIZE + psutil_value = psutil.virtual_memory().free + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + def test_vmem_active(self): + vmstat_value = self.vmstat(['pages active']) * PAGESIZE + psutil_value = psutil.virtual_memory().active + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + def test_vmem_inactive(self): + vmstat_value = self.vmstat(['pages inactive']) * PAGESIZE + psutil_value = psutil.virtual_memory().inactive + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + def test_vmem_cached(self): + # NetBSD / OpenBSD + vmstat_value = ( + self.vmstat(['cached file pages']) + + self.vmstat(['cached executable pages']) + ) * PAGESIZE + psutil_value = psutil.virtual_memory().cached + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + def test_vmem_wired(self): + vmstat_value = ( + self.vmstat(['pages wired', 'pages wired down']) * PAGESIZE + ) + psutil_value = psutil.virtual_memory().wired + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif( + not (OPENBSD or NETBSD), reason="NETBSD / OPENBSD only" + ) + def test_vmem_shared(self): + out = sh("vmstat -t") + if "vm-sh" not in out: + return pytest.skip("can't find 'vm-sh' in vmstat output") + lines = out.splitlines() + headers = lines[1].split() + values = lines[2].split() + row = dict(zip(headers, values)) + expected = int(row["vm-sh"]) * PAGESIZE + assert ( + abs(psutil.virtual_memory().shared - expected) < TOLERANCE_SYS_MEM + ) + + # --- swap_memory() + + def test_swap_total(self): + vmstat_value = self.vmstat(['swap pages']) * PAGESIZE + psutil_value = psutil.swap_memory().total + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + def test_swap_used(self): + vmstat_value = self.vmstat(['swap pages in use']) * PAGESIZE + psutil_value = psutil.swap_memory().used + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + def test_swap_sin(self): + vmstat_value = self.vmstat(['pages swapped in']) * PAGESIZE + psutil_value = psutil.swap_memory().sin + assert abs(vmstat_value - psutil_value) < 1024 + + def test_swap_sout(self): + vmstat_value = self.vmstat(['pages swapped oud']) * PAGESIZE + psutil_value = psutil.swap_memory().sout + assert abs(vmstat_value - psutil_value) < 1024 + + # --- cpu_stats() + + def test_cpu_stats_interrupts(self): + vmstat_value = self.vmstat(['device interrupts', 'interrupts']) + psutil_value = psutil.cpu_stats().interrupts + assert abs(vmstat_value - psutil_value) <= 100 + + def test_cpu_stats_soft_interrupts(self): + vmstat_value = self.vmstat(['software interrupts']) + psutil_value = psutil.cpu_stats().soft_interrupts + assert abs(vmstat_value - psutil_value) <= 100 + + def test_cpu_stats_syscalls(self): + vmstat_value = self.vmstat(['system calls', 'syscalls']) + psutil_value = psutil.cpu_stats().syscalls + assert abs(vmstat_value - psutil_value) <= 100 + + def test_cpu_stats_ctx_switches(self): + vmstat_value = self.vmstat(['cpu context switches']) + psutil_value = psutil.cpu_stats().ctx_switches + assert abs(vmstat_value - psutil_value) <= 100 + + +# ===================================================================== +# --- FreeBSD +# ===================================================================== + + +@pytest.mark.skipif(not FREEBSD, reason="FREEBSD only") +class FreeBSDProcessTestCase(PsutilTestCase): + @classmethod + def setUpClass(cls): + cls.pid = spawn_subproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + @retry_on_failure() + def test_memory_maps(self): + out = sh(f"procstat -v {self.pid}") + maps = psutil.Process(self.pid).memory_maps(grouped=False) + lines = out.split('\n')[1:] + while lines: + line = lines.pop() + fields = line.split() + _, start, stop, _perms, res = fields[:5] + map = maps.pop() + assert f"{start}-{stop}" == map.addr + assert int(res) * PAGESIZE == map.rss + if not map.path.startswith('['): + assert fields[10] == map.path + + def test_exe(self): + 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(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(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() + gids = p.gids() + 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): + tested = [] + out = sh(f"procstat -r {self.pid}") + p = psutil.Process(self.pid) + for line in out.split('\n'): + line = line.lower().strip() + if ' voluntary context' in line: + pstat_value = int(line.split()[-1]) + psutil_value = p.num_ctx_switches().voluntary + 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 + assert pstat_value == psutil_value + tested.append(None) + if len(tested) != 2: + raise RuntimeError("couldn't find lines match in procstat out") + + @retry_on_failure() + def test_cpu_times(self): + tested = [] + out = sh(f"procstat -r {self.pid}") + p = psutil.Process(self.pid) + for line in out.split('\n'): + line = line.lower().strip() + if 'user time' in line: + pstat_value = float('0.' + line.split()[-1].split('.')[-1]) + psutil_value = p.cpu_times().user + 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 + assert pstat_value == psutil_value + tested.append(None) + if len(tested) != 2: + raise RuntimeError("couldn't find lines match in procstat out") + + +@pytest.mark.skipif(not FREEBSD, reason="FREEBSD only") +class FreeBSDSystemTestCase(PsutilTestCase): + @staticmethod + def parse_swapinfo(): + # the last line is always the total + output = sh("swapinfo -k").splitlines()[-1] + parts = re.split(r'\s+', output) + + if not parts: + raise ValueError(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]) + return total, used, free + + 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. + sensor = "dev.cpu.0.freq" + try: + sysctl_result = int(sysctl(sensor)) + except RuntimeError: + return pytest.skip("frequencies not supported by kernel") + assert 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]) + 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 + 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 + 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 + 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 + 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 + assert abs(psutil.virtual_memory().free - syst) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_buffers(self): + syst = sysctl("vfs.bufspace") + assert abs(psutil.virtual_memory().buffers - syst) < TOLERANCE_SYS_MEM + + def test_cpu_stats_ctx_switches(self): + assert ( + abs( + psutil.cpu_stats().ctx_switches + - sysctl('vm.stats.sys.v_swtch') + ) + < 1000 + ) + + def test_cpu_stats_interrupts(self): + assert ( + abs(psutil.cpu_stats().interrupts - sysctl('vm.stats.sys.v_intr')) + < 1000 + ) + + def test_cpu_stats_soft_interrupts(self): + 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. + assert ( + abs(psutil.cpu_stats().syscalls - sysctl('vm.stats.sys.v_syscall')) + < 200000 + ) + + # --- swap memory + + def test_swapmem_free(self): + _total, _used, free = self.parse_swapinfo() + assert abs(psutil.swap_memory().free - free) < TOLERANCE_SYS_MEM + + def test_swapmem_used(self): + _total, used, _free = self.parse_swapinfo() + assert abs(psutil.swap_memory().used - used) < TOLERANCE_SYS_MEM + + 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 " ' + Mirrors the same uvmexp_sysctl fields that psutil reads via + sysctl(CTL_VM, VM_UVMEXP2) in C, without requiring procfs. + """ + out = sh("vmstat -s") + for line in out.splitlines(): + line = line.strip() + if look_for in line: + return int(line.split()[0]) + raise ValueError(f"can't find {look_for!r} in vmstat -s output") + + # --- virtual mem + + @retry_on_failure() + def test_vmem_buffers(self): + # uv.filepages: file-backed pages excluding executable mappings + assert ( + abs( + psutil.virtual_memory().buffers + - self.parse_vmstat("cached file pages") * PAGESIZE + ) + < TOLERANCE_SYS_MEM + ) + + # --- swap mem + + @retry_on_failure() + def test_swapmem_total(self): + assert ( + abs( + psutil.swap_memory().total + - self.parse_vmstat("swap pages") * PAGESIZE + ) + < TOLERANCE_SYS_MEM + ) diff --git a/tests/test_connections.py b/tests/test_connections.py new file mode 100755 index 0000000000..95d4d906a8 --- /dev/null +++ b/tests/test_connections.py @@ -0,0 +1,572 @@ +#!/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.net_connections() and Process.net_connections() APIs.""" + +import os +import socket +import textwrap +from contextlib import closing +from socket import AF_INET +from socket import AF_INET6 +from socket import SOCK_DGRAM +from socket import SOCK_STREAM + +import psutil +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil._common import supports_ipv6 + +from . 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()) + + +def this_proc_net_connections(kind): + cons = psutil.Process().net_connections(kind=kind) + if kind in {"all", "unix"}: + return filter_proc_net_connections(cons) + return cons + + +@pytest.mark.xdist_group(name="serial") +class ConnectionTestCase(PsutilTestCase): + def setUp(self): + assert this_proc_net_connections(kind='all') == [] + + def tearDown(self): + # Make sure we closed all resources. + 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 + 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 exclude PIDs from the tuple. + sys_cons = [c[:-1] for c in sys_cons if c.pid == pid] + sys_cons.sort() + proc_cons.sort() + assert proc_cons == sys_cons + + +class TestBasicOperations(ConnectionTestCase): + @pytest.mark.skipif(SKIP_SYSCONS, reason="requires root") + def test_system(self): + with create_sockets(): + for conn in psutil.net_connections(kind='all'): + check_connection_ntuple(conn) + + def test_process(self): + with create_sockets(): + for conn in this_proc_net_connections(kind='all'): + check_connection_ntuple(conn) + + def test_invalid_kind(self): + with pytest.raises(ValueError): + this_proc_net_connections(kind='???') + with pytest.raises(ValueError): + psutil.net_connections(kind='???') + + +@pytest.mark.xdist_group(name="serial") +class TestUnconnectedSockets(ConnectionTestCase): + """Tests sockets which are open but not connected to anything.""" + + def get_conn_from_sock(self, sock): + cons = this_proc_net_connections(kind='all') + 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. + return smap[sock.fileno()] + else: + assert len(cons) == 1 + if cons[0].fd != -1: + assert smap[sock.fileno()].fd == sock.fileno() + return cons[0] + + def check_socket(self, sock): + """Given a socket, makes sure it matches the one obtained + via psutil. It assumes this process created one connection + only (the one supposed to be checked). + """ + conn = self.get_conn_from_sock(sock) + check_connection_ntuple(conn) + + # fd, family, type + if conn.fd != -1: + assert conn.fd == sock.fileno() + assert conn.family == sock.family + # see: http://bugs.python.org/issue30204 + assert conn.type == sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) + + # local address + laddr = sock.getsockname() + if not laddr and isinstance(laddr, bytes): + # See: http://bugs.python.org/issue30205 + laddr = laddr.decode() + if sock.family == AF_INET6: + laddr = laddr[:2] + assert conn.laddr == laddr + + # XXX Solaris can't retrieve system-wide UNIX sockets + 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 + + 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 conn.raddr == () + assert conn.status == psutil.CONN_LISTEN + + @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: + conn = self.check_socket(sock) + 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) + assert conn.raddr == () + assert conn.status == psutil.CONN_NONE + + @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: + conn = self.check_socket(sock) + assert conn.raddr == () + assert conn.status == psutil.CONN_NONE + + @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: + conn = self.check_socket(sock) + assert conn.raddr == "" + assert conn.status == psutil.CONN_NONE + + @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: + conn = self.check_socket(sock) + assert conn.raddr == "" + assert conn.status == psutil.CONN_NONE + + +@pytest.mark.xdist_group(name="serial") +class TestConnectedSocket(ConnectionTestCase): + """Test socket pairs which are actually connected to + each other. + """ + + # On SunOS, even after we close() it, the server socket stays around + # in TIME_WAIT state. + @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') == [] + server, client = tcp_socketpair(AF_INET, addr=addr) + try: + cons = this_proc_net_connections(kind='tcp4') + 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 + # commented. + # client.close() + # cons = this_proc_net_connections(kind='all') + # assert len(cons) == 1 + # assert cons[0].status == psutil.CONN_CLOSE_WAIT + finally: + server.close() + 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) + try: + 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: + # 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'] + assert len(cons) == 2 + if LINUX or FREEBSD or SUNOS or OPENBSD: + # remote path is never set + assert cons[0].raddr == "" + assert cons[1].raddr == "" + # one local address should though + assert testfn == (cons[0].laddr or cons[1].laddr) + else: + # On other systems either the laddr or raddr + # of both peers are set. + assert (cons[0].laddr or cons[1].laddr) == testfn + finally: + server.close() + client.close() + + +class TestFilters(ConnectionTestCase): + def test_filters(self): + def check(kind, families, types): + for conn in this_proc_net_connections(kind=kind): + assert conn.family in families + assert conn.type in types + if not SKIP_SYSCONS: + for conn in psutil.net_connections(kind=kind): + assert conn.family in families + assert conn.type in 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_NET_CONNECTIONS_UNIX: + check( + 'unix', + [AF_UNIX], + [SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET], + ) + + @skip_on_access_denied(only_if=MACOS) + def test_combos(self): + reap_children() + + def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): + all_kinds = ( + "all", + "inet", + "inet4", + "inet6", + "tcp", + "tcp4", + "tcp6", + "udp", + "udp4", + "udp6", + ) + check_connection_ntuple(conn) + 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: + assert cons != [] + else: + assert cons == [] + # compare against system-wide connections + # XXX Solaris can't retrieve system-wide UNIX + # sockets. + if HAS_NET_CONNECTIONS_UNIX: + self.compare_procsys_connections(proc.pid, [conn]) + + tcp_template = textwrap.dedent(""" + import socket, time + s = socket.socket({family}, socket.SOCK_STREAM) + s.bind(('{addr}', 0)) + s.listen(5) + with open('{testfn}', 'w') as f: + f.write(str(s.getsockname()[:2])) + [time.sleep(0.1) for x in range(100)] + """) + + udp_template = textwrap.dedent(""" + import socket, time + s = socket.socket({family}, socket.SOCK_DGRAM) + s.bind(('{addr}', 0)) + with open('{testfn}', 'w') as f: + f.write(str(s.getsockname()[:2])) + [time.sleep(0.1) for x in range(100)] + """) + + # must be relative on Windows + testfile = os.path.basename(self.get_testfn(dir=os.getcwd())) + tcp4_template = tcp_template.format( + family=int(AF_INET), addr="127.0.0.1", testfn=testfile + ) + udp4_template = udp_template.format( + family=int(AF_INET), addr="127.0.0.1", testfn=testfile + ) + tcp6_template = tcp_template.format( + family=int(AF_INET6), addr="::1", testfn=testfile + ) + udp6_template = udp_template.format( + family=int(AF_INET6), addr="::1", testfn=testfile + ) + + # launch various subprocess instantiating a socket of various + # families and types to enrich psutil results + tcp4_proc = self.pyrun(tcp4_template) + tcp4_addr = eval(wait_for_file(testfile, delete=True)) + udp4_proc = self.pyrun(udp4_template) + 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)) + udp6_proc = self.pyrun(udp6_template) + udp6_addr = eval(wait_for_file(testfile, delete=True)) + else: + tcp6_proc = None + udp6_proc = None + tcp6_addr = None + udp6_addr = None + + for p in psutil.Process().children(): + cons = p.net_connections() + assert len(cons) == 1 + for conn in cons: + # TCP v4 + if p.pid == tcp4_proc.pid: + check_conn( + p, + conn, + AF_INET, + SOCK_STREAM, + tcp4_addr, + (), + psutil.CONN_LISTEN, + ("all", "inet", "inet4", "tcp", "tcp4"), + ) + # UDP v4 + elif p.pid == udp4_proc.pid: + check_conn( + p, + conn, + AF_INET, + SOCK_DGRAM, + udp4_addr, + (), + psutil.CONN_NONE, + ("all", "inet", "inet4", "udp", "udp4"), + ) + # TCP v6 + elif p.pid == getattr(tcp6_proc, "pid", None): + check_conn( + p, + conn, + AF_INET6, + SOCK_STREAM, + tcp6_addr, + (), + psutil.CONN_LISTEN, + ("all", "inet", "inet6", "tcp", "tcp6"), + ) + # UDP v6 + elif p.pid == getattr(udp6_proc, "pid", None): + check_conn( + p, + conn, + AF_INET6, + SOCK_DGRAM, + udp6_addr, + (), + psutil.CONN_NONE, + ("all", "inet", "inet6", "udp", "udp6"), + ) + + def test_count(self): + with create_sockets(): + # tcp + cons = this_proc_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.type == SOCK_STREAM + # tcp4 + cons = this_proc_net_connections(kind='tcp4') + 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') + assert len(cons) == 1 + assert cons[0].family == AF_INET6 + assert cons[0].type == SOCK_STREAM + # udp + 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.type == SOCK_DGRAM + # udp4 + cons = this_proc_net_connections(kind='udp4') + 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') + assert len(cons) == 1 + assert cons[0].family == AF_INET6 + assert cons[0].type == SOCK_DGRAM + # inet + 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} + # 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} + # Skipped on BSD because 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') + assert len(cons) == 3 + for conn in cons: + assert conn.family == AF_UNIX + assert conn.type in {SOCK_STREAM, SOCK_DGRAM} + + +@pytest.mark.skipif(SKIP_SYSCONS, reason="requires root") +class TestSystemWideConnections(ConnectionTestCase): + """Tests for net_connections().""" + + def test_it(self): + def check(cons, families, types_): + for conn in cons: + assert conn.family in families + if conn.family != AF_UNIX: + assert conn.type in types_ + check_connection_ntuple(conn) + + with create_sockets(): + from psutil._common import conn_tmap + + for kind, groups in conn_tmap.items(): + # XXX: SunOS does not retrieve UNIX sockets. + if kind == 'unix' and not HAS_NET_CONNECTIONS_UNIX: + continue + families, types_ = groups + cons = psutil.net_connections(kind) + assert len(cons) == len(set(cons)) + check(cons, families, types_) + + @retry_on_failure() + def test_multi_sockets_procs(self): + # Creates multiple sub processes, each creating different + # sockets. For each process check that proc.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 + with create_sockets() as socks: + expected = len(socks) + pids = [] + times = 10 + fnames = [] + for _ in range(times): + fname = self.get_testfn() + fnames.append(fname) + src = textwrap.dedent(f"""\ + import time, os, sys + 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: + f.write("hello") + [time.sleep(0.1) for x in range(100)] + """) + sproc = self.pyrun(src) + pids.append(sproc.pid) + + # sync + for fname in fnames: + wait_for_file(fname) + + syscons = [ + x for x in psutil.net_connections(kind='all') if x.pid in pids + ] + for pid in pids: + assert len([x for x in syscons if x.pid == pid]) == expected + p = psutil.Process(pid) + assert len(p.net_connections('all')) == expected + + +class TestMisc(PsutilTestCase): + def test_net_connection_constants(self): + ints = [] + strs = [] + for name in dir(psutil): + if name.startswith('CONN_'): + num = getattr(psutil, name) + str_ = str(num) + assert str_.isupper(), str_ + assert str not in strs + assert num not in ints + ints.append(num) + strs.append(str_) + if SUNOS: + psutil.CONN_IDLE # noqa: B018 + psutil.CONN_BOUND # noqa: B018 + if WINDOWS: + psutil.CONN_DELETE_TCB # noqa: B018 diff --git a/tests/test_contracts.py b/tests/test_contracts.py new file mode 100755 index 0000000000..c6893f4ce7 --- /dev/null +++ b/tests/test_contracts.py @@ -0,0 +1,478 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Contracts tests. These tests mainly check API sanity in terms of +returned types and APIs availability. +Some of these are duplicates of tests test_system.py and test_process.py. +""" + +import platform +import socket + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil import BatteryTime +from psutil import ConnectionStatus +from psutil import NicDuplex +from psutil import ProcessStatus + +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 +# =================================================================== + +# Make sure code reflects what doc promises in terms of APIs +# availability. + + +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): + 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): + 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): + self.check_constants(("RLIMIT_MSGQUEUE",), LINUX) + if kernel_version() >= (2, 6, 12): + self.check_constants(("RLIMIT_NICE", "RLIMIT_RTPRIO"), LINUX) + if kernel_version() >= (2, 6, 25): + self.check_constants(("RLIMIT_RTTIME",), LINUX) + if kernel_version() >= (2, 6, 8): + 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): + def test_win_service_iter(self): + assert hasattr(psutil, "win_service_iter") == WINDOWS + + def test_win_service_get(self): + assert hasattr(psutil, "win_service_get") == WINDOWS + + @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 + ) + + def test_sensors_temperatures(self): + assert hasattr(psutil, "sensors_temperatures") == (LINUX or FREEBSD) + + def test_sensors_fans(self): + assert hasattr(psutil, "sensors_fans") == LINUX + + def test_battery(self): + assert hasattr(psutil, "sensors_battery") == ( + 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): + assert hasattr(psutil.Process, "environ") == ( + LINUX + or MACOS + or WINDOWS + or AIX + or SUNOS + or FREEBSD + or OPENBSD + or NETBSD + ) + + def test_uids(self): + assert hasattr(psutil.Process, "uids") == POSIX + + def test_gids(self): + assert hasattr(psutil.Process, "uids") == POSIX + + def test_terminal(self): + assert hasattr(psutil.Process, "terminal") == POSIX + + def test_ionice(self): + assert hasattr(psutil.Process, "ionice") == (LINUX or WINDOWS) + + @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) + + def test_io_counters(self): + hasit = hasattr(psutil.Process, "io_counters") + assert hasit == (not (MACOS or SUNOS)) + + def test_num_fds(self): + assert hasattr(psutil.Process, "num_fds") == POSIX + + def test_num_handles(self): + assert hasattr(psutil.Process, "num_handles") == WINDOWS + + def test_cpu_affinity(self): + assert hasattr(psutil.Process, "cpu_affinity") == ( + LINUX or WINDOWS or FREEBSD + ) + + def test_cpu_num(self): + assert hasattr(psutil.Process, "cpu_num") == ( + LINUX or FREEBSD or SUNOS + ) + + 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 +# =================================================================== + + +class TestSystemAPITypes(PsutilTestCase): + """Check the return types of system related APIs. + https://github.com/giampaolo/psutil/issues/1039. + """ + + @classmethod + def setUpClass(cls): + cls.proc = psutil.Process() + + def assert_ntuple_of_nums(self, nt, type_=float, gezero=True): + assert is_namedtuple(nt) + for n in nt: + assert isinstance(n, type_) + if gezero: + assert n >= 0 + + def test_cpu_times(self): + self.assert_ntuple_of_nums(psutil.cpu_times()) + for nt in psutil.cpu_times(percpu=True): + self.assert_ntuple_of_nums(nt) + + def test_cpu_percent(self): + 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): + assert isinstance(psutil.cpu_count(), int) + + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_cpu_freq(self): + if psutil.cpu_freq() is 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): + # 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) + + def test_disk_partitions(self): + # Duplicate of test_system.py. Keep it anyway. + for disk in psutil.disk_partitions(): + assert isinstance(disk.device, str) + assert isinstance(disk.mountpoint, str) + assert isinstance(disk.fstype, str) + assert isinstance(disk.opts, str) + + @pytest.mark.skipif(SKIP_SYSCONS, reason="requires root") + def test_net_connections(self): + with create_sockets(): + ret = psutil.net_connections('all') + 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(): + assert isinstance(ifname, str) + for addr in addrs: + assert isinstance(addr.family, enum.IntEnum) + 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))) + + def test_net_if_stats(self): + # Duplicate of test_system.py. Keep it anyway. + for ifname, info in psutil.net_if_stats().items(): + assert isinstance(ifname, str) + assert isinstance(info.isup, bool) + assert isinstance(info.duplex, enum.IntEnum) + assert isinstance(info.speed, int) + assert isinstance(info.mtu, int) + + @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) + + @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(): + assert isinstance(name, str) + for unit in units: + assert isinstance(unit.label, str) + assert isinstance(unit.current, (float, int, type(None))) + + @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(): + assert isinstance(name, str) + for unit in units: + 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. + assert isinstance(psutil.boot_time(), float) + + def test_users(self): + # Duplicate of test_system.py. Keep it anyway. + for user in psutil.users(): + 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))) + if isinstance(user.pid, int): + assert user.pid > 0 diff --git a/tests/test_heap.py b/tests/test_heap.py new file mode 100755 index 0000000000..b623697764 --- /dev/null +++ b/tests/test_heap.py @@ -0,0 +1,330 @@ +#!/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 + +# 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 + + +# ===================================================================== +# --- 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 on MACOS") + @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_linux.py b/tests/test_linux.py new file mode 100755 index 0000000000..6ff50fe27a --- /dev/null +++ b/tests/test_linux.py @@ -0,0 +1,2435 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Linux specific tests.""" + +import collections +import contextlib +import errno +import io +import os +import platform +import re +import shutil +import socket +import struct +import textwrap +import time +import warnings +from unittest import mock + +import psutil +from psutil import LINUX + +from . import AARCH64 +from . import GITHUB_ACTIONS +from . import GLOBAL_TIMEOUT +from . import HAS_BATTERY +from . import HAS_CPU_FREQ +from . import HAS_PROC_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 + from psutil._pslinux import RootFsDeviceFinder + from psutil._pslinux import calculate_avail_vmem + from psutil._pslinux import open_binary + + +SIOCGIFADDR = 0x8915 +SIOCGIFHWADDR = 0x8927 +SIOCGIFNETMASK = 0x891B +SIOCGIFBRDADDR = 0x8919 +if LINUX: + SECTOR_SIZE = 512 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class LinuxTestCase(PsutilTestCase): + pass + + +# ===================================================================== +# --- utils +# ===================================================================== + + +def get_ipv4_address(ifname): + import fcntl + + 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 + ] + ) + + +def get_ipv4_netmask(ifname): + import fcntl + + 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) + )[20:24] + ) + + +def get_ipv4_broadcast(ifname): + import fcntl + + 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) + )[20:24] + ) + + +def get_ipv6_addresses(ifname): + with open("/proc/net/if_inet6") as f: + all_fields = [] + for line in f: + fields = line.split() + if fields[-1] == ifname: + all_fields.append(fields) + + if len(all_fields) == 0: + raise ValueError(f"could not find interface {ifname!r}") + + for i in range(len(all_fields)): + unformatted = all_fields[i][0] + 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) + return all_fields + + +def get_mac_address(ifname): + import fcntl + + 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) + ) + return "".join([f"{char:02x}:" for char in info[18:24]])[:-1] + + +def free_swap(): + """Parse 'free' cmd and return swap memory's s total, used and free + values. + """ + out = sh(["free", "-b"], env={"LANG": "C.UTF-8"}) + lines = out.split('\n') + for line in lines: + if line.startswith('Swap'): + _, total, used, free = line.split() + nt = collections.namedtuple('free', 'total used free') + return nt(int(total), int(used), int(free)) + raise ValueError(f"can't find 'Swap' in 'free' output:\n{out}") + + +def free_physmem(): + """Parse 'free' cmd and return physical memory's total, used + and free values. + """ + # Note: free can have 2 different formats, invalidating 'shared' + # and 'cached' memory which may have different positions so we + # do not return them. + # https://github.com/giampaolo/psutil/issues/538#issuecomment-57059946 + out = sh(["free", "-b"], env={"LANG": "C.UTF-8"}) + lines = out.split('\n') + for line in lines: + if line.startswith('Mem'): + total, used, free, shared = (int(x) for x in line.split()[1:5]) + nt = collections.namedtuple( + 'free', 'total used free shared output' + ) + return nt(total, used, free, shared, out) + raise ValueError(f"can't find 'Mem' in 'free' output:\n{out}") + + +def vmstat(stat): + out = sh(["vmstat", "-s"], env={"LANG": "C.UTF-8"}) + for line in out.split("\n"): + line = line.strip() + if stat in line: + return int(line.split(' ')[0]) + raise ValueError(f"can't find {stat!r} in 'vmstat' output") + + +def get_free_version_info(): + out = sh(["free", "-V"]).strip() + if 'UNKNOWN' in out: + return pytest.skip("can't determine free version") + return tuple(map(int, re.findall(r'\d+', out.split()[-1]))) + + +@contextlib.contextmanager +def mock_open_content(pairs): + """Mock open() builtin and forces it to return a certain content + for a given path. `pairs` is a {"path": "content", ...} dict. + """ + + def open_mock(name, *args, **kwargs): + if name in pairs: + content = pairs[name] + if isinstance(content, str): + return io.StringIO(content) + else: + return io.BytesIO(content) + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", create=True, side_effect=open_mock) as m: + yield m + + +@contextlib.contextmanager +def mock_open_exception(for_path, exc): + """Mock open() builtin and raises `exc` if the path being opened + matches `for_path`. + """ + + def open_mock(name, *args, **kwargs): + if name == for_path: + raise exc + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", create=True, side_effect=open_mock) as m: + yield m + + +# ===================================================================== +# --- system virtual memory +# ===================================================================== + + +class TestVirtualMemoryAgainstFree(LinuxTestCase): + def test_total(self): + cli_value = free_physmem().total + psutil_value = psutil.virtual_memory().total + assert 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/05d751c4f07 + # Newer versions of procps (>=4.0.1) are using yet another way to + # compute used memory. + # 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 + psutil_value = psutil.virtual_memory().used + 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 + assert abs(cli_value - psutil_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_shared(self): + free = free_physmem() + free_value = free.shared + if free_value == 0: + return pytest.skip("free does not support 'shared' column") + psutil_value = psutil.virtual_memory().shared + assert ( + abs(free_value - psutil_value) < TOLERANCE_SYS_MEM + ), f"{free_value} {psutil_value} \n{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]: + 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 + + +class TestVirtualMemoryAgainstVmstat(LinuxTestCase): + def test_total(self): + vmstat_value = vmstat('total memory') * 1024 + psutil_value = psutil.virtual_memory().total + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_used(self): + # Older versions of procps used slab memory to calculate used memory. + # This got changed in: + # https://gitlab.com/procps-ng/procps/-/commit/05d751c4f07 + # Newer versions of procps (>=4.0.1) are using yet another way to + # compute used memory. + # 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 + psutil_value = psutil.virtual_memory().used + 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 + 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 + 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 + 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 + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + +class TestVirtualMemoryAgainstMeminfo(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 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 + # raise a warning. + content = textwrap.dedent("""\ + Active(anon): 6145416 kB + Active(file): 2950064 kB + Inactive(anon): 574764 kB + Inactive(file): 1567648 kB + MemAvailable: -1 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + SReclaimable: 346648 kB + """).encode() + with mock_open_content({'/proc/meminfo': content}) as m: + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always") + ret = psutil.virtual_memory() + assert m.called + assert len(ws) == 1 + w = ws[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): + # Make sure that our calculation of avail mem for old kernels + # is off by max 15%. + mems = {} + with open_binary('/proc/meminfo') as f: + for line in f: + fields = line.split() + mems[fields[0]] = int(fields[1]) * 1024 + + a = calculate_avail_vmem(mems) + if b'MemAvailable:' in mems: + b = mems[b'MemAvailable:'] + diff_percent = abs(a - b) / a * 100 + assert diff_percent < 15 + + def test_avail_old_comes_from_kernel(self): + # Make sure "MemAvailable:" coluimn is used instead of relying + # on our internal algorithm to calculate avail mem. + content = textwrap.dedent("""\ + Active: 9444728 kB + Active(anon): 6145416 kB + Active(file): 2950064 kB + Buffers: 287952 kB + Cached: 4818144 kB + Inactive(file): 1578132 kB + Inactive(anon): 574764 kB + Inactive(file): 1567648 kB + MemAvailable: 6574984 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + Shmem: 577588 kB + SReclaimable: 346648 kB + """).encode() + with mock_open_content({'/proc/meminfo': content}) as m: + with warnings.catch_warnings(record=True) as ws: + ret = psutil.virtual_memory() + assert m.called + assert ret.available == 6574984 * 1024 + w = ws[0] + assert "inactive memory stats couldn't be determined" in str( + w.message + ) + + def test_avail_old_missing_fields(self): + # Remove Active(file), Inactive(file) and SReclaimable + # from /proc/meminfo and make sure the fallback is used + # (free + cached), + content = textwrap.dedent("""\ + Active: 9444728 kB + Active(anon): 6145416 kB + Buffers: 287952 kB + Cached: 4818144 kB + Inactive(file): 1578132 kB + Inactive(anon): 574764 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + Shmem: 577588 kB + """).encode() + with mock_open_content({"/proc/meminfo": content}) as m: + with warnings.catch_warnings(record=True) as ws: + ret = psutil.virtual_memory() + assert m.called + assert ret.available == 2057400 * 1024 + 4818144 * 1024 + w = ws[0] + assert "inactive memory stats couldn't be determined" in str( + w.message + ) + + def test_avail_old_missing_zoneinfo(self): + # Remove /proc/zoneinfo file. Make sure fallback is used + # (free + cached). + content = textwrap.dedent("""\ + Active: 9444728 kB + Active(anon): 6145416 kB + Active(file): 2950064 kB + Buffers: 287952 kB + Cached: 4818144 kB + Inactive(file): 1578132 kB + Inactive(anon): 574764 kB + Inactive(file): 1567648 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + Shmem: 577588 kB + SReclaimable: 346648 kB + """).encode() + with mock_open_content({"/proc/meminfo": content}): + with mock_open_exception("/proc/zoneinfo", FileNotFoundError): + with warnings.catch_warnings(record=True) as ws: + ret = psutil.virtual_memory() + assert ret.available == 2057400 * 1024 + 4818144 * 1024 + w = ws[0] + assert ( + "inactive memory stats couldn't be determined" + in str(w.message) + ) + + def test_virtual_memory_mocked(self): + # Emulate /proc/meminfo because neither vmstat nor free return slab. + content = textwrap.dedent("""\ + MemTotal: 100 kB + MemFree: 2 kB + MemAvailable: 3 kB + Buffers: 4 kB + Cached: 5 kB + SwapCached: 6 kB + Active: 7 kB + Inactive: 8 kB + Active(anon): 9 kB + Inactive(anon): 10 kB + Active(file): 11 kB + Inactive(file): 12 kB + Unevictable: 13 kB + Mlocked: 14 kB + SwapTotal: 15 kB + SwapFree: 16 kB + Dirty: 17 kB + Writeback: 18 kB + AnonPages: 19 kB + Mapped: 20 kB + Shmem: 21 kB + Slab: 22 kB + SReclaimable: 23 kB + SUnreclaim: 24 kB + KernelStack: 25 kB + PageTables: 26 kB + NFS_Unstable: 27 kB + Bounce: 28 kB + WritebackTmp: 29 kB + CommitLimit: 30 kB + Committed_AS: 31 kB + VmallocTotal: 32 kB + VmallocUsed: 33 kB + VmallocChunk: 34 kB + HardwareCorrupted: 35 kB + AnonHugePages: 36 kB + ShmemHugePages: 37 kB + ShmemPmdMapped: 38 kB + CmaTotal: 39 kB + CmaFree: 40 kB + HugePages_Total: 41 kB + HugePages_Free: 42 kB + HugePages_Rsvd: 43 kB + HugePages_Surp: 44 kB + Hugepagesize: 45 kB + DirectMap46k: 46 kB + DirectMap47M: 47 kB + DirectMap48G: 48 kB + """).encode() + with mock_open_content({"/proc/meminfo": content}) as m: + mem = psutil.virtual_memory() + assert m.called + assert mem.total == 100 * 1024 + assert mem.free == 2 * 1024 + assert mem.buffers == 4 * 1024 + # cached mem also includes reclaimable memory + 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 + + +# ===================================================================== +# --- system swap memory +# ===================================================================== + + +class TestSwapMemory(LinuxTestCase): + @staticmethod + def meminfo_has_swap_info(): + """Return True if /proc/meminfo provides swap metrics.""" + with open("/proc/meminfo") as f: + data = f.read() + return 'SwapTotal:' in data and 'SwapFree:' in data + + def test_total(self): + free_value = free_swap().total + psutil_value = psutil.swap_memory().total + 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 + 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 + 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: + warnings.simplefilter("always") + ret = psutil.swap_memory() + assert m.called + assert len(ws) == 1 + w = ws[0] + assert ( + "'sin' and 'sout' swap memory stats couldn't be determined" + in str(w.message) + ) + assert ret.sin == 0 + assert ret.sout == 0 + + def test_no_vmstat_mocked(self): + # see https://github.com/giampaolo/psutil/issues/722 + with mock_open_exception("/proc/vmstat", FileNotFoundError) as m: + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always") + ret = psutil.swap_memory() + assert m.called + assert len(ws) == 1 + w = ws[0] + assert ( + "'sin' and 'sout' swap memory stats couldn't " + "be determined and were set to 0" + in str(w.message) + ) + assert ret.sin == 0 + assert ret.sout == 0 + + def test_meminfo_against_sysinfo(self): + # Make sure the content of /proc/meminfo about swap memory + # matches sysinfo() syscall, see: + # https://github.com/giampaolo/psutil/issues/1015 + if not self.meminfo_has_swap_info(): + return 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 + import psutil._psutil_linux as cext + + _, _, _, _, total, free, unit_multiplier = cext.linux_sysinfo() + total *= unit_multiplier + free *= unit_multiplier + 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 + # in which case sysinfo() syscall is supposed to be used + # as a fallback. + with mock_open_content({"/proc/meminfo": b""}) as m: + psutil.swap_memory() + assert m.called + + +# ===================================================================== +# --- system CPU +# ===================================================================== + + +class TestCpuCountLogical(LinuxTestCase): + @pytest.mark.skipif( + not os.path.exists("/sys/devices/system/cpu/online"), + 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: + value = f.read().strip() + if "-" in str(value): + value = int(value.split('-')[1]) + 1 + assert psutil.cpu_count() == value + + @pytest.mark.skipif( + not os.path.exists("/sys/devices/system/cpu"), + 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 + + @pytest.mark.skipif( + 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 shutil.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('#')]) + assert psutil.cpu_count(logical=True) == num + + def test_emulate_fallbacks(self): + import psutil._pslinux + + original = psutil._pslinux.cpu_count_logical() + # Here we want to mock os.sysconf("SC_NPROCESSORS_ONLN") in + # order to cause the parsing of /proc/cpuinfo and /proc/stat. + with mock.patch( + 'psutil._pslinux.os.sysconf', side_effect=ValueError + ) as m: + 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: + assert psutil._pslinux.cpu_count_logical() is None + assert m.call_count == 2 + # /proc/stat should be the last one + assert m.call_args[0][0] == '/proc/stat' + + # Let's push this a bit further and make sure /proc/cpuinfo + # parsing works as expected. + with open('/proc/cpuinfo', 'rb') as f: + cpuinfo_data = f.read() + fake_file = io.BytesIO(cpuinfo_data) + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + 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: + assert psutil._pslinux.cpu_count_logical() == original + assert m.called + + +class TestCpuCountCores(LinuxTestCase): + @pytest.mark.skipif( + not shutil.which("lscpu"), reason="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]) + 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: + meth_2 = psutil._pslinux.cpu_count_cores() + assert m.called + if meth_1 is not None: + 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: + assert psutil._pslinux.cpu_count_cores() is None + assert m1.called + assert m2.called + + +class TestCpuFreq(LinuxTestCase): + @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): + if path.startswith("/sys/devices/system/cpu/cpufreq/policy"): + return False + else: + return orig_exists(path) + + orig_exists = os.path.exists + with mock.patch( + "os.path.exists", side_effect=path_exists_mock, create=True + ): + assert psutil.cpu_freq() + + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + @pytest.mark.skipif( + 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 + # exist and /proc/cpuinfo is used instead. + def path_exists_mock(path): + if path.startswith('/sys/devices/system/cpu/'): + return False + else: + return os_path_exists(path) + + os_path_exists = os.path.exists + try: + with mock.patch("os.path.exists", side_effect=path_exists_mock): + reload_module(psutil._pslinux) + ret = psutil.cpu_freq() + assert ret, ret + assert ret.max == 0.0 + assert ret.min == 0.0 + for freq in psutil.cpu_freq(percpu=True): + assert freq.max == 0.0 + assert freq.min == 0.0 + finally: + reload_module(psutil._pslinux) + reload_module(psutil) + + @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( + "/sys/devices/system/cpu/cpufreq/policy" + ): + return io.BytesIO(b"500000") + elif name.endswith('/scaling_min_freq') and name.startswith( + "/sys/devices/system/cpu/cpufreq/policy" + ): + return io.BytesIO(b"600000") + elif name.endswith('/scaling_max_freq') and name.startswith( + "/sys/devices/system/cpu/cpufreq/policy" + ): + return io.BytesIO(b"700000") + elif name == '/proc/cpuinfo': + return io.BytesIO(b"cpu MHz : 500") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + 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 + # when /proc/cpuinfo is used min and max frequencies are not + # available and are set to 0. + if freq.min != 0.0: + assert freq.min == 600.0 + if freq.max != 0.0: + assert freq.max == 700.0 + + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_emulate_multi_cpu(self): + def open_mock(name, *args, **kwargs): + n = name + if n.endswith('/scaling_cur_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy0" + ): + return io.BytesIO(b"100000") + elif n.endswith('/scaling_min_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy0" + ): + return io.BytesIO(b"200000") + elif n.endswith('/scaling_max_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy0" + ): + return io.BytesIO(b"300000") + elif n.endswith('/scaling_cur_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy1" + ): + return io.BytesIO(b"400000") + elif n.endswith('/scaling_min_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy1" + ): + return io.BytesIO(b"500000") + elif n.endswith('/scaling_max_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy1" + ): + return io.BytesIO(b"600000") + elif name == '/proc/cpuinfo': + return io.BytesIO(b"cpu MHz : 100\ncpu MHz : 400") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + 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 + ): + freq = psutil.cpu_freq(percpu=True) + assert freq[0].current == 100.0 + if freq[0].min != 0.0: + assert freq[0].min == 200.0 + if freq[0].max != 0.0: + assert freq[0].max == 300.0 + assert freq[1].current == 400.0 + if freq[1].min != 0.0: + assert freq[1].min == 500.0 + if freq[1].max != 0.0: + assert freq[1].max == 600.0 + + @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): + if name.endswith('/scaling_cur_freq'): + raise FileNotFoundError + if name.endswith('/cpuinfo_cur_freq'): + return io.BytesIO(b"200000") + elif name == '/proc/cpuinfo': + return io.BytesIO(b"cpu MHz : 200") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + 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 + ): + freq = psutil.cpu_freq() + assert freq.current == 200 + + +class TestCpuTimes(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 TestCpuStats(LinuxTestCase): + + # XXX: fails too often. + # def test_ctx_switches(self): + # vmstat_value = vmstat("context switches") + # psutil_value = psutil.cpu_stats().ctx_switches + # assert abs(vmstat_value - psutil_value) < 500 + + def test_interrupts(self): + vmstat_value = vmstat("interrupts") + psutil_value = psutil.cpu_stats().interrupts + assert abs(vmstat_value - psutil_value) < 500 + + +class TestLoadAvg(LinuxTestCase): + def test_getloadavg(self): + psutil_value = psutil.getloadavg() + with open("/proc/loadavg") as f: + proc_value = f.read().split() + + 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 + + +# ===================================================================== +# --- system network +# ===================================================================== + + +class TestNetIfAddrs(LinuxTestCase): + def test_ips(self): + for name, addrs in psutil.net_if_addrs().items(): + for addr in addrs: + if addr.family == psutil.AF_LINK: + assert addr.address == get_mac_address(name) + elif addr.family == socket.AF_INET: + assert addr.address == get_ipv4_address(name) + assert addr.netmask == get_ipv4_netmask(name) + if addr.broadcast is not None: + assert addr.broadcast == get_ipv4_broadcast(name) + else: + 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: + # "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] + 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") + # 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() + # assert name in nics + # assert len(nics) == found + + +class TestNetIfStats(LinuxTestCase): + @pytest.mark.skipif( + not shutil.which("ifconfig"), reason="ifconfig utility not available" + ) + def test_against_ifconfig(self): + for name, stats in psutil.net_if_stats().items(): + try: + out = sh(f"ifconfig {name}") + except RuntimeError: + pass + else: + 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(f"/sys/class/net/{name}/mtu") as f: + assert stats.mtu == int(f.read().strip()) + + @pytest.mark.skipif( + not shutil.which("ifconfig"), reason="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(f"ifconfig {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(",")) + assert 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(",")) + assert ifconfig_flags == psutil_flags + + if not matches_found: + return pytest.fail("no matches were found") + + +class TestNetIoCounters(LinuxTestCase): + @pytest.mark.skipif( + not shutil.which("ifconfig"), reason="ifconfig utility not available" + ) + @retry_on_failure() + def test_against_ifconfig(self): + def ifconfig(nic): + ret = {} + out = sh(f"ifconfig {nic}") + ret['packets_recv'] = int( + re.findall(r'RX packets[: ](\d+)', out)[0] + ) + ret['packets_sent'] = int( + re.findall(r'TX packets[: ](\d+)', out)[0] + ) + ret['errin'] = int(re.findall(r'errors[: ](\d+)', out)[0]) + ret['errout'] = int(re.findall(r'errors[: ](\d+)', out)[1]) + ret['dropin'] = int(re.findall(r'dropped[: ](\d+)', out)[0]) + ret['dropout'] = int(re.findall(r'dropped[: ](\d+)', out)[1]) + ret['bytes_recv'] = int( + re.findall(r'RX (?:packets \d+ +)?bytes[: ](\d+)', out)[0] + ) + ret['bytes_sent'] = int( + re.findall(r'TX (?:packets \d+ +)?bytes[: ](\d+)', out)[0] + ) + return ret + + nio = psutil.net_io_counters(pernic=True, nowrap=False) + for name, stats in nio.items(): + try: + ifconfig_ret = ifconfig(name) + except RuntimeError: + continue + assert ( + abs(stats.bytes_recv - ifconfig_ret['bytes_recv']) < 1024 * 10 + ) + assert ( + abs(stats.bytes_sent - ifconfig_ret['bytes_sent']) < 1024 * 10 + ) + assert ( + abs(stats.packets_recv - ifconfig_ret['packets_recv']) < 1024 + ) + 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 + + +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): + # see: https://github.com/giampaolo/psutil/issues/623 + 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("""\ + 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 + + @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 +# ===================================================================== + + +class TestDiskPartitions(LinuxTestCase): + @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() + # against "df -a" + def df(path): + out = sh(f'df -P -B 1 "{path}"').strip() + lines = out.split('\n') + lines.pop(0) + line = lines.pop(0) + dev, total, used, free = line.split()[:4] + if dev == 'none': + dev = '' + total, used, free = int(total), int(used), int(free) + return dev, total, used, free + + for part in psutil.disk_partitions(all=False): + usage = psutil.disk_usage(part.mountpoint) + _, total, used, free = df(part.mountpoint) + 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. + with open("/proc/filesystems") as f: + data = f.read() + if 'zfs' in data: + for part in psutil.disk_partitions(): + if part.fstype == 'zfs': + return + + # No ZFS partitions on this system. Let's fake one. + fake_file = io.StringIO("nodev\tzfs\n") + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m1: + with mock.patch( + 'psutil._pslinux.cext.disk_partitions', + return_value=[('/dev/sdb3', '/', 'zfs', 'rw')], + ) as m2: + ret = psutil.disk_partitions() + assert m1.called + assert m2.called + assert ret + assert ret[0].fstype == 'zfs' + + def test_emulate_realpath_fail(self): + # See: https://github.com/giampaolo/psutil/issues/1307 + try: + with mock.patch( + 'os.path.realpath', return_value='/non/existent' + ) as m: + with pytest.raises(FileNotFoundError): + psutil.disk_partitions() + assert m.called + finally: + psutil.PROCFS_PATH = "/proc" + + +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 + 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) + 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, + # lines reporting all metrics: + # https://github.com/giampaolo/psutil/issues/767 + content = " 3 0 hda 1 2 3 4 5 6 7 8 9 10 11" + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=True + ): + ret = psutil.disk_io_counters(nowrap=False) + 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, + # where one line of /proc/partitions return a limited + # amount of metrics when it bumps into a partition + # (instead of a disk). See: + # https://github.com/giampaolo/psutil/issues/767 + with mock_open_content({"/proc/diskstats": " 3 1 hda 1 2 3 4"}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=True + ): + ret = psutil.disk_io_counters(nowrap=False) + assert ret.read_count == 1 + assert ret.read_bytes == 2 * SECTOR_SIZE + assert ret.write_count == 3 + assert ret.write_bytes == 4 * SECTOR_SIZE + + 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, + # see: + # https://github.com/giampaolo/psutil/pull/1313#issuecomment-408626842 + content = textwrap.dedent("""\ + 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 + 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 + """) + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=False + ): + ret = psutil.disk_io_counters(perdisk=True, nowrap=False) + 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', + # 'nvme0n1p1') are skipped and not included in the total count. + # https://github.com/giampaolo/psutil/pull/1313#issuecomment-408626842 + content = textwrap.dedent("""\ + 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 + 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 + """) + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=False + ): + ret = psutil.disk_io_counters(perdisk=False, nowrap=False) + assert ret is None + + def is_storage_device(name): + return name == 'nvme0n1' + + content = textwrap.dedent("""\ + 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 + 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 + """) + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', + create=True, + side_effect=is_storage_device, + ): + ret = psutil.disk_io_counters(perdisk=False, nowrap=False) + assert ret.read_count == 1 + assert ret.write_count == 5 + + def test_emulate_use_sysfs(self): + def exists(path): + return path == '/proc/diskstats' + + 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) + assert len(wprocfs) == len(wsysfs) + + def test_emulate_not_impl(self): + def exists(path): + return False + + with mock.patch( + 'psutil._pslinux.os.path.exists', create=True, side_effect=exists + ): + 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): + 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: + with pytest.raises(FileNotFoundError): + finder.ask_proc_partitions() + if os.path.exists(f"/sys/dev/block/{self.major}:{self.minor}/uevent"): + finder.ask_sys_dev_block() + else: + with pytest.raises(FileNotFoundError): + finder.ask_sys_dev_block() + finder.ask_sys_class_block() + + @pytest.mark.skipif(GITHUB_ACTIONS, reason="unsupported on GITHUB_ACTIONS") + def test_comparisons(self): + finder = RootFsDeviceFinder() + assert finder.find() is not None + + a = b = c = None + if os.path.exists("/proc/partitions"): + a = finder.ask_proc_partitions() + 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() + + base = a or b or c + if base and a: + assert base == a + if base and b: + assert base == b + if base and c: + assert base == c + + @pytest.mark.skipif( + not shutil.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 /") + assert 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: + assert part.device != "/dev/root" + assert part.device == RootFsDeviceFinder().find() + else: + assert part.device == "/dev/root" + + +# ===================================================================== +# --- misc +# ===================================================================== + + +class TestMisc(LinuxTestCase): + def test_boot_time(self): + vmstat_value = vmstat('boot time') + psutil_value = psutil.boot_time() + assert int(vmstat_value) == int(psutil_value) + + def test_no_procfs_on_import(self): + my_procfs = self.get_testfn() + os.mkdir(my_procfs) + + with open(os.path.join(my_procfs, 'stat'), 'w') as f: + f.write('cpu 0 0 0 0 0 0 0 0 0 0\n') + f.write('cpu0 0 0 0 0 0 0 0 0 0 0\n') + f.write('cpu1 0 0 0 0 0 0 0 0 0 0\n') + + try: + orig_open = open + + def open_mock(name, *args, **kwargs): + if name.startswith('/proc'): + raise FileNotFoundError + return orig_open(name, *args, **kwargs) + + with mock.patch("builtins.open", side_effect=open_mock): + reload_module(psutil) + + with pytest.raises(OSError): + psutil.cpu_times() + with pytest.raises(OSError): + psutil.cpu_times(percpu=True) + with pytest.raises(OSError): + psutil.cpu_percent() + with pytest.raises(OSError): + psutil.cpu_percent(percpu=True) + with pytest.raises(OSError): + psutil.cpu_times_percent() + with pytest.raises(OSError): + psutil.cpu_times_percent(percpu=True) + + psutil.PROCFS_PATH = my_procfs + + 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) + assert sum(per_cpu_percent) == 0 + + # ditto awkward length + per_cpu_times_percent = psutil.cpu_times_percent(percpu=True) + assert sum(map(sum, per_cpu_times_percent)) == 0 + + # much user, very busy + with open(os.path.join(my_procfs, 'stat'), 'w') as f: + f.write('cpu 1 0 0 0 0 0 0 0 0 0\n') + f.write('cpu0 1 0 0 0 0 0 0 0 0 0\n') + f.write('cpu1 1 0 0 0 0 0 0 0 0 0\n') + + 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) + + assert psutil.PROCFS_PATH == '/proc' + + def test_cpu_steal_decrease(self): + # Test cumulative cpu stats decrease. We should ignore this. + # See issue #1210. + content = textwrap.dedent("""\ + cpu 0 0 0 0 0 0 0 1 0 0 + cpu0 0 0 0 0 0 0 0 1 0 0 + cpu1 0 0 0 0 0 0 0 1 0 0 + """).encode() + with mock_open_content({"/proc/stat": content}) as m: + # first call to "percent" functions should read the new stat file + # and compare to the "real" file read at import time - so the + # values are meaningless + psutil.cpu_percent() + assert m.called + psutil.cpu_percent(percpu=True) + psutil.cpu_times_percent() + psutil.cpu_times_percent(percpu=True) + + content = textwrap.dedent("""\ + cpu 1 0 0 0 0 0 0 0 0 0 + cpu0 1 0 0 0 0 0 0 0 0 0 + cpu1 1 0 0 0 0 0 0 0 0 0 + """).encode() + with mock_open_content({"/proc/stat": content}): + # Increase "user" while steal goes "backwards" to zero. + cpu_percent = psutil.cpu_percent() + assert m.called + cpu_percent_percpu = psutil.cpu_percent(percpu=True) + cpu_times_percent = psutil.cpu_times_percent() + cpu_times_percent_percpu = psutil.cpu_times_percent(percpu=True) + 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: + 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(): + 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 + with pytest.raises(OSError): + psutil.virtual_memory() + with pytest.raises(OSError): + psutil.cpu_times() + with pytest.raises(OSError): + psutil.cpu_times(percpu=True) + with pytest.raises(OSError): + psutil.boot_time() + with pytest.raises(OSError): + psutil.net_connections() + with pytest.raises(OSError): + psutil.net_io_counters() + with pytest.raises(OSError): + psutil.net_if_stats() + with pytest.raises(OSError): + psutil.disk_partitions() + with pytest.raises(psutil.NoSuchProcess): + psutil.Process() + finally: + psutil.PROCFS_PATH = "/proc" + + @retry_on_failure() + @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(): + threads = p.threads() + assert len(threads) == nthreads + 1 + tid = sorted(threads, key=lambda x: x.id)[1].id + assert p.pid != tid + pt = psutil.Process(tid) + pt.as_dict() + assert tid not in psutil.pids() + + def test_pid_exists_no_proc_status(self): + # Internally pid_exists relies on /proc/{pid}/status. + # Emulate a case where this file is empty in which case + # psutil is supposed to fall back on using pids(). + with mock_open_content({"/proc/%s/status": ""}) as m: + assert psutil.pid_exists(os.getpid()) + assert m.called + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +@pytest.mark.skipif(not HAS_BATTERY, reason="no battery") +class TestSensorsBattery(LinuxTestCase): + @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('%', '')) + psutil_value = psutil.sensors_battery().percent + assert abs(acpi_value - psutil_value) < 1 + + def test_emulate_power_plugged(self): + # Pretend the AC power cable is connected. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + return io.BytesIO(b"1") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock) as m: + assert psutil.sensors_battery().power_plugged is True + assert ( + psutil.sensors_battery().secsleft + == psutil.POWER_TIME_UNLIMITED + ) + assert m.called + + def test_emulate_power_plugged_2(self): + # Same as above but pretend /AC0/online does not exist in which + # case code relies on /status file. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + raise FileNotFoundError + if name.endswith("/status"): + return io.StringIO("charging") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock) as m: + assert psutil.sensors_battery().power_plugged is True + assert m.called + + def test_emulate_power_not_plugged(self): + # Pretend the AC power cable is not connected. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + return io.BytesIO(b"0") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock) as m: + assert psutil.sensors_battery().power_plugged is False + assert m.called + + def test_emulate_power_not_plugged_2(self): + # Same as above but pretend /AC0/online does not exist in which + # case code relies on /status file. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + raise FileNotFoundError + if name.endswith("/status"): + return io.StringIO("discharging") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock) as m: + assert psutil.sensors_battery().power_plugged is False + assert m.called + + def test_emulate_power_undetermined(self): + # Pretend we can't know whether the AC power cable not + # connected (assert fallback to False). + def open_mock(name, *args, **kwargs): + if name.startswith(( + '/sys/class/power_supply/AC0/online', + '/sys/class/power_supply/AC/online', + )): + raise FileNotFoundError + if name.startswith("/sys/class/power_supply/BAT0/status"): + return io.BytesIO(b"???") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock) as m: + assert psutil.sensors_battery().power_plugged is None + assert m.called + + def test_emulate_energy_full_0(self): + # Emulate a case where energy_full files returns 0. + with mock_open_content( + {"/sys/class/power_supply/BAT0/energy_full": b"0"} + ) as m: + assert psutil.sensors_battery().percent == 0 + assert m.called + + def test_emulate_energy_full_not_avail(self): + # Emulate a case where energy_full file does not exist. + # Expected fallback on /capacity. + with mock_open_exception( + "/sys/class/power_supply/BAT0/energy_full", + FileNotFoundError, + ): + with mock_open_exception( + "/sys/class/power_supply/BAT0/charge_full", + FileNotFoundError, + ): + with mock_open_content( + {"/sys/class/power_supply/BAT0/capacity": b"88"} + ): + 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 against /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( + "/sys/class/power_supply/AC/online", FileNotFoundError + ): + with mock_open_exception( + "/sys/class/power_supply/AC0/online", FileNotFoundError + ): + with mock_open_exception( + "/sys/class/power_supply/BAT0/status", + FileNotFoundError, + ): + assert psutil.sensors_battery().power_plugged is None + + def test_fully_emulated(self): + def open_mock(name, *args, **kwargs): + if name.endswith("/energy_now"): + return io.StringIO("60000000") + elif name.endswith("/power_now"): + return io.StringIO("0") + elif name.endswith("/energy_full"): + return io.StringIO("60000001") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch('os.listdir', return_value=["BAT0"]) as mlistdir: + with mock.patch("builtins.open", side_effect=open_mock) as mopen: + assert psutil.sensors_battery() is not None + assert mlistdir.called + assert mopen.called + + +class TestSensorsTemperatures(LinuxTestCase): + def test_emulate_class_hwmon(self): + def open_mock(name, *args, **kwargs): + if name.endswith('/name'): + return io.StringIO("name") + elif name.endswith('/temp1_label'): + return io.StringIO("label") + elif name.endswith('/temp1_input'): + return io.BytesIO(b"30000") + elif name.endswith('/temp1_max'): + return io.BytesIO(b"40000") + elif name.endswith('/temp1_crit'): + return io.BytesIO(b"50000") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + 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'] + ): + temp = psutil.sensors_temperatures()['name'][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): + 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("critical") + elif name.endswith('type'): + return io.StringIO("name") + else: + return orig_open(name, *args, **kwargs) + + def glob_mock(path): + 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'] + 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 + 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 == '' + assert temp.current == 30.0 + assert temp.high == 50.0 + assert temp.critical == 50.0 + + +class TestSensorsFans(LinuxTestCase): + def test_emulate_data(self): + def open_mock(name, *args, **kwargs): + if name.endswith('/name'): + return io.StringIO("name") + elif name.endswith('/fan1_label'): + return io.StringIO("label") + elif name.endswith('/fan1_input'): + return io.StringIO("2000") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock): + with mock.patch( + 'glob.glob', return_value=['/sys/class/hwmon/hwmon2/fan1'] + ): + fan = psutil.sensors_fans()['name'][0] + assert fan.label == 'label' + assert fan.current == 2000 + + +# ===================================================================== +# --- test process +# ===================================================================== + + +class TestProcess(LinuxTestCase): + @retry_on_failure() + def test_parse_smaps_vs_memory_maps(self): + sproc = self.spawn_subproc() + 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)) + < 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 + 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({f"/proc/{os.getpid()}/smaps": content}) as m: + p = psutil._pslinux.Process(os.getpid()) + uss, pss, swap = p._parse_smaps() + assert m.called + assert uss == (6 + 7 + 14) * 1024 + assert pss == 3 * 1024 + assert swap == 15 * 1024 + + def test_open_files_mode(self): + def get_test_file(fname): + p = psutil.Process() + giveup_at = time.time() + GLOBAL_TIMEOUT + while True: + for file in p.open_files(): + if file.path == os.path.abspath(fname): + return file + elif time.time() > giveup_at: + break + raise RuntimeError("timeout looking for test file") + + testfn = self.get_testfn() + with open(testfn, "w"): + assert get_test_file(testfn).mode == "w" + with open(testfn): + assert get_test_file(testfn).mode == "r" + with open(testfn, "a"): + assert get_test_file(testfn).mode == "a" + with open(testfn, "r+"): + assert get_test_file(testfn).mode == "r+" + with open(testfn, "w+"): + assert get_test_file(testfn).mode == "r+" + with open(testfn, "a+"): + assert get_test_file(testfn).mode == "a+" + + 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() + # execution + p = psutil.Process() + files = p.open_files() + with open(self.get_testfn(), 'w'): + # give the kernel some time to see the new file + call_until(lambda: len(p.open_files()) != len(files)) + with mock.patch( + 'psutil._pslinux.os.readlink', + side_effect=FileNotFoundError, + ) as m: + assert p.open_files() == [] + assert m.called + # also simulate the case where os.readlink() returns EINVAL + # in which case psutil is supposed to 'continue' + with mock.patch( + 'psutil._pslinux.os.readlink', + side_effect=OSError(errno.EINVAL, ""), + ) as m: + assert p.open_files() == [] + assert m.called + + def test_open_files_fd_gone(self): + # Simulate a case where /proc/{pid}/fdinfo/{fd} disappears + # while iterating through fds. + # https://travis-ci.org/giampaolo/psutil/jobs/225694530 + p = psutil.Process() + files = p.open_files() + with open(self.get_testfn(), 'w'): + # give the kernel some time to see the new file + call_until(lambda: len(p.open_files()) != len(files)) + with mock.patch( + "builtins.open", side_effect=FileNotFoundError + ) as m: + assert p.open_files() == [] + assert m.called + + def test_open_files_enametoolong(self): + # Simulate a case where /proc/{pid}/fd/{fd} symlink + # points to a file with full path longer than PATH_MAX, see: + # https://github.com/giampaolo/psutil/issues/1940 + p = psutil.Process() + files = p.open_files() + with open(self.get_testfn(), 'w'): + # give the kernel some time to see the new file + call_until(lambda: len(p.open_files()) != len(files)) + patch_point = 'psutil._pslinux.os.readlink' + with mock.patch( + patch_point, side_effect=OSError(errno.ENAMETOOLONG, "") + ) as m: + with mock.patch("psutil._pslinux.debug"): + assert p.open_files() == [] + assert m.called + + # --- mocked tests + + def test_terminal_mocked(self): + with mock.patch( + 'psutil._pslinux._psposix.get_terminal_map', return_value={} + ) as m: + assert psutil._pslinux.Process(os.getpid()).terminal() is None + assert m.called + + def test_cmdline_mocked(self): + # see: https://github.com/giampaolo/psutil/issues/639 + p = psutil.Process() + 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('foo\x00bar\x00\x00') + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + assert p.cmdline() == ['foo', 'bar', ''] + assert m.called + + def test_cmdline_spaces_mocked(self): + # see: https://github.com/giampaolo/psutil/issues/1179 + p = psutil.Process() + fake_file = io.StringIO('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('foo bar ') + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + assert p.cmdline() == ['foo', 'bar', ''] + assert m.called + + def test_cmdline_mixed_separators(self): + # https://github.com/giampaolo/psutil/issues/1179#issuecomment-552984549 + p = psutil.Process() + fake_file = io.StringIO('foo\x20bar\x00') + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + 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)' + ): + 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) + # which no longer exists by the time we open() it (race + # condition). threads() is supposed to ignore that instead + # of raising NSP. + def open_mock_1(name, *args, **kwargs): + if name.startswith(f"/proc/{os.getpid()}/task"): + raise FileNotFoundError + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock_1) as m: + ret = psutil.Process().threads() + assert m.called + assert ret == [] + + # ...but if it bumps into something != ENOENT we want an + # exception. + def open_mock_2(name, *args, **kwargs): + if name.startswith(f"/proc/{os.getpid()}/task"): + raise PermissionError + return orig_open(name, *args, **kwargs) + + 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=FileNotFoundError + ) as m: + # de-activate guessing from cmdline() + with mock.patch( + 'psutil._pslinux.Process.cmdline', return_value=[] + ): + ret = psutil.Process().exe() + 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. + with mock_open_exception( + f"/proc/{os.getpid()}/smaps", FileNotFoundError + ) as m: + p = psutil.Process() + with pytest.raises(FileNotFoundError): + p.memory_maps() + assert m.called + + def test_issue_2418(self): + p = psutil.Process() + with mock_open_exception( + f"/proc/{os.getpid()}/statm", FileNotFoundError + ): + with mock.patch("os.path.exists", return_value=False): + with pytest.raises(psutil.NoSuchProcess): + p.memory_info() + + @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: + # https://travis-ci.org/giampaolo/psutil/jobs/51368273 + with mock.patch( + "resource.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 pytest.raises(psutil.ZombieProcess) as cm: + p.rlimit(psutil.RLIMIT_NOFILE) + assert m1.called + assert m2.called + assert cm.value.pid == p.pid + assert cm.value.name == p.name() + + def test_stat_file_parsing(self): + args = [ + "0", # pid + "(cat)", # name + "Z", # status + "1", # ppid + "0", # pgrp + "0", # session + "0", # tty + "0", # tpgid + "0", # flags + "0", # minflt + "0", # cminflt + "0", # majflt + "0", # cmajflt + "2", # utime + "3", # stime + "4", # cutime + "5", # cstime + "0", # priority + "0", # nice + "0", # num_threads + "0", # itrealvalue + "6", # starttime + "0", # vsize + "0", # rss + "0", # rsslim + "0", # startcode + "0", # endcode + "0", # startstack + "0", # kstkesp + "0", # kstkeip + "0", # signal + "0", # blocked + "0", # sigignore + "0", # sigcatch + "0", # wchan + "0", # nswap + "0", # cnswap + "0", # exit_signal + "6", # processor + "0", # rt priority + "0", # policy + "7", # delayacct_blkio_ticks + ] + content = " ".join(args).encode() + with mock_open_content({f"/proc/{os.getpid()}/stat": content}): + p = psutil.Process() + 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() + 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("""\ + 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({f"/proc/{os.getpid()}/status": content}): + p = psutil.Process() + assert p.num_ctx_switches().voluntary == 12 + assert p.num_ctx_switches().involuntary == 13 + assert p.num_threads() == 66 + uids = p.uids() + assert uids.real == 1000 + assert uids.effective == 1001 + assert uids.saved == 1002 + gids = p.gids() + 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 + # a file with full path longer than PATH_MAX, see: + # https://github.com/giampaolo/psutil/issues/1940 + with mock.patch( + 'psutil._pslinux.os.readlink', + side_effect=OSError(errno.ENAMETOOLONG, ""), + ) as m: + p = psutil.Process() + with mock.patch("psutil._pslinux.debug"): + 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) + + def test_memory_info_ex(self): + mem = psutil.Process().memory_info_ex() + assert mem.rss == mem.rss_anon + mem.rss_file + mem.rss_shmem + + +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 + /proc/pid/stat (by psutil) matches the one found in + /proc/pid/status. + """ + + @classmethod + def setUpClass(cls): + cls.proc = psutil.Process() + + def read_status_file(self, linestart): + with psutil._psplatform.open_text( + f"/proc/{self.proc.pid}/status" + ) as f: + for line in f: + line = line.strip() + if line.startswith(linestart): + value = line.partition('\t')[2] + try: + return int(value) + except ValueError: + return value + raise ValueError(f"can't find {linestart!r}") + + def test_name(self): + value = self.read_status_file("Name:") + assert self.proc.name() == value + + def test_status(self): + value = self.read_status_file("State:") + value = value[value.find('(') + 1 : value.rfind(')')] + value = value.replace(' ', '-') + assert self.proc.status() == value + + def test_ppid(self): + value = self.read_status_file("PPid:") + assert self.proc.ppid() == value + + def test_num_threads(self): + value = self.read_status_file("Threads:") + assert self.proc.num_threads() == value + + def test_uids(self): + value = self.read_status_file("Uid:") + value = tuple(map(int, value.split()[1:4])) + assert self.proc.uids() == value + + def test_gids(self): + value = self.read_status_file("Gid:") + value = tuple(map(int, value.split()[1:4])) + assert self.proc.gids() == value + + @retry_on_failure() + def test_num_ctx_switches(self): + value = self.read_status_file("voluntary_ctxt_switches:") + assert self.proc.num_ctx_switches().voluntary == value + value = self.read_status_file("nonvoluntary_ctxt_switches:") + 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('-')) + 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:") + with mock.patch("psutil._pslinux.per_cpu_times") as m: + self.proc._proc._get_eligible_cpus() + if '-' in str(value): + assert not m.called + else: + assert m.called + + +# ===================================================================== +# --- test utils +# ===================================================================== + + +class TestUtils(LinuxTestCase): + def test_readlink(self): + with mock.patch("os.readlink", return_value="foo (deleted)") as m: + assert psutil._psplatform.readlink("bar") == "foo" + assert m.called diff --git a/tests/test_memleaks.py b/tests/test_memleaks.py new file mode 100755 index 0000000000..07cc89b53d --- /dev/null +++ b/tests/test_memleaks.py @@ -0,0 +1,483 @@ +#!/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. + +"""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 +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS + +from . import HAS_CPU_FREQ +from . import HAS_HEAP_INFO +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_PROC_IONICE +from . import HAS_PROC_MEMORY_FOOTPRINT +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 +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() + + +MemoryLeakTestCase.retries = 30 # minimize false positives +MemoryLeakTestCase.verbosity = 1 + +TIMES = MemoryLeakTestCase.times +FEW_TIMES = int(TIMES / 10) + +# =================================================================== +# Process class +# =================================================================== + + +class TestProcessObjectLeaks(MemoryLeakTestCase): + """Test leaks of Process class methods.""" + + proc = thisproc + + def test_coverage(self): + ns = process_namespace(None) + ns.test_class_coverage(self, ns.getters + ns.setters) + + def test_name(self): + self.execute(self.proc.name) + + def test_cmdline(self): + if WINDOWS and self.proc.is_running(): + self.proc.cmdline() + self.execute(self.proc.cmdline) + + def test_exe(self): + self.execute(self.proc.exe) + + def test_ppid(self): + self.execute(self.proc.ppid) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_uids(self): + self.execute(self.proc.uids) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_gids(self): + self.execute(self.proc.gids) + + def test_status(self): + self.execute(self.proc.status) + + def test_nice(self): + self.execute(self.proc.nice) + + def test_nice_set(self): + niceness = thisproc.nice() + self.execute(lambda: self.proc.nice(niceness)) + + @pytest.mark.skipif(not HAS_PROC_IONICE, reason="not supported") + def test_ionice(self): + self.execute(self.proc.ionice) + + @pytest.mark.skipif(not HAS_PROC_IONICE, reason="not supported") + def test_ionice_set(self): + if WINDOWS: + value = thisproc.ionice() + self.execute(lambda: self.proc.ionice(value)) + else: + self.execute(lambda: self.proc.ionice(psutil.IOPRIO_CLASS_NONE)) + + @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) + self.execute_w_exc(OSError, fun, retries=20) + + @pytest.mark.skipif(not HAS_PROC_IO_COUNTERS, reason="not supported") + def test_io_counters(self): + self.execute(self.proc.io_counters) + + @pytest.mark.skipif(POSIX, reason="worthless on POSIX") + def test_username(self): + # always open 1 handle on Windows (only once) + psutil.Process().username() + self.execute(self.proc.username) + + def test_create_time(self): + self.execute(self.proc.create_time) + + @skip_on_access_denied(only_if=OPENBSD) + def test_num_threads(self): + self.execute(self.proc.num_threads) + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_num_handles(self): + self.execute(self.proc.num_handles) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_num_fds(self): + self.execute(self.proc.num_fds) + + def test_num_ctx_switches(self): + self.execute(self.proc.num_ctx_switches) + + @skip_on_access_denied(only_if=OPENBSD) + def test_threads(self): + kw = {"times": 50} if WINDOWS else {} + self.execute(self.proc.threads, **kw) + + def test_cpu_times(self): + self.execute(self.proc.cpu_times) + + @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") + def test_cpu_num(self): + self.execute(self.proc.cpu_num) + + def test_memory_info(self): + self.execute(self.proc.memory_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): + self.execute(self.proc.terminal) + + def test_resume(self): + times = FEW_TIMES if POSIX else self.times + self.execute(self.proc.resume, times=times) + + def test_cwd(self): + self.execute(self.proc.cwd) + + @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_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_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 + ) + + def test_open_files(self): + kw = {"times": 10, "retries": 30} if WINDOWS else {} + with open(get_testfn(), 'w'): + self.execute(self.proc.open_files, **kw) + + @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) + + 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): + self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE)) + + @pytest.mark.skipif(not LINUX, reason="LINUX only") + @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_PROC_RLIMIT, reason="not supported") + def test_rlimit_set_badarg(self): + self.execute_w_exc( + (OSError, ValueError), lambda: self.proc.rlimit(-1), retries=20 + ) + + # Windows implementation is based on a single system-wide + # function (tested later). + @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 + # 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), times=times) + + @pytest.mark.skipif(not HAS_PROC_ENVIRON, reason="not supported") + def test_environ(self): + self.execute(self.proc.environ) + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_proc_oneshot(self): + self.execute(lambda: cext.proc_oneshot(os.getpid())) + + +class TestTerminatedProcessLeaks(TestProcessObjectLeaks): + """Repeat the tests above looking for leaks occurring when dealing + with terminated processes raising NoSuchProcess exception. + The C functions are still invoked but will follow different code + paths. We'll check those code paths. + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.subp = spawn_subproc() + cls.proc = psutil.Process(cls.subp.pid) + cls.proc.kill() + cls.proc.wait() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + terminate(cls.subp) + + def call(self, fun): + try: + fun() + except psutil.NoSuchProcess: + pass + + def test_cpu_affinity_set_badarg(self): + return pytest.skip("skip") + + if WINDOWS: + + def test_kill(self): + self.execute(self.proc.kill) + + def test_terminate(self): + self.execute(self.proc.terminate) + + def test_suspend(self): + self.execute(self.proc.suspend) + + def test_resume(self): + self.execute(self.proc.resume) + + def test_wait(self): + self.execute(self.proc.wait) + + def test_proc_oneshot(self): + # test dual implementation + def call(): + try: + return cext.proc_oneshot(self.proc.pid) + except ProcessLookupError: + pass + + self.execute(call) + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +class TestProcessDualImplementation(MemoryLeakTestCase): + def test_cmdline_peb_true(self): + self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=True)) + + def test_cmdline_peb_false(self): + self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=False)) + + +# =================================================================== +# system APIs +# =================================================================== + + +class TestModuleFunctionsLeaks(MemoryLeakTestCase): + """Test leaks of psutil module functions.""" + + def test_coverage(self): + ns = system_namespace() + ns.test_class_coverage(self, ns.all) + + # --- cpu + + def test_cpu_count(self): # logical + self.execute(lambda: psutil.cpu_count(logical=True)) + + def test_cpu_count_cores(self): + self.execute(lambda: psutil.cpu_count(logical=False)) + + def test_cpu_times(self): + self.execute(psutil.cpu_times) + + def test_per_cpu_times(self): + self.execute(lambda: psutil.cpu_times(percpu=True)) + + def test_cpu_stats(self): + self.execute(psutil.cpu_stats) + + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_cpu_freq(self): + 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): + psutil.getloadavg() + self.execute(psutil.getloadavg) + + # --- mem + + def test_virtual_memory(self): + self.execute(psutil.virtual_memory) + + # TODO: remove this skip when this gets fixed + @pytest.mark.skipif(SUNOS, reason="worthless on SUNOS (uses a subprocess)") + def test_swap_memory(self): + self.execute(psutil.swap_memory) + + def test_pid_exists(self): + times = FEW_TIMES if POSIX else self.times + self.execute(lambda: psutil.pid_exists(os.getpid()), times=times) + + # --- disk + + def test_disk_usage(self): + times = FEW_TIMES if POSIX else self.times + self.execute(lambda: psutil.disk_usage('.'), times=times) + + def test_disk_partitions(self): + self.execute(psutil.disk_partitions) + + @pytest.mark.skipif( + LINUX and not os.path.exists('/proc/diskstats'), + reason="/proc/diskstats not available on this Linux version", + ) + def test_disk_io_counters(self): + self.execute(lambda: psutil.disk_io_counters(nowrap=False)) + + # --- proc + + def test_pids(self): + self.execute(psutil.pids) + + # --- net + + @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)) + + @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'), 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) + + def test_net_if_stats(self): + self.execute(psutil.net_if_stats) + + # --- sensors + + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + def test_sensors_battery(self): + self.execute(psutil.sensors_battery) + + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + @pytest.mark.skipif(LINUX, reason="too slow on LINUX") + def test_sensors_temperatures(self): + times = FEW_TIMES if LINUX else self.times + self.execute(psutil.sensors_temperatures, times=times) + + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") + def test_sensors_fans(self): + times = FEW_TIMES if LINUX else self.times + self.execute(psutil.sensors_fans, times=times) + + # --- others + + 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): + 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 + + def test_win_service_iter(self): + self.execute(cext.winservice_enumerate) + + def test_win_service_get(self): + pass + + def test_win_service_get_config(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_config(name)) + + def test_win_service_get_status(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_status(name)) + + def test_win_service_get_description(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_descr(name)) diff --git a/tests/test_misc.py b/tests/test_misc.py new file mode 100755 index 0000000000..6fd0612ae4 --- /dev/null +++ b/tests/test_misc.py @@ -0,0 +1,733 @@ +#!/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. + +"""Miscellaneous tests.""" + +import collections +import contextlib +import io +import json +import os +import pickle +import socket +import sys +from unittest import mock + +import psutil +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_when_activated +from psutil._common import parse_environ_block +from psutil._common import supports_ipv6 +from psutil._common import wrap_numbers + +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(), ... +# =================================================================== + + +class TestSpecialMethods(PsutilTestCase): + def test_check_pid_range(self): + with pytest.raises(OverflowError): + psutil._psplatform.cext.check_pid_range(2**128) + with pytest.raises(psutil.NoSuchProcess): + psutil.Process(2**128) + + def test_process__repr__(self, func=repr): + p = psutil.Process(self.spawn_subproc().pid) + r = func(p) + assert "psutil.Process" in r + 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() + p.wait() + r = func(p) + assert "status='terminated'" in r + assert "exitcode=" in r + + with mock.patch.object( + psutil.Process, + "name", + side_effect=psutil.ZombieProcess(os.getpid()), + ): + p = psutil.Process() + r = func(p) + assert f"pid={p.pid}" in r + assert "status='zombie'" in r + assert "name=" not in r + with mock.patch.object( + psutil.Process, + "name", + side_effect=psutil.NoSuchProcess(os.getpid()), + ): + p = psutil.Process() + r = func(p) + assert f"pid={p.pid}" in r + assert "terminated" in r + assert "name=" not in r + with mock.patch.object( + psutil.Process, + "name", + side_effect=psutil.AccessDenied(os.getpid()), + ): + p = psutil.Process() + r = func(p) + assert f"pid={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): + assert repr(psutil.Error()) == "psutil.Error()" + + def test_error__str__(self): + assert str(psutil.Error()) == "" + + def test_no_such_process__repr__(self): + assert ( + repr(psutil.NoSuchProcess(321)) + == "psutil.NoSuchProcess(pid=321, msg='process no longer exists')" + ) + assert ( + repr(psutil.NoSuchProcess(321, name="name", msg="msg")) + == "psutil.NoSuchProcess(pid=321, name='name', msg='msg')" + ) + + def test_no_such_process__str__(self): + assert ( + str(psutil.NoSuchProcess(321)) + == "process no longer exists (pid=321)" + ) + assert ( + str(psutil.NoSuchProcess(321, name="name", msg="msg")) + == "msg (pid=321, name='name')" + ) + + def test_zombie_process__repr__(self): + assert ( + repr(psutil.ZombieProcess(321)) + == 'psutil.ZombieProcess(pid=321, msg="PID still ' + 'exists but it\'s a zombie")' + ) + 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): + assert ( + str(psutil.ZombieProcess(321)) + == "PID still exists but it's a zombie (pid=321)" + ) + assert ( + str(psutil.ZombieProcess(321, name="name", ppid=320, msg="foo")) + == "foo (pid=321, ppid=320, name='name')" + ) + + def test_access_denied__repr__(self): + 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): + 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): + assert ( + repr(psutil.TimeoutExpired(5)) + == "psutil.TimeoutExpired(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): + 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() + assert p1 == p2 + p2._ident = (0, 0) + assert p1 != p2 + assert p1 != 'foo' + + def test_process__hash__(self): + s = {psutil.Process(), psutil.Process()} + assert len(s) == 1 + + +# =================================================================== +# --- Misc, generic, corner cases +# =================================================================== + + +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', + 'tests', + 'test', + 'PermissionError', + 'ProcessLookupError', + }: + continue + if not name.startswith('_'): + try: + __import__(name) + except ImportError: + if name not in psutil.__all__: + fun = getattr(psutil, name) + if fun is None: + continue + if ( + fun.__doc__ is not None + and 'deprecated' not in fun.__doc__.lower() + ): + return 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 + # 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 + + def test_version(self): + 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' + assert 'foo' not in p.as_dict() + + def test_serialization(self): + def check(ret): + json.loads(json.dumps(ret)) + + a = pickle.dumps(ret) + b = pickle.loads(a) + assert ret == b + + # --- process APIs + + proc = psutil.Process() + check(psutil.Process().as_dict()) + + ns = process_namespace(proc) + for fun, name in ns.iter(ns.getters, clear_cache=True): + with self.subTest(proc=str(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') + ) + ) + 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') + ) + ) + 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')) + ) + 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') + ) + ) + assert isinstance(b, psutil.TimeoutExpired) + assert b.seconds == 33 + assert b.pid == 4567 + assert b.name == 'name' + + 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, '_get_ident', side_effect=psutil.AccessDenied + ) as meth: + psutil.Process() + assert meth.called + + with mock.patch.object( + psutil.Process, '_get_ident', side_effect=psutil.ZombieProcess(1) + ) as meth: + psutil.Process() + assert meth.called + + with mock.patch.object( + 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 pytest.raises(psutil.NoSuchProcess): + 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 pytest.raises(ImportError) as cm: + reload_module(psutil) + assert "version conflict" in str(cm.value).lower() + + +# =================================================================== +# --- psutil/_common.py utils +# =================================================================== + + +class TestCommonModule(PsutilTestCase): + def test_memoize_when_activated(self): + class Foo: + @memoize_when_activated + def foo(self): + calls.append(None) + + f = Foo() + calls = [] + f.foo() + f.foo() + assert len(calls) == 2 + + # activate + calls = [] + f.foo.cache_activate(f) + f.foo() + f.foo() + assert len(calls) == 1 + + # deactivate + calls = [] + f.foo.cache_deactivate(f) + f.foo() + f.foo() + assert len(calls) == 2 + + def test_parse_environ_block(self): + def k(s): + return s.upper() if WINDOWS else s + + 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 + 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 + 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 + assert parse_environ_block("a=1\0b=2") == {k("a"): "1"} + + def test_supports_ipv6(self): + if supports_ipv6(): + with mock.patch('psutil._common.socket') as s: + s.has_ipv6 = False + assert not supports_ipv6() + + with mock.patch( + 'psutil._common.socket.socket', side_effect=OSError + ) as s: + assert not supports_ipv6() + assert s.called + + with mock.patch( + 'psutil._common.socket.socket', side_effect=socket.gaierror + ) as s: + assert not supports_ipv6() + assert s.called + + with mock.patch( + 'psutil._common.socket.socket.bind', + side_effect=socket.gaierror, + ) as s: + assert not supports_ipv6() + assert s.called + else: + with pytest.raises(OSError): + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + try: + sock.bind(("::1", 0)) + finally: + sock.close() + + def test_isfile_strict(self): + this_file = os.path.abspath(__file__) + assert isfile_strict(this_file) + assert not isfile_strict(os.path.dirname(this_file)) + with mock.patch('psutil._common.os.stat', side_effect=PermissionError): + with pytest.raises(OSError): + isfile_strict(this_file) + with mock.patch( + '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): + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with contextlib.redirect_stderr(io.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 mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with contextlib.redirect_stderr(io.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 mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with contextlib.redirect_stderr(io.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 + + def test_cat_bcat(self): + testfn = self.get_testfn() + with open(testfn, "w") as f: + f.write("foo") + 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" + + +# =================================================================== +# --- Tests for wrap_numbers() function. +# =================================================================== + + +nt = collections.namedtuple('foo', 'a b c') + + +class TestWrapNumbers(PsutilTestCase): + def setUp(self): + wrap_numbers.cache_clear() + + tearDown = setUp + + def test_first_call(self): + input = {'disk1': nt(5, 5, 5)} + assert wrap_numbers(input, 'disk_io') == input + + def test_input_hasnt_changed(self): + input = {'disk1': nt(5, 5, 5)} + 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)} + assert wrap_numbers(input, 'disk_io') == input + input = {'disk1': nt(10, 15, 20)} + assert wrap_numbers(input, 'disk_io') == input + input = {'disk1': nt(20, 25, 30)} + assert wrap_numbers(input, 'disk_io') == input + input = {'disk1': nt(20, 25, 30)} + assert wrap_numbers(input, 'disk_io') == input + + def test_wrap(self): + # let's say 100 is the threshold + input = {'disk1': nt(100, 100, 100)} + assert wrap_numbers(input, 'disk_io') == input + # first wrap restarts from 10 + input = {'disk1': nt(100, 100, 10)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(100, 100, 110)} + # then it remains the same + input = {'disk1': nt(100, 100, 10)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(100, 100, 110)} + # then it goes up + input = {'disk1': nt(100, 100, 90)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(100, 100, 190)} + # then it wraps again + input = {'disk1': nt(100, 100, 20)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(100, 100, 210)} + # and remains the same + input = {'disk1': nt(100, 100, 20)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(100, 100, 210)} + # now wrap another num + input = {'disk1': nt(50, 100, 20)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(150, 100, 210)} + # and again + input = {'disk1': nt(40, 100, 20)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(190, 100, 210)} + # keep it the same + input = {'disk1': nt(40, 100, 20)} + 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)} + assert wrap_numbers(input, 'disk_io') == input + input = {'disk1': nt(5, 5, 5), 'disk2': nt(7, 7, 7)} + assert wrap_numbers(input, 'disk_io') == input + input = {'disk1': nt(8, 8, 8)} + 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)} + assert wrap_numbers(input, 'disk_io') == input + # disk 2 wraps + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 10)} + 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)} + 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)} + assert wrap_numbers(input, 'disk_io') == input + # remains the same + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 100)} + assert wrap_numbers(input, 'disk_io') == input + # and then wraps again + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 10)} + assert 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), + } + 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), + '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') + assert out['nvme0n1'][0] == 400 + + # --- cache tests + + def test_cache_first_call(self): + input = {'disk1': nt(5, 5, 5)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + 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)} + wrap_numbers(input, 'disk_io') + input = {'disk1': nt(10, 10, 10)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + 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 + input = {'disk1': nt(100, 100, 100)} + wrap_numbers(input, 'disk_io') + + # first wrap restarts from 10 + input = {'disk1': nt(100, 100, 10)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + 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': {('disk1', 2)}}} + + def check_cache_info(): + cache = wrap_numbers.cache_info() + assert cache[1] == { + 'disk_io': { + ('disk1', 0): 0, + ('disk1', 1): 0, + ('disk1', 2): 100, + } + } + assert cache[2] == {'disk_io': {'disk1': {('disk1', 2)}}} + + # then it remains the same + input = {'disk1': nt(100, 100, 10)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + 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() + 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() + 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': {('disk1', 2)}}} + + def test_cache_changing_keys(self): + input = {'disk1': nt(5, 5, 5)} + wrap_numbers(input, 'disk_io') + input = {'disk1': nt(5, 5, 5), 'disk2': nt(7, 7, 7)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + 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') + assert wrap_numbers.cache_info() == ({}, {}, {}) + wrap_numbers.cache_clear('disk_io') + wrap_numbers.cache_clear('?!?') + + @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(): + return pytest.skip("no disks or NICs available") + psutil.disk_io_counters() + psutil.net_io_counters() + caches = wrap_numbers.cache_info() + for cache in caches: + 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: + 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() + assert caches == ({}, {}, {}) diff --git a/tests/test_osx.py b/tests/test_osx.py new file mode 100755 index 0000000000..7bf09e33b7 --- /dev/null +++ b/tests/test_osx.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""macOS specific tests.""" + +import re +import time + +import psutil +from psutil import MACOS + +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 +from . import pytest +from . import retry_on_failure +from . import sh +from . import spawn_subproc +from . import terminate + + +def sysctl(cmdline): + """Expects a sysctl command with an argument and parse the result + returning only the value of interest. + """ + out = sh(cmdline) + result = out.split()[1] + try: + return int(result) + except ValueError: + return result + + +def vm_stat(field): + """Wrapper around 'vm_stat' cmdline utility.""" + out = sh('vm_stat') + for line in out.split('\n'): + if field in line: + break + else: + raise ValueError("line not found") + return ( + int(re.search(r'\d+', line).group(0)) + * psutil._psplatform.cext.getpagesize() + ) + + +@pytest.mark.skipif(not MACOS, reason="MACOS only") +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 + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + 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] + year = start_ps.split(' ')[-1] + start_psutil = psutil.Process(self.pid).create_time() + assert hhmmss == time.strftime( + "%H:%M:%S", time.localtime(start_psutil) + ) + assert year == time.strftime("%Y", time.localtime(start_psutil)) + + +# ===================================================================== +# --- Test system APIs +# ===================================================================== + + +class TestVirtualMemory(MacosTestCase): + + def test_total(self): + sysctl_hwphymem = sysctl('sysctl hw.memsize') + assert sysctl_hwphymem == psutil.virtual_memory().total + + @pytest.mark.skipif( + CI_TESTING and MACOS and AARCH64, + reason="skipped on MACOS + ARM64 + CI_TESTING", + ) + @retry_on_failure() + def test_free(self): + vmstat_val = vm_stat("free") + psutil_val = psutil.virtual_memory().free + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif( + CI_TESTING and MACOS and AARCH64, + reason="skipped on MACOS + ARM64 + CI_TESTING", + ) + @retry_on_failure() + def test_active(self): + vmstat_val = vm_stat("active") + 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_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_wired(self): + vmstat_val = vm_stat("wired") + psutil_val = psutil.virtual_memory().wired + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + +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") + psutil_val = psutil.swap_memory().sin + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_sout(self): + vmstat_val = vm_stat("Pageout") + psutil_val = psutil.swap_memory().sout + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + +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(): + try: + out = sh(f"ifconfig {name}") + except RuntimeError: + pass + else: + 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 "