From 7014a682befa3f6fccd38b274c78676f707488ff Mon Sep 17 00:00:00 2001 From: alexpolo1 Date: Tue, 17 Jun 2025 23:33:34 +0200 Subject: [PATCH] Add comprehensive Unicode handling tests for psutil - Introduced a new test suite for validating Unicode support in various psutil APIs, ensuring that all string outputs are correctly handled without raising UnicodeDecodeError. - Implemented tests for Process methods such as cmdline(), cwd(), and name(), as well as disk and network-related functions. - Added tests for handling invalid Unicode paths to verify robustness against encoding issues. - Created a separate test suite for Windows-specific functionalities, ensuring compatibility and correctness across different system calls and APIs. - Enhanced existing tests to cover edge cases and improve overall test coverage for psutil's handling of Unicode strings. --- .../psutil-7.0.0.dist-info/INSTALLER | 1 + .../psutil-7.0.0.dist-info/LICENSE | 29 + .../psutil-7.0.0.dist-info/METADATA | 546 ++++ .../psutil-7.0.0.dist-info/RECORD | 65 + .../psutil-7.0.0.dist-info/REQUESTED | 0 .../psutil-7.0.0.dist-info/WHEEL | 8 + .../psutil-7.0.0.dist-info/top_level.txt | 1 + .../site-packages/psutil/__init__.py | 2407 +++++++++++++++++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 88080 bytes .../__pycache__/_common.cpython-312.pyc | Bin 0 -> 33501 bytes .../psutil/__pycache__/_psaix.cpython-312.pyc | Bin 0 -> 24668 bytes .../psutil/__pycache__/_psbsd.cpython-312.pyc | Bin 0 -> 34520 bytes .../__pycache__/_pslinux.cpython-312.pyc | Bin 0 -> 89825 bytes .../psutil/__pycache__/_psosx.cpython-312.pyc | Bin 0 -> 21532 bytes .../__pycache__/_psposix.cpython-312.pyc | Bin 0 -> 5747 bytes .../__pycache__/_pssunos.cpython-312.pyc | Bin 0 -> 30380 bytes .../__pycache__/_pswindows.cpython-312.pyc | Bin 0 -> 43035 bytes .../site-packages/psutil/_common.py | 950 +++++++ .../python3.12/site-packages/psutil/_psaix.py | 565 ++++ .../python3.12/site-packages/psutil/_psbsd.py | 971 +++++++ .../site-packages/psutil/_pslinux.py | 2295 ++++++++++++++++ .../python3.12/site-packages/psutil/_psosx.py | 544 ++++ .../site-packages/psutil/_psposix.py | 207 ++ .../site-packages/psutil/_pssunos.py | 734 +++++ .../psutil/_psutil_linux.abi3.so | Bin 0 -> 115336 bytes .../psutil/_psutil_posix.abi3.so | Bin 0 -> 71640 bytes .../site-packages/psutil/_pswindows.py | 1103 ++++++++ .../site-packages/psutil/tests/__init__.py | 2025 ++++++++++++++ .../site-packages/psutil/tests/__main__.py | 12 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 91398 bytes .../__pycache__/__main__.cpython-312.pyc | Bin 0 -> 400 bytes .../__pycache__/test_aix.cpython-312.pyc | Bin 0 -> 5155 bytes .../__pycache__/test_bsd.cpython-312.pyc | Bin 0 -> 35778 bytes .../test_connections.cpython-312.pyc | Bin 0 -> 26965 bytes .../test_contracts.cpython-312.pyc | Bin 0 -> 22178 bytes .../__pycache__/test_linux.cpython-312.pyc | Bin 0 -> 133734 bytes .../__pycache__/test_memleaks.cpython-312.pyc | Bin 0 -> 35860 bytes .../__pycache__/test_misc.cpython-312.pyc | Bin 0 -> 44588 bytes .../__pycache__/test_osx.cpython-312.pyc | Bin 0 -> 11401 bytes .../__pycache__/test_posix.cpython-312.pyc | Bin 0 -> 24636 bytes .../__pycache__/test_process.cpython-312.pyc | Bin 0 -> 98267 bytes .../test_process_all.cpython-312.pyc | Bin 0 -> 26399 bytes .../__pycache__/test_scripts.cpython-312.pyc | Bin 0 -> 14247 bytes .../__pycache__/test_sunos.cpython-312.pyc | Bin 0 -> 2147 bytes .../__pycache__/test_system.cpython-312.pyc | Bin 0 -> 54145 bytes .../test_testutils.cpython-312.pyc | Bin 0 -> 38056 bytes .../__pycache__/test_unicode.cpython-312.pyc | Bin 0 -> 16033 bytes .../__pycache__/test_windows.cpython-312.pyc | Bin 0 -> 55216 bytes .../site-packages/psutil/tests/test_aix.py | 142 + .../site-packages/psutil/tests/test_bsd.py | 593 ++++ .../psutil/tests/test_connections.py | 566 ++++ .../psutil/tests/test_contracts.py | 325 +++ .../site-packages/psutil/tests/test_linux.py | 2292 ++++++++++++++++ .../psutil/tests/test_memleaks.py | 487 ++++ .../site-packages/psutil/tests/test_misc.py | 873 ++++++ .../site-packages/psutil/tests/test_osx.py | 197 ++ .../site-packages/psutil/tests/test_posix.py | 488 ++++ .../psutil/tests/test_process.py | 1667 ++++++++++++ .../psutil/tests/test_process_all.py | 535 ++++ .../psutil/tests/test_scripts.py | 240 ++ .../site-packages/psutil/tests/test_sunos.py | 39 + .../site-packages/psutil/tests/test_system.py | 979 +++++++ .../psutil/tests/test_testutils.py | 577 ++++ .../psutil/tests/test_unicode.py | 313 +++ .../psutil/tests/test_windows.py | 914 +++++++ assets/questions.txt | 88 +- assets/session.log | 3 + assets/total_time.txt | 2 +- import-hosts-netbox.py | 49 - main.py | 88 +- 70 files changed, 23771 insertions(+), 149 deletions(-) create mode 100644 .venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/INSTALLER create mode 100644 .venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/LICENSE create mode 100644 .venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/METADATA create mode 100644 .venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/RECORD create mode 100644 .venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/REQUESTED create mode 100644 .venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/WHEEL create mode 100644 .venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/top_level.txt create mode 100644 .venv/lib/python3.12/site-packages/psutil/__init__.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/__pycache__/__init__.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/__pycache__/_common.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/__pycache__/_psaix.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/__pycache__/_psbsd.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/__pycache__/_pslinux.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/__pycache__/_psosx.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/__pycache__/_psposix.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/__pycache__/_pssunos.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/__pycache__/_pswindows.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/_common.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/_psaix.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/_psbsd.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/_pslinux.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/_psosx.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/_psposix.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/_pssunos.py create mode 100755 .venv/lib/python3.12/site-packages/psutil/_psutil_linux.abi3.so create mode 100755 .venv/lib/python3.12/site-packages/psutil/_psutil_posix.abi3.so create mode 100644 .venv/lib/python3.12/site-packages/psutil/_pswindows.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__init__.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__main__.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/__init__.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/__main__.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_aix.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_bsd.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_connections.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_contracts.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_linux.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_memleaks.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_misc.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_osx.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_posix.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_process.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_process_all.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_scripts.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_sunos.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_system.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_testutils.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_unicode.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_windows.cpython-312.pyc create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_aix.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_bsd.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_connections.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_contracts.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_linux.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_memleaks.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_misc.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_osx.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_posix.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_process.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_process_all.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_scripts.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_sunos.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_system.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_testutils.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_unicode.py create mode 100644 .venv/lib/python3.12/site-packages/psutil/tests/test_windows.py delete mode 100644 import-hosts-netbox.py diff --git a/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/INSTALLER b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/LICENSE b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/LICENSE new file mode 100644 index 0000000..cff5eb7 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +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. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/METADATA b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/METADATA new file mode 100644 index 0000000..685d2e2 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/METADATA @@ -0,0 +1,546 @@ +Metadata-Version: 2.1 +Name: psutil +Version: 7.0.0 +Summary: Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7. +Home-page: https://github.com/giampaolo/psutil +Author: Giampaolo Rodola +Author-email: g.rodola@gmail.com +License: BSD-3-Clause +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,performance,metrics,agent,observability +Platform: Platform Independent +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Environment :: Win32 (MS Windows) +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows :: Windows 10 +Classifier: Operating System :: Microsoft :: Windows :: Windows 7 +Classifier: Operating System :: Microsoft :: Windows :: Windows 8 +Classifier: Operating System :: Microsoft :: Windows :: Windows 8.1 +Classifier: Operating System :: Microsoft :: Windows :: Windows Server 2003 +Classifier: Operating System :: Microsoft :: Windows :: Windows Server 2008 +Classifier: Operating System :: Microsoft :: Windows :: Windows Vista +Classifier: Operating System :: Microsoft +Classifier: Operating System :: OS Independent +Classifier: Operating System :: POSIX :: AIX +Classifier: Operating System :: POSIX :: BSD :: FreeBSD +Classifier: Operating System :: POSIX :: BSD :: NetBSD +Classifier: Operating System :: POSIX :: BSD :: OpenBSD +Classifier: Operating System :: POSIX :: BSD +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: POSIX :: SunOS/Solaris +Classifier: Operating System :: POSIX +Classifier: Programming Language :: C +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: System :: Benchmark +Classifier: Topic :: System :: Hardware :: Hardware Drivers +Classifier: Topic :: System :: Hardware +Classifier: Topic :: System :: Monitoring +Classifier: Topic :: System :: Networking :: Monitoring :: Hardware Watchdog +Classifier: Topic :: System :: Networking :: Monitoring +Classifier: Topic :: System :: Networking +Classifier: Topic :: System :: Operating System +Classifier: Topic :: System :: Systems Administration +Classifier: Topic :: Utilities +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +License-File: LICENSE +Provides-Extra: dev +Requires-Dist: pytest ; extra == 'dev' +Requires-Dist: pytest-xdist ; extra == 'dev' +Requires-Dist: setuptools ; extra == 'dev' +Requires-Dist: abi3audit ; extra == 'dev' +Requires-Dist: black (==24.10.0) ; extra == 'dev' +Requires-Dist: check-manifest ; extra == 'dev' +Requires-Dist: coverage ; extra == 'dev' +Requires-Dist: packaging ; extra == 'dev' +Requires-Dist: pylint ; extra == 'dev' +Requires-Dist: pyperf ; extra == 'dev' +Requires-Dist: pypinfo ; extra == 'dev' +Requires-Dist: pytest-cov ; extra == 'dev' +Requires-Dist: requests ; extra == 'dev' +Requires-Dist: rstcheck ; extra == 'dev' +Requires-Dist: ruff ; extra == 'dev' +Requires-Dist: sphinx ; extra == 'dev' +Requires-Dist: sphinx-rtd-theme ; extra == 'dev' +Requires-Dist: toml-sort ; extra == 'dev' +Requires-Dist: twine ; extra == 'dev' +Requires-Dist: virtualenv ; extra == 'dev' +Requires-Dist: vulture ; extra == 'dev' +Requires-Dist: wheel ; extra == 'dev' +Provides-Extra: test +Requires-Dist: pytest ; extra == 'test' +Requires-Dist: pytest-xdist ; extra == 'test' +Requires-Dist: setuptools ; extra == 'test' + +| |downloads| |stars| |forks| |contributors| |coverage| +| |version| |py-versions| |packages| |license| +| |github-actions-wheels| |github-actions-bsd| |doc| |twitter| |tidelift| + +.. |downloads| image:: https://img.shields.io/pypi/dm/psutil.svg + :target: https://pepy.tech/project/psutil + :alt: Downloads + +.. |stars| image:: https://img.shields.io/github/stars/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/stargazers + :alt: Github stars + +.. |forks| image:: https://img.shields.io/github/forks/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/network/members + :alt: Github forks + +.. |contributors| image:: https://img.shields.io/github/contributors/giampaolo/psutil.svg + :target: https://github.com/giampaolo/psutil/graphs/contributors + :alt: Contributors + +.. |github-actions-wheels| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/build.yml.svg?label=Linux%2C%20macOS%2C%20Windows + :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Abuild + :alt: Linux, macOS, Windows + +.. |github-actions-bsd| image:: https://img.shields.io/github/actions/workflow/status/giampaolo/psutil/.github/workflows/bsd.yml.svg?label=FreeBSD,%20NetBSD,%20OpenBSD + :target: https://github.com/giampaolo/psutil/actions?query=workflow%3Absd-tests + :alt: FreeBSD, NetBSD, OpenBSD + +.. |coverage| image:: https://coveralls.io/repos/github/giampaolo/psutil/badge.svg?branch=master + :target: https://coveralls.io/github/giampaolo/psutil?branch=master + :alt: Test coverage (coverall.io) + +.. |doc| image:: https://readthedocs.org/projects/psutil/badge/?version=latest + :target: https://psutil.readthedocs.io/en/latest/ + :alt: Documentation Status + +.. |version| image:: https://img.shields.io/pypi/v/psutil.svg?label=pypi + :target: https://pypi.org/project/psutil + :alt: Latest version + +.. |py-versions| image:: https://img.shields.io/pypi/pyversions/psutil.svg + :alt: Supported Python versions + +.. |packages| image:: https://repology.org/badge/tiny-repos/python:psutil.svg + :target: https://repology.org/metapackage/python:psutil/versions + :alt: Binary packages + +.. |license| image:: https://img.shields.io/pypi/l/psutil.svg + :target: https://github.com/giampaolo/psutil/blob/master/LICENSE + :alt: License + +.. |twitter| image:: https://img.shields.io/twitter/follow/grodola.svg?label=follow&style=flat&logo=twitter&logoColor=4FADFF + :target: https://twitter.com/grodola + :alt: Twitter Follow + +.. |tidelift| image:: https://tidelift.com/badges/github/giampaolo/psutil?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-psutil?utm_source=pypi-psutil&utm_medium=referral&utm_campaign=readme + :alt: Tidelift + +----- + +Quick links +=========== + +- `Home page `_ +- `Install `_ +- `Documentation `_ +- `Download `_ +- `Forum `_ +- `StackOverflow `_ +- `Blog `_ +- `What's new `_ + + +Summary +======= + +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 classic UNIX command line tools +such as *ps, top, iotop, lsof, netstat, ifconfig, free* and others. +psutil currently supports the following platforms: + +- **Linux** +- **Windows** +- **macOS** +- **FreeBSD, OpenBSD**, **NetBSD** +- **Sun Solaris** +- **AIX** + +Supported Python versions are cPython 3.6+ and `PyPy `__. +Latest psutil version supporting Python 2.7 is +`psutil 6.1.1 `__. + +Funding +======= + +While psutil is free software and will always be, the project would benefit +immensely from some funding. +Keeping up with bug reports and maintenance has become hardly sustainable for +me alone in terms of time. +If you're a company that's making significant use of psutil you can consider +becoming a sponsor via `GitHub Sponsors `__, +`Open Collective `__ or +`PayPal `__ +and have your logo displayed in here and psutil `doc `__. + +Sponsors +======== + +.. image:: https://github.com/giampaolo/psutil/raw/master/docs/_static/tidelift-logo.png + :width: 200 + :alt: Alternative text + +`Add your logo `__. + +Example usages +============== + +This represents pretty much the whole psutil API. + +CPU +--- + +.. code-block:: python + + >>> import psutil + >>> + >>> psutil.cpu_times() + scputimes(user=3961.46, nice=169.729, system=2150.659, idle=16900.540, iowait=629.59, irq=0.0, softirq=19.42, steal=0.0, guest=0, guest_nice=0.0) + >>> + >>> for x in range(3): + ... psutil.cpu_percent(interval=1) + ... + 4.0 + 5.9 + 3.8 + >>> + >>> for x in range(3): + ... psutil.cpu_percent(interval=1, percpu=True) + ... + [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:: python + + >>> psutil.virtual_memory() + svmem(total=10367352832, available=6472179712, percent=37.6, used=8186245120, free=2181107712, active=4748992512, inactive=2758115328, buffers=790724608, cached=3500347392, shared=787554304) + >>> psutil.swap_memory() + sswap(total=2097147904, used=296128512, free=1801019392, percent=14.1, sin=304193536, sout=677842944) + >>> + +Disks +----- + +.. code-block:: python + + >>> psutil.disk_partitions() + [sdiskpart(device='/dev/sda1', mountpoint='/', fstype='ext4', opts='rw,nosuid'), + sdiskpart(device='/dev/sda2', mountpoint='/home', fstype='ext', opts='rw')] + >>> + >>> 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:: python + + >>> psutil.net_io_counters(pernic=True) + {'eth0': netio(bytes_sent=485291293, bytes_recv=6004858642, packets_sent=3251564, packets_recv=4787798, errin=0, errout=0, dropin=0, dropout=0), + 'lo': netio(bytes_sent=2838627, bytes_recv=2838627, packets_sent=30567, packets_recv=30567, errin=0, errout=0, dropin=0, dropout=0)} + >>> + >>> 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:: 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)]} + >>> + >>> psutil.sensors_fans() + {'asus': [sfan(label='cpu_fan', current=3200)]} + >>> + >>> psutil.sensors_battery() + sbattery(percent=93, secsleft=16628, power_plugged=False) + >>> + +Other system info +----------------- + +.. code-block:: python + + >>> import psutil + >>> psutil.users() + [suser(name='giampaolo', terminal='pts/2', host='localhost', started=1340737536.0, pid=1352), + suser(name='giampaolo', terminal='pts/3', host='localhost', started=1340737792.0, pid=1788)] + >>> + >>> psutil.boot_time() + 1365519115.0 + >>> + +Process management +------------------ + +.. code-block:: python + + >>> import psutil + >>> psutil.pids() + [1, 2, 3, 4, 5, 6, 7, 46, 48, 50, 51, 178, 182, 222, 223, 224, 268, 1215, + 1216, 1220, 1221, 1243, 1244, 1301, 1601, 2237, 2355, 2637, 2774, 3932, + 4176, 4177, 4185, 4187, 4189, 4225, 4243, 4245, 4263, 4282, 4306, 4311, + 4312, 4313, 4314, 4337, 4339, 4357, 4358, 4363, 4383, 4395, 4408, 4433, + 4443, 4445, 4446, 5167, 5234, 5235, 5252, 5318, 5424, 5644, 6987, 7054, + 7055, 7071] + >>> + >>> p = psutil.Process(7055) + >>> p + psutil.Process(pid=7055, name='python3', status='running', started='09:04:44') + >>> p.pid + 7055 + >>> p.name() + 'python3' + >>> p.exe() + '/usr/bin/python3' + >>> p.cwd() + '/home/giampaolo' + >>> p.cmdline() + ['/usr/bin/python3', 'main.py'] + >>> + >>> p.ppid() + 7054 + >>> p.parent() + psutil.Process(pid=4699, name='bash', status='sleeping', started='09:06:44') + >>> p.parents() + [psutil.Process(pid=4699, name='bash', started='09:06:44'), + psutil.Process(pid=4689, name='gnome-terminal-server', status='sleeping', started='0:06:44'), + psutil.Process(pid=1, name='systemd', status='sleeping', started='05:56:55')] + >>> p.children(recursive=True) + [psutil.Process(pid=29835, name='python3', status='sleeping', started='11:45:38'), + psutil.Process(pid=29836, name='python3', status='waking', started='11:43:39')] + >>> + >>> p.status() + 'running' + >>> p.create_time() + 1267551141.5019531 + >>> p.terminal() + '/dev/pts/0' + >>> + >>> p.username() + 'giampaolo' + >>> p.uids() + puids(real=1000, effective=1000, saved=1000) + >>> p.gids() + pgids(real=1000, effective=1000, saved=1000) + >>> + >>> p.cpu_times() + pcputimes(user=1.02, system=0.31, children_user=0.32, children_system=0.1, iowait=0.0) + >>> p.cpu_percent(interval=1.0) + 12.1 + >>> p.cpu_affinity() + [0, 1, 2, 3] + >>> p.cpu_affinity([0, 1]) # set + >>> p.cpu_num() + 1 + >>> + >>> p.memory_info() + pmem(rss=10915840, vms=67608576, shared=3313664, text=2310144, lib=0, data=7262208, dirty=0) + >>> p.memory_full_info() # "real" USS memory usage (Linux, macOS, Win only) + pfullmem(rss=10199040, vms=52133888, shared=3887104, text=2867200, lib=0, data=5967872, dirty=0, uss=6545408, pss=6872064, swap=0) + >>> p.memory_percent() + 0.7823 + >>> p.memory_maps() + [pmmap_grouped(path='/lib/x8664-linux-gnu/libutil-2.15.so', rss=32768, size=2125824, pss=32768, shared_clean=0, shared_dirty=0, private_clean=20480, private_dirty=12288, referenced=32768, anonymous=12288, swap=0), + pmmap_grouped(path='/lib/x8664-linux-gnu/libc-2.15.so', rss=3821568, size=3842048, pss=3821568, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=3821568, referenced=3575808, anonymous=3821568, swap=0), + pmmap_grouped(path='[heap]', rss=32768, size=139264, pss=32768, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=32768, referenced=32768, anonymous=32768, swap=0), + pmmap_grouped(path='[stack]', rss=2465792, size=2494464, pss=2465792, shared_clean=0, shared_dirty=0, private_clean=0, private_dirty=2465792, referenced=2277376, anonymous=2465792, swap=0), + ...] + >>> + >>> p.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='ESTABLISHED'), + pconn(fd=117, family=, type=, laddr=addr(ip='10.0.0.1', port=43761), raddr=addr(ip='72.14.234.100', port=80), status='CLOSING')] + >>> + >>> p.threads() + [pthread(id=5234, user_time=22.5, system_time=9.2891), + pthread(id=5237, user_time=0.0707, system_time=1.1)] + >>> + >>> p.num_threads() + 4 + >>> p.num_fds() + 8 + >>> p.num_ctx_switches() + pctxsw(voluntary=78, involuntary=19) + >>> + >>> p.nice() + 0 + >>> p.nice(10) # set + >>> + >>> p.ionice(psutil.IOPRIO_CLASS_IDLE) # IO priority (Win and Linux only) + >>> p.ionice() + pionice(ioclass=, value=0) + >>> + >>> p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) # set resource limits (Linux only) + >>> p.rlimit(psutil.RLIMIT_NOFILE) + (5, 5) + >>> + >>> p.environ() + {'LC_PAPER': 'it_IT.UTF-8', 'SHELL': '/bin/bash', 'GREP_OPTIONS': '--color=auto', + 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', + ...} + >>> + >>> p.as_dict() + {'status': 'running', 'num_ctx_switches': pctxsw(voluntary=63, involuntary=1), 'pid': 5457, ...} + >>> p.is_running() + True + >>> p.suspend() + >>> p.resume() + >>> + >>> p.terminate() + >>> p.kill() + >>> p.wait(timeout=3) + + >>> + >>> psutil.test() + USER PID %CPU %MEM VSZ RSS TTY START TIME COMMAND + root 1 0.0 0.0 24584 2240 Jun17 00:00 init + root 2 0.0 0.0 0 0 Jun17 00:00 kthreadd + ... + giampaolo 31475 0.0 0.0 20760 3024 /dev/pts/0 Jun19 00:00 python2.4 + giampaolo 31721 0.0 2.2 773060 181896 00:04 10:30 chrome + root 31763 0.0 0.0 0 0 00:05 00:00 kworker/0:1 + >>> + +Further process APIs +-------------------- + +.. code-block:: python + + >>> import psutil + >>> for proc in psutil.process_iter(['pid', 'name']): + ... print(proc.info) + ... + {'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) + >>> + +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'} + +Projects using psutil +===================== + +Here's some I find particularly interesting: + +- https://github.com/google/grr +- https://github.com/facebook/osquery/ +- https://github.com/nicolargo/glances +- https://github.com/aristocratos/bpytop +- https://github.com/Jahaja/psdash +- https://github.com/ajenti/ajenti +- https://github.com/home-assistant/home-assistant/ + +Portings +======== + +- Go: https://github.com/shirou/gopsutil +- C: https://github.com/hamon-in/cpslib +- Rust: https://github.com/rust-psutil/rust-psutil +- Nim: https://github.com/johnscillieri/psutil-nim + + + diff --git a/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/RECORD b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/RECORD new file mode 100644 index 0000000..e7d2481 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/RECORD @@ -0,0 +1,65 @@ +psutil-7.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +psutil-7.0.0.dist-info/LICENSE,sha256=uJwGOzeG4o4MCjjxkx22H-015p3SopZvvs_-4PRsjRA,1548 +psutil-7.0.0.dist-info/METADATA,sha256=BFPkVTphhSTYAB4vZKg7HCRBmCbKuHv2tzTmcj04PIk,22315 +psutil-7.0.0.dist-info/RECORD,, +psutil-7.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +psutil-7.0.0.dist-info/WHEEL,sha256=rgpVBmjjvbINeGKCkWEGd3f40VHMTsDkQj1Lgil82zE,221 +psutil-7.0.0.dist-info/top_level.txt,sha256=gCNhn57wzksDjSAISmgMJ0aiXzQulk0GJhb2-BAyYgw,7 +psutil/__init__.py,sha256=tPSkFUoX9EGtvKiDzRcSMYhUbHlIdFbCw57a4ysNzLE,86668 +psutil/__pycache__/__init__.cpython-312.pyc,, +psutil/__pycache__/_common.cpython-312.pyc,, +psutil/__pycache__/_psaix.cpython-312.pyc,, +psutil/__pycache__/_psbsd.cpython-312.pyc,, +psutil/__pycache__/_pslinux.cpython-312.pyc,, +psutil/__pycache__/_psosx.cpython-312.pyc,, +psutil/__pycache__/_psposix.cpython-312.pyc,, +psutil/__pycache__/_pssunos.cpython-312.pyc,, +psutil/__pycache__/_pswindows.cpython-312.pyc,, +psutil/_common.py,sha256=0V6pk_6dqkaIMB2fbdlxERWV0jIt8m1yLxMPCqsVzAo,28642 +psutil/_psaix.py,sha256=5haK5IsqogH7ws-uikbh7LJywqsQTL4ihV-KT3CJ-f8,18252 +psutil/_psbsd.py,sha256=3pdjaz9QVPj2OeCRQSXXXGf89is9Fijt328dNIi8Jng,31756 +psutil/_pslinux.py,sha256=29sX7gw6tLXwp8eSLZ_NcVqa42glkZlGGonckWWlTrc,86028 +psutil/_psosx.py,sha256=O0HA58WMQMfMw6SrjyCn2Q3oN3GpQzC66BK9826w038,15877 +psutil/_psposix.py,sha256=kHwIVJ0J3aLvtNJrcs4h0OFMRVFKRtVmGMUvBhZwaRU,7142 +psutil/_pssunos.py,sha256=_N0t0vKPv4hl-rbVkROieAWgWP_pPUBsNO29me7xgc4,24920 +psutil/_psutil_linux.abi3.so,sha256=bC66whE5AHQQCAgRoBXCSe1vsvIHsaZOAY5HrhkXQIE,115336 +psutil/_psutil_posix.abi3.so,sha256=Kl35Rx_zW8_N1mNhs4OH_y4ZR3tq9P2eHCBUWSu6kcA,71640 +psutil/_pswindows.py,sha256=12fUxO7XxwHO0-IVCsWflc2CXbXg8NzfvgHptgTr4nI,35949 +psutil/tests/__init__.py,sha256=naU3g4k4xVCBqXZel9nHJEMoJkQKltxbKyfjDE-6HwM,64104 +psutil/tests/__main__.py,sha256=GYT-hlMnWDtybkJ76DqQcjXPr0jnLeZDTe0lVVeDb7o,309 +psutil/tests/__pycache__/__init__.cpython-312.pyc,, +psutil/tests/__pycache__/__main__.cpython-312.pyc,, +psutil/tests/__pycache__/test_aix.cpython-312.pyc,, +psutil/tests/__pycache__/test_bsd.cpython-312.pyc,, +psutil/tests/__pycache__/test_connections.cpython-312.pyc,, +psutil/tests/__pycache__/test_contracts.cpython-312.pyc,, +psutil/tests/__pycache__/test_linux.cpython-312.pyc,, +psutil/tests/__pycache__/test_memleaks.cpython-312.pyc,, +psutil/tests/__pycache__/test_misc.cpython-312.pyc,, +psutil/tests/__pycache__/test_osx.cpython-312.pyc,, +psutil/tests/__pycache__/test_posix.cpython-312.pyc,, +psutil/tests/__pycache__/test_process.cpython-312.pyc,, +psutil/tests/__pycache__/test_process_all.cpython-312.pyc,, +psutil/tests/__pycache__/test_scripts.cpython-312.pyc,, +psutil/tests/__pycache__/test_sunos.cpython-312.pyc,, +psutil/tests/__pycache__/test_system.cpython-312.pyc,, +psutil/tests/__pycache__/test_testutils.cpython-312.pyc,, +psutil/tests/__pycache__/test_unicode.cpython-312.pyc,, +psutil/tests/__pycache__/test_windows.cpython-312.pyc,, +psutil/tests/test_aix.py,sha256=O5IqMAU3qw1NXvI-nhNb9v1LbNHb1q3iFRe5OkqgpoI,4408 +psutil/tests/test_bsd.py,sha256=wALt2KBsv8LmuSbADGnXx3h3AL7dTheCmPBB9_iwE38,20191 +psutil/tests/test_connections.py,sha256=nL8675MBoPLoWc35ujR7QvMGRWtZjh6u3CmnwLTmts8,21157 +psutil/tests/test_contracts.py,sha256=ffPNOZ1nK4xV7gKYxJs1nOjORkM0WL-PSSxPQvq8pnI,12001 +psutil/tests/test_linux.py,sha256=Vw1hwyn2mGXQ1DYSksBWzfBBAynaiyWhXKRMWF_-3Sg,88895 +psutil/tests/test_memleaks.py,sha256=hC0Ra6v3ZwhMmLuJoCTeLUCJuh5wa0ER8UvHVSVTsio,15121 +psutil/tests/test_misc.py,sha256=PTelE-5fdcmL_49kz46966JNl8VB3-PowwmkwcIyodc,29672 +psutil/tests/test_osx.py,sha256=ysb3dSwJfzo07ElnFJThoJKmwquqQoUsaKeL1sz8zf8,6315 +psutil/tests/test_posix.py,sha256=bNQFfIrgLYNSFxbep9Qsqej46EG7yzH69_tlbpz3odY,17187 +psutil/tests/test_process.py,sha256=7g0vLr5ZdIj_sEzWkdSIpdBlRlcxTGI2UtUDa6KufUE,59881 +psutil/tests/test_process_all.py,sha256=kbe6Hq0UNGuHCx0T9jLok8O-E7RDQi5RLBZyCU_we5Q,18347 +psutil/tests/test_scripts.py,sha256=K--xQZrcNv4ABtax76ADBU_DjWK17cceTPg-jFoGpbI,7725 +psutil/tests/test_sunos.py,sha256=78nUq_4I2ALidWLiOGRjgP7ykFiGm8eGV3JNAiIoOVs,1190 +psutil/tests/test_system.py,sha256=UhmDw0psTdmAgfNmlS176Hv7hOwUl7F148vc7qH19GE,36107 +psutil/tests/test_testutils.py,sha256=2enHiV8mMapwrLwadIj04V3Z4M1OsyH49Hhg5bf3VxU,18338 +psutil/tests/test_unicode.py,sha256=Cv2pp9qS5iQJ8RMPKRow9--QoHsP1Q9ng6aKvbJnSWs,10392 +psutil/tests/test_windows.py,sha256=OESmHBNsYCwfpcqeloFmkvRfAzJf-nV75a0b0ztPUcE,33214 diff --git a/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/REQUESTED b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/WHEEL b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/WHEEL new file mode 100644 index 0000000..cd91456 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/WHEEL @@ -0,0 +1,8 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: false +Tag: cp36-abi3-manylinux_2_12_x86_64 +Tag: cp36-abi3-manylinux2010_x86_64 +Tag: cp36-abi3-manylinux_2_17_x86_64 +Tag: cp36-abi3-manylinux2014_x86_64 + diff --git a/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/top_level.txt b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/top_level.txt new file mode 100644 index 0000000..a4d92cc --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil-7.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +psutil diff --git a/.venv/lib/python3.12/site-packages/psutil/__init__.py b/.venv/lib/python3.12/site-packages/psutil/__init__.py new file mode 100644 index 0000000..cf4a580 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/__init__.py @@ -0,0 +1,2407 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""psutil is a cross-platform library for retrieving information on +running processes and system utilization (CPU, memory, disks, network, +sensors) in Python. Supported platforms: + + - Linux + - Windows + - macOS + - FreeBSD + - OpenBSD + - NetBSD + - Sun Solaris + - AIX + +Supported Python versions are cPython 3.6+ and PyPy. +""" + +import collections +import contextlib +import datetime +import functools +import os +import signal +import socket +import subprocess +import sys +import threading +import time + + +try: + import pwd +except ImportError: + pwd = None + +from . import _common +from ._common import AIX +from ._common import BSD +from ._common import CONN_CLOSE +from ._common import CONN_CLOSE_WAIT +from ._common import CONN_CLOSING +from ._common import CONN_ESTABLISHED +from ._common import CONN_FIN_WAIT1 +from ._common import CONN_FIN_WAIT2 +from ._common import CONN_LAST_ACK +from ._common import CONN_LISTEN +from ._common import CONN_NONE +from ._common import CONN_SYN_RECV +from ._common import CONN_SYN_SENT +from ._common import CONN_TIME_WAIT +from ._common import FREEBSD +from ._common import LINUX +from ._common import MACOS +from ._common import NETBSD +from ._common import NIC_DUPLEX_FULL +from ._common import NIC_DUPLEX_HALF +from ._common import NIC_DUPLEX_UNKNOWN +from ._common import OPENBSD +from ._common import OSX # deprecated alias +from ._common import POSIX +from ._common import POWER_TIME_UNKNOWN +from ._common import POWER_TIME_UNLIMITED +from ._common import STATUS_DEAD +from ._common import STATUS_DISK_SLEEP +from ._common import STATUS_IDLE +from ._common import STATUS_LOCKED +from ._common import STATUS_PARKED +from ._common import STATUS_RUNNING +from ._common import STATUS_SLEEPING +from ._common import STATUS_STOPPED +from ._common import STATUS_TRACING_STOP +from ._common import STATUS_WAITING +from ._common import STATUS_WAKING +from ._common import STATUS_ZOMBIE +from ._common import SUNOS +from ._common import WINDOWS +from ._common import AccessDenied +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 + + +if LINUX: + # This is public API and it will be retrieved from _pslinux.py + # via sys.modules. + PROCFS_PATH = "/proc" + + from . import _pslinux as _psplatform + from ._pslinux import IOPRIO_CLASS_BE # noqa: F401 + from ._pslinux import IOPRIO_CLASS_IDLE # noqa: F401 + from ._pslinux import IOPRIO_CLASS_NONE # noqa: F401 + from ._pslinux import IOPRIO_CLASS_RT # noqa: F401 + +elif WINDOWS: + from . import _pswindows as _psplatform + from ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS # noqa: F401 + from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS # noqa: F401 + from ._psutil_windows import HIGH_PRIORITY_CLASS # noqa: F401 + from ._psutil_windows import IDLE_PRIORITY_CLASS # noqa: F401 + from ._psutil_windows import NORMAL_PRIORITY_CLASS # noqa: F401 + from ._psutil_windows import REALTIME_PRIORITY_CLASS # noqa: F401 + from ._pswindows import CONN_DELETE_TCB # noqa: F401 + from ._pswindows import IOPRIO_HIGH # noqa: F401 + from ._pswindows import IOPRIO_LOW # noqa: F401 + from ._pswindows import IOPRIO_NORMAL # noqa: F401 + from ._pswindows import IOPRIO_VERYLOW # noqa: F401 + +elif MACOS: + from . import _psosx as _psplatform + +elif BSD: + from . import _psbsd as _psplatform + +elif SUNOS: + from . import _pssunos as _psplatform + from ._pssunos import CONN_BOUND # noqa: F401 + from ._pssunos import CONN_IDLE # noqa: F401 + + # This is public writable API which is read from _pslinux.py and + # _pssunos.py via sys.modules. + PROCFS_PATH = "/proc" + +elif AIX: + from . import _psaix as _psplatform + + # This is public API and it will be retrieved from _pslinux.py + # via sys.modules. + PROCFS_PATH = "/proc" + +else: # pragma: no cover + msg = f"platform {sys.platform} is not supported" + raise NotImplementedError(msg) + + +# fmt: off +__all__ = [ + # exceptions + "Error", "NoSuchProcess", "ZombieProcess", "AccessDenied", + "TimeoutExpired", + + # 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", + "STATUS_PARKED", + + "CONN_ESTABLISHED", "CONN_SYN_SENT", "CONN_SYN_RECV", "CONN_FIN_WAIT1", + "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", + "CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING", "CONN_NONE", + # "CONN_IDLE", "CONN_BOUND", + + "AF_LINK", + + "NIC_DUPLEX_FULL", "NIC_DUPLEX_HALF", "NIC_DUPLEX_UNKNOWN", + + "POWER_TIME_UNKNOWN", "POWER_TIME_UNLIMITED", + + "BSD", "FREEBSD", "LINUX", "NETBSD", "OPENBSD", "MACOS", "OSX", "POSIX", + "SUNOS", "WINDOWS", "AIX", + + # "RLIM_INFINITY", "RLIMIT_AS", "RLIMIT_CORE", "RLIMIT_CPU", "RLIMIT_DATA", + # "RLIMIT_FSIZE", "RLIMIT_LOCKS", "RLIMIT_MEMLOCK", "RLIMIT_NOFILE", + # "RLIMIT_NPROC", "RLIMIT_RSS", "RLIMIT_STACK", "RLIMIT_MSGQUEUE", + # "RLIMIT_NICE", "RLIMIT_RTPRIO", "RLIMIT_RTTIME", "RLIMIT_SIGPENDING", + + # classes + "Process", "Popen", + + # functions + "pid_exists", "pids", "process_iter", "wait_procs", # proc + "virtual_memory", "swap_memory", # memory + "cpu_times", "cpu_percent", "cpu_times_percent", "cpu_count", # cpu + "cpu_stats", # "cpu_freq", "getloadavg" + "net_io_counters", "net_connections", "net_if_addrs", # network + "net_if_stats", + "disk_io_counters", "disk_partitions", "disk_usage", # disk + # "sensors_temperatures", "sensors_battery", "sensors_fans" # sensors + "users", "boot_time", # others +] +# fmt: on + + +__all__.extend(_psplatform.__extra__all__) + +# Linux, FreeBSD +if hasattr(_psplatform.Process, "rlimit"): + # Populate global namespace with RLIM* constants. + from . import _psutil_posix + + _globals = globals() + _name = None + for _name in dir(_psutil_posix): + if _name.startswith('RLIM') and _name.isupper(): + _globals[_name] = getattr(_psutil_posix, _name) + __all__.append(_name) + del _globals, _name + +AF_LINK = _psplatform.AF_LINK + +__author__ = "Giampaolo Rodola'" +__version__ = "7.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 +# up importing a python module using a C extension module which +# 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 = f"version conflict: {_psplatform.cext.__file__!r} C extension " + msg += "module was built for another version of psutil" + if hasattr(_psplatform.cext, 'version'): + v = ".".join(list(str(_psplatform.cext.version))) + msg += f" ({v} instead of {__version__})" + else: + 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) + + +# ===================================================================== +# --- Utils +# ===================================================================== + + +if hasattr(_psplatform, 'ppid_map'): + # Faster version (Windows and Linux). + _ppid_map = _psplatform.ppid_map +else: # pragma: no cover + + def _ppid_map(): + """Return a {pid: ppid, ...} dict for all running processes in + one shot. Used to speed up Process.children(). + """ + ret = {} + for pid in pids(): + try: + ret[pid] = _psplatform.Process(pid).ppid() + except (NoSuchProcess, ZombieProcess): + pass + return ret + + +def _pprint_secs(secs): + """Format seconds in a human readable form.""" + now = time.time() + secs_ago = int(now - secs) + 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: + """Represents an OS process with the given PID. + If PID is omitted current process PID (os.getpid()) is used. + Raise NoSuchProcess if PID does not exist. + + Note that most of the methods of this class do not make sure that + the PID of the process being queried has been reused. That means + that you may end up retrieving information for another process. + + The only exceptions for which process identity is pre-emptively + checked and guaranteed are: + + - parent() + - children() + - nice() (set) + - ionice() (set) + - rlimit() (set) + - cpu_affinity (set) + - suspend() + - resume() + - send_signal() + - terminate() + - kill() + + To prevent this problem for all other methods you can use + is_running() before querying the process. + """ + + def __init__(self, pid=None): + self._init(pid) + + def _init(self, pid, _ignore_nsp=False): + if pid is None: + pid = os.getpid() + else: + if pid < 0: + 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._lock = threading.RLock() + # used for caching on Windows only (on POSIX ppid may change) + self._ppid = None + # platform-specific modules define an _psplatform.Process + # implementation class + self._proc = _psplatform.Process(pid) + self._last_sys_cpu_times = None + self._last_proc_cpu_times = None + self._exitcode = _SENTINEL + self._ident = (self.pid, None) + try: + self._ident = self._get_ident() + except AccessDenied: + # 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 + # not always) and pids() return them so just go on. + pass + except NoSuchProcess: + if not _ignore_nsp: + 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 process creation time to the mix. We need + this in order to prevent killing the wrong process later on. + This is also known as PID reuse or PID recycling problem. + + The reliability of this strategy mostly depends on + create_time() precision, which is 0.01 secs on Linux. The + assumption is that, after a process terminates, the kernel + won't reuse the same PID after such a short period of time + (0.01 secs). Technically this is inherently racy, but + practically it should be good enough. + """ + if WINDOWS: + # Use create_time() fast method in order to speedup + # `process_iter()`. This means we'll get AccessDenied for + # most ADMIN processes, but that's fine since it means + # we'll also get AccessDenied on kill(). + # https://github.com/giampaolo/psutil/issues/2366#issuecomment-2381646555 + self._create_time = self._proc.create_time(fast_only=True) + return (self.pid, self._create_time) + else: + return (self.pid, self.create_time()) + + def __str__(self): + info = collections.OrderedDict() + info["pid"] = self.pid + if self._name: + info['name'] = self._name + with self.oneshot(): + if self._pid_reused: + info["status"] = "terminated + PID reused" + else: + try: + info["name"] = self.name() + info["status"] = self.status() + except ZombieProcess: + info["status"] = "zombie" + except NoSuchProcess: + info["status"] = "terminated" + except AccessDenied: + pass + + if self._exitcode not in {_SENTINEL, None}: + info["exitcode"] = self._exitcode + if self._create_time is not None: + info['started'] = _pprint_secs(self._create_time) + + return "{}.{}({})".format( + self.__class__.__module__, + self.__class__.__name__, + ", ".join([f"{k}={v!r}" for k, v in info.items()]), + ) + + __repr__ = __str__ + + def __eq__(self, other): + # Test for equality with another Process object based + # on PID and creation time. + if not isinstance(other, Process): + return NotImplemented + if OPENBSD or NETBSD: # pragma: no cover + # Zombie processes on Open/NetBSD have a creation time of + # 0.0. This covers the case when a process started normally + # (so it has a ctime), then it turned into a zombie. It's + # important to do this because is_running() depends on + # __eq__. + pid1, ident1 = self._ident + pid2, ident2 = other._ident + if pid1 == pid2: + if ident1 and not ident2: + try: + return self.status() == STATUS_ZOMBIE + except Error: + pass + return self._ident == other._ident + + def __ne__(self, other): + return not self == other + + def __hash__(self): + if self._hash is None: + self._hash = hash(self._ident) + return self._hash + + def _raise_if_pid_reused(self): + """Raises 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): + """The process PID.""" + return self._pid + + # --- 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. + + >>> import psutil + >>> p = psutil.Process() + >>> with p.oneshot(): + ... p.name() # collect multiple info + ... p.cpu_times() # return cached value + ... p.cpu_percent() # return cached value + ... p.create_time() # return cached value + ... + >>> + """ + 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 + else: + try: + # cached in case cpu_percent() is used + self.cpu_times.cache_activate(self) + # cached in case memory_percent() is used + self.memory_info.cache_activate(self) + # cached in case parent() is used + self.ppid.cache_activate(self) + # cached in case username() is used + if POSIX: + self.uids.cache_activate(self) + # specific implementation cache + self._proc.oneshot_enter() + yield + finally: + self.cpu_times.cache_deactivate(self) + self.memory_info.cache_deactivate(self) + self.ppid.cache_deactivate(self) + if POSIX: + self.uids.cache_deactivate(self) + self._proc.oneshot_exit() + + def as_dict(self, attrs=None, ad_value=None): + """Utility method returning process information as a + 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. + *ad_value* is the value which gets assigned in case + AccessDenied or ZombieProcess exception is raised when + retrieving that particular process information. + """ + valid_names = _as_dict_attrnames + if attrs is not None: + if not isinstance(attrs, (list, tuple, set, frozenset)): + msg = f"invalid attrs type {type(attrs)}" + raise TypeError(msg) + attrs = set(attrs) + invalid_names = attrs - valid_names + if invalid_names: + msg = "invalid attr name{} {}".format( + "s" if len(invalid_names) > 1 else "", + ", ".join(map(repr, invalid_names)), + ) + raise ValueError(msg) + + retdict = {} + ls = attrs or valid_names + with self.oneshot(): + for name in ls: + try: + if name == 'pid': + ret = self.pid + else: + meth = getattr(self, name) + ret = meth() + except (AccessDenied, ZombieProcess): + ret = ad_value + except NotImplementedError: + # in case of not implemented functionality (may happen + # on old or exotic systems) we want to crash only if + # the user explicitly asked for that particular attr + if attrs: + raise + continue + retdict[name] = ret + return retdict + + def parent(self): + """Return the parent process as a Process object pre-emptively + checking whether PID has been reused. + If no parent is known return None. + """ + lowest_pid = _LOWEST_PID if _LOWEST_PID is not None else pids()[0] + if self.pid == lowest_pid: + return None + ppid = self.ppid() + if ppid is not None: + ctime = self.create_time() + try: + parent = Process(ppid) + if parent.create_time() <= ctime: + return parent + # ...else ppid has been reused by another process + except NoSuchProcess: + pass + + def parents(self): + """Return the parents of this process as a list of Process + instances. If no parents are known return an empty list. + """ + parents = [] + proc = self.parent() + while proc is not None: + parents.append(proc) + proc = proc.parent() + return parents + + def is_running(self): + """Return whether this process is running. + + It also checks if PID has been reused by another process, in + which case it will remove the process from `process_iter()` + internal cache and return False. + """ + if self._gone or self._pid_reused: + return False + try: + # Checking if PID is alive is not enough as the PID might + # have been reused by another process. 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. + return True + except NoSuchProcess: + self._gone = True + return False + + # --- actual API + + @memoize_when_activated + def ppid(self): + """The process parent PID. + On Windows the return value is cached after first call. + """ + # On POSIX we don't want to cache the ppid as it may unexpectedly + # change to 1 (init) in case this process turns into a zombie: + # https://github.com/giampaolo/psutil/issues/321 + # http://stackoverflow.com/questions/356722/ + + # 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): + """The process name. The return value is cached after first call.""" + # Process name is only cached on Windows as on POSIX it may + # change, see: + # https://github.com/giampaolo/psutil/issues/692 + if WINDOWS and self._name is not None: + return self._name + name = self._proc.name() + if POSIX and len(name) >= 15: + # On UNIX the name gets truncated to the first 15 characters. + # If it matches the first part of the cmdline we return that + # one instead because it's usually more explicative. + # Examples are "gnome-keyring-d" vs. "gnome-keyring-daemon". + try: + cmdline = self.cmdline() + 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: + extended_name = os.path.basename(cmdline[0]) + if extended_name.startswith(name): + name = extended_name + self._name = name + self._proc._name = name + return name + + def exe(self): + """The process executable as an absolute path. + 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 + cmdline = self.cmdline() + if cmdline and hasattr(os, 'access') and hasattr(os, 'X_OK'): + exe = cmdline[0] # the possible exe + # 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) + ): + return exe + if isinstance(fallback, AccessDenied): + raise fallback + return fallback + + if self._exe is None: + try: + exe = self._proc.exe() + except AccessDenied as err: + return guess_it(fallback=err) + else: + if not exe: + # underlying implementation can legitimately return an + # empty string; if that's the case we don't want to + # raise AD while guessing from the cmdline + try: + exe = guess_it(fallback=exe) + except AccessDenied: + pass + self._exe = exe + return self._exe + + def cmdline(self): + """The command line this process has been called with.""" + return self._proc.cmdline() + + def status(self): + """The process current status as a STATUS_* constant.""" + try: + return self._proc.status() + except ZombieProcess: + return STATUS_ZOMBIE + + def username(self): + """The name of the user that owns the process. + On UNIX this is calculated by using *real* process uid. + """ + if POSIX: + if pwd is None: + # might happen if python was installed from sources + msg = "requires pwd module shipped with standard python" + raise ImportError(msg) + real_uid = self.uids().real + try: + return pwd.getpwuid(real_uid).pw_name + except KeyError: + # the uid can't be resolved by the system + return str(real_uid) + else: + return self._proc.username() + + def create_time(self): + """The process creation time as a floating point number + expressed in seconds since the epoch. + The return value is cached after first call. + """ + if self._create_time is None: + self._create_time = self._proc.create_time() + return self._create_time + + def cwd(self): + """Process current working directory as an absolute path.""" + return self._proc.cwd() + + def nice(self, value=None): + """Get or set process niceness (priority).""" + if value is None: + return self._proc.nice_get() + else: + self._raise_if_pid_reused() + self._proc.nice_set(value) + + if POSIX: + + @memoize_when_activated + def uids(self): + """Return process UIDs as a (real, effective, saved) + namedtuple. + """ + return self._proc.uids() + + def gids(self): + """Return process GIDs as a (real, effective, saved) + namedtuple. + """ + return self._proc.gids() + + def terminal(self): + """The terminal associated with this process, if any, + else None. + """ + return self._proc.terminal() + + def num_fds(self): + """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. + """ + return self._proc.io_counters() + + # Linux and Windows + if hasattr(_psplatform.Process, "ionice_get"): + + def ionice(self, ioclass=None, value=None): + """Get or set process I/O niceness (priority). + + 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). + + Available on Linux and Windows > Vista only. + """ + if ioclass is None: + if value is not None: + 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 / FreeBSD only + if hasattr(_psplatform.Process, "rlimit"): + + def rlimit(self, resource, limits=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. + + See "man prlimit" for further info. + Available on Linux and FreeBSD only. + """ + if limits is not None: + self._raise_if_pid_reused() + return self._proc.rlimit(resource, limits) + + # Windows, Linux and FreeBSD only + if hasattr(_psplatform.Process, "cpu_affinity_get"): + + def cpu_affinity(self, cpus=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). + """ + if cpus is None: + return sorted(set(self._proc.cpu_affinity_get())) + else: + self._raise_if_pid_reused() + if not cpus: + if hasattr(self._proc, "_get_eligible_cpus"): + cpus = self._proc._get_eligible_cpus() + else: + cpus = tuple(range(len(cpu_times(percpu=True)))) + self._proc.cpu_affinity_set(list(set(cpus))) + + # Linux, FreeBSD, SunOS + if hasattr(_psplatform.Process, "cpu_num"): + + def cpu_num(self): + """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. + """ + return self._proc.cpu_num() + + # 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. + """ + return self._proc.environ() + + if WINDOWS: + + def num_handles(self): + """Return the number of handles opened by this process + (Windows only). + """ + return self._proc.num_handles() + + def num_ctx_switches(self): + """Return the number of voluntary and involuntary context + switches performed by this process. + """ + return self._proc.num_ctx_switches() + + def num_threads(self): + """Return the number of threads used by this process.""" + return self._proc.num_threads() + + if hasattr(_psplatform.Process, "threads"): + + def threads(self): + """Return threads opened by process as a list of + (id, user_time, system_time) namedtuples representing + thread id and thread CPU times (user/system). + On OpenBSD this method requires root access. + """ + return self._proc.threads() + + def children(self, recursive=False): + """Return the children of this process as a list of Process + instances, pre-emptively checking whether PID has been reused. + If *recursive* is True return all the parent descendants. + + Example (A == this process): + + A ─┐ + │ + ├─ B (child) ─┐ + │ └─ X (grandchild) ─┐ + │ └─ Y (great grandchild) + ├─ C (child) + └─ D (child) + + >>> import psutil + >>> p = psutil.Process() + >>> 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 listed as the reference to process A + is lost. + """ + self._raise_if_pid_reused() + ppid_map = _ppid_map() + ret = [] + if not recursive: + for pid, ppid in ppid_map.items(): + if ppid == self.pid: + try: + child = Process(pid) + # if child happens to be older than its parent + # (self) it means child's PID has been reused + if self.create_time() <= child.create_time(): + ret.append(child) + except (NoSuchProcess, ZombieProcess): + pass + else: + # Construct a {pid: [child pids]} dict + reverse_ppid_map = collections.defaultdict(list) + for pid, ppid in ppid_map.items(): + reverse_ppid_map[ppid].append(pid) + # Recursively traverse that dict, starting from self.pid, + # such that we only call Process() on actual children + seen = set() + stack = [self.pid] + while stack: + pid = stack.pop() + if pid in seen: + # Since pids can be reused while the ppid_map is + # constructed, there may be rare instances where + # there's a cycle in the recorded process "tree". + continue + seen.add(pid) + for child_pid in reverse_ppid_map[pid]: + try: + child = Process(child_pid) + # if child happens to be older than its parent + # (self) it means child's PID has been reused + intime = self.create_time() <= child.create_time() + if intime: + ret.append(child) + stack.append(child_pid) + except (NoSuchProcess, ZombieProcess): + pass + return ret + + def cpu_percent(self, interval=None): + """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. + + When *interval* is > 0.0 compares process times to system CPU + times elapsed before and after the interval (blocking). + + In this case is recommended for accuracy that this function + be called with at least 0.1 seconds between calls. + + A value > 100.0 can be returned in case of processes running + multiple threads on different CPU cores. + + The returned value is explicitly NOT split evenly between + all available logical CPUs. 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%. + + Examples: + + >>> import psutil + >>> p = psutil.Process(os.getpid()) + >>> # blocking + >>> p.cpu_percent(interval=1) + 2.0 + >>> # non-blocking (percentage since last call) + >>> p.cpu_percent(interval=None) + 2.9 + >>> + """ + blocking = interval is not None and interval > 0.0 + if interval is not None and interval < 0: + msg = f"interval is not positive (got {interval!r})" + raise ValueError(msg) + num_cpus = cpu_count() or 1 + + def timer(): + return _timer() * num_cpus + + if blocking: + st1 = timer() + pt1 = self._proc.cpu_times() + time.sleep(interval) + st2 = timer() + pt2 = self._proc.cpu_times() + else: + st1 = self._last_sys_cpu_times + pt1 = self._last_proc_cpu_times + st2 = timer() + pt2 = self._proc.cpu_times() + if st1 is None or pt1 is None: + self._last_sys_cpu_times = st2 + self._last_proc_cpu_times = pt2 + return 0.0 + + delta_proc = (pt2.user - pt1.user) + (pt2.system - pt1.system) + delta_time = st2 - st1 + # reset values for next call in case of interval == None + self._last_sys_cpu_times = st2 + self._last_proc_cpu_times = pt2 + + try: + # 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 + except ZeroDivisionError: + # interval was too low + return 0.0 + else: + # Note 1: + # in order to emulate "top" we multiply the value for the num + # of CPU cores. This way the busy process will be reported as + # having 100% (or more) usage. + # + # Note 2: + # taskmgr.exe on Windows differs in that it will show 50% + # instead. + # + # Note 3: + # a percentage > 100 is legitimate as it can result from a + # process with multiple threads running on different CPU + # cores (top does the same), see: + # http://stackoverflow.com/questions/1032357 + # https://github.com/giampaolo/psutil/issues/474 + single_cpu_percent = overall_cpus_percent * num_cpus + return round(single_cpu_percent, 1) + + @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 macOS and Windows children_user and children_system are + always set to 0. + """ + return self._proc.cpu_times() + + @memoize_when_activated + def memory_info(self): + """Return a namedtuple with variable fields depending on the + platform, representing memory information about the process. + + The "portable" fields available on all platforms are `rss` and `vms`. + + All numbers are expressed in bytes. + """ + return self._proc.memory_info() + + def memory_full_info(self): + """This method returns the same information as memory_info(), + plus, on some platform (Linux, macOS, Windows), also provides + additional metrics (USS, PSS and swap). + The additional metrics provide a better representation of actual + process memory usage. + + Namely USS is the memory which is unique to a process and which + would be freed if the process was terminated right now. + + It does so by passing through the whole process address. + As such it usually requires higher user privileges than + memory_info() and is considerably slower. + """ + return self._proc.memory_full_info() + + def memory_percent(self, 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 (defaults to "rss"). + The list of available strings can be obtained like this: + + >>> psutil.Process().memory_info()._fields + ('rss', 'vms', 'shared', 'text', 'lib', 'data', 'dirty', 'uss', 'pss') + """ + valid_types = list(_psplatform.pfullmem._fields) + if memtype not in valid_types: + msg = ( + f"invalid memtype {memtype!r}; valid types are" + f" {tuple(valid_types)!r}" + ) + raise ValueError(msg) + fun = ( + self.memory_info + if memtype in _psplatform.pmem._fields + else self.memory_full_info + ) + metrics = fun() + value = getattr(metrics, memtype) + + # use cached value if available + total_phymem = _TOTAL_PHYMEM or virtual_memory().total + if not total_phymem > 0: + # we should never get here + 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"): + + def memory_maps(self, grouped=True): + """Return process' mapped memory regions as a list of namedtuples + 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 + address space ('addr') and permission set ('perms'). + """ + it = self._proc.memory_maps() + if grouped: + d = {} + for tupl in it: + path = tupl[2] + nums = tupl[3:] + try: + 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] + else: + nt = _psplatform.pmmap_ext + return [nt(*x) for x in it] + + 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. + """ + return self._proc.open_files() + + def net_connections(self, kind='inet'): + """Return socket connections opened by process as a list of + (fd, family, type, laddr, raddr, status) namedtuples. + The *kind* parameter filters for connections that match the + 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 | + +------------+----------------------------------------------------+ + """ + _check_conn_kind(kind) + return self._proc.net_connections(kind) + + @_common.deprecated_method(replacement="net_connections") + def connections(self, kind="inet"): + return self.net_connections(kind=kind) + + # --- signals + + if POSIX: + + def _send_signal(self, sig): + assert not self.pid < 0, self.pid + self._raise_if_pid_reused() + + pid, ppid, name = self.pid, self._ppid, self._name + if pid == 0: + # see "man 2 kill" + msg = ( + "preventing sending signal to process with PID 0 as it " + "would affect every process in the process group of the " + "calling process (os.getpid()) instead of PID 0" + ) + raise ValueError(msg) + try: + 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): + """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(). + """ + if POSIX: + self._send_signal(sig) + else: # pragma: no cover + self._raise_if_pid_reused() + if sig != signal.SIGTERM and not self.is_running(): + msg = "process no longer exists" + raise NoSuchProcess(self.pid, self._name, msg=msg) + self._proc.send_signal(sig) + + def suspend(self): + """Suspend process execution with SIGSTOP pre-emptively checking + whether PID has been reused. + On Windows this has the effect of suspending all process threads. + """ + if POSIX: + self._send_signal(signal.SIGSTOP) + else: # pragma: no cover + self._raise_if_pid_reused() + self._proc.suspend() + + def resume(self): + """Resume process execution with SIGCONT pre-emptively checking + whether PID has been reused. + On Windows this has the effect of resuming all process threads. + """ + if POSIX: + self._send_signal(signal.SIGCONT) + else: # pragma: no cover + self._raise_if_pid_reused() + self._proc.resume() + + def terminate(self): + """Terminate the process with SIGTERM pre-emptively checking + whether PID has been reused. + 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() + + def kill(self): + """Kill the current process with SIGKILL pre-emptively 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 + of os.getpid(), also return its exit code, else None. + On Windows there's no such limitation (exit code is always + returned). + + If the process is already terminated immediately return None + instead of raising NoSuchProcess. + + If *timeout* (in seconds) is specified and process is still + alive raise TimeoutExpired. + + To wait for multiple Process(es) use psutil.wait_procs(). + """ + if timeout is not None and not timeout >= 0: + msg = "timeout must be a positive integer" + raise ValueError(msg) + if self._exitcode is not _SENTINEL: + return self._exitcode + self._exitcode = self._proc.wait(timeout) + return self._exitcode + + +# The valid attr names which can be processed by Process.as_dict(). +# fmt: off +_as_dict_attrnames = { + x for x in dir(Process) if not x.startswith("_") and x not in + {'send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', + 'is_running', 'as_dict', 'parent', 'parents', 'children', 'rlimit', + 'connections', 'oneshot'} +} +# fmt: on + + +# ===================================================================== +# --- Popen class +# ===================================================================== + + +class Popen(Process): + """Same as subprocess.Popen, but in addition it provides all + psutil.Process methods in a single class. + For the following methods which are common to both classes, psutil + implementation takes precedence: + + * send_signal() + * terminate() + * kill() + + This is done in order to avoid killing another process in case its + PID has been reused, fixing BPO-6973. + + >>> import psutil + >>> from subprocess import PIPE + >>> p = psutil.Popen(["python", "-c", "print 'hi'"], stdout=PIPE) + >>> p.name() + 'python' + >>> p.uids() + user(real=1000, effective=1000, saved=1000) + >>> p.username() + 'giampaolo' + >>> p.communicate() + ('hi', None) + >>> p.terminate() + >>> p.wait(timeout=2) + 0 + >>> + """ + + def __init__(self, *args, **kwargs): + # Explicitly avoid to raise NoSuchProcess in case the process + # spawned by subprocess.Popen terminates too quickly, see: + # https://github.com/giampaolo/psutil/issues/193 + self.__subproc = subprocess.Popen(*args, **kwargs) + self._init(self.__subproc.pid, _ignore_nsp=True) + + def __dir__(self): + return sorted(set(dir(Popen) + dir(subprocess.Popen))) + + def __enter__(self): + if hasattr(self.__subproc, '__enter__'): + self.__subproc.__enter__() + return self + + def __exit__(self, *args, **kwargs): + if hasattr(self.__subproc, '__exit__'): + return self.__subproc.__exit__(*args, **kwargs) + else: + if self.stdout: + self.stdout.close() + if self.stderr: + self.stderr.close() + try: + # Flushing a BufferedWriter may raise an error. + if self.stdin: + self.stdin.close() + finally: + # Wait for the process to terminate, to avoid zombies. + self.wait() + + def __getattribute__(self, name): + try: + return object.__getattribute__(self, name) + except AttributeError: + try: + return object.__getattribute__(self.__subproc, name) + except AttributeError: + msg = f"{self.__class__!r} has no attribute {name!r}" + raise AttributeError(msg) from None + + def wait(self, timeout=None): + if self.__subproc.returncode is not None: + return self.__subproc.returncode + ret = super().wait(timeout) + self.__subproc.returncode = ret + return ret + + +# ===================================================================== +# --- system processes related functions +# ===================================================================== + + +def pids(): + """Return a list of current running 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. + """ + if pid < 0: + return False + elif pid == 0 and POSIX: + # On POSIX we use os.kill() to determine PID existence. + # According to "man 2 kill" PID 0 has a special meaning + # though: it refers to <> and that is not we want + # to do here. + return pid in pids() + else: + return _psplatform.pid_exists(pid) + + +_pmap = {} +_pids_reused = set() + + +def process_iter(attrs=None, ad_value=None): + """Return a generator yielding a Process instance for all + running processes. + + Every new Process instance is only created once and then cached + into an internal table which is updated every time this is used. + Cache can optionally be cleared via `process_iter.clear_cache()`. + + 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). + """ + global _pmap + + def add(pid): + proc = Process(pid) + pmap[proc.pid] = proc + return proc + + def remove(pid): + pmap.pop(pid, None) + + pmap = _pmap.copy() + a = set(pids()) + b = set(pmap.keys()) + new_pids = a - b + gone_pids = b - a + for pid in gone_pids: + remove(pid) + 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) + if attrs is not None: + proc.info = 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): + """Convenience function which waits for a list of processes 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 (may be None). + + *callback* is a function which gets called every time a process + 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 + *timeout* occurs. + + Typical use case is: + + - send SIGTERM to a list of processes + - give them some time to terminate + - send SIGKILL to those ones which are still alive + + Example: + + >>> def on_terminate(proc): + ... print("process {} terminated".format(proc)) + ... + >>> for p in procs: + ... p.terminate() + ... + >>> gone, alive = wait_procs(procs, timeout=3, callback=on_terminate) + >>> for p in alive: + ... p.kill() + """ + + def check_gone(proc, timeout): + try: + returncode = proc.wait(timeout=timeout) + 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 = f"timeout must be a positive integer, got {timeout}" + raise ValueError(msg) + gone = set() + alive = set(procs) + if callback is not None and not callable(callback): + msg = f"callback {callback!r} is not a callable" + raise TypeError(msg) + if timeout is not None: + deadline = _timer() + timeout + + while alive: + if timeout is not None and timeout <= 0: + break + for proc in alive: + # Make sure that every complete iteration (all processes) + # will last max 1 sec. + # We do this because we don't want to wait too long on a + # single process: in case it terminates too late other + # processes may disappear in the meantime and their PID + # reused. + max_timeout = 1.0 / len(alive) + if timeout is not None: + timeout = min((deadline - _timer()), max_timeout) + if timeout <= 0: + break + check_gone(proc, timeout) + else: + check_gone(proc, max_timeout) + 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 # noqa: PLR6104 + + return (list(gone), list(alive)) + + +# ===================================================================== +# --- CPU related functions +# ===================================================================== + + +def cpu_count(logical=True): + """Return the number of logical CPUs in the system (same as + os.cpu_count()). + + If *logical* is False return the number of physical cores only + (e.g. hyper thread CPUs are excluded). + + Return None if undetermined. + + The return value is cached after first call. + If desired cache can be cleared like this: + + >>> psutil.cpu_count.cache_clear() + """ + if logical: + ret = _psplatform.cpu_count_logical() + else: + 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. + 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. + """ + if not percpu: + return _psplatform.cpu_times() + else: + return _psplatform.per_cpu_times() + + +try: + _last_cpu_times = {threading.current_thread().ident: cpu_times()} +except Exception: # noqa: BLE001 + # Don't want to crash at import time. + _last_cpu_times = {} + +try: + _last_per_cpu_times = { + threading.current_thread().ident: cpu_times(percpu=True) + } +except Exception: # noqa: BLE001 + # Don't want to crash at import time. + _last_per_cpu_times = {} + + +def _cpu_tot_time(times): + """Given a cpu_time() ntuple calculates the total CPU time + (including idle time). + """ + tot = sum(times) + if LINUX: + # On Linux guest times are already accounted in "user" or + # "nice" times, so we subtract them from total. + # 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+ + 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. + """ + busy = _cpu_tot_time(times) + busy -= times.idle + # Linux: "iowait" is time during which the CPU does not do anything + # (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 + busy -= getattr(times, "iowait", 0) + return busy + + +def _cpu_times_deltas(t1, t2): + assert t1._fields == t2._fields, (t1, t2) + field_deltas = [] + for field in _psplatform.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 + # 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 + # https://github.com/giampaolo/psutil/issues/1210 + # Trim negative deltas to zero to ignore decreasing fields. + # top does the same. Reference: + # 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) + + +def 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 (non + blocking). That means the first time this is called it will + return a meaningless 0.0 value which you should ignore. + In this case 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. + + Examples: + + >>> # blocking, system-wide + >>> psutil.cpu_percent(interval=1) + 2.0 + >>> + >>> # blocking, per-cpu + >>> psutil.cpu_percent(interval=1, percpu=True) + [2.0, 1.0] + >>> + >>> # non-blocking (percentage since last call) + >>> psutil.cpu_percent(interval=None) + 2.9 + >>> + """ + tid = threading.current_thread().ident + blocking = interval is not None and interval > 0.0 + if interval is not None and interval < 0: + msg = f"interval is not positive (got {interval})" + 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) + + try: + busy_perc = (busy_delta / all_delta) * 100 + except ZeroDivisionError: + return 0.0 + else: + return round(busy_perc, 1) + + # system-wide usage + if not percpu: + if blocking: + t1 = cpu_times() + time.sleep(interval) + else: + 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 = [] + if blocking: + tot1 = cpu_times(percpu=True) + time.sleep(interval) + else: + 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 a separate dict for cpu_times_percent(), so it's independent from +# cpu_percent() and they can both be used within the same program. +_last_cpu_times_2 = _last_cpu_times.copy() +_last_per_cpu_times_2 = _last_per_cpu_times.copy() + + +def cpu_times_percent(interval=None, percpu=False): + """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() + cpupercent(user=4.8, nice=0.0, system=4.8, idle=90.5, iowait=0.0, + irq=0.0, softirq=0.0, steal=0.0, guest=0.0, guest_nice=0.0) + >>> + + *interval* and *percpu* arguments have the same meaning as in + cpu_percent(). + """ + tid = threading.current_thread().ident + blocking = interval is not None and interval > 0.0 + if interval is not None and interval < 0: + msg = f"interval is not positive (got {interval!r})" + raise ValueError(msg) + + def calculate(t1, t2): + nums = [] + times_delta = _cpu_times_deltas(t1, t2) + all_delta = _cpu_tot_time(times_delta) + # "scale" is the value to multiply each delta with to get percentages. + # We use "max" to avoid division by zero (if all_delta is 0, then all + # fields are 0 so percentages will be 0 too. all_delta cannot be a + # fraction because cpu times are integers) + scale = 100.0 / max(1, all_delta) + for field_delta in times_delta: + field_perc = field_delta * scale + field_perc = round(field_perc, 1) + # 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) + + # system-wide usage + if not percpu: + if blocking: + t1 = cpu_times() + time.sleep(interval) + else: + 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 = [] + if blocking: + tot1 = cpu_times(percpu=True) + time.sleep(interval) + else: + 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(): + """Return CPU statistics.""" + return _psplatform.cpu_stats() + + +if hasattr(_psplatform, "cpu_freq"): + + def cpu_freq(percpu=False): + """Return CPU frequency as a namedtuple including current, + min and max frequency expressed in Mhz. + + If *percpu* is True and the system supports per-cpu frequency + retrieval (Linux only) a list of frequencies is returned for + each CPU. If not a list with one element is returned. + """ + ret = _psplatform.cpu_freq() + if percpu: + return ret + else: + num_cpus = float(len(ret)) + if num_cpus == 0: + return None + elif num_cpus == 1: + 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 + + if set_none: + min_ = max_ = None + else: + min_ = mins / num_cpus + max_ = maxs / num_cpus + + return _common.scpufreq(current, min_, max_) + + __all__.append("cpu_freq") + + +if hasattr(os, "getloadavg") or hasattr(_psplatform, "getloadavg"): + # Perform this hasattr check once on import time to either use the + # platform based code or proxy straight from the os module. + if hasattr(os, "getloadavg"): + getloadavg = os.getloadavg + else: + getloadavg = _psplatform.getloadavg + + __all__.append("getloadavg") + + +# ===================================================================== +# --- system memory related functions +# ===================================================================== + + +def virtual_memory(): + """Return statistics about system memory usage as a namedtuple + including the following fields, expressed in bytes: + + - total: + total physical memory available. + + - 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. + + - percent: + the percentage usage calculated as (total - available) / total * 100 + + - used: + memory used, calculated differently depending on the platform and + designed for informational purposes only: + macOS: active + wired + BSD: active + wired + cached + Linux: total - free + + - 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, macOS): + cache for various things. + + - wired (macOS, BSD): + memory that is marked to always stay in RAM. It is never moved to disk. + + - shared (BSD): + memory that may be simultaneously accessed by multiple processes. + + The sum of 'used' and 'available' does not necessarily equal total. + On Windows 'available' and 'free' are the same. + """ + global _TOTAL_PHYMEM + ret = _psplatform.virtual_memory() + # cached for later use in Process.memory_percent() + _TOTAL_PHYMEM = ret.total + return ret + + +def swap_memory(): + """Return system swap memory statistics as a namedtuple 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 + - 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. + """ + return _psplatform.swap_memory() + + +# ===================================================================== +# --- disks/partitions related functions +# ===================================================================== + + +def disk_usage(path): + """Return disk usage statistics about the given *path* as a + namedtuple including total, used and free space expressed in bytes + plus the percentage usage. + """ + return _psplatform.disk_usage(path) + + +def disk_partitions(all=False): + """Return mounted partitions as a list of + (device, mountpoint, fstype, opts) namedtuple. + '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 + all others. + """ + return _psplatform.disk_partitions(all) + + +def disk_io_counters(perdisk=False, nowrap=True): + """Return system disk I/O statistics as a namedtuple 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) + + Platform specific: + + - busy_time: (Linux, FreeBSD) time spent doing actual I/Os (in ms) + - read_merged_count (Linux): number of merged reads + - 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. + """ + 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) + return rawdict + else: + return nt(*(sum(x) for x in zip(*rawdict.values()))) + + +disk_io_counters.cache_clear = functools.partial( + _wrap_numbers.cache_clear, 'psutil.disk_io_counters' +) +disk_io_counters.cache_clear.__doc__ = "Clears nowrap argument cache" + + +# ===================================================================== +# --- network related functions +# ===================================================================== + + +def net_io_counters(pernic=False, nowrap=True): + """Return network I/O statistics as a namedtuple including + the following fields: + + - bytes_sent: number of bytes sent + - bytes_recv: number of bytes received + - packets_sent: number of packets sent + - packets_recv: number of packets received + - errin: total number of errors while receiving + - errout: total number of errors while sending + - dropin: total number of incoming packets which were dropped + - dropout: total number of outgoing packets which were dropped + (always 0 on macOS and BSD) + + If *pernic* is True return the same information for every + network interface installed on the system as a dictionary + with network interface names as the keys and the 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. + "net_io_counters.cache_clear()" can be used to invalidate the + cache. + """ + rawdict = _psplatform.net_io_counters() + if not rawdict: + return {} if pernic else None + if nowrap: + rawdict = _wrap_numbers(rawdict, 'psutil.net_io_counters') + if pernic: + for nic, fields in rawdict.items(): + rawdict[nic] = _common.snetio(*fields) + return rawdict + else: + return _common.snetio(*[sum(x) for x in zip(*rawdict.values())]) + + +net_io_counters.cache_clear = functools.partial( + _wrap_numbers.cache_clear, 'psutil.net_io_counters' +) +net_io_counters.cache_clear.__doc__ = "Clears nowrap argument cache" + + +def net_connections(kind='inet'): + """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 + 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 | + +------------+----------------------------------------------------+ + + 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: + + - family: can be either socket.AF_INET, socket.AF_INET6 or + psutil.AF_LINK, which refers to a MAC address. + - address: is the primary address and it is always set. + - netmask: and 'broadcast' and 'ptp' may be None. + - ptp: stands for "point to point" and references the + destination address on a point to point interface + (typically a VPN). + - broadcast: and *ptp* are mutually exclusive. + + Note: you can have more than one address of the same family + associated with each interface. + """ + 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: + 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 += f"{separator}00" + + nt = _common.snicaddr(fam, addr, mask, broadcast, ptp) + + # On Windows broadcast is None, so we determine it via + # ipaddress module. + if WINDOWS and fam in {socket.AF_INET, socket.AF_INET6}: + try: + broadcast = _common.broadcast_addr(nt) + except Exception as err: # noqa: BLE001 + debug(err) + else: + if broadcast is not None: + nt._replace(broadcast=broadcast) + + ret[name].append(nt) + + return dict(ret) + + +def 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 namedtuple 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 + - 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. + """ + return _psplatform.net_if_stats() + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +# 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). + 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 + + ret = collections.defaultdict(list) + rawdict = _psplatform.sensors_temperatures() + + for name, values in rawdict.items(): + while values: + label, current, high, critical = values.pop(0) + current = convert(current) + high = convert(high) + critical = convert(critical) + + if high and not critical: + critical = high + elif critical and not high: + high = critical + + ret[name].append( + _common.shwtemp(label, current, high, critical) + ) + + return dict(ret) + + __all__.append("sensors_temperatures") + + +# Linux +if hasattr(_psplatform, "sensors_fans"): + + def sensors_fans(): + """Return fans speed. Each entry is a namedtuple + representing a certain hardware sensor. + All speed are expressed in RPM (rounds per minute). + """ + return _psplatform.sensors_fans() + + __all__.append("sensors_fans") + + +# Linux, Windows, FreeBSD, macOS +if hasattr(_psplatform, "sensors_battery"): + + def sensors_battery(): + """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_plugged: True if the AC power cable is connected. + """ + return _psplatform.sensors_battery() + + __all__.append("sensors_battery") + + +# ===================================================================== +# --- other system related functions +# ===================================================================== + + +def boot_time(): + """Return the system boot time expressed in seconds since the epoch.""" + # Note: we are not caching this because it is subject to + # system clock updates. + return _psplatform.boot_time() + + +def users(): + """Return users currently connected on the system as a list of + namedtuples 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. + """ + return _psplatform.users() + + +# ===================================================================== +# --- Windows services +# ===================================================================== + + +if WINDOWS: + + def win_service_iter(): + """Return a generator yielding a WindowsService instance for all + Windows services installed. + """ + return _psplatform.win_service_iter() + + def win_service_get(name): + """Get a Windows service by *name*. + Raise NoSuchProcess if no service with such name exists. + """ + return _psplatform.win_service_get(name) + + +# ===================================================================== + + +def _set_debug(value): + """Enable or disable PSUTIL_DEBUG option, which prints debugging + messages to stderr. + """ + import psutil._common + + psutil._common.PSUTIL_DEBUG = bool(value) + _psplatform.cext.set_debug(bool(value)) + + +del memoize_when_activated diff --git a/.venv/lib/python3.12/site-packages/psutil/__pycache__/__init__.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a0cdfaee7a804ee53ac7c92f51e97c58fb3cdcb GIT binary patch literal 88080 zcmeFa3shX!nI?Mb1yxW$K^1R=4v1IrDD-|yHUcCe%SaK(k8~AuuGe z$CIE~iNSIr6x%JFxZ85u=_u)>$8)Fqw(U6GeOKQ*mooT*tJQ0qOlIAiS$EyRlIOWiXFHUfoqnfek*tHcLwWu@lK{VMFmK4^cQJnk{Q3TT=Ffz`z+b@pcKF?X zH}gB-_xL@`p9O!RzmWN};V<$RF~1Z3Vt+C7=fGd$FJb;%_)Gnz%%2B;nZJzrUGSIt z%b7nP{tABu^B2Is&cBZN-SAiXE1BN||9by=<}ZX_^oz`21b>ykiusG-ul83ne+m2> z{2Q3R6#g224fB`5U+b@B{&M*1{B_J<0e`)}p840o-{5ax{z~}0elPQ{hriL^$owMw zP5vh4uY$kX-^~2g@VEF|n12KOKEIFoYv6D7w=#b%{2TonnZFMHP5w>HUl0Fg|7PZI zfPagB3-f#7-|FAW{EhH$^KWDRCiu7ew=;h;{5$+Rn7;-7o&KH7?}LAre;4z&!oS{2k1{6aG$rC-d)u|DgXM^Y4cLkpGZL7`_tPA$K8%J;>p(n#10~uAw9T zBP`}V`0w-I$Nc-@Kk7fq{P)0rzyE&bzZd>)e>d~D!GFwujQI}?9v(XGKhFH^@PE$# zIp*(x|AZgw5#@Hm-{bFL{)6!M`g>8Uqajhw#MeWp)k&(=aC<19g&ao611w~BXuDj9 zI9&*NP_6lq!3T#P@;}6KyAS?{{SU*BJ{sN-a>)+FK8o0W7Mns#r29jTfxqA&LH~VT zkh)*A_&;wJLXU<%|C&kp@}%jsARYUT$^TgBv4OwVV{>1qec(;**ME6xlYrcUi1UvA zjr*kIubCCU{1|HaIn=Vx-^Wr&uc?vzA<69TM;@m`eLT+&;n4$v;O`Ih55#$1`mc0? z$BUXS6(Y`}{*C*1?ub!`yaz&kp?+SMP$<;D7ZK6gWgcJuMLe}%Ov0rOtMEwZk$$vp zMUTOK{z0k7KP2_~hfzXAI{B0k8Xou=|Em8=5AYm21YR#5;{PJ6UAFXKD0`qtPs4q$ zSrngNmdwE@{$hwf%5(9b!`m22{gB7kf29Ychj@#5X{@aFfm?bk?voyt%)G3#I|M=g zj^szH)5IM3h^NtipEpa7(7XzLUi$n*4n~e<7|n?^>7|Ext>pdE0|UR(bKyRuoy6Z% z`t$?8;qmle-oDgd|CdptM?+u6ynie-3^(wiO{w3o17E(NhFP>a52P9;U3TZ9Q4Ykjh2Glj7*B?Fh5EU9{Ffvd zv;R`y>%2`;G;|3qE#vR{Z$R@RZ47C@A$?mqhu>$U=cO_H&Nv97WFd967FF}`lJT`h z3)DPFH^XCuP5#TjTTasG>*smDA-8Wz!;6QLRYL6S^1SY37jhRo=GH9~nP1b& zd?l&OS3b4OKa!qCnb)ODX#K0w*Wv!DIwIfTd9v|+2C-hdLyvw#?@=}FjF#3T%_7g& z?~rn4P0D8tDK8`C_wJDLSv{rn9OC_%^i8VDefvxP z&3i2fd{P*Rj>f`+VmK-W#XdO_jkb&o24npZc}N@#pOS;}xQG`~4#nhf=v;XCv=|z^G{aAN*p#A8vo=&{w>2HAt+q!xY*)=&wcm$;Y$c$pr?;~kuMYOweXN`6p+xR^xI1v7v;BdjfS%57 zs+Lk@Z`b{uyrP*0Pjq%Fjj|o>>OM)0wB6s^zP@9;d70DC-h6&e3D-_pv;3 z)PUn{CzOC(HQ>a_?rvqQ^3)KPr4qy=_4FP)ew^jwRwMPEXlqBD0DVF6JR(g4B`GiP z!M6L71N_JCKhQ;WwDp|qX5F3nU{`m?u?MN=oo#)z@;XAp;gCdeI%PQ`!;{k;=^5=i zb6i=O2*~XX4}~J5vCi`&VVT9v@kfSEg+qE2n-n@VdKz(yXr+cHLV>X}q2WNVFBU!* z#8Si;=a?KE2@H=8ox&oSu(i_4N3cCG5*?(434dPKvEwJYj$zif_4EV|P|fp`g48Zg z4pgURa^e#-7qgJ}n6lRKR^E2t*aMw`?qetJZ#x>Gf=+bxKCBc*pAU2%J@()oKf4ci z9Xh-=m7A)xHn{Lkr4*m&Y&*&(Xfavp30{7sIRCe-g4A8 zr6>gCssRslo_JV^l7SA0M5$}ZcPdKbKph?(9galdSAficV<)?5wks=L?PE$144N<$hp^s5vJY@fZY5_EIW6RDgp+Wpz%W3NKam{>@b`dBSfgA;PAxei zId$aJlha7fdUCwv5CSSUXub{PBi5UoPmYV6Y;wSA6XXJNi2Wwp$pIN8$hqV=$jO6~ z$ZR_p02sPYE~cOoa!ScrM@}U<8RWRh@sL9lp-c#nTu2U3d_k@tr-__qa%^xCnd)*# z*p5fAgCw#>!crh~J{*lj6IS@62`9~Ythqoq7LpTLW5IANKta(&?zylW8x0Nyl#MQt z9Ua4BQC}Q=Bcp*B)?hS|P2Q1^+!q>-CGxd!Er>yrnaIzTvfuJPeGk;T3yJ*u*PR>F`f^saZB+p_2qtW2$P{KAEr6L@s zB9R!Ybs|F^3=f54m|;1XRJ6GVMk3Mh`GoZZAn!!}p>S|$Bp4Zth$kWvw%Ga!+YaAG z-$pPg^v4P>js5M-aPAXK;Hypwk77ipOffCqq={oo-!fe>fp?H^6~=|{S;kDSnrNS} zgok5ZbHeP~n6Lm6MTrO$#ZOMOo{0>FT7!e3^R1`Du~zv^XfQAs3d)3{wEE74hR?ME zt!W)$7{(Ugrp>KU3}MSiuXe z8TWq)A72m_Jom=E_snm2u4m4D?C# zsqlnZY?yEYcLLTGlqfu5@?x1cNa6nekc>Gd#?AzXMZkCzOau&9FBW z4MYaP!9ieWGA0xdwTWGO#PP@|u&Z$~23#-}5$i|7BjPAJREeropdOfAa2WG`a2&Ye zP~==loY*7+aqL6y52Nsqs4ffr#^yv$F>i zHb!uWJuxOPAaB1Jn773wJS<$gN5iX46OW$=#YW{}F(^Kc=Ij=!QO%;y=bJ*u_NhaO zUS5kjhljI7^@$9J#ORqw%qO0V0s}_JM@K^NjE;yZc=Gj~2@gsb+y<{tOXkgxPoQQ1 z(<4z1@a1C^Vx?;603NHy0<1n^L8!WXG?ov12b_yXA96ZYD}yKC>?UE4E} zU;A)N?NCdtB=R7yEn%fCM5g8^>=KYNdQIf?%aI|rx5NO16ZU9K?x$C;HDN`uQNYQ_ z7=qyq1W!j2mj0nwlv+*mO&NJ7MqUPB4NxTcEBKZTfE>LKo(sY+vT~pCUi3cQbfNub zUct~O<;VdxCFNoL`gF_+gvc8F;@bJ0dU|16A z3rND#qeBEzCkpn6N;noPBNcax-RN&yY;N^G5E07(iMg>w)0lXh=cMEeR{- zm9UYEu^EbLn zp4k@9ELm~Y&Bx}?Ecotyv+&K#h1|nfu(>63?x!OQ_H|g1_EPz)$e<5@+Cw#^=XNvd za773S5N`@3+JsoMU9y0YVwE~1D}HT~4eW^wDFbe1C==q$O!yp<9d4H7fSWC4!F7gm zr0l0O2E9Kwls6!|<4sIzAu19jgEesV&`G%OqQUO9sBsbhr z$pg1cDui1u6~V2Lis7!4O5j$a{-wxsJ%mO1$feAXOO;fP7}Zh*+zrw?xHVt~)rRT@ zx|1r&UZhHm&0~?)BW}Z8;)+^aE80|r^1XM7U2TZnE^R>U#=FF>Ns3*I*iCndU8lvi zNcE_7^IhUL7~*;nwm_ z$uI)j@V?pbzFpdaR6C@D(pJRYd6$y5X(d^uL(+D{-X-lo+TGG&X(!(HAno3Pi8SN5 z>-F6n6d>-Nr%Zgy@bAYQu}FK7%RSOQxc5r?;kHTlz&#+{3%6ZrgWG}D96%YJ<7RIc z+gpRhST`p^BXS6AtQc{?#A7|2BPNc8V`mr!bUF+KR6O3*p={S({p2T7ECQsO*j#<1 zGQ-tW>Vkc4h(vv-Low{q4PJ1^q9U+zNy*?uFdPkuNkqOFRx*_$07yjPF$~WKt{I;0 zNGwEnU`!l}L}P>{5?%%vcP4^8iM=BKzQJHLDoPQSd?`b`tjMn{O31bqDjZ@!YgjzyFh}oA|aNPCwp|1@JS+4h&REW0gbTWA%ZZFt^2vp*eit62DEe%`Z1s( zq3lf~QP`W7o0Nf=#9v_#d!ueq-Y6Ghv9Kpbd>i%*h^oi%!`qD%#Jph-D~Z@P?9oxL z0UGL*M{f&qAl?jnSPsOR(cd&7e$2txuXIVaoa?>zIj?fXHDdkIf0haUr%l zO_6$6%vCY@n{I- zf$WGu+%1a@r?E50F^XEk;Wq>6qik0I5Ih}v)g*rd5e`li8e`Cw(hug7)a$KGm?P0d zhO#%wPb02;9!|pA2R=q3PeET%kpRmsk#h`OkN&~P7(;*=9h$HP@Er?#7>G7rLgzyX zXP^%hQ7oh|oNR&9K&^?GrF=rPl^{Acmoo`R>2@FW@o2VqiEr!g?`b3CVJlKmmb7h1`;##p^_l2CE2o-9vy1%1{KT zvU8{R+$`{1@y>cLH(hN107Td4zBaZ}viFUiH#*Y=iQeh3$EH#vo&YW zO;`D{ON_h3W!J{IYvT>~l4~32@4}L4>m|oZVJUvHRy@jEg*a_pvX`zDma))s_FAGw zx>;CxB`_OUEUcrCA1En5$`FdGmJ1u=g$>u;i-j#L-G+L~MqH_Bpx>&SrPT<|T`4ZV za(?#wyl0_^a>!ma=Q%1@$||q5UTs}0Yntx(`h6dN)FODQZVBd`Ju5{UsVN)g+pqU5 z6>Yi^U2yIG^{NeDe*KY8a8=wAtd9H-?Ltoe%{;tV@RGq_td7d}-IXgvElD}V7F>H) zO4m(yeB;Qf15rMSl0L!r*SGH#eo|N1u}=8OUPnin1kGu;Y#<N2)taJVl-T)}gZKE?zD2T&G?|KJ1aryz{MLE>p4TL2L)kI-;r^z@l# z5%^*_#?TA`Sy4i0fQEtV6$Fhw#!(6}*bmf3L0XWRmWzh(K%^Xvh-1h?g49?+10dH1 zL?N~+)rayVBxNXk-X}g7Lhd0+&xf!fFlQn%7Cpt}XcA#h9K9KnBf7j>gtQz{A+FXk z>1COK3K&FH&VuU#W>1u%%nX7NSCoJ)IIQD+EK}5z&@UCMB>+KPCWi*Y!Bdc*1103R zIhZ;q?({fA;DPu`Awmv;yANxn8^9;fGt|E?Of2PQeN@4a-$a}!N~Va?*%)a#p!Y`! zN5)t524U(L9@g*JO4Kejb~yFs*^oRO8q~5FiwxIeharH?Iy(vg&l;`d6NOX~0}J-1 z91|e~2}c;unubTqqCu;s7u5^(odGt@W){O(*)V|>4iuI+&cQyY6r38>r_~68Vrl}+ zE0i=kDA8_sI)bv4NoN=_pqGw*+9HUY04Qg&pjWWmAg9r{Bl#La7=*{>NYFk?$g2v? zzX$I`fx0Ao`W7+FKsr8N5dLeW;B>D#g{sRbt?|m%1^31U=f)58KwrGlw^+FmJk$l}CIx+^8t{jZ34$iKU2u(jv~-Y( zLj>ZUrQ@yX&`@kOA%2in)Ze&opql&i-#f&t(PMC*lr>P#{rc~uX~3($aUbtl zXhNEwvGiF^lP)BXO{ky>Q|2klq#1gj*^`#%%`aMJ&6jea?Fb@gO54x4rehzFV*1C? zWRoI8At5h&fbQC%*JR^k4GGFZOd}rDuNj7~{~@N43HYkWTz<(BU#1Pk>OuNYo3Jj& zpda-xW*HHXipG@J*{vXFR>C$XiZuu(iA_3imJ%5xD;))5JHa#~5?Ol0gqR;$S!Uh57P}FN>5G!G2LVVNNgy~!g_dhcb!1e;kV4eoVlz?f1U=T|Hg5At3Th6PC z=hZFd0pD9GD!X!K_RO?(`sBszmF&D{c7JX6O8NS0yRYt^?wBdMcyuKv|C!HU{QTD+ zTgh`T=dFJ?Z~aQu?iuUML(k@}TJ8CzD;w%x+4a({@9vrDm@Aq+x>Dj`R=25 zEt&0JbqOxdGhe#+rI*7uJPWnk<9XXD!Ty)_BMU#UT$ZaC&l4B&nr~M7uE&;J?~S+K zyI9?Z2oKNZec-NKcGt$;wJR>qmE76fWtTVZ@@g;jaaTRPIA$F;I{wX(9~}A7(WSDs z9~^zP<9gfmFU|$$dge{@Rr7`4-h1QdyJc;Qt^>C-t)ARp)eE^57a#lhqg80k%nwWz{*BDP*+l*= z4vMqacF>lY#B{Nu6t*vTFo?!|VbY{aSolhfX6Ul89s`>K;TpCn-vj8A26}5y!gR;4 z)+yVhdD42e0aLnS(ju2l+M*?&2IpfMnM~;z=9{K;K(sWWN(zwVM%bgkj~Wg?6c&>s zkp)zV#M;1R6`5%+Sf*V=5ay7)CnPD#BeXfluam=8Y$N%6a3Fdmq#$8q>@%h*vMG_l z@a9eIZL|8eIZ7Bk;dxy0mJtYqzKErlt1mrzzfLRf0^(i}KzzB%=4xLqUdr>{bXP4n ztNt;&;G6xxCMv{hd#>)ecoa*nZlzqjw)g5@^(D_e6Z_ia`|i@0>L9EwT(`1*!?nk+ zKK}gFk8)tP^z_cw~_eqpM z^IO?PZF_{b_Bak?THeku!A+WFME$T?MuSDHy-5@Gy%Xcb`uA1AYgTCF{|yn6Sa)dt zFzF;1nosY4Ma`#;9n`S9cy8P5z6GpxySNtfp>kQ-{c70*_)UE-lo@2gX>lHHj}`)2 zV$5{SuY1%sc`fpH5SXwMOG23we~xzpUP$1WKB9F7wxkmBCbh2)@rl|8^t`xxA$#4; z(sgsvwX;{xE|xYrZJ&JCM-@;e+hF10a!h&du71Hti8OQY zGo;E&DG0BhmP+Rh%jKB+)K4kTr{#cD+E%0|9dPS@?&B+z4gh@1LcN5v>H0_Gne&uH zNN`|w$Nu^BRO$NXQ&N3~{`r&~?$AG^a+mTL`$vM>2cK0y`f+&T|L-efcjcC+YYG*iX=5iGit4Dt^$ClvP#|LZiTLWN0sqE6QmQPLB}3M`Pls zBHbWFiv18s493F1r8KZ)JV^qfH1UxbqlmE$Rp~2Gq%IULL%d1+E=`@0gEq=TYzX;I z`$VF8npI^xa5JOLS~X778!6yeW@1Qg&1jYF)-8GEd78T#?zVPs@vCow5A z<6asLzkmmY_Ce846(3Xm8R~~!2_jKw()I;G`>DcuG(>!mv>kbQNS@lSlmp3VsI{V6 z5%A!&c0fSVH%f{E5#qKsfKf>)Lg%6Ig>F3+f^>y=dejl5EI@lBB527Ku`cT|s$gG` zx*&z?$O4J)M~a&uTQSzCGts@gmr^Tzilia@; zDVbJXG+*;WD#oTvx7R8EF#t{?6IfU<7L#St8BOG}kJ|B(M7E*{&yEtvRxuSGNnf1<&}O z_TS7anu*Olw3Ju(UqvCmXw~Y@_Yt?Xd$xPovnlS`bfad;vjZW^o|?F)X4$hP?%8sq zW686NVs*`SEqj{dp62VlOPaM6 zPpOu_mKT`5JpXNR&$b)Vl4lPwf?58)xCeZVCC}D>G~~ZIUb6Yd)}@l26piKI6!$bO zd0Ku2TMM}*)BcZF5153)Ql%lk_k!N@w0y!#`b~5@I{e9;L$#)7+=ogn-#y^!w42}F za=?j~ckXc@{HOK*`RNnA!QDvL4sl$ilg(Skpt1y zcz90y9rdOIEbN&y)EnCI;G3Q|MZp1`v`l3J4bnK%Jbnr%J`=k0;5JU#fq>g3J5lX& z4${#Xnv`eCG3<_2@j%e{cC9{HW??dmqZw0JT~f#zwm60D0h(q4xxrZ3!2+Ha6J>Dm{A zIATG25SXhdQQyqvP(Nc5qZYw)!SEm}Mcr!@CA@l(G7ke0ftV6|x}H`+cpjAa-1!OXA4rDV#158%R$=1K3Q$rDv?*Doo^O#8euCQh;qYtYw5{us0R+ z!{&-~Tu2KvgeZ8e1mkf;5_TpXG1PVSsLL1yt>741auqL7&<&u4>~vD!C=TqV8!de> zx+mORO`DC07#kl6iMsAK1+s2@d`f(LN+w{FFhyk~i*n!tdWhETFa255IH(9K~(6Iat^O63yRfdMBq9{YV zlp>ADL|;-lx+Y1&oy>Gp6vnev-Xd#S!p?^$k&SVIZj4e*;$S3l)Lu}+5}D{vYE8mC z7**s96BNrzOCg#KFT+(7M>z(NskUF=M%93FfTAc2BnedVEV)D%fkzPZ^-Ia_(I>yHP>?%E4MFt zcHFjEiVJ2eAK8WCDpmdEg96WV=1NY%yE#=eXQ$1Y;Nc%i%4V#q=G^>sP)V5^eRgW4 zvijPYt7ooSez6{U^4EJ8*KeM)zF%4WeE*zf)gx5Z%w*14pUwHON+>P=l?812)f&N7 zJe~XTM}2D1QTwaie@9Lsv%`cIq1@2g#C|PDkEZ+{?heX`IBtnTVM3@LcHd%oCdtN829t zh*}&A^c!hvu}438F=d^!-YwmvP@qKU_t_T!qCo!2t);tA!T6~QM( zNmm0A1jIE3mB=ZWECM81qy0cDaU?E_&*wJ6FvaIO|m^&U>C;H+}ei|+dC z+un6=Tq&=bxqsCncs8v%1ZM?_#z?g*>6jOjAmprXYZcyVb+qlVytUf|H)(3qv}RNL z96VQq)5>I3F){>ck2cY0GS)!uL#a|8ecBNwkxtDxDHqcvXd)vxLh3ybGF+zDOm*-f zOM(_o(s3af$ew}hQ6oM|Q{Y}CWk>hig@BM4W{7*9p29Ha7A)jet~lM#bYJYA+q&cw zF%7QlncZ`F-*gAg3FW!txs}8K2JlLC zZXc7l081T%S{txoc3zhOQB}Q*&Jv1WP-i%mDw5d2)D*FuNYna%^#en^#2B#=zktkT6eXLJY$iHg%j>IZY4 z_~M{c1BZW5fMCBfj_5n_8@f{!3%?;U+AShS)EP_JR&U{9mpLU=GJ={V^5tx3YCF9JM*11iDzM~ri%u#^6T!cu1t?X(k46;As|JsjrQglHFn0UApL zN6xanDsHb@vTyjuY!|`7-Lt!|+%tR6qPu3nS@S{Zx@%=u%ib;BIUjpv;-!gKzWmac zF_)NhR*AddTz3~3`85V3b?i(oNe{uExC6XoL#TB+Dy=?u#nVhWG?X7SU}KtL1)Ns- zJo>8Z)08g=7ox0e*J6MOzZ|mQLzr*blIA z10omke$m+{h6DOdoLK<8aldhL66vHUDSy^E_)F4BU?(lSIf|+%sdOom@WBq#`oU3p z%06Yfm{v#Q!fuCvFB05V<-`( z3jOm_(a0dNk!S+yF!X*{^(70L)O9+FJD?re;_-mg8taxum4I_gK@#KKMoi;119hea!yZE%aIMK!iPzc<(t+6o zOIRNY9J@~eRau0R3i5wIDh7?@Y`j7{G>XXq#k45dg^}|qN&z{4Pw@yS!JvV?A6xG! z*vMg7;4stq5F<&r2I&Qbab!O|s@O!DSjYET#PscjfDp56zmI?ZD*{l&ZTpz1nBc6? z;OyY#;ps!~=M}xMiBrx+EUY1`OdueQ`r1gXH%9$fK^GfF$UOu{% zw`rxQawd1hT|Rw`ZP&0QB*Qp5nkbV7l>)(GA)Pl|QCBOb02cavL}G3Jf(CvmTvwk# zbN>xBj+lgK+(*uQN5yI#;ikJT?gFQ5)tr+flG^OR?7)@b+2O^ax)}>`kIGl=P|$n7 zvhIdusd6h=Q5nKIamMTL_Mji9{k7XiHRt!W>1^$QZ)%NbHO~(Fa19WP-}X zO-1TxHtQ@(b)Wqi^*+f~pns((&MoIv#q+8b^ENElH{1nBJH^_@a3T0opS!mXz?(KF z_5idyfU;}AoWOREenQhUvDpZy`GJ2$=bVlS1x3|ZU?XVhv~_>;V)2vq9EQ# zzSIt(&pH=K$?`u@e-LSfm1ea#h}AK>YwN+U!$gN+(p1*y57eqy5>`r;@IcfriT z7>@rc2E?Lg!eAmP>j@$#EI#O@h>HC)Z?+_+$R8{0nKV!x|%vHPx*h- zTpdF^;H`GSiNl@+(~o~pbeNuRpGA^9gVHJO^x`-%W7SgTfI+y%Q+Bus%I`cQ>rl$S6W;>~=+uxhGGd1fngo=?nkg!1KlLex^g<(%J_B+{Mg7)z zA4dNgYOcVmACfN>N>Sj1o8t$D({=xyhMyQf097DcT#6Z~h3uLYu|_{Hw*I(=|t^?TT|wyU+&?6Q!lV(MMZJRXDCspz%3Yb7jwW73-DMExeaAu?O|M%|#Ni%e3+cg{ zFCEO*sds2yn!M(T`a{sXg47pcQ4LO!Aru^Uii!=eFBXxZQt0);P?icg=qsIHiH(TV2hPE?f?uU*sow2e+*`X69UIcww2+9hZG3Q)2m z7mqAE*T(=51zn@9;zzIc0#Sxv8T^-ovX`?5({9v;ff`vv%cg{iOFd94u z3%lBS1-xXth{TR%>yVzpIcoHCGiT2>cdO_b7M~7MB7Rbq3czSKAN$UfZ6q zl3wSU#*=_rZ#+e*Ff`u&&)0YYpz7hlLDH6p^o7}OuG)jb7O&0;FzBJ8I6ST^tkMWG zHI8J7qG8|@cAoW`4%nEqyYgt&)Ct9GfxwRA+KurKH3$0C`A;~HvL6vrM8qaeuSfgj z@CfXTLc#_~mxAe#AS7uC@RifREP+G86-TtwE`~%V3JCP4nG+2Ac6eKDp1wwSS9wSUGMy6SDNS|OuQhzXg9>oMOPW1zt zhWHBriWE3C9t%O+S^2~3C^O^#6%)u}$n+#JMl4lj?^TyrY zMR(JJvq>4qENE}3J57@2p%rTa83?y;z+qde7gA*+UM#xtC!_JE-yXYT#K&D^G;G5) zZu6)KJ!yGU;7W3y4{$o>q{I~VliCC-Lz#yVdy+LXusjJxL7g@yWzqn{Jww4k zZ?m{bY(Oo%gv@Ld8<_Gsy&EL5)PrrB?p}oVCCzsAW(sk?_yE>%kU_2FKAWhiSNj}# ze%z!eR~yuH2_fcs8Z$vbsBw9mpkPUSG83Mpp6t$4bCVk>7O8@9_M9Rq0FRF?0G&mk zyzisc(~Ia#6b5nsmmxe|aM!Gq-8<8Mv#e&m=lRwJkCzB9h!K{Ho8!gJi^aYLm+u3Y zM>$fx2p~d383$DFv>B%AFo_@_ehA&ui<|kZORj7CBlZ29{TOn!CSmN6o zR){Hx`~A|b5YR%AQ5awcHwyL_IZ_sl^uylC8Boc|U{l4xlI9A?aV3k?RYT^`yLtn6 zUN1_KESb^B4)Nh(|4*-iVv#3h%JqaoVl~+g9#O0bRx`tW{i8D0lMLFfKaxDt?l48w zi&D}|O;B-6w#v9pG=(Tr>Oe$QYv?nrWf&%F}*y3t7svw3Wx6X zd1GWq%|CSg+@gE?f^)k9vR$O#%IzW&iI8;Qm^9EB;Qj{2mjUi(9boI?0Zc3c@hML_ zT82-y*VKu~e;N3`i@qDcw<-FNLB^W4ME!gkJ2{xHPMQXaURhozSQxC=71ebvl&)+Z z(^SeO%j=p#IB!Gh@167j2M8fbd153-VUM9#HEv#N&WsMU-D%O`5+Dfm6-$@Nv$P8; z(p-bOS!~46L#>*_!yr?UBJht4u%Q4ZE1E1g_LD_mm_K7+m=aKDupMXyFH1cm_Q*!S z^+!|CN0h8dznD@Nq2@r?Loka)deouQVKUIoN+$eB)s{{ww}F5@LF42`HK_YQv%1Yv zEL?As4vFoC<}s#t0A`<9d1RC>+WRVQ{GhWvBct9N*e1bgC72=j&ln74N5FNT%8sDO zb?&8C5q5ZV9sWdEY zvehv?d9!%k3w`r7uQa~YxLmz2UcGOz`kuwgdl!q_7F=z{?c)97@|(q#bFpg^S0@&l z?pZ3nmy8qOWqJMH`1-wz>-S%^{$hR2%O_vo^7{Cj)}_|Y#ioOcwTBkhAD*+4(T=Ry ztYueI+yyn`AGvSz{(0$Z$KF%78wlM|bzmWX0Ce4t4p!0iF=FS^bWYXWQknW>mrA`G z%V-^pCTcGxrp%8Hs_2q#qH3>p@{HQSuKF;XJ)`tAix3Oy*hObJ#|igg=n!e{M}`MRhuP5vCUZ%e)Sr?$t+U7}@O@!gl2Cf((yyc(h+_tx zq(rA7)uRl+!q`P)Y&|8f7S7Z-Ow%}(aY%tW4W|zABuzn;hI47Ce<}@^i9094u-f+f zpAZwz(pak5Go$e5aIqLk-c_YhjnGkeKBS3{(}B?N5A9|XC#koFfEI&mOU5~s!y(Z1 zBU_wNjG9ES48%O^X-*OYvm}6!z88qQFGdr__UMf9S)g`$5ItN9PmOP^261VGy) z)=^2cKc@_ndWOT%bbwQxHHmDZ>(xBh%pRPKGZ2r4n6ri%r1M2mh{r!bmz+)8CFh`~ zF^oYaB~D_~>+kBZTdh|}YY3X4Msl1{);e2BpB21^T-5Hq*!ciYyr!FQDKl)sa$uht z{Yp|_!E_7^lPu>o!fwoBUh{&z`42UHHmB_?#Q>~g5MALa7~0O8%qE2zb4_c>sFlu9 zpw_A}U!;bnh>>gKd9{mq^$T{DF_fvs{R7O6|Dv^*xF<&D7+?5|fvhT^Yg2-@h6ZNK z6EK+>&;?7GUBt*QOxdFv%p+r0Hk71k1E75Mq0=_FG`I*- zZH=>2QI zpZ=@sRq~WLRQhQ>0E&Sb1EU_uoU{=Ek+wD(or2nwTLqoc;BA?Lwd-{KciKcpOy#F0 z9RuZhUAa$7jT1+c_HzO*YA&9%r#lF%_c^Gz%tshB*^}9F z^{0;y+LmqvwVJ3S=s@e9qH#28E9mCVm=3I!%pu}x*lvTup{8(_64$~mw0e@q^qNMA zQ?7v|Xz{4`PAS|t9q1}3&Zi!WfsGF1L9E~Y02bITz_FtcnK1?mA-XC$&B!%EMOBS+ zk}%)|4QN6@91Y5Kss=L}+QfbP6i}tYRakM*Vu@|y&!4;S^XF#t5P+(mKlfzH`wNIA z9uON?uXyi}hM=7G{QSA+D9uB#!wxD<`s@j{9CVMG*Ta+-)B!|8j_K;z&g+@-kyWTe z`>1{W9gSp+815+3@;uNiwl|9%+B-A1p-0pPSD|3EXh`-@v-q%@SPLdp8e@p2rS1*M3`uk(@(qn6}~32rBHtm=5dHTV01Yn@o_g> zWQ0z<3ox#6nc_LL&XLbiu5^@FKTL>Ig0wOdF1p)8hT$@PksDo^fqN`%AQxaK-@#(h z2^_G5vHxRNCv7i%PI0S*b`}wh*L8Jx2pM;aPNQZZ{kIy=Puz4@T;4NnU&(b%Ti(wr zSy@+m?Xjzm{U7UEW*xW`t`}nYxvkfBUfsFq-mq%UEL?X}tiNvgu5-=?jmHXcxx6V} z-bBZL3zs(?h;KTuxT)ja@{W~?_1BJEJu*MKRMGPBYMxN(TRm!G*UEjgO%R)w*Kd!n z-~OZ6;`+S{INFn1qzlIu&## z7$)DUcihSsaF(b}y)JK`u&(j-eGBEgn4)JwwZ!=Ak9H=7>lPYoF@$er?CWeYzvF3h z9W>dU7A) zrwOI_1ZjYA0heRuPqOY-8)(rwCbK58VdUCA0Zf_jor^il=hV5>lTIl^7t;WDDdM$& z4lTn$#?2ZI;s}-^BNYb;icHTfiG;xx0K_Ea{wz=}8la}zYWY?4jCxYpWiV)FKn<9M zu10pi5yD7@Uce4Li2z9x1NW&aaN0|n%!FfTIvmWdmAL^o+E+a5JsIUq-%Tx&1m zN@90X^{FBCR1bTqSCdi>$gqEOP^1GX~H;8=7rF;O|3eG#{r}G)QZMNs$O6P)`Ke*w=~b;|A%G+AaFA zAQq<#dvTQZDjb`+=`OvzbM6duwsAax@f3e4;R}%9r|~2E$zh`D2g&y(I6!R&LttoK zr+`-}z)EZ?2+SBvf;5cotV-njLvo}ee2(3SqS)2RRS;`C?=YOsBzuOKuS|tP4xu%q z=DrEYI62YSX7X)LWJ#gHSWvlNO7&Ai5+1tw2CI*W@Aw=_6p)cg+`DahGZWCwTjZ&S2{d(&;(`n`K=4?`gh&CuOGetFoN(G}0;8#`|_zY$z0xR(qLSIqTXZiVr1hNi#k-Y{P^ZU4w>a@6B!yVC>o_j3Jo z`$|F4mB!h|<$}6+LEU0O0}?9gvNPvlCgKi}F1CMAP_$go7%ymKIV=}6#S5CI+wTw^ zaY(&?u5a$_N|SHKy5wqDX>O%oXquFLP*h4c3(ULeOn$t`d%f@b1FsIevE{FK|J&Vh zUuV4VAS{>^mdtILeRw+SgAKT2YKiWkVs}whty)VR8-cXfw%#a<*X(&C8sBhW0S8+e zgzD{Y*yC0AE;uW1LdB)D>H6mu-ROU1pIBmh04P>vznTO{j?`Y2r#)*@k(jPw9o)co{h-~kT> z`*8ay?ebg+ic^iO73_-^qGCxvrL?vL2gicrC|J?X+NkHv*~Dzw|2YATt#zd-)L=iSoP1`cbC7~>&6sltfD3s6zUKli? zpycocIU4-}8>=sz8;X9xurs$IE`a?huozg(q+=ya>2vLn{fq!}>X1>O4&w}#K_5;# zXek1iKfp>PgE=a4M0etSR5RrD|%&81#URm)6*;-?@?A) zbSyYxkigI=r%9vc3+xRjBFT@nnWP*Z0^$YJs-r>ObgsIl)z(2%Aelt!2B-nTiK-BC zf!WFB3#@xsT5{l;&x#Ym`<@iD_Z zgfauW<`^b%Ao(PP9a=%avFS!Rmf6^u2=sP%mSklnN%YhPAVW8i0gE1uq6g3_?FgN! zp-NyE831J%#LbzE;u_XkkTm>?z?Ru`Hdw`GYOHIPL&Bvd1@$$k*uqU+8{>cs}PbW5bf7Mi3RvnnK<*at(% ztg~YD!$Iz=8&0`G|ZOaY=oy_cT8E6m@qb7+y&vB zW|0((Bu)4QW|ap=J!#uu566B7w7bIi7ce(6Co{nq>%cZcjIm6OF_yl?88J;RWqhoUZA6iT@AD%ij|EwW^Xgnua(c&omw@r`OUTS zR3?HLJRKwgP~)p$fknh>>^#*efQ<{4^i4}KQ$hR2lBZFbwSWY|x1L4SM ztSB{GH|FhAs-*&NUqIOvwaPctqZVXU6`LRZzzQs^p_7j+;nVM6_!Rv}!B~*}hUM5e zc|ZZt&j@~W;%&mkZIi0qDozyaA>IPS?9^|{PO9AA`zV@(+Y|3Nd@2D2c-|KTe7gID zSVRA@Yk%c_Ofhe+B5F8~8JMt-5G+FF5}9gOFg-#>alvj4KFB{qJelB<{2`^t3G^Q8 zZ95t`e)!@0JMWiIQkacR&V-He=V1HmG6iO;P3E>rS$oxMalx#i?`(Txit7m{%OZgJ zjbu?WFU4o1Vq_v0!L;eBy)aR9T%|3O!|6nRlr$V+g~1~fm%p%Qxx6)A-nv}ABVN8^ zv1lh5FD;mBif7k;P*6R;W4>usF!?+;J*9I^FnPG(_R@u2O^_7ixF9Kjg;RxaVixgU zXj(37jF&Yomu-&Ye;x@Ga`Km*74JGLKFBXz&aaE-*R5s>4(Eqig1hK)?R4f%SMglt zeDR{oJ8i>VP9?SP7Vm#Ocd=pTa>M?3!~SXKN?!R~5C+LsGfeqgR<_);u=&87RaA#g z)0#@GxC&pe%?&P=kOJiOC@QdE-^?&s&A>Vg&PJ0s{nV3e1CTL-;Pe01r$!0f%a6dk zG%f*zguD@;57KqXL=toGzpc1|!%_z0(~z2jMW8(TE#Q*%DM$zuO#tX1P)+b~07zf< z;{8&yRnQH1r`r+$ZY{W+5K;_wH6gZvDI35$_|m6gnp?^LJEretd_%zXgf1(tFPySL z!vP#@`3kUj(Qw5xf7t3G9CdD>Yn4wj3Z$=9hD*#jQ69TJiZUUQhE{u<|2v+D>^ z?iflpQ4Q>4xF0(B8+5!%=c%Bv9=ISoov+MYU>~P}@BtPOpyeP>CmwX%kd_3H8!Uc~ z%mZWBsESl_LPJ3f$P?wz1@qOXfi1y4fG$X92%SgyT zVx-;#^P0RWdRHp^N``pj{QKZ2v@6#%ygs?VRrC>T*I*wc0rm7=uUxVLSTqy{`bj81 zsMdyh3IO=m5kKmsUlKm{OPcxJNh?!hyOCgtR_3IMq%A6wO8rt@~ zgTbLwQgHu7Z3+ye#jo?%c(m6EB+>-FAiQCEqwRmPF^B;u&3I{FCBRMqQ~odHi1f;K zhMW=DY#9ltu05Gp2v2sGRA*rIYif zt2M|g{*IlC#k+p+#p|)F=DDp4C5`h{^BphMFO@Xj_+q?x*Me);FABxw!p3-E zBO*66E*3YvYM$SO$Zg-wxNeRYH!ZlDKK>|2sGw^g3+q>^>!_&bGXcg6>wj|#V1M_2 zBeyw9Ci%a=?m)Tut!npyR_ohN=Yg%(x65-6G~3>8GQs`*Q@0ayD)6DRx1U+3uz^;4 zv)GTz!S&SxrjPnyCDpER7PXiRaZU=G86O)noleN{AL9{oU?{W2A(PqGD{1#x#gYPdc?f zJr!B#2=?k?N*wq;3upko+AtmPC%~?sa^}df5!7p)*3gfWC;dS%-f*!Ep-auUW*p!K zY#`-_jt7rMlZHg^&RziBMsTnJ)FGn8U=Y=hUd4}ca>iIAhR5SJS&+d1fLaauabHbj zjK&uyw2?3zG@#Q>EruuQ#Sng1Zm`i;_6Fxa04nmRC|$ zl3wmw8N)|oy+BRG)Iul4uH)ynvTX#O?RTt7(g(d>lUOdH*jDAMnpaZPPkl+vORWOS z>oZnIE5n#qtZ#&ePl~vw1)^5U(T6peZD(BGzw!{PI4bmT==q*UP3a0-`xvD^s|)2IOO z#3Hzk3eZD;jQvHdU@b2~ih!h%c$UKH6{5r_I}gVIRG3ItWUXZ@>UkwC!K4?%lDn%{ zUynF}`V1hjf~!0V->d3nvX|gDqD3-&oG9iP5f&bfP-zhOFKN4|LJW|F&4QzJ&a-5% zRIQ;d=Xv9K-s{*nS{LlC3^`(%LAaR>rs*KiK04u(gu7{*NgeWJR2BBA)CaerYNsXX z$bR>$CYh=Y>w{ET`P)ciYyw>%uGLsajtqhCQ8P$ih8Yc7&Lerobo7 zof=T59GE_}8Q6>3#Y#`YUa-zwOh+-|x@nEcN7um{wWLAC?F8dbV;ah(2Iin<-%ZXn zXf5G(3a~KM)=y?U391*GSQ@iW*I=h>uDN~_iwD~HOBrLPSQ+mDlwN?+2^VC~ol
`VVN6{3qnRNY2ybT!E9Yo`u!< z1jpr$Mj~fNM-;Pj)HCwu$RRsm@;o`DDVxYUu2EP@Ogd2R%~a?Q3!1Jg!76r>zd=dA z4<`z0UQofA!a#BcsLKLMf9VHXjiLWY%jN|VyfL*vue)D z*>TfVa3ym#bIvl?^Pa2ftfl?H~JPl zZ7b`lU>h`xEY-JP_be9qZnQ7BcKrILyP8aZ?zmZ0J-_9;753|IY*}z^XJ$Zmd=jle zgWhT=YP&~x>mEmYspW0^mIHe%Z|`x!|C7v|c8}#J9vl2g0FGEW3|lT?fFp)i<&g`p zaMJ(*9UsPG!_r8HGaCSfNjH_5&g>&y{UC2eF4_X)qjXWV@qV|o(gEb5Ept5& zSa>#ERE;=k)E@`5gY|}V8bGI3!v1oi2a`$+@@3eF>D6dlj99TPbk_$CEkMk~@0`(4 zSAGiwEg{VmsvH7)SAsnV*n3cyw`$c9TLDM_oNDljI^KUwx2Vs|pY(Jc>g_ynKXFME zMlIE=t5rgL*o}0; zZX{cn>*CIJbA9hQtCOt0I-r?}tByOdrruMHLM0LBvbV;7FVa4~L{B?cDTH85^Hy~hYy{HM;^w9}Hr zGHiMi9;w*9qfI8NREG8#LNfVmQ?(`aoVo$=E6^FrO#2gb6q{jGQ{7%kB2&$74GV#K zRzFCn#)s4{b!mC5Ydkg{Zpp`)zdH)GFWDzH8bJQXeQ(yR|Q8(B-cM}A`qN; zAK{FgovuJm$^3x(2C4^DBAz;=^A1V{i>;5!l1>Q3E()-HVc74)?}((*jLrsz;4 z9AI2wDq*wJ6~nZkavJX~zvaSz=Get!@1?uzQzZ`=2ovKpC5!D4;V?bT>|v zs7|x41g$HzFbde13xoZ*`q!#@gi#YyQ%MttS(8db@uu7Abz~YerUqSr7NpJBYYBdY zNDl@<>k%TSZKO~d;VXfdHO$^9|43_3Q!~}@QS8bFDo<5WQ%)p+lBA;uD5EbTfis=P zPN@MC#q^Rx^`wNZ=vpzeVT$HigO&+lrHa<4Rsp3fSi?042O+Oy3@8;Wu|#B@0H;5A z^MK$Qj?SnRCOvvBV*|ZPx6jf}(;ex78W`UWbiGNEkCGB8E}w6NVFk@*wwDPWxveVB z{^A$6!D^@B9(thHIEBjYxfOerygJX1z)HJO>e(BCW{f&$+F=p3J`Eut`K0Ed8snje zOzRwKxH@X2@CB;X+8WGeX^Q`(;C>pK`l>xaLI~wQC5PeRR?3!JdCOF@NrQy{2=hx{ zjg)93t;QZ?1UX#=ZW~(2uACpcQT3h4m4fX?jdQwia{tBsFPFx%8_78H{@MM|L&qg* zbhXyC^;g$lFJ3I&G;O_Wa#qM8 zwgt3dWQb~@(8Kr&i7e1tr1k@qAKYlK_+ft!dXcjH*OcfJa{dQ7bP=OM?$F+%+)4Ih z^05my_&0AEiDADT;T z_S@S{wvyW!Ew;*AkC{qrn{REcu;s4q#kG?c4&So6ZB4ftO}46A85UdFt*k8DJ-Dqc zBlknI*;abn0^ch6R-HnIYuQ#Aw^e> zkWulW*=4JwsJNB|zFRfxY@S>D8*HVwdQE0q%`LkP1?ReK-di=9@D-SC;%$3|t^Pxo zV9)u$vHnA|)lrM|St9NraMa#%s*aa@4IkzT_PpuGm-DLQdDZjxFJ*4NWzBRnf9Np= zA6?4abld87?EA3U7<_msv*otc>DcjMr6G8sX8Tg+j@#CJL~k&J&vz|l`fge6D6?p7 zFjd8g;9FL!qZu``wcK)=Z4J0|#!x*ahYUxe>=ArnQq3|TzF6!u7#$( zOSXNt%qCka<+gv7e9)P)=iIUuJ9gaKoMUUaRp~(Q7ddPqIhD7H?6%5}JRV!6GQDVK zB}{?+&;&BhKeiaz#AIuPEl{>p*#c$LmaPXiZ5exl&1{;~eaK0B2tAsUze zn=a_MhZ^;49NyJQ_6sAm+b6@vdpI(e#f+M-sH~$^WPyKM@B(A z^6epQ8Zcp^qo>r(*ms-+#Let<1-2W=dYJ||@oo`=sfQDV%=R*MJ5YBCQW1y7Itk{* zpsER@Tq3XR5eI<|(B7%bb$J3sYnwE9=wu}r0N@}qp%F6&OqQgTl31ZQdpF30Aj%FN z3!a7ehd@L~A`=O!jJOe6FdBzdHC+d07~i9{5CzRw_FI13M51H1XhcMoAU#A~g6AU8 zk))`MD8yklQZ7kK$ZUOaeBskFw#) zp~?Y3+Osq(fAvZxT**p_L1z557^IiZ?hn|9Q?Q%`PcDYiB^Px%9K=cH z$Y3P7G@1<1<0sg!71}_pXl5s&lXFcSTD5b0gda4h$i%+QJPkiLiDt2`OyZ^in-KD7 zy?q6aQ9C0Y$~$Qa!*=k?rf+~#7QmjOJV1|$>7Vi-R71}7Pw9Bhk_kLvH8&C%c`e<& z5Y_z|Qh9B-vr${FY$+%50&LX=0_q0^(;zDe5=r>9b1p*00{fST$ za3W~#_yW*H25uJ^C5%d8NkQ|ptOhsQ#!J1|w=R_KzOg-Cx|`uhb|+n}IJ1@TDCIKl z8(XKd-m~vq%aYPGKM8{{cByWq!Jx6JQ6{ee6w+h(rfEOkjd3}FG(0W;ayuah z{~1vd7Tlu3dQkow3QHy43403zKby$my~AE7sdMU({(^8bukg9P7dE^QdcN`H=ont0bSViEl;zc^d)&GI+tBytj#jsi|LKs z>uhV(K-1yX)3ChKgr}pW^}N_EZ%#cI?qj*73BP-8s2KsQlt(X(B+j1}N0abkrhqab z!Wa|9ZkQ->j2ezJv+Vc)skxik!xv! zSIrqY6(5jw?`LC6fj{4SSP`lYCKoO4wMl&H2?{I!WAHU z(b^|ayt!>Wo+2e@Am%O9gNxP}CAwC$wn9^+hy$GDOrN8vGKM@r-sNP#wFXKKJ?EHIWWFp_P^RvM^M#>^zC znKqfR65Eq@+UYJAGC+t-sa8&+&ZGqarcq?iwBPqT=ROu(QH<+O`$w**gS+>hd+xdC z`Fno9H@|qTKVH;wJ=6ll_F{%zTT7O|;z|X~YoiTYt{u9zD_XdN3OEEBh{MynKN?!Y zcx-Dwo=roMA4kG?_m>NbU1RCZD?Hz;Z1$$S<96fp9dB)Oqwk$YFU}KG)?D3Xg&)TW zVGJ<)aJKMBww43s=z;UXtHA2~5h%KU3s`-&ypFaj$;)!c@etHiq#Fp~z^j)3VA0v-~{9>P)(H#`jf@C}&P<=?c>pSc>GA$p$h(XC-rh+xGU`;f* zQp;ba@|MN&mPIp`-OS1xIW}@~Y<)bdde|d=^e<(NWKH@P(@8&|7%hrn5HavC@JFOA z#-Fk}f5t=vEdP;bvY&CqF*v5AQg9S%7ha;X8_tgHprqbm?Qz?su@1-)2_^6(>)eQ6 zA5l6`=JU^O^7%H-Wx0KiPy&!Y7s&FJ&xQTIwf~Us^X1)fAyJiM5uNzoI@mQb2zKMd zx!u`BPcnMYT((WZyfCIo0t%%@D8|3Pw9FFCE%5 z9n2cqeeX8UZtMqO?|MDd))RJr!rNg=h}BIehh!I#?WRMM5uBJt2Bp7Y<( zZ6Z*NJx9aGAoT17v(!RCaRl;W@M&Z}&6YCRH~P3J2G!*Mjsls7j302E;oVENtqMSO zd|orW_ysnoic;4A?Rsq7bnu~o>J!N-Z%7BUq6b8*CJijzK>H#LLK5iC9uTu5CV$8t zScelVc#uQ{hd=Y>v(wr6)9D$b?ynVpvm4T;P~OX3-#9vaA?h#xAiH!bo1W?8o$>63 zsJ}sViIYy`hER?F=f8%lUk&bYxiKJzgFr!J@bVNnOTn4P?v`X*n%OyD3))>{`&XHF zt%2*cbBzS=N5NQ6z=untd!@oL)+;z)aBhXK7bq<8i1G~j&D_8q&2ai^8LJ#VR#*ua z9%lV6`0Xebu$=S1>iV{2XXj?n^@MlWWdh@L6*m1Gvs~Lw4_{YcUbX+4?=$Ob=eKdj zPSAC2!C~X$K1iufUWa(JXsafTeCsEE>|kJso@790GwX37Fz9-*crcI*wU$K7+dh+Q z84R>%nUxz1e9QBS8*RSQ3({pI*x@?o1@S?g9yvhN1n+G<_@wPJ$%$%t%KY{Fp0n=y z)Q<0CKw7KWF@%aTIuML4e2xGSB5etq;w5TLItYSEy^<+{GS@pjn=J&Z9TF?4xBKk9 zk3(l8wn-}PCt#mgChd~2I#84Y!zeYaAv6Km(~F6Zo4=w=iNN(k1ji z(5#>cl#YKino^6(c9tg|!hNv3B9ww4WkE0o3{X6Lrl%wP1zr1gx;Zq-opz~5UA+l< zaR`Ml@&=nk$)&IyshU`yE0?wTde>pct~eqWr%8~+f}zU zTn!_@8|4?VBvELg>rrJ|g@tPgbb+u`COOG7yKm4licmpzT_yCB8X&6SsX4Q2g>Izj z8xY2@+>0S970)Qb=t1`-*!=Ll(qwfJ6>E#lmmnbG=9Y*5=eHn91gRRdHYG&2agV_6 zHyMq`=LX#-<&~mT6Wk3zB=e8q0ck>)XE>qpW#O=gkLn`K0)ee!GZ1DWv&erODW3H zZG0Aa;Xx%;|F1Y#mgK6=Gq8X;1s{d;<;kn9?*13noGUA8J`KT=f5bnUH`?{Czf3l> z^NrY8Fyv@GmzSrD|LBKQ0KsLDKwGVUU&56$@6*^o2d&c4&A8DjQa?{$hyxG z;5IUoehdO-IaMw65uaxd8`4(f_Bq7#p$XE1%*5p1v1N$Q^wHn4aqiZE2)!F{`^rI^ zW#qiHX=KyIEki9g{lQBu7h5j|hnjIZ-11V}NZZ9daewhp^K^D-bk)ef(5~rF&S>*x z-%#6o*~LJ{^=A!7ek%ya?oi3k1BExUN~f|aVp$dOtnjesrav%*_KiV*! z5fASjZF#wEy0G*cfjgP5(sJm0atcO#AKguJ<%5ZHXO`axVSs-NAAs{;2s=OG`(VxCSEV;!WVI1T4HJ3z==H||Fx*|YnvIVd16!Gga~C;4@b0w({9y$l)$6yF;U z{odx+H@~q}4g8wze9lxh$PNMQD{D)VZK5e5upQ-EQrqh&_p2#iO$ViyGMs)C0svBY zur4VORNc?vjUmuKZqh#&y#2_n4jYd?8H(=L(rsehvD*1YQeG@~(0dj|MM#p76b(t@ zwW&qtTTrJ=+q1%?o0JbS;t=?Zr;sC6YF&y3bN4`g4>Gkn%(g*XgiQ0$Hiw)j+c22oT2wTmYcGR-CEhJ#aULn8~C}oK&0z871$s zS_ny+;({`xUdavmjk-$+&SW+~K)sUBytj0@eCXV1!T6#1k5Y45 zs5Z+*#~LMDRzG-5-Mf9Sq&BG zvju}nsctn=t5*I}SqYS6Jk@TsB*MP;bt1L`ciE<P*9_^zrZ zuo-YlR?jKDA)b@xS-u#zVPeS2j3#lEd%DSf>%9*;rdN2Y*j#B-DpmukZc|se22hK7 z@NRW}h$=Cm3Nlv{st`Wiq&3^DSAuBLBK3ZT)&83)lw(6kXf6KcCO`t!b{V87aftCz zxv?xb%%wC^Lvf?610C@u*MYvJ-{p3VcwS6TL27R!Br5qI7)wirZ8PA>G^G++1jN)~ zJY}+$P4^2^5#eRDrZwfs6jMl?M_Dk!J&jlfiRV=TNe zn!O1GQF_jej1Y*S^qh}{kp4<;bB61kjFRR$&pWklq{=?e)P>*2K5rS8=l<~dbh9OZ z6Gi5X3kb|KF(%0dAH#>?n0*8)qKi0D3RP)6-ZuV@*E-qa{~c#D9&9!O?NJ|?@ySS6 zqU3qz__fY3&ve!7D_1`{sKA%ho+bh@r_J8 zZ1_rK`$srb>~;rz)w89r2#Thaj<28S_}-T3(1vS8*V3a|+d$}M6_4dz?1Q1-&0yBW zhd@YYWRLnT?jG7PorWjM-q)X_&dS~mEuUTFDkz<;aOD(^ zc`s)S`{tIof+3n>WzMJK&Tz|(jKb0WvCqIC0K=uA^meE~W&9u`G@5^L_iNs?T32qh2-;FI1t2{7MUj;lfE>O@YV-N#@rOqgOFuhQImDGAUe2pOQjd=#A_ zq7M5-7Nnh^p<@F%c}`aLy6&c9P2uB!!Ju522ET%1E)-7p^8(&unF;GJAQ?hLNq{~R z{Q~3PvbQA#q+c&m7bPE|+)0DdD=d9ZHA|KOSWa5(V*Z)(q$AjnwGe3`;2d`cEvvTL zqe-5G8U*ZwHmBJy_}h>QvUy=p^22h&5r_=`=FQy&IZ{S>ju{$~c zHsIHg>rOc)-p#C%sjS9WR%1MC`H1HO{;H2<)yK0Mq3B5q41H;A&3Ml5te?)x#qZ=Q z1#4V>7R7R0Mx3LKcNZMyXoC{L8sGO*wqX3ZJ6=LTy7$!RT7DLAsE+UnBABA>lEOJw zoufpJW^_7qER+DCVnA7P5Nl9lIcFYvR?_dyj1?rqOX(`$SlO*&)7!6r*}92UrI7Ry zK>|`N;%tFVT!^wIg_&TlCR)mJN+S)q1v*T9nE9&*!7t5oSXu#7ipUL$#6aerK5lUu zHP*grJZAyPwgNa6!Gx`Qj2qr#-M23TVE?3kDE#o&@am>@O{+{Sdk}{M-9%s%a9Z>B z>b2I>a=TsC&&so=3Gnyziu_X{GcY+ZUTqjLJ;iLI)NWSA9(7|bv3FHJw)PmPM7J_7 zG#$@HTqsH%a!%+A=D{N{qZ71hGru$LZIKj~uoa*TB}O=o6Q}mlrq(XFZRA!X3?x+&OL9=p2XQ_lVK!O&`iJ{QuQ7n>JOYU@=v!4K zTk$MJbADGaYqdet_R5wTepG;7K}!yYBgu58U7qqQh)&HEEs%xE@Bmz#qf-upfa?L`;AY zta`xAuE#UNeaV`lRMQ@VO$aXv^p$RV1L2Brg|A9z`YdUBURG_NiEP){_DW5Pw$CUT zD~Jaxhgzn+{^1=j?H<`3^A-+~@baZ!+Bv#oY{U3x;u*CuZ!NL}vxoKw{gc2OS&d$d zsr5-l8xZNs7*9;7)sCbSwq($KG{xM0zU)T8&J=r=B-?_Ll?LG!Hc+TzEh|!VwZ{}W zIj%C~6!hCcKcWG7Fv~LQUX4fOF{JYV0F~HeEi5n)%a6!@A7D{dOELQXz$IOa)pLE zmD4nt(=_b6S-5EI$oP_YVcpy7udcp25O3TXE8IF9yj$!lDji!q_B3pO0L|*ppYoT* z{AFW%-u2gk?#^9tx5AZCG!+cTg5mLH(cp@xS3&e7j3M?I*S-1YQB~)}y&w6UthK>& zV->Xmp}BM*G;qr_5po1VbG_=4Kxm&JT%{)fz){_iu0ao|*L2iP4s(nG{>2&@Nx^F@ zl@>&zi~B8w@Ci#1Zk7m{fhG0@el`avGNP^p4lP7BTy&!O9j;yRwzu7ol5nBnecu zAU>BW*Fiv=@yApcE-8g%(u0KJO7b>mSTqgi4dU7g0VcaY{6tmEGkF<&EkWd3anC4T z)o>=txq?A1D1H_K!r{fq0BBNx8JTBc5Hx4}Gf;OBm*5}iIIwJ50xQp3G^y6C17|W~ z2S3xobHbEsn<5}8K_#yC+BO<6NwM@c`6mS;15mfTUe`#Rs~Ksinn{;)wr7N7X;=>N*g^DM zlMy;!;TTp9A+&iv71T)NFdm0JNMY?n%EY#K;fmql&HTmVfS7L@PQO`DJ-#PiuyQys z>-DAAPM1`V_ryw8TwN0@UN_tZsv2PQCF9L;iGxP5GhOS4V& z&3K@po5@pG(G<=*=X*{If%#^@pxn^d#sWP3Jzd5r{^W2m7O};p+0U~kUhs3PqzAMB zSv9IDz)Goj{8!h! znFZ?#kr$8qCsutgV>;9{@%+SzYwl>)CZ~*f&KrR5ZYfyu8^OGrIi=qy8{THcH3OE< z2RY%XoSIk;?a*`Tujg!d`|w*&BTHr7t1n#29`nTf(KWdp=(}rBt%dWOeR&SgH zk-I(S58o^-8LOELuerKC7T)l)!i^)X!`ntvrZcix*5z~Kx#N$=i|XFZKwJV>(Wcv$ zU3S8R{eMCKszAX1O#fo+((K`%>0i*lKg9es-J2bYq!~EQ^ksGuCVKM%rh@Y1rH)q! z6K&JjX;HFN*$V|mVi6=BfMF%3TQ{z3dI;z$B4v>*dh<2cI{bxzRzIwy zwfYBTwCYr(V~$giz4EtNDem7=t|Lw{3Hb(Al`>eknF7v>Lhw&BDCL)%AE-rjpH3BU zk;%jV^LLTFLWajKcJPyjhiVl7OF+*(Hpv!@hN~!?NvIlI~CY+@rx`t0^gfs_@ zkY>{e7t(Ad)KQ`bft9fL@UXo%8RH|V5XK6^1O{#kgfN!HQnE-PS_tFae@vBs_)!P_ z;djs zSjn>C-8V}r#?DWyikB=O-u*#I`D+Kq^Ttoa7p=PbP`qU0@NPPy?itxL7I-(KdN$}P zue}RTi83Yp9`A}~H%9%9O6}<+QH%ta769y9`pX??)pWK3F0?pX zLHMT}$PT)?yq`lYfNSl&;Op{XeRE@_Yrz!=)F5oKjBZd_oWS@S1P&U228+3bCDJ74{FN8sNsBcV9@q5a1IH+;66!ox@R>9Gd*d8z?R6M?2 zu(f+baztMRTa8NN1jPQa#V@)pR17<56HqZ$A(ohhP8-UgN7Rra5Te0?ss3~npoC8( zAB0c0j0S{rx02RF^#L_`nz;TAp`n%KPnvE(rC^h-NOmSKI%>vGwE-IuUY%s%lxr!Y zQo~>xY-41`i{@t9@q7fifus)sMfZ#cj~OfRBDvo#_uC`byIriOK7?5|q&iHVEbY)(8soacjuLzmk+?|-Tra8Wj4oEUI`obvha8v?1M(r-p?yT?4D@; z($SVHYi}dCP~DYBRB$0V`Jkv`s;D+rR6CJ&HUBF7o3}(mTh#g!0Bw1)ucNEu%&|^; zGN{n%nES5zRwNI+0zsjbOgW)lQQaLw;Fa}^vKLZBuGwk>}Oqg2|-Pfd@mqRI!P?XB{5#6p-!j=_}ezO&^(VUda0qN6$kkT+zvmeE) zb$$s*qk}ps(+ow&_rt^%I(}_0ifP7a!w)s3oTz~oKOYmnQSl&Sj9hCDnLehL1V?xW z=$|pn=?4X1e1nz%x+Y>CCus7gKb zt4oZto6s{{)(~n3p*8Gs;R<^kPHT3%X-Ykz)$}#6O?Vws$H!q^ty7)iX!H8;3f)r; zVPFR-P6Fm8tgKbC*#x{Orm!lIXqVkc=#7O2Fm}zcz?zMPTm%D0Ly)0!#7|}n(*XnU zgy>4HrjBF-?51-;w)Q2wx9raOVF z@sVzh0_ihI=cz-lJKt^e0fG|UjVrP$cXS~plRbbpo7F-AFnEwCsMHcLE-_S(J~k`M zwTn11z)6O2iEdmLmTuB^;h4V4ypH1ps+Qhk5$vvr z_*sOhl{LvZm?Tg(y91%fP#&5CWrKW@h#^?S)f#!gjv(NCn1iC0(+1_jtAvA@hM(#g zIM@r>{scBQ<4YtXzR2W1BXQbC$!#WYlM+~r6Ho$RkU_);YGQ$!cwp(!_S^YIue4om z8`|fvL_|*O_?3B%4*CX~JEf-jurrvG(Ybkk5-lq8l%KwEi?nB;+77Lqkb95$223T{ zJJoF0t1WV>_2zy#Ww~Nbo`mn0)7&1UgBI=!=8K_(-Ne!`H=8(Kq*kg+RlA6dBx;VJ zpj<1&tA!&uT-P~(0SI>OOt)nm7{LZC#XU%A+^j#LIH_GN>ZV67%dN$>5PlNAM}23} zCm455tu2S7IS~=24byU^9*&xeq^;^gE>f;B`5TTwim-(9r&&o=ELat{4YL!tN78LV zyDh`Xr_pY8V8l4bQ&0J5%N2Hue3-~o=8U7GBt463~F3R zP||y8H7VxwNm=?pvOtoA)!rf&5}WJ@J`b)N!38symklPrWUCQs0j(AgW1PjY;NtO` zc(69=tyKnHc-A`p58AC`jXhsa^$kE{iwI*K@W5AL6op|F-bsfWuj}eQ1F7sXEWXHm z8uVX3U>_qYbRvBJ6GA1bude0gT4mtNy}bhtT4y1T09+%2DRnb)BHv2kGN2IgM%xQc zy+xW<;R-MSI$hehgk!6@4{4+qY+A*G!f*y?L=_Tj<1IQ`eVWEiV23NS1bIEF6amCE zEmrmD??5LID;f8li!d)6OuUdam|dAGUAZ(CTnc$qurcaYLb~L7C1~V01*|?cG8`ga zm+iCE3uHzo)>SeLnXc3xjot{ySWhIC;+7uRxOKtN5}~FuY~k*pr^~XmgE)k&rW`f^ zQ@W)VDIA{L^x?P(DS8iib)RA zPRdKPR~uLarE+#GILuPN?rL$_=OPf9_AcC%WSMANalj!LTgbFw9BS!^jDY*^NOYD^ z9vN7rZBGkc*1?UJhtD#=rHcA&N_{g}Maq@YcZj81#wwk%sW z-D6wtte3(g(sVp}31ncE_!5ZAt2M_q9e0@amGh9fp zs`G2_vAs9xG>jN>4hpiD)U{xyNSoM;40m{8w8d&lv$&e-mgSaU41`cIlv9oft*j+k zq<2_Dm2EiD8iWF64eUKk-UxOO5oM2HWrdi6;2-J{#cMlO$KJlPM#pLUo4}FKm8TP8 zI@$ZXP7J^qKwdC#>Ihj1?fVQ89JWwrvtV#0vR#kLFlUKvs=Rn%@^c7&_?P67so z!Dp*DYO3f3t$%_IBJnQ;@jZ|MDt)6G)!KVn6CvRCF)z`lpkW9SgWRV&FgVOUP!h}| zpnw#U(A6!OH4R&G=Vl&Q!#*WNqJq!CUdq2jH6h=*{4CZb<05SVt{#=2Y62Ml9;MBsfi&U#Grq-NURHZ1Rq_A# z#5XkfR!Ksv`giaPo|uoiUA4v_BA2bFJD#?v;3E;K#0DveWK_G6>nC=>o=n1 z;=mNm-V*gI!Ku-Vk3L^6cpH=($gDN>*h(;vK#)*ZA~g9Hw8*E?NC*%)_kY6H zM57io^jdJ3wIE^flCLx67;N`QxFAldWgKp!S6ogZ;nImHs&pgh1i}W-&(ia=(nkPa#*R<1Lo2Nve!iU!uxA~eA8Ai8=)mu%uYNq(po<$W&=a^4+ zA?Aqv+J$e$_R=SEYV!ltes>lk>oA`LzwRu^uEWBDa7PKqKdm5CKjEm;d)-Ph<{~na ztynw|<<*)PfqjgwF?+6V7Li~oJI*z!M}9=W#&QXl_Jm;B0f2^#d*LY8nG z?f0o-@pmP7;8lpsU4#A7FfnDaLIT+5_=15j^}P0PQ&l%va0WH zy|VQ?kBsaApjP2c{CxPz!xQVGWvj94ZU5R{?0V&mvg>`2UC1~_f8@FPBK6a5j6j9YIJ35ta4{Gdso!I>mRRI?vz^pxhHp9iQm|RPf4JQEP2W{ zRNlGg*)k_M$uO53$ux&{PD-ab#<2AzNJW*;wT>MR-&jbCKS|JPmhXa5!6Q8#no&%MV4v{gzA@|_H&rN*2=<;lXc17e(yC^pFu3x-TiCTZC%d2dGB|{ zS&n|Sl=Fbqu!=Uz>F+#kUK}16_{qdZOi5{w|bbe;3R76~7DRm|rj$c9vsaebDdf?Rj2jML7UNfG(0$Ubs#o z60u}Xd-ecSVEuiaeJ3H=wGXp>C>FKK5f)r2$9SDTp>>Y*!2UojV6@p`u~v~`RZhZ` zO89Z#N#(fNsDw1&#}&Dh$UD3^+(I~}Y znXIy4YWtfm#LZ#-a9|^{7^h^0ZMI39`2@}Fz)<}i7%Et`Tu4!7au-rBqyhFBY>hgS z$#&s|t=~#HvMrlFL3Vt>Atrg7`-{!XfWYzfx?w!$AM_@LWVSq8yV9N|ZLtW_V1QP4 z`M;f_jp`okIrpVS{p=z+PFTRr<`uY3c~CMuZ(+QwTlYe`?Wbz&!UGofHt!7KY`1-}X!SoZg<~{rGVCvfeGan;qz&k#r1h-Et zxAT~1c8M^r{vTdf{|f7lWk4yeULo^(4UktcdMuMdqma^n>9?Hw_w5Bw@0xzgW7p2x zWqINo)ekg2LiV%C#?S@NVb`ep(mK->4R~pFj@lPo2=q6azhMGt&4|OUE*MjOF>vXM z`g_)GmQWw;XuuTIq!?Ey(ood2*d$HiM?-*7mmjI}XA9LN*8U_d1AO<>ltm zkH4DXz!Q7h_U~DyWyCh5Oh^pI*f9ql~^v zl$fM<G z;3WLn$M$Pq5DI0?4^SV}fO^uw1lst4eprvtii8kc5r}5A^m#u_p*Dr-Mop=;c%Ynu z8#QW-pCs6=>M1B5!4Oj&ZjK#uz>AVWhP+j6)nEyH5te1;kHYnlz#*o+HJ}KUd&VSg zAn1nhg`AFh110wF`3iJcd(Y8!)+*u<0H5qQb)>7~;R)`o+$e`!W6uF8t6#QJ>Q*sr zKKx%ejQzMfSZl{A80aX62wEM;J5)5Nau(tf?UKlmVVDcXm$jcs(=%hnuTR#^WLcGv4@7V& zKxCan_s^v7c)qh+Q1KP`wp%KLq&_I}X3lkG5&Xw|qX#`SaTv`$2vX;iP>V|esdxKxy`{LfqpC8&g z>8-gLD3}T?js+IK0Slqpq3!Rj+7RVZ~jMjcenvr zIp<1GZ@86R2-vo&-`RMxaPd@OeXOwlvgdMDi~CIKqwVovebEhg1GyO+o9T zlpjCbT-;jf`)P@r>7w-3&Ay*54YscH{dAoV=bgxI9h`^Q#Vn+5bgw$@M^1_y0ukoS z&CjW;REEs=+49tOC#%N)tu4>7U&(63{lJzGEVRMwfd%j;>@o<(Dqw|_Ve0jk+rnd4QJaDv#&I=9-oYMvW1z`MwKDV441rFWq2uH*4 zG;W(p2kMRA@C~L%5QgVMdT;7rx&eMCk}{YM4hgQBLgyz@M%gN-6Bgxe*Nj{Z9fICM zy?B`QM7%r#wjaNQoy>vHV|@n}2Tunze3~*o4(b*`R6$EPI#3gqQ1^gDBTeR@G1Vl% z__cr`sh)?WJz~34J#Q7LMh5ITVEqqeIL2>>LK^JowaQ>-Wmf|Fj6m9AO$@J{2!x;a z7{ebpsOdy)LxV~{?0B^2*Z>9|M!ZR4;d(R05{@qt`YK^AAaq)!2S$7Yy@FZOaI^zB zsJ)0P)8AlK51+0jq;zsp6NfW5VSsaRbB@fz!B)q{d$D-LZ8#7;Z5gpb=;#uEuPzT! z92nE{AoT3@-u)kPt{6{Z<^C{?GZ+pIp`(D`=kA5$s#U*)Xe4Q90Z7jJ&o4x>2y`^{(l%>d}_lp}f(S%jr|0 za4ZzQnP2wGfy)QRcTB9m+H$Qfp1=LBCnYC$zQ~nVBC7nmVOJ=Bbo<3~QGc;oP&3SK zJc^aP6Q5)q_|9LA7C8SsJ=a;J+O2+#6WpE^oO7}@-aFYIe{3HNhG0kvRt(!j7+TkX z{%*&7HMgU)91-L#)R;TPGr7hj2lK<+jsoHbT^q};1r}x0U$0s(+iblf2(^GHR==JG z>|h*w-G;hg5FwTUm_&?1L{aM&4O_u?S^=UsWDPNOiimzib9FM3mzx&Eo2=(g_dx?4 zKG}T~E;C$v(O}xd*lufV{(xqUFcY{)h&u<49S_68+~@-#pjcMufsXf`#o7xaA``Wj z3q7B)Rye7HnJ5%9p=-=48rAfV-!uw<~PhP@f2cF#Vc>AHYeLLEp*uS@J zU)!M_Ei~Oqc0J)^r0jNBRUd<)-zMc#3ZjW57NiNPMLH#18P;U1WmNB&A<0;MbNrl3 zGJfP5lM2W9$(Qld5X)|eXD^HTlZbNhz^^#pn)O&=?X|rhNK{A+Xpfn&37P!@9aSgU zdE7qT*LmD>-0tG|Uoaiy8k5zI&jHJ%ksKYa+rOsKPr^-q5jhMgcoum&7MRd>8Vh3q*1QLdi90=xY`lRt7_sucRZe2Ji5PI-VMfQwX)h1@yJtdYdwOA;BpD@e)WSyi zA}2qSeU3&zPznHMG{&+UFr=-+gERg=PrB&SbT&3AM>hNXiJefTbu9c0 zx;Qj&&^Fj#@puPKJ@@w=9Oyj$n2ykbQHXs*f0B6L!skp&bg@uCT%>jTjwNm;+u028 zjRu)W63VH@8e&5*Imm`6z+DK1R6}sZn^+sqUK#bTytgSRY?G!Km8Kx*^-0=@@|qm> z=L0s_YhJbgwp%&-L6fN?neAROW-HlS-qIa~Kq@J~cK(eWy|M{ln!{pKP9Hn?#G$sm z?JYaDJ+U*ad`C>7w8X=qxA75dE2dQ#jF;%t3vn-6zP}5$WvY)97(jj6OqP~#!*ci( z+>C+U4Do`J3uZW9rbyo__(qEB`T==|*I=wRpvthN5tPIP$makCug+$C6`k`8JBP}f zA=kX$=zq zjqxW;{xuTFWn1f0S`QijhClvL#gG<>=#QA?OHxZQF~-M4j92j6_?Nu%G>6JhaR$_I zgb(~aZ+wwS8Sj=eG5C{xZE$!PP$jwy^5F)1*uY1)45Dcmt+Vw_~2t(azY+TU(fHW%4kSN0>ayq?yS!Cfk{` zFxkPRmB~&fyO^{w+0A4Rk{Q40E3c3N=Xfoxy&Xg+g4U;YKM$Xfu9>X%cAOa<`nr;K zA7B|rCI7LLeMi7sL4>W(jBoSie_*d?BAH2}^bPc~edwGE@vT|GfC4Wxw=fC3F3Fb`GB6EY&#~5JpVatldyvV$VDb`^zh&|mlgF8ShRI*^;s3@nAI}crY$hm{{3=w4MAVV69ty0{hDr)^ zCeR|Tzek=gorH z-CcOgRV@E1Z@Fr2xfb7YExF~YmVZ^ZT;W@;id!z6;2$oQ-*T18zcR^;{IzlsCEz;F z>*bN9l3Gt88=kE*%fxkLMp1YdPE^V2V&g5>>N}|}Z#rO_@=AyH{M=iAi~kz&@2|Y| z@B1?3e9J8_{;idN>uz~B$i>=Q-lfR=SKfyAeSyF7*1qraOB#?8YHoR}p*P4YeQ^(N z-164yA9zt_A*A;wFP|L!e6)IPH1N>S_8S2xDB=MEBF*`i2UCX9W{Z4XkN!c=J@SB8FH={z*^$&%KrJXBS>P2Q1Bu(9n(>Ir*c{{4A$>*f$*v zO$Ezh!LqT|cZ1avz8_@1nfd$KH!7BlpSt=)ykhh4uF)q(4oqj{y|jO1|JXzCW-Ojf zbLG~clIc04OKwwtvnUo=G?aEbGdvWS&MFuR-pJ061dI_{}?Gi}=3f<}AB^XEM&^(k|%6mJUKsdDUvsmc|x$`w(BQmFlr)rz)Ca6-`qW>tYq_;-%~3 z1smcS8;9KpDKVNm+B~{u^yK*F@#PcGMYC7M{HsPhQ0YW-s+`vdbdx*n&r@2SX+O?a z%%^!XQV}H~;PUySnXNzS|IyKCa9_;3Pgm^WnD=4!DU>wvKxx!l%5O)%jRht>)we+^ zhH9>dYF=!cOAYw)=1P6;N9IC4cf~)Hc7@y(cU+PlT8wb@qw6mX&U<`?DL1mqh8K^o z`n@f$Z@KDuw|echoY|C=)bO0gvnl-L=HbCR%oKIy-YwtjcIOT6yzAoMx#KB5clKN; z+ub-@<4O+>`EF$AUK|+8m``2fF1P~-*z#3F8I!K!>FTzjj44;~byxAED?Fe2l-s>z z{!vf9JA1yV1P^cUx=W^uR*asTDq3;9Xhpne)l|{?>qYDTq~zb0MGJQf9heQIx)xN)j@<@Mr~@#57{SHXPh27JsJcZ$3EPQY%-dGAu+*4fA0 zE?)+znbA#?-U>BCRu2zOddgo;w+q5k z1&!AW8ixb7p}S-OTPD5XxzGmG@Qgdty=1Pz=dPO%rMs8S7cX;fo&CJq6<#%VZmM$S z^~#mg-kd4#qU+v8(aH@Nv|+S?v2-bywM>t9m}Q%)NYW%O-ch+20{8YLb`g{$uh~%MK=iy0jmA)MBW)YEaUg*XwJV%kcVE&E==Lm)*&C`N}?;-3k4s zKM#sXclLB3>no7o;_R1myX!FqKbrk~z~#@u40e~?E-e1afmt8UelZ);7Z=?wEsuMP zcy-ao5mFvky-#g${in5shf}@LpqFWgX{@~FaL{$#z4UO2YkEW4VUOoVikoRL?Qo&z SMuD4YN!sBW&y8v~(*F$-L7NEx literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/__pycache__/_common.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/__pycache__/_common.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4fec73b6d0aa558fd28d20f3a3e65af08407802f GIT binary patch literal 33501 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 XY%@QxUr6lZDLiC8YZLBRd4&HD9!hwx literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/__pycache__/_psaix.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/__pycache__/_psaix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff956d5a96c1ab45774e122b6d365f78e1ec9d70 GIT binary patch literal 24668 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 literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/__pycache__/_psbsd.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/__pycache__/_psbsd.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1831bbedc33537a852d2e3851b366089711af64 GIT binary patch literal 34520 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

Zo?5e5tkp6iVo5Ad{>;;Q zJXEH5?P+MI;bCUHWcj#1Mn;Bj$yy+_lZeO;5)nCv#jmgBvJmmOKab6U)XBp4()#mR zI7r`LFzsx}PZk8i(T$K<6}@nWGe+j!L`@Hj_F-hS;oKl1f5w&m{Xb0~3$+bURb@p9 z!>phrm<2$%mo=D~!q^$gLgi!;(bk_EUMG+hrvx(iKvaG!23BeX%OV~XmNf%C@J6w` z0dh{5gkoy2CN1MY21s;SnTaF{xnigYB*HN=K7KI>t&{X1R|WTY1PiFiWF=X?d^9%o zh}f3&DM%gfO_joTbSa*N)*#O3yXRYP=U2r;KPf4lw_pULqt}kc_kG~qey4ovLiDxR z%duBx7CT;kPAW$~cZde>+k+xmwU}S^VM*!F&5$LZ7F;+O$B#ahwo3&&1jh~(&8mg` zs{1^OCodLv)S2J(L-NA{8q+#!M{By2W`Syi$_+tAj~GVuOqoz4Z50@-4NyR%m1)`> zIDnSZ7ak=p+Xr9?5(+W^Baq#-&*E~+6pnSrX=F>;)(l`1W?b{;&P8xHLZ3i7@LL$^ zj@gz(+cB~2*!u^iwrq!%)O9nL0Uk{WZUhV!P}n}>0zj!M2dzj0lwO21w#DQ3G8_Ot zR0bRXJp;-%IuH^wJOLq!;F)kFJZ|if4PiG3o#rFx4nkw%L50o@wW6z5a@8kX+eH^x z(G#s5Vr$3yjZ$mMqvuXR1$sF6RSyS))7W2JSjwJ)ya6o#5+S%TEdL6MWBv_cneqkn zA~-u0a1w_!6`tsj2BDi1o*L0p6Yo#+O2Y;2N3S#hYsxF_%LLY3K+!WpA@Imv4_N%P z1D0$I+obONmRvVhu_2)43R|+kxkYrfXmEZ;^6WrosGYPrr1JMqmn05bQ}?AZI~ zh`k#FRd&Cw-Ix|2OqYC#_D%>U{XPVePI)_hscAv(8Sq6)?Mr&fO1x``Gsni%0A>1E zr4f>*Ic_wsgJ_lLs*+qa30I@&YFs>T)`hcwp)g{K{ zRRJjORy#L_XM^Z!SnNt{KO$~Fa_bw?_7kG(gycNA84N|Hh%iPokl8F_L_S}LELFw@ zt}jNM06mO5bjF4Izn)o=J@2gY2Cq4J5nM$0rL0pBQUdr)y?Kf#nzDlr{kZlc`0!gK zDL$!EWGW@58Uq27*u4u~ul2s%yXbqj^k%8JWydmue5T0kAfiv2#H?=c+hE@GAY*kh zuPSD=Z!BhL@z^1>Y~iNGjI>XQ&ze)8Cs&y*Eu_h}Aq-5W=%LvSw69tjId!P4ohxJB z;OBE`j+CrT)Bj_jk_^ky{*<6%58Tg{1r_vuhp$kbCvq})5_3dkx$jvYJ685V&f%T} zKpP1zI+pP7VO*RA>M6qc8EjLf+|7{5txy=-W<896@2|PM81w;c^OCnV;U&kX4v5~v5_9Bz_?}(2;OG{a?w>PITgb1yzlJmNTgAs5 z=HF|CjYikOh;6frdhmC4j*hD7r|<3fQxuH0~b+& z9&5~wi~8dwJ)6bGYW!Go?Dg0iGfN$>KPOf1oj>{T*hr498KB*L)6ZZz^&n_T5m;UBW^2 zKt5?2=eaTb^$ko8VM0WA+$cHR_vA0Q;W|@-MiYe|FIzc?Jc{&KHm~Gi4o!~aTk_-! z?Ma`kEo_0$XXB^ef)nF>~YW#?*|A?yih-&zVDku3W2v}yZe`IOJutb#3&yTH9@F&#-a-il< kYhau2jn9(N?f%qsnx=c{`CnV3NCq42xhvn~s$p*aA4>{SIRF3v literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_connections.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_connections.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ae5a0ce0cb7eb381d8762c1f14fcaee0c15ae81 GIT binary patch literal 26965 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 literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_linux.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_linux.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16b963ce8c8cd6a684ddc5049a080bdcfe3cda59 GIT binary patch literal 133734 zcmeFa30z!PdM8@jr538Ff&vPGBuXF&&;nu+i*^EWhIO$2#>AZ<7cf0$SS>7!QH@L#QIEmjRo=Ik;mS>!3 zGV|X5JGbsqRhLDU`=v8)EvexgH_~N} zNA6rm*Xb_le7X*uPwzAI=sWc6*U({Lzs3$D`!#i#*e}=ocv;+y_?nJ2ES`;cQAZJr=OA9( zQOx4Gh_CHf%iAmvnuXsx@-bue5TYRfJDt)Uvs(ghV)jn@W z4ccn(t?Ah6D|$)SQR^#4Sm#@du->;0VS{fy!fn11gxh_k2zU6(5bpGCK)B0qIQ5)T zpA_?Ld`b6K{H9~i0%(lq+loO(&ht^A()h7y-z{!F{hxB2bzw+!MFgKnSBSMie0 zx5dBuEu;8MuDj(v%dJGYRT|~)z0Y#1QErV!xvlqE?pBmrt5NR0`z*H(<<@JIyZ=7R zZ9uu(G|Fwe&vLh;+#MR_K5(Dq?nJq}G|D}2pXKgGxqCFqJ$RqxHlo}njdBm&XSvNN zw?(7e_WLY%FUoDzDEIJvmb(w-?$;>y!TT(?4dp(dQSL+cS?&Rpdr+g?hwro8Lnyah zque9+S?*z!`=Ca-NAI)Thfwar8s$Fn#ml{ioVlPhN0KPbQMBlh)E3DU=h1s8UyG7G zit@jbT7Jh@?z7ylqTI(c%KhqnmfL}HAJ-`NvHL9d36%S!M!6mLS?$+YYxMVNVVoZfZQ(d9>}&-ouX^#gp{?jYm40 z8jl{``|xA9wbO0$gO7B!KD_rK%0Tz~_8vWWxUr@2(S3MgqbCm^Y&+CO*-Z~V_8{eR zKKR(ty+@9AKG^thITFdyuYWv z%h%c4?;Gr)Y%bm(;?H*W_jMlc3iJ%}^ejEtb==>{_YQQ0PU6-Oq|g(5DlkCl`uani zf!={0f3LqU1_PY7MsdPY1gFt${wDpU2Z|w5WTqrz_;| z>^hCAI#2iddnt4OfWNQvSfH^Fxpc&R-gy*H zA${q6x*pv*eJIY6>U2Yp$b5!;ozEz7uIF?ij^#yg$|S{<-;n(5RGRWsimBdOq-V;n zRL54`IfJSeCOs;tC^n=&2*hR#kxveQ2&fUuX{|V$KP4q8RKKRXY|+2?L0C4pHj9dDAL{H2^>@X%Kz}!Kr5z9S`1|@}=||cQH}7j}ZER_I7-fX`!R}DZG|<)k)OBMl zEpWWAtJlx3rFQ6pK^kMP*L%04;$(lXzoM(hf2QI@AXLGh^!If3_`CSNK;Maq^3(pl z(-l2|V-*1Wll^_w<&{+x!9d8rnTqW?;SW{}u=!KLumcM_dkAlo51fs;PN1cMfzvge zT|OW04+i;a)Y^jo;AsRe=>F8?`oxhnQae^WRyDGH+#Ge3zR)^vbzWR`e$}^6UwUrz zxof9qOX?#f^)uFnsfXsBIkV2fh_i6co_X=W`2%D6E0)Wa*KF4gd_VUG1@9J2J$mEO zXyw7E{m`5%YozLy(cs9qV>G#JpBr_yZL`*fX=}sXI|MoJr8PR63XL}lxu#O{|DF>% z-T#+!f?tghC`)Ic$ge}3Ur#{^1*H^}QLur6jR;~Hak$1zIMjIL0Kb{ulv7ZFK!Rkx ziqa||hgs4K6wcS-@q_rka~8pUgWQ}e^O9xM@~Z9i_P1N#IdJ{JRN9TS=%#&l2-ZH% zaE)Z$;tYlKA>%2T93y}Hl8D+l(~!w$Sb?WQ zx~DLAg8B<9hYY7=5-p|2dy`a$UU#9~qzg%^SKW|7dgC)v8GJ^_D&-?$_?qFna*7xA zRz~{N#4ki_1iO?npOY^ z;e@W+@FZA*bLOG6A@h*g$AwW&zHZcT!EVx>vz#;g(lnU^i&Ww{Ye;4jPRUFG`v4Y% zGNkyaEGee^4p}9#f6k^VM}v`Aq8yu44)w?mRB&O`gt16vB`&-xvJ%~3F1iip%+Kh` z%9QB5=Huc!Jtb{~uh8+X%1d?jP*&dBI_NkArXY2EbtQ$Gf`y-cL=a zDyH8Cro%)GV~h(9^aMh&w5|c%`C>+}5;5DOT|I;Ty*%H~$2h*L?}R^QI@KTOV~9G= zfeeKD`(laTQj|coczF^D*w!lxk3cJ$>V(Ei@$NTx-t`KE(j}sLX$ICx+=V~LA4S5G_zyzC4rD)XbqEg?XnojTjv&$4wH z?r&F*gkRnLb4T&8elFKLo;lh(-WbVUKb-!_rpn1R-#;={5Z$`>#?v#K+CVO{Y#(iE z8-IEjT+CSGxc>6qX?u~dsSQP1owsy&eJf3ulXq$V=zcUWd+o4wKGQRsxh9gi=Gyu< zE3Q^dHbgUb4yVoMt)9&*jpUWSo%>GV^}?z3(Y(fC+eqchj$2M$#@bshojq4D<$S&l zjTEfK|L}`E6dlY(A6@XaIE>$`KyY)Vvt>ovj~!e~ZXz>8cz|K7-}sdi5Fi4`cn7VF zU%-7Df`NoBl^GezlyYdzaV-(xoYBXf%8_y@ze7e}n$MgMuJ)oSj-7z#8D6%$X7X9T zYXD}lnP}$97#PBXEgw%(#fbPEH$AhUKv9-13KV=H+Q54$Sc4#@`;?FsUrcf1mZ~%S zZo1uxU@=ZhVX_=?CTss!l1N`G6~VD7Eplv?RD~&k$s03upX}m;L~YpA6z8RrC}VF| zw=^wzB8jx%5*$Zx&m7pCGyCGR=bxRkJ1*`&zyF&LyzaPm`psvrK08@ERU0j7!c216 zhE13(uB?mCoPTDrX38*C_3rklXZO3?Cu+uO#*Jee#v6YtV{&`MvpeeCbHjb3NO11` z+zOE(Vr}^QUs!Z`d-Uv9fOrt3eua2oZ>lri%yKkU*=`nEDPF}(89TXLND`%Apty*_Uj$QD#8gM6Pi_)OP;7fOzP{%lFm8}x?y zLtQ-v}xQf*9c zimLja42HJZ%FMh6J;*mAh}nXnEG5}m0{wl#n2m&PUuY0oo|q9-m~TPyGE2;eVqzST#2|l=O3-)4IM$As ziN1^f8a*`hg@QCvY<3|KCzzEQ*-4$k58x$KJG$Vj2wu>AX3?cPhL4ZGjAxalKz&bPbA*1dY_TJgB=jk33oOlH6JSkzrN zZLhm!)nyW~OwSQKJEo3I?GCqo*Gq7u;1YCwK$df;8R!TK9J^=!DV(k%k8*8{c+MJoHX3qJJNx zroNct(SJ(q`jomlyees0dxIcWz!>5L4i~E@Ilf$Tj;ASeoIj32`4beJq@V>snT0<^ z@uw)DY0URh&_@BAz-eNgd_SIO%w)Uzv-xvKbcqtajsTO{b&r#I>xb!UJ}NyhmHo&0 zKg|Ep${Srj)1z;q=q&rX;k_d@W7%V;<%vD#$`$hJh!4#eZn~YO%g!C~j~yManaG`V zN8MYe?OSg-XjW&iS-pL#d1|YW(-^fi38p6gX*4haQb^ZJMCi^8f<)0)b5?`IY0W@| z2tr)ky6}va7&;J0%;nQ0_U0+XO?!4LMAK&>(NiK`@_bZ;ULsHipY6LwDb0&|uwAjV z)4f3Rp|D|dP;>t1y-fJJo34Vau$z$%F=t5}_P|alq=SRvATYqDi3n51n2D+pA&Y=2 zLBqvAk0&gkP6!g@LC&$+hfj`~1jnlJ)`>QuxOT=?2ej{UU$Tx`$JTzw35@S_UkslQ ze{+Z-|B;%Pcg(wUM?#mvqv7%5SDzn$I^rpdx;ISQH$e2s^b#e>8F_ZRX1r9`uv5s{ z6}9aaOuNOAN1gHoQCuAP|A0v9$V)w_HQMqhLK!fmJFVk$R9b(6sHDp$Gte*U6%~cT ze1z;$29;p0A>jKsuwZT}t@10i$Ct+7gHRPN37<<}a)iWhHoE}`kH*Z8v>#|c{FV0b zN^#VEen{HApzCq;>*s@D1j5EAd>ch>+$N6wizu3>MqsqD9t7eq@(8`4>Vz_52Hqbt zlkg51nlTx`rG#vPgyG_xKn}6m42PT+>wzKw((^Zw;`@}8jI%`OU_3$8wsO{16tNXW zZEFD@X4gn_#O#^R$iDd0`KN^Z`e;UjU}>07cYWLa?IW)jUD&&j(I)is~oUi$Q`N>gE?-i1WB?F_V^kcy_#XXyN)=}1k0($qu^)Ky0J(c@?(iN z;PZF-j~@re-+tZ5??D@QB84#y5Yc_I%*2n;qs#P2-^()sZ^F&jz1}_Iw`Qh%Ab%AJ zeuV#EErP#iOwWI{TeupinKE-;XoFtsV*dI3k;4-$Q-Key`+wFrm*ITbOv_wt`dRD# zh;{!z1oM#frFD%){dKO+_`6m_e!w;A%ztD?tXqaBB$DUtw{U}btN(^6K6CZMx?$6C znBHRU8dP)Fw2ZlHNSM3OV}HT9+p*Bx<+}nw%-yg1&vauF9`}Y$`q|9%9`gel*=+aw z*xZGVvHK(?#U$>edHVImX5k3;zjaVTd%5nithKeG4-NO07E@Smg5;5r0x2(O<`FPO{98E*T? znKza#I4j0Y)6R;Cw1~4puv9Rx<$nvwkMge}Z0CO)AsCN;jvxM?Q9uyMk5j;4k}%TO z6`*u$w2FTdX$1T;u)NE_^1s=Q+!n}w_XL)|zP^#uzr{5eC#{J5kZUxU|CJfBu#Fj$ zc>DW$&O%I2!xRhl_jRL2`H}$l4ypYw>HIoaitETq9G1OCgU)Y~sklL9tIH+4F{<9A zCA=}I-arMSDveXUv7AjSGslXK!V=`j*d(f}xQR&`OjLnE z3tPUAWtZYfui z{}sM0NE$`3DcRmv+h?;&BiW@;c6(OMdNxHoo1&ibSx-&GQ!{Lz1E3$Ac9sg3(ulK^ zL3lz36INv%3~TuWR#fUFpZ+OyF(@acVwX1OA+dU3GiClpF2nfL6U!(=&8o^;H>nIQ z?HdL|5v4z4R)eepMIEsu%3_ao{V7WS1lZUv+g zdj!j#h;z>(aQGcGNi7n|WB8>{bVVFW8Gpcnc&W!<Ab3Idi8#%_l%6Jf%4YmOAfnaNq)XRCtB@st4NCE)O9`FwO14_)JSF!k zDvn=^j;>MBe`CGZm@q@-^=39gD;@yX{ThkHMuaA`Uqgh2{+yHQBgBsREH3=6W0TX@ z4{nDRe}jVq1O0r+TO#VApjq$l8SL%jUqG9~2CofsmQB(j+^Z(81~A0ZM7BLebHq0GM8s7VwQrc&yldLN3szy)Z69ak z4L>oKIksc0e8M=g@S%7<-<@LeDA)QePU+qgaA-V}9ip4rkk?Q9e*jS*)f1K{>DYs}KwNs@SHXUx_a zH&#a6-r4yy7*_EO|NGP>ITZX61^6z#+OZMp3n{p{%=Hr)cG&zeql0l zWw&j*wA?7?xovQAjru!AU0U8P7QJ00y)Y^-D5~VJaNFV;5WAf%rMGfTVnMeJ6ct-R z4d}+dd`Z;w4hdiXSRz4!u#lz_7OZE@SnpfAI50iRG|SrD?91${6i!sJ7V{axfMIZs zGQ!c~XC*qJB|bo^m(||O;~dJ2?P0sPvLcEKpXy8yEYn22v_d#~(Q0KHf{UbS7_*Ct za5c%Gwo7x6L1MUKIen#)A&A`%?m{t;Av~+b`l6oAlO`rHBu*JoiXcXA$M~j*yFBWw zn5k->b~X!^=7_U-5uBfe8noDpe{6vQN>H?nE?-(46y)Jtlts}HFaQd~ma{7b6l4}8 z0n0-V{x6Z9pQ7N06#OBAa5`I<7omgyH+YgN8L&kR1z7qhuZ*+#e~a4$$$;Udr8o$2 z4vBC)aXKLqrz67(%`iezD=)FxGpV4kK8us475y@MTKsNgg166i@CluMe`3MWu`AU z$N4NFGCdK~a;IpGFzJ%M0#-FlS?sgka}L`*=SaWj9QJ$8;kf4<8TXoF$aE?{sXKA& zGpk0zbZX_2DON9;!nY7@Obax%%ULmmeWFB->Tv_ez=Y81q)ALN%*&xq! zY@|mC7mGrGmrT%tP#0qx5!pf?^Lu5>CS>dG!h*VwXKLdQk&U0DV48v-BPh#`Swj9Z zp=bE6fmoW)@5Ay?EY087-H$EoSCJrQfkg=IFb>8tx@mR3Q@*zc*`9dxISi>3y&lyG zvuYpTOqE=uGR(c$Qj9&w{GZb!r5UVMa$B&FiIKUUCh7rcNro+Y#|+N|V@4R5$Mnz0 z)+drG(8HJusY$d~zc&zsu|c?IG5REmo~gMW1Uv`dMZna8?t)7dqZPMw)?(KkozqqeJ6QPkTxQSE=AB)k;C;JzdkD62Vm> zcuFY6xzTeI`e{#@;3^Y5W%T6C=$Y}#SI!BpwWH@gFVSVy!f+;IyWrUNXPH}t%&m8& z+q*%+k?(r;tuVf4*;~EO!F{m9f}5W@xP7_SpJtgUzJl9Vl(=h3UX@D2m|ue51uzzf ziI>8Ph!$-ybS;ApZ^?b0iWz{k<5MEkJ|n4qpt3RfOtdJl1OFE`nslk?Sz&J!qiwJR z+>joYXKRM^;-UohiO8%DnXaqV{X<+hT~;>H?n|>;A*6xsw~6S!%+k*Pd$dbLG^7(^ zs{lal1^EZ)@j(g>A&BX_V)|n- zyDxD3c;|p0=6#xbD00~hXPWy?ta3{!!_`Yq@RUq6xWO^h{ete61>2B@P4kYtvBDWg z*~CMCnzv%aG`C{amHf;3udN(On=e{7ethDgtARJlA!`)XjhIFr9cvtY>=oy2PPZbT zAa4V#X&h_Dv!nL)*hiRNFcylWV|QYjXqW`|kDTyv*# zm9nImG!G?)Id$G?%!>z?V1q2}UD=?=sqBr!6yQxmO-)T*RSG-uWwT37OkEXJRa{kH z*RZuJnQduiJb@}%O?_p3WzE9um%>)n)mK)hl=n+#(`vzeHPolH5BTWIWz$XqO{u!Y z>)l#cx3xZH+?LDEF+_Tl|Gxl#JtP6^yssCf#cgvoNy?45err)S?z)}-3uNd2l7det zxJm)7>+%1Ag6~oAyA=F21wW>MI4}NF3jPy<6wZlxiHiFrBD~rS=Q2TmaYNfuufP_XR(-~8ogq5| z+qNBIj8Lx@Lvd#y zoKVvbMAT%FQIm~OlMSdTTS84agqm`{4Adm7ZJu^F3-)Hgjm=*7i<|iWj;M0EjI#%dHWx)f^U~H(FU2l_qJ>h^z_32VGnvpJh0Ih!V1KFn#D zdU!tHJDXn-$*+j!SB^B#6|BCp^YTt%(}C%N148xzq2Rz=UjA6+Wp1SLGR5c@n`m%EMl%?I?7uIaCG8ibq%Qa$0JklChELXkq}-OOgE z@jZj5*~q=OqS4e;XTG`Gf}5M`EqKA)lBD0zK?C-l2d5V0JYGMT9%>$MlA(E z!upO;iz4_eifRG1Xsr=h6H<`oE9vst5>jZb5v40BQfW>qO_6DYy}j#SLJg$mWWm`b ztU<)zRcfwQBw|A`X^J?H5;ayyyw%BSgj6cQ3QQ5xQ`8M&y<3yZBmXTLX%|>MN|4j3 z#Jc42Br1`VGDVzEQNxJsYgni}Wi^T=tV~%JN+Gjift4xiOi~L=B{~*Qm@*`f^pR)b zkg|RhPna?$hv1L{e%O*e*;|=1DAd^+a8s!{DjHyux~SKJeQ9TtEB1OTQ^w^hJlxVA zgTJ`M#FeQ7gLT9Q$#rc~!jzFYL@x1CKH6KAGBQU}G(4;$t5Qbh2v(vJYZViwj7+L7 zNKBYAGDjZi!|p3u1xzTnDrIC2Fzw5cP*Mj~rHl-9%~1l0#Qv#DnK13GM>-#&&EN^k zp5CgIk!b=4(XCqc6rs$Us=1Vh@M!OV>T{}6My9#Hw>J!YKG^F^g7oT?In&(RmE1Kniq)wj z)7zynt-UoVBO^DjGoj#`l#x*qCKOzgGG~;84+l>xgHw|-GD^ZDgS-N0shVm^I6#;p z5LSb4P0GjsNn!gxda1{y(4^Y!b`L7uVG*iN~)5 zkAF-rG5JtxfXnyk6*ga7TF=Ux%_@mxl|-}3hMVUSHNOp`4db?H_XfefL2z#%U2y$q z{n(jlcZpyx5!@y8Wc%p$@lrX>$9S@BblZ60w0nzS-y*oTP#Su&LQW$+$(weU3HCC< zT}DrKjP4k(pLTB)>>CC5MtVYZot<`<3ieXLT}n?#6TN@hy-BcR>%ykbS3ti!>(~}? zY`Yu8nCjkb+RUwLCvy|FOELMt;YwE4^^C7s;9 zb=c2Z%WOCwLf*Jf{0bOT?08RTpDZMuX6;kfL=#r;7x3gLweK7@2GXCTzJlKxs= zIYS5SVVlB;g{>54xUw~*Ld{IL^@+k&%r+PQrqeKI7J zZWk=uKj-j9a1`BT{?aaFRv6#m>Jw#HLM%)#NeD+R!%AJRwR8<5446rXm)2IOMf2YC zHYsU?H4t&!DIDQfc#FYU#Cf^IH>mxbqadv#qTXI*lyZi#rd{4d?BU!xvm48oGV`mc~QyhyJe zhXrIMdX>i!o8q?81bCCJQEuY3dTMJ;Y1(@IwtO@V6#h$0aimroX{!&Jp{0(bE5S%R zU3#gdq>@kvlvsK#MU|+4Qf;D^w~H^h#Nd|dgGsT5QYzWz1WKh`DTngA48@enG&xme zw2WnmrObJ)?RVXx-yrGd+`y8Q{z;&<9IV)OJ0-V|<(vEUv=NVdz&{ z1w;v*RKNm+QZsEGVeLOgP1w5X$h_EbzC*~{Jn8$;v3=_4WmtZeMsiD|xf_;e`T247 zVX_LXywY&F;nnA$kv6?3OVB78QS4vs-0R2A2(@MVSc}Wna$hPT1g~sdiSS_@&Hl3-*GGT&cLPJHE*D zF-;w1J|FnP^O@m*XZ*b18}u_5jax9o-=xy96nt+poW~K(Iv!^;>v-}!oxB<)PUXIS z9HvH=?~-7u&GWLUoSe2*naZCyyyL59Dw}2;%{RXEX?)A1&#C=HHIX?3O=J^IWQxI^ z{Sp&-MSLQ+=mkf!Y9_OLaVn2`nlp?On92t3W~Bx3_YGWgy7hgtnc^8-vuA;+{9EW- za-DYyzsZ+GCWA{k;F9-U!RHAf33i>*n#3|8W$0mf)7X$q)}GfS+Pt{?$%r%YEWOrq zM!gP1R^DeNwMwxfcmgejn|<*Q&h~LY4L4*CW6cSN-r%Sj7E62Hphc*MREQ!(cBkyZ zikQYqQKCV1Xr+`|ZHS$`yXFEP7b;Ed;Wde3BxF2z3X&)+6C%S!62%?}if6fXV=qTJ zi*^R#d7OOA0@`(DPTo!viqM(pi$$TniR3nxtrSVeEsAHv#T=b+pK`41m?N=@UMiMx z@QnW?Ha>3oE$-kjkuZfbD9|{*htjePls4uZ>xV0cyGAlc_Tq4h!a0j$q%dOf!mEtMKH`j63T0oxm4U~~r@sq^wT{npPf5!1DLD^Bvz`zesp#wFg zu_et15PV##60Se0p)nGnVeyWF>yjNL0M zA4ls+|4}#VsEs&k?*_>p>RZLzjX&ZVP0;H(lJ*$K>_n^r-Du~@exEoQNn4QY%x4A7 z(gF&KDOgXzHA<(r^|8=13RY2*zmAtL=|1K%ZyQY94pQ=gx*+xoc>8Qh*jpTT&~pnr zg%^483melAI-w>ka&irKiqg21V&zn2H)@kF+_UQrt=22LLD9h{4yHB6vXs%lfw4ye zurN-LA!)7?BD!-FWvL-mONNw)l9uNj^@dcFH7{VPnrb`*7?nPA?)-0!O3+gPkE7>zFxitjV9JTrbmGLw_*F z4U#)6{u}ggn1TQWjR>Umi9?9TY@!`{C;VK-oJySOIFptvL>CX@iDhb?h6HsY1`yw$ zp=m?P4@h#-L0!?z;@QlNk<5)arYpxohmHx}E#EKtLFv1tzrSJXp?BfeYtN0$NKP{p zO8JG?jMyrdKU>xiDQlQ2j+X6;dUr?j_Y6Dd((}hoO{ZfSAb;#QHuqqQkKkVWu{(c! zeMDX&SY{gdzQoc+;dpI?EkGdr75?#+UIGeR~g+tFbkQ0M)Wf}c@PPh*pWGRo)~ z1*hrTiBWq=cgN)9%Eg&YZxUvEU+i%pGTUw`k`O~ztE2m^5PqZL@DSCPw zhpy5Qk=p@j#&=kMDK*OmsHizj(N57Sgj}<2`88vWBVALrlg~3mJyumcHdQ^2FQJ}v z3B#ZsyQ&_Cs-6sA23A*`XPsrP*op+~+>EvV(82x_bXv{b)fM0#D}3xY>mbG9I=KS# zx&r>5abiMv^WyKlvRh04nE;&6PhhYU5Wmc#kd{)7_!V#B$rtn&@`vbn z!d2{eLKz;H>nn6fWq)}V#~HHYNewuj6zq5-;3{m!_=7`Oi=)zN2=>f9n6|zEFYe(d z>WyQ{anyMCNs(LMg+y^$9W(R<`7@M)WAK+mnF|(zw!XpM@YV$}(1M_bY?lbm9z%^H zoVi40Y-Q9@HEfu7xvv_c*$1u~#wxEJdoyq~Fln1=jIQ4mS^vN*4~-il*$1MogTtJB zKzY=;W*pMj2Eno+;@t4}zcA`@4(fw8lzye4(QEmU2|F7dNf9=;bv2rTtD4bCQ1#^fkx*0+mB zo{VM{M>EzA8|6-)FwHnOPr9d_HG-uk;;dn82oBi`0u1jKu%Bi0vMfFPJ5;CcWu7{~<^HkE$%lUM#Jf-2DE@G3 z>noLG{gJGF(Tx3zzyeJ{_kM8-UdwGPqA6HpNtj+VENpuH4#G=1s5NvwBDa86C0|;D zF41JrZh45caj>$tNnxnKnb7j#J=UeB|Ikm8DNxcCUwmA=MJ6BjcA6woezBJP8WR=K zhK!^J;MS*FW<&~?MhZQtJ$+)RvX)O(11XCqbWWqH%{U5OOJtTug-h1^D&C#680_a| zMt;eIQ%yr@=)ZK;QC0Ujk{VwP+Ad!BYI}mZ?LO;T3~Ex_k=%zG9A`v(N7{v{1)ldU z@Vr+0xx8z^5qEi?uc(%p{?SN;T6hX2hIHwZXx5TuUCyBzBi^E1hvN$rC;iSEE}f z_SXnvHgaSnrh`_4;&eEciX9)Jq_0qL6#)S<|9EI7bJL`LuF!ks^ySm9of)yy&a2IzZkx}{p@z)4)=jqE@ZmGEGCt1CN^Z^6$!Slk;A)L{T0b`<369LnDjq*P zc|<5~n8}0-r-d>Io>tjo)94w<99i2x--@n+8-Wbl=V<31Yy{hVmtvcR%uRPcPebW+ zGYZCBxldFUI>18@YX)@x9%RTx$;ZK9`M~_g7@LuHoL_uodwi7kXPa zb3f7LApVn_^413K1Cs^u4|2HH)z%O4%@kk5wQjI}u%5*?bFH=352{$af!k-Wez1$; zKjpZ68Bj>OlJ!Ki6cjV{cJWVvwa2-Jf#V|UZ=z$j-~x;UcfdN$MDo9hL??;yjd9PM z3_uJxL@$m|K%_ZY;k1pOQSdUs-+xC{q@zy%#^b;C_$=p%aGp7P#>IW-_g!p1-!2qv zkJ@(#mK}4>U6AS0^6u!2X=%4D99MD2ir6iR-OA9V*%!!O6SZ#@EL-QCb?WR!Zeu(< zVz=P&J|TPFaO=f`=MRo~qV_d{WsR_5-z{>3e@ACP)oBK9>m4&XNlB^&idH#$PTBxdx^U*ORfQL>RyttJhZS7jGbsaftx;bhe`ZHj-2k2?+JwZb1I zDA3gAyq;8X4_hg4t~OaN^M@%&(G}lyl_sQ)c>=QsC%|ogN~I3^;RnI}TV+mr`hPf#^y3(0Ik;D0Bh%e!8A2 z#f*w8`^j0?L)2|Pyx<>3ET-SYv_`F>tYBeBn8IcXM#qhmhGYet?Bw$)0t1urrR~6{ z=)DCV2mPG^pLpOobJmq)g~4XN+I&i)Mr=G2g?A<|{G9LrnS6?3p(HQ3b6?#$Y?;gU zTxuI_8}ohV@NmnV+e0*Jy!u0!?u+iY#+zR&97&sV@=qiEdtemx= zL9X0cXJN!yIKj<0%fDYddGhx+Pdj%DmfaDjcna<=)D2V@sI9M!rJbp-?X0Z{yTsoL zddLp3OAs(*^`z(*j&xe$b+1hS((6(ubZ^UQ9{chV#SWIU8|hw-$zW_ADWN6_}y1N2lGz zf}M1bAQSf0pPO|VwSr@-sCE2<%}e-eD@XUJQJ2=p`t(VVpz=hhncm3s2n5==80475 zIMmPtze$%CdXkW(C=jMXNU{h4^P2_X3+6HlzSDrJa&3{}LRB2v(%ShDdW|2XKoJT6 za0;U?gC2jHQZXQlpI&04`B`UX*Kq)KE ze9ByK6`YcbTrj!gkAkc9>=9ghBAz|Ox$a?(1PHoZSr^Z~d{(gJ4WFHJX2#>^&jPq4 zeiCYO5zLA(nqzrQ>Bh-5jk~$`vMh+cx0`FSSZ|ulh~G@-nzA*_4n%_=?5EQ4ehN-f z5TJlqx0q!BwiGZC>V=0^_=LmC4u6(jo}=Iz>J(X*DvCWt&nTFoo_GDC9Sk0BCackrGDdyt16=L(KQQDl1aWx(?ClA1_3eI?KJ zg#K_b4@>D{90*@Sn<(|`LgYUcDa-mAu07#HsY;R-@6;psP5nYfql(s@*Pk`u6lVR9 ze75NkT57tx(8?RFEMvFH(-i~#J%R4Ct>7+*PsP5s0D3FSa6)%Wd5*LZ8 zo#G2-2OK`6J?bK-Afa^^x?Td1Pb8>P2gD~A{wB+wz(9JpB7oR`>yimAr z#<_pav+~b9MdLM5=HHlMp}QGSW>Ep9umLtGrP%J_L*IW!`4rXp19OE z+BcjYCw6o8mBNN?H){SHX^5aS%FZVyeMa(P3v~(&JdNJyoeKlwcMZi)Xs4XTB5i*g~}X3qjt7@0Jx$H z96j%paOZID;eyn8_wd}sr$f5LXD$s*>rwTKvF(6N9BHXrIq!*uF3nM%iP<;x*YnX9v zV+e2ExDQ)X1=mKA?K+RT`9~1^l>{^FyaMv%JunDkz#4i_2s|0%5hN*cBmXjz#EiXN zXAlJXBt-;6fPagWF%#iM{#!^TD(C1@l*4@m%)M;hw`8w+wb`k<90B61F|?^UMSDu2rueztVrXf5ue~_x_jqNBg6> zB_qakJASn`LD0PQgOLr#=c?RzT)-uAKKS^j-6iz zp^856-?iW04VK{>f5+3}(EYF}s|D8Mb|Zx@3!eN~&$ZahKTb1K+_9mh&X|CQNl8YW zz@6`4B)(w;9yX@p;g^lddb<{zLx=SwVOJ`XU7G2WmOFu4Wo4b#f~|x}+3LN9Jesh) zsp-ZH4#4UFo|(YJGPajm2%M;P{$F4%d0BaV9C;T^1TMGTWSwydLpJG-6=Wf zzw=udvSl%D>#-k59uU=M%i6~!PeGQ=BaSSvw@5LJ=I_5YdF6M(+T>H2zl7Rky)4xx zdWu`JKdch*I-&zhY#h>^fK}t~=)ad10O7x;|CS!Cv90}XWz0I*|wGFXK^o`*J7{EqW@XhxQgTp1p)n z@ApwkoXI|S{+zIC%Z#&f()WY@cl+mD^`99Hbs69}^Tbm};2&X=;Mz2YCBIdptAxU; znat`b(;qv3=oHg7+;`fs!s*P?d2qqu(Xdbi4Njfls{3<~cRXA0)J>SCJ#~|5IB5AOHHE>v6MAnr-5`GgD_VVrT{o z@Hh>Ff0fF71p$VFEiF@T^WUbY>Y<2}!$kcSb$7UQxdXF~M&X~MbRvKzm$m-0wYr=Y z!&XVx<(z!%=Z@V91)Qq+dFGzDo(obCPR{TYum#@1X*N27XA?AB*?G)APVg0(lnqW7 zY!b3J2?d)-&ri;CG+%z6smp3)iZZOMI(GkA<{lw)4|BB1?!~Q-*YcWn8~>`^B=f z(qw9srPVg2{bE^KX|j~El4v~#6gTT3=*i%z zr1O0-`fog?RhztO3W|KcwW;a9c5Q0A!Yu^2+;y-zIMcxfjj>{q)6pa{0B{ z6h38v+LrFg<<}-}RbiZzohHzd{oznr2u{kR$w_6j^JD0#aLGcVHTWNurg017k>Dy9 z7f(zwNE}!M{cU2Qio2Gz?#sI;*Ul8w zG1bSo{&F2Xnea~+R14YFLIDmXEWnX#&oDc|ZX66aS6B?=k7uqtd-++UDy$ars)fSk zrYb1>CD-tI0X~eDcvYP1=O#RZC17qAa~R(Us*vj-v4)1fOOBg3rDm1RpKfBx<&IT;4HJJzY>PWS2(@ z%0IW>JF?t{MnUM`1Z3Id1+rWZo0s>tTM)l#wBYg0Y_4fl`pp$)dgA4p%B(lnndvzl z+O`bq7_#8xiM!&zN&Wwu6#O#^-lkxZf+VAuU!!Lf{8v!y;2K1djAF(P3VEfIg#zb^ za*cBd7SI-_kk_QPZ?VHkFIgRv6OShSEeq~%txPbIS)#O7b!oX=Q9^0BzqMuI(rz18 zahu~cQ}ouZ#nNu!YNWJ?-hzRV+C(~`nYRsY7;>{uVo_0LMt#=NhQLPN0J=W+X1zdFA}lQGN6NLrflJhBYX|&!*+d! zr)c~ot6XDy%ogaQL$To{J{TY?ByjW3qgia(OZ1Qtqz~Y~k9VTyq<@RoL2AEO+63Zc zlU*q&T$F$kSr3S7`%0QIn!IGE0p}K;RbxF^u!Fx-&#GBZNyJkU^_0zeYH*IT=mT}f zI6PFBN1YXuaO_Ja^bzM)Har?`CH{aKngsp7N6#p@NVS?#E8I$C8M#_Z61GUB1TCC-@hsfn?&704Rz@0bVa_&DL#KsqrAh+zoC?h^per~sf=8Spq`Oe%xj z-(wl<)M`~b*!@CfXz*48aqCVFUTMUQT|F47E{qfjJd8B5ZC|jLA#HffcETT$QtY7= zHX6`Iiv5V*bI0*E6t^s9{+Pv$RyNwMEp7>uSPQRcKqJm6rPy$R3SPv?q`Vv_u%>~8 zPhpxurP!tv^=!c8ab!x4vRC9^$-SKWS{|(vM^}^$KR~_>iXq;(+~3|hmJe}d72JhQ zY?^%fdPT(D5Or>wdT83YOR($`oV(!c>&r+Kaqhx7RdHY3>1gkGVN<#BW;ut$r`q8^ z;sffipHe`4N3u;QEtB&92X$=H^q!(;6#O>z!zLt4CT{IQ-iFEDjJP%5Hki5Hgy&oI zEGpsv%ASA&aL=xV@x03Ed%Hi>cDxar6%sMMJb`}$V6)r_thIwfnxri_mElS3+k67$ zozv5lW>4V9j_FLPzN9N8-=DI`Q8~i&Sq0FeX)Hl26I(gjou$`b*hXeF$fZ@Eta#Si zlgsk=EKnbr?y)PeK9O1>Pqy@4`BnDYE|4crNh`&EjlNgoc?kU~UWsMFZ0Pssd$r#C zn0o`Hcz;M#7IcAuQ0Rq>=tk*(rxzpzU=v_?v#v>hH}3?uprF45!( zvDGG>=z_t#rJVi6^y?TtE6WtIn7z**>I@w3q+K^~eucx2dV{gF<6XUhp0hD?V{7NZ zw)O)tGrjf)gZvYylV49k2?f;%VrCT4+ZB9@hiw8pY#rl)HoLpvS%>ro{C)}=DImfs zSUcFr)r`b_e2Z! z2-$mtf<1H42|Ygg__fwI+po4y=Tya$&1~F0)e_yvWH$;0jSD`hg z5Se=}_sp)|5?Q@v(j8sBb+Rk6x_)HeTtU&y`ueGCI1h;y>=Lqf2?e`;u}-&Q^=+eW zWy3;z(!WL*B_q;5re_pPvQEUfko&D{PTZcv-X!2i5<%$tN7@-E9?7wMuI!%oA*!s= zJ8WmoWft{89}M;wZyU1&j?>=oz=^x7rI@x`kW?OzkTN=3cV|7i{mUT{Pd=#-2(=Zd z4+b5t>Aov3k%43wPH7MY*ghFgi`)0e>eDC@A7=HrD4|fc0cs(CAGR<`&J}zK99o>L zJSn0o;>Q%Pg=?XB(CZ%4UFebR969XplKVMke)y60_O|wYVOQzyK-r-&RN~!4WF;1I z3Q3dLHqq&`2NyH1)_Jn{5wUS(JpqelB6KTLz0GYd$w`tYQ4< zc&o5tV>EZuh>?6XuOf4h@xWxYu)Y>1AcAYhlsoF$0h16|M_xKRdiGlLe0J`o2Sy)& zP0D0*G`oH(Gm^a>r_Qg-!oK1>QUk;FGsSK@fkFNbOk%e87dpKo=Ob74EhB#KG8>(L zk}1zzT3Zby7+akAc@ktpVto|*IuWjYhVRNIUCiBx zCTuG*2MDeMElj%Pc93h}q)U1y$rX%rGf_HKpL^j^BDN_`le^?SieV>P`NKJT!C(c5 zW<}Uk0T=`m%*<<|1fL8@_n;O0hjbgD7!xdBL`)5k{4BkH08f*^Q6!ouH-%`bgmS$8 zj|6llkzb2w${+`;nla~uY5erWQ6ZPCncy-_f~X1oJ89R`-m<)taXlkiS|458Fzsm& zTn!OV1FbaF2$|K49Q|2lHKL;3{Zw7!c8+;LvnD`}%r@7cS>!ecGA%w(yuLW}fOS3; zhabZ^V*+LbBb|U5O^OOdi!}x&Gf9{XuDCh=B^9&GomvL$z#<*&6MqsiG8YCM;tOS; zlSAAonH844nylO#ZhV;QqEU2tX8bI9oWZS71 z^^88|OKCtKO++72%F{B1w26*v_z0#=*nPaG>qKxTBZ<3rl$LMUUA9Z~j7uthzL(Oh z!s9Zp7u0hbONS^s{!6@L&UC{$aO&RM73w~T6T;XD3diBb(Fc$1Nf?VjP;{hAtFL@B z-9AJ?A8z5Gm+uF`<$C&21``HIJAwxC_MkHAW%%PhfvA-qW2P~m~yu3M2m>{vvmxMgrV zcBN2cZ2VtQaK(DrDiqbj2XEf45$+>*-s?xkbKYEeb>$nYqpP+|HcqeFI+wj>{Gmwp z`jJEP>q;kb-YK|VF!|71t7pr0M#^?Z%XZI}?TwV}z0n;ldmy^*z)1Ui@%r(>H=n)w z>}*MWq@*4szGP>#c-P3mxq`BZC!z%nLUw~t(17Y!6<%q(+$OAV{h2Yk^5Dqcm}liR z_xK}kK5_MlH@+5KQ!}NHu4$OdTSwk3H;g&v%Qj6s^3LPeAD_y6>ua-{8zY+=qnn#& zH@8JLw?#J}h?X53%a||OFtPQW?bo-@Zrl;sxMS)FhC@)kr<0u0%P&Lj8>cB1Dmzg!+nse zN7#;S8Xph{EvOtiNub6y49`(4M!_c(T%&AHQ^0ly|66)S!ACT-=kPp9S2Smrb_eH2 z;T74kR@i(ayD^cD%%X>JC#p5@BYVk*ZdG+z!gr>i&%f&X{Zt^X$l6KJtEHUDj<1fzwLrRX`% zIHWD4NKY5jTuVJ7K4PKNUP%SUI78z(?3^>e?<8amG>5KpoI=gv&`m>v8RA6uCkYzj zG?k`WrfSHqVWo-uX3|^2!TNkL`rR0-h$j_{46Y5_mzs3$a<+mjzk~yC)qo4JLzlob8k8+Xi=PXr_L!Tp++o^*3YT3Hr}*AYDJkuqN-{4}lI07R zWTj!1CL$O%+dY+(zDS$wU$~^?HaYI8q>M${+H~6BQg_c*Mt@I5GNk*nKoLosuMo-5Oyh7_WX_DJI6@L7K8~w)@ywFjz`18@m_VR*beSHv_d4GA?5# zndEyOJ;D?-IIqKqa~$};r3Xw015z%pMl(_r-kqo;mI;-%&P6dZm3T>y3EaVE5D*xd zW@QgI&u3+ioE>+Mb3)GgXjTax6s?Z)xMDvZMpGK#JexLa)12wD(|(dH}kLN3!8S|a7Wkdz0nm} zvp>434X>@|+vmN-aswN0;4Ak13}5j8zG4l1g*{?lg|Aq%cBw?Pg~l`d0OMwkww4)U zY1mzhF^Z-2^*_UR4e^wyYb2ghZGM`-{Aj~e7D3R{$oGQo7dE%m^VtTSGiNqqZ6ssuM9WmpOvdhE<6OaN zIy&*nc5zVxC*C+#d{kI6VS4Sk5j!~d%dyin>Be?g>vjpTwvuCnCvVKRzvm@%>dBY&P=UirjbED_*>1~mc zT@lZ&sC)N~q8WFKU~duJEw>?DlqkB%6){&H=_Xf*y2;0;9-+n& z=0w_gm;|Nx9HL`#cqT_&qnIK+{0B-*fs01>0AAjs^dOyczLDFjr#0w2CQID5(7jj> zm98xdgixI#3xH$SJmtiv_p2Q0ESSou_R8f`Ep$j+bw`?yPB}N9wUk+7c?syWxxcT^ z-yI6{_XWe=WNYzYKhdaixM&J>_J#Tf*nY$Cs+44d1LRgC*cli&UE9ibB6brRmoD_L zs6Do$KQ8H@t;4T)5z0@#sBgqeOFyW3OXVhAvYdfUuUZx|&=gyevfx%?YS8bA( z+Z*hRlQn{x>mqcTO#JKgOlz$8g^*W2iew(u)Lv!?8@Fw%WXl^^rbSQJ!jZn|sQ|Pw zR&hmkKN=KEKXSPFK2P%BW27{X)dZy~+p$FR@sqV8aw~*?sTPhRE z2sKrGwf(yVII)EIhuqswLJ{E%(fSKhtODmho{5<# zSdwNm1?LkN*z68b^?5Gnu#tno3PtBvc{U#K}QvAEpU$ z5QSpBCM*9^#c0JX9arqSqvPC}!)Evd$tc9RV3Q?6;kFre@G~6@SQN1rO{9HjuSjwr zxntI`Jwhig6Bc`8U1OHcK8P6kwDxW~N2s z)5nA1eDWqQ#!Jf*4`)f$o)OBRUcd7&zMa;>mZLpW=m|V0eUmh3D(Z<5Nxpzkq@Gm0 zdJ8qubq+91hQy1dc><;4pjz>eT1|SHa3HNrFF{+i^l4_bCMP6o%v2gFbqQJ;v9yIt zfVtK;B})UGNRxousk@oRr<`@S(ru!)N4?0bwg31n)?w%hIuT69_VI`KQ4VEU$Q)N$ z<^yA)X|SuLU^agmwMeai(Jq&-OBfoHj~(F68+_(-szuHrd4Wu&DwUTFRnO#|#pld8 zkC+=W;Y4CA0UkF&Q$a^}`;L1zKj_`up@zh@u_9QlGEW+G>%{G*7%_=}15 zG;zgMAmad-MUwQf`g0t;FCGl@6ul=*7c=7cFY!FyK6*xn@@lN^7<|XWH42xJI0q9Y zHQ#YX?)M$fzzNbN0-O zZRguYx-J}?bLX+ca36Yg2V6t1D|@r!YR4N-OcqU63hTB4@j{x;88eOf=5TE8@t4od z<>rkTA$^bUeZ%&4^Q7smw(ke0YJUIhSoTN_%5~?CabuzJqXIipks;RBGCllZ_w<0n z6J=r_&GV3y)CKFFR7++-LvoJg{IMe4t zIPa2jt4=;%tfxf_WyyR6-LCTCv3R%psjJCP3q<1#t98R?xw@Q!+3XFG>!#f$g1tm= zmx!Vz*}$}r9kjb_X49S|!8Gw@RFYb6r|WXWJplh_SO?eGz1RFb`T*0cinN(`$9V2{Qqt5UEtfe&O1@Oh5$&A00@#GDN2MWlH&U< zilUx;=q*#SB`2{QmzN?SiLyj01IiL<`H`%*rW~avZMULpWlcBgwX|^?HO+0*ZTqW8 zId+omV?ai2ZI>3^)t0^Y_q+e^4CVz4K+3VxZg0lc;lY_RXJ*cP=lj0% zJ@ujrMh$>dlANV6PMF-=Jf=GnXI|^eX*!IY=oo+qy+AlBG~# zDJp{z^DsDp)cye=H9M-Q(b&XtqAo2}*SSURFM`YUI@;qazaI?j74ZkrB?Sl>-CK zIaXh{-z<(Mj&Vi<1kJgc=T>bNR&M!6&WuDTDTd4}1jw7ssS4#(g>z~~&2tE@H1yJt z;PQ5|J!9mR7hhL65ww-!os)vCFWqc!XgxYAMdK{d@f*1|4D+`Pfk)W6( z(dN}}Q4*VV%g>>+I;-?o;lbq3SB z$g#ByDy%FTVi+SSd{18x$po0!u)NsB{7h*F9@^ir<6!rJwnMx45{#lrWtgv#nMgP9 zCPNr?q+C6kDhW(97ySk;C?3xQX^F*VFPCAx=z5`)afq@CJT~^&%$la#+3Og!CF(nq zRRv?coMuXSbnc~dqfX=yN}~)x8HS?ri#u*3p@1Eyh3s_%zy!-E{#iM@7~Fu{e#P6K zW_UHfZL9hBv)dZENm~x8lZ6NcZmEQf$e(^&uII&J?tnF zY(+w)CcBOyWo4LKMb4mYOjuhe8h+U5_uYydu4bVwl`9wT#_oaPGPi)Rl|yM|8%Y}zyxL<0uk`(Q4@edl`V0!qL#)AMtgH#*$S5%f zp~S4j9)68zm@RceFznDJtg1XPY*(Wrn-XIXMv3$%4Op2vC~x$JvFl+B4E}>4Rj)F{?~-81ZV5 z`{U4x22OT2bT_FpaVXC3OC+P-z4Mt z#+${6U4yi7DF%3h=2VArs;@shle3Z7nS63Zy6T?xRtT;N!CQfrS#MRyi_CS?-g?1R zPi{(5ueTn(RwyRB6TB_dN_-Cbl?$$N0a0$yf+TuZS5LEO%YwI(jYcfcb=;u=fctvVjTv&st9?k8v zn6I{P>1g3@t+kr79;E&UQ%V)mlPFOg+EcF$)`WJztrfAa1P z$Z{*Gqkf|!j;6jAiYS5X5qvaTdF_y?Hm805!SQJ1~RNII` zt1Zx`Q;=~&bNA6RCr_%glP?REh?m_Ud}0^ziOt~I^hB)YU|*qJHC^x=dA)5p^s>H7u6#gdU5fX2OiRES9#1 z9#f_5p~qCIdVD5K6;cP&@*nm}$f910ES85CFoFRN6>kut&@w*C?cy6$Bx7?A0j)VVKhESz~IEOGP7PC#zmh+iLyW; zZ#hEgi7d)~gn#ae;Zazd7y|m&ayw1t{}W6~Gk2@RhWf2$u4AkH)7&*cub-(`ZD^7B=Yjw&k$ zJ+#z8T^Md+7%85A(PEpv{@#;k`~j8nYPr}Z{>q;aw)tz^pSeqWR*1F`X0nAaGg}B7 zvxU&xB$dQ%lKeN<&E)KcV$PdC-uUvliTanG5?sZjPhqn%zG3X?S#Mp)OWs=Eb%JZ1 z;9W=U8xgK**4q&BHeByv-q!1xqjmIx`jEH&`h(NnX2I1gc$<|A8be;V&nlV3-AinB zg0$87MsA1O{6}pq9UM2+WJCQH$8}`bZ>3tP?&do36JU`}V2yvJSi}M>lA>Ud^QPq{ z`6MaXQ)Wd&|CwS&+j%ooS_JzzmKJ~exn z1t#t|Vbbuy<*_1xbFpYe(eK9%YJmhXn2y`Rz~AE?-wm^MaG=VCp|ECz{v^kISs=vkMQeBpK%uk+ci@Wwdu}oRQ9(xoH<@cgbt*ltGsiqt+S-}Q z-CAu!{Z<3lvC)2Oy%lF~ZQ(kt*0)o+PLKUwFs81rU)1o{sG1PTW5i3 zlFfJYG;cs7l!uDbARbyP?9fGcDqCWmsC{o8Idvd#}4DqrIBeTi$GDc__0S0m(DhlhDTf;^7O2=Nwhn58Zr1 zN_z<h#H2xL!@)pC*jpI#*OR9>9G>8inwa3Wzdq zmNhsR@pKIg?#5t&Q_xxm{EBrygK9}6K#!-S4?To9OMT=3)BksW_gM!CsEGCLD{N?<%GV>o~ zw-s=cX*N_R3%Irt`{ZgX)yufHde|?g@-gbI&v$`%f4} zTNQWw<&PZme5xGU@^d;UBlh8RB@^~T0|SFQf}Ko@xC8DfNYY75N;(MK84mi7s4Ra+ zqo?Oq`*Q0MUnP>_c|$kdlO3x4hHQ=^ogZNj2mBwU2hmD5*1|?jV-kMNE(bBBb&TOesV`;)Ccn1?4#bjFqX$B04 z1^{Yj(33OajOFXHjWv67@-d7#q>Wpj^kDP>eU@cD$A;Z?%Gl3__$=kt`NIxbGZ^VNrqTMV!vNDnw5twM4 zXl6wbKlsN`MBLOHj2JDbZgv-O0BT|}*2Ko>>VgR*LoJ#GCVJ!`x#h|3Q-Q(b1OA|B z(X6xB1}V9Lx@Q9|yzk&Gu<+b#!tRohlsjqZFRs6^{>3d9wg~xEGilXxZa>ox{>v#`G1&y!*O$` zkiTyx?EyAmamZ18+ff=nU|GmjHsh*b1Abu3n6(v!Y(=+i#rK@JgW#@Tyq6lS1j&wS zGRxhu5i%rqr`3FGBiCuMzHQ|?UDn~1D*rv-K)-M2?nApCZtrgEIJA3z*THy)6&Y)M z2Z&A|YXI(Ke2QB~4+-v`Wi~y2!#Tt^#ytd=%T29y`LHua5?`mjbZGCr)dvtBWbrR%C$w?7?-$?22Npx_XWwDcx?je z7Ta+ft+yEr2bZC%^iX;4BktWhg($$`x$6BStFPMvd_cQd)OmSJgU7dYr@2U8f?-GLi17ob>xCyr+g^Bb+&yA>FU>jDMxv$qmo^BFg3%32HH>XQn5pcX zktRloBUYOEQ;62FGFs7(D665^w5sUe!WU5Jt-WOHPNO@L`50LA!Szdl9LCEGd!=Wp z9`GI9|L{W{+kMr1zpu93EaLKG1OY@_(7^PM-$p%>9=$3W@SdI&;2^?67W>4KH;MG_ zmC4{Crt=|!i5iRv!B?sww*na@M1g(S+ki;;x%tA1rf}}Mk@Pv|it(qWomGOZD&!PJ z1e#eSFlDp2BL;)%3S3}J4R;4Quq`=)Bp2#Pt5^(yOY?g|hm#bo5*6d;U<<4qb#RWK zqa>)Z4WmvEMK34aIT`L@U)n?NaS88NCfzTVpNQudWVtc2F_8&Hxn)swTHwLvGmbc7 zfp0?-zls2c?5mkmJAaz`s!gn!OoqG{m_ap9p9t}2*e*eVI0@J$aF+QjXpLxAFc2f{ zmrC=*QaeN4-H;5@^sjl00B#ib0@0U*Fb#A0twP>LxD!C6f}OAP4#YK{@{p%I?5Vuk zhAbxV4ufk$%S$aI`$t>e_pGLT#7~YrIq|s4{EoFCrgylsb+&YCsB~+%6iTNy!PO>s z+dhc9;9fTFcF%y~BPtey1O>0Qt!ww1r@UOd4?E$E*q!jN&|C(nU~&T`3IZwjfzT|i%YnacE@IJ&D~U;q(B)$Z5PYIu>T{In_EfoFAf;zsXAY( z$;+d};L8g;)(EyWLe*X+1|Q~Clf*|otXXs*K46TRYSZu3G_FD1j?XK}U0P838|V|{iWGAM(y^7cN$M`z{7Pd;e-CH)Q?Vs>4yBblA~u4`2l|4& z8U#fQ3qw$YsHoAr!u9|OQ4e+f&Gz<0+h|Gs!dkq$C3d15iDUJS-?Ur`V4L) zx+St^W*95REBc<3bS; zO?Ev;UI&0D9iSnUtcSEyF^A$m%)Ep3dT8TB==G5Iq4CZ+2(v=#pB*oxjHDbj!Xv>3 zEvszXp||h#TcI75ftqPY38}DSFiWaQdvwg0yzA66_ZpN9yre7Gi1N<%gpPTa^xfj$ z=+nQK-~N`IgZNXw-zsUgl)sO(Wvs!q@>>*3 zHN*&r;wRnv$YkUffCU}W6C}ifat`iYF z-E^QwZw#HUsCqY@anGQqC%=Ee>LX#fc*2da0)No{HaX zjG8Y-6D-6gUcebnUvfEWx)@1#=Jyhw`PGDH9!q$pA;B5r#SQvrE`C#L|FmXTc6W7k zF&zX?b_>J#l-S+pL$IgV|5DjG_&vm(@zsgvVCJlMPQ!c=mtBZ|3wu!9e~RIRDSn|; zDFRA@Q1>VX7Dd8|vh=c;APSd>oU)?+zI&jb6t4(Hr>I`#RJ{L=SWp7z|HaG-E^Qs# zIsfjN}&xkpA<`h3dbk@q8a z#~s(o%SUHiB_E28*jLuy&aPyB*c9z^CaV;3`lUF-$XVvSUrl6fOS!Ib&UiIJ3 zYScen70Rlb$*M_w@;dHIxm=|<;FQQN4KtZlby2Y}H)dF^c3 zwouu&?^|Zd9uQn5G-l)FpisOayrMPi-FO4H+a|cS3Epk!os)mL==HU)u6@1g)vAf1 z>!soR4L2HRvbWx>pUcX=v~z5yP_SVpt2OZte6yTwMiLyZn)N1pqw3I#YM9i-K0vbf z{V>aW*?hUv zxzwL~#q^$`?5&IUw$cp+cgH{L-SoC&^MZ{QAWvJ2~3eJl5%-5#<~J>=jX%(jNyR@^0&Tl(M@ zZBR#IUzvd5FHovwD*8fSiFwlt@+C26kip1IajY{2^H zm~jYjG9B1-D}8koRLcF5_tTCU$2dpthQI5{!O&ual&wyLLpDM$IUO)A$J(YqS$0;h7U*2r0HgNjq<@rl+y>i~FH~yf)c)O&z zJ1?8wOAxI;_A;+k-50T(WL93f8ytdI__?UMg3~uUQl342o1w$>8!v8>*krAdyR3Xb zV7jM87D6ob*j>h5rN;D%V|;ac#j(Kgy?CJYC@-k@D0-}2{G%+RR{}OsJ(j3op|1_Y zi=W`jh<5ECQtb^Yeus*ui3$7})t;f+vs5r9uND^~3TiOGM_D{2z$v>ZlBr_s7^isp z3>zbo8^=IKPpeqGs4`Ftnn3-7gNVfgqmEt#Z3qU2eOeALzQ+w>@csbL2qmCmnrO*SFyA+uUlq!)3g_31>{Z`&_1f9h zjiJ?zH=1TvBd+?YJHFCcUrWf>685zUj+J-TR?n{88d|&c=HSfQeFB0Vdsa=Xp7yK} zpfmBTK^Q#5Xb5{MuJ%lOY6VBF;HhP86cX?1gVUZG!BHc4YLvEzr#%gVqe1XApzW35 z5h}2ID=|$% z`ucl?5(BeJY*-AE%H;Rj*6hqM-*R-c>|9}fH-p=mYkhYGx3f5bHF74t_R<9VQwez` zlN>!wte%CYL?Ghj#L)W6NEq)NJw(K~9JHqdwn~8XEM3u%a@m+4jV@K$>ir0(#P$9~ zs-2Y=eO!zx(KcAt%}83)Roe9!d;YI!HGUrNCTmEA-0JK8+bkrpXaaTl{B3s?#q)NK zIcHtvAy@f~s}j46%<2y$HR)IjZKyU0=^OvrT_w1ym|F+^TnrXr?D71z&E{_xw$*c! z?i^Gn>$$d8`((3~>YKUt6fN8klgAjn&!Msu+(_RK!586W%0fI^a9PaZH$GuRP}xj5 zsH~}L3#QVrXlkunw6r4EHZ;?ORw=+H(pN+*#|MIglBMVK=&CV96BE?2p2+4BRRD@x zTGseqX;w=y9K&(gKkggb_ZwZLSN2i}(upp486)Fn_L_LZPEzr(Dez;YPg50Uhw-JN zKvE1~KZzNq!aM6N6zRjnFRY*QUii$h$1n0;e&buR#`RNwHywWLGkZ6kNDrUcy>v2y zeP;KPm6HmTd}jB$KSgJn^hA2ZNm-RA^i&$a{&X-!tGs7D;bV=OBJsQW&B{HLm9NXM zPwVsR$Y6Rc;hCoso_RRonFKK*aUZk(H5cDY(EhCi?N3VW`fo3b+bvyDK`@h7yZI-H z(lSqg^HgIjH%U`f2F^?j`@5KYCW{qmvzfa5Ba!Mb6=V+;N$(msc&7LG0e+x25DZ4F z(TZg0Y#D?cSVLY!tIMeLn}xZ|(-IlgssBc#=0)5K;!>yQ#b+)&18a*BizF`1x|lI< z^4iPiR+n7dfeagWqBQ;NZTDK{(^C?1mCU%-5fAT!$u%L@8i-}tmCGFXiP^%&P+{Y3cN6h38DkkUE+1XL z4suc{Y-S}MB;;PZ_*3`V_&?<16mwQUU)D^OpSs}z)%4HGaErme0bsmYyr;-8nU=k) z&OGT`jrx=^d)H?3R1T}J_wG(L-^!r+t=jC}UW7uU`r9ShyNk_lw^IFkE3$Xjo4;2_ z^>^IaySJF%DW&?m*6cm0=64G!QU5;bIp*&d*r;B|?P|4te;uoD=5|}HKQK}K2dUid zEb9-PtnTG@ueSbx_5VRJx4XvrgL2kh&+Xo5{XsLUZ{hY>tv@tdaqSONxji2151p)@ z!|f@w{%{4W7p3p1H4l4(kN2JS^&Rz{rmS*<)vy$Xfqvj*yxqCX)I1GvRr7Tc%3{ck z*dBHyHHxBuOEMaiu26}WRKh#OkydnYkBXRNWQ^z|i1n%1hl*C8dMFy9y>&z5o;NE3 zk>o~Qs+6Y)+nvnH6JkZPYOEwo&l9jOC%$$$dlFgT!gaCy(i$ZozKZBLNfeYZoyx?@ zwSzDM#SetKH}(19wtEggR4$bf+oqE}r;hl0whWhRaaLb2cqY)@+_GNL;z?aSfD9I> z`dPzu<999JwlId6qUk>#7-C-jFm@*)oQQ^hosg<^@Hj<@4DualhoctMg8@JPn{>*2 zx(_*%df;i&*E@)~`F;K%yt3t{i)a=e%N#`<14Yu}{ZSIO(|1|D3! z{wUnEI#k+Hrim)XU&o40TFsKUGAc^c4UqN;AlKXlZzUptwI#t?igtP?D5k4OQG{cW zRDAUAsLwbN@kq+Cz#!U0fmz(%c@<%rB}&*wbC9#lrmrWZ5zF@TYb|Xq^Q4Pw z%Y%)ui+=}$^Y2peeJaiYnE8KC#S2tiq~awiE>S^-F0sbLWNyg+Hl4jj#TEMbB`U~{ zlRrakRGcQZ;)`e!t%aNKrf%h`@3xytxZ1m2MhjPP*J0rr?>h3i@{d}?lzDSp+VhWo z`O#U<8{)jlItlhx>TV)Ef+L{ryI3%Mn}i4R1FG!ispGV#SD&0%;d*Q#0hL~^>4iQC&rWV76@t;@}|3qyuDhLrA z#VDW;HiOeWn)lUHX~**}T?JUh9pxB4Bo!9bV!XSsw|3UM9=0o@HFfKR1CcYr&T51McGe5F`jE3ei2$hu^Yy&>l3~OQ zOJO4{g^h!FEb(|j&0$!iR^=}XZKC#@QJJFadBn&cUY5eD#M`c-7QrRjNPIrQ(HFeOC(1yy~bSfibN(>+Y*7T6jI{!z*!jnoIZKx7{YWbjGAn2 zwGME-(1cr%bq=%Q3vd3f@LfOB}x{R@1kD z1S090CyCgCf!^TBz|p}-)`9(>-~Ledq1_K`?|!&z-|h!?AKKo@$oe%4K1&5-@!~!lVA{LInEmYoCB6j0pfE zFVo+^D=%xArYIKC@T$}GJ5dNJpPk%$Q1JjsvZ$f^`M&_?i`FO$8?p5s@8OU2^&bNm zv(*fPkm!;g!)qj5((U)Pq`Mn*7WGbotLb_#-pUwY1F16%OWIHG->V zSDAPmPoav21o?*L;cI=ppfqK2EWN z(?j;!W$nzcBNO*!i3lPS$zG{#TWOwL$+eXx;Pc+Xy?-A0x-3V*i^Lufl9l20#K=KO z;*#8#HptSoSe6t{1IP(|Y>4Hu;lEE51^*~i-W&KMDrIQb&R?hEXVexaV%KiGRpWKn zExqy9bbn3)SjtAaY7SmMS%PEB$7X~`{UALO3*_V)>1Y7L%c!69ZR(bDp_6g~;Ttlq>Q5nbv)A|e@I$9c7OvPi=*sKJW3@SZ0qq361#(5mib z7YQmya64x5m$7=z(R|0Tl0}&T63r}`bG6=at!7DV@jGYjT=u3L0U@gs={|EZKTa_e z6wR)v53Q&VuV|cIu`#q_;|+d#MVpY-7FyAUSgSdi_w1Ci)=p7tD1^*C7upaKI45(F zorxx`mCuWbKF8ecDdykGXlv!J7q`_|C$nv+POh|}b+U$QTc0-BXr-1`uH9^(+-gPp zR0`Mb{OrMBSs>=67KmO*IT=OL(u_Hlou0rz3qV@7lu<0!DfttFKg2sB!i54A(6>%Z&eXTFtjYj0 z0>q}7=AA6dEPm&d&t+}6lT`x11Q^=1U`Vhhtk^AN?GCNj4JZQ;QchV_=wAog0C@z2 za<*Ab9AL^eD*~Y#aQ6GF?+XaK%H8}s7(k`)WR}rNC?q8S(zj`*|AdOSsrUgEe@;c1 ziXTxCq2dk|S7=a0>q#KMQ?Zt2?l?|9M+qF&A&iuGM>%1KPq6uf^6g5zqmNQ7T!R?T zh-!BoPAEu=GEn%^P~@|k8?{nsJ@2|QxOU@$&%y06iVs1z)@VJY#j%iyXHm4CVhrLT zQ@J^kACIKApE`502SyN@{W{smFv>+*@aHYi16g$RK$fM{6n--zs9B_?LeX!QxPn7W z@f1~m^YXMlVr0psDHoi_baYH2SM-cI@j0C(UXy^rMPmV!TeU&-W#KZ(XO|^hz7HKx z0@1S^^&g^V`k3s{GmrAi0Y(lc2%n^Q_$@h>99%ky7~9%4oK_p9hy4R*Bev!`@fc2x zZknjoSU2pDdQmz=xtmsXl~m5HLzN|f7xx2wdf@?nmlkv%=1Y-Gj6XKxtc;Ok&$+WH z*z5TD8F)HGMcj>|CQCZ?^o+YkI<=(qdpH$&3r_Y0$+9zIp`D%>IZu`lf_2%3r_xh> zG5$GfuOvF-+fGSCai>TVC+f8b5TVzS6kArt+4Kz#7bV6F(nS3Z&C1i_tbA;;VS|*V z*&sQt@3`KAbhS5a(QI(!5r5m=ar2B?DGg6?_=|_U8_ANg(}*%-Gq9#B_r+&kct)_T z8hK{Unf2oN7tRZ|6&KFKK}7UrR;BzX!Xx~4ZhMCLPrU6W4$KLvQzouG%|2zdQXPoW z3$qtXEEV_}A?z3={#VpS#W(5g2=6^-Sg^PxD-$}Sk7LO;Jr>7`*}sR(<(3Eweiq$; z9txa-f*&S9d?2WgW-k(&{gl-0*a$4K+Wj2RvI@;EckAG{k{6fXMrFc;;C5vzp)U-I zNsgrBv#bttq_31$XP;ae?dzcLrBZ39CqUoh|CZ1vSO8t4>Y7eZ!DYC zGa*^AOFfnxOODKzgsh}>EpoN}JxKKZGTsZsV-O3kzWZd)k-*6a$NtETef_7;3@#8h zY$VbH!XtJ*)pL$ncQ&!sv}kKD(uuRq>!=x+MX{V<0mQAFNvJ+iYN5jX@_W$Y<3yC~D1lIWPusi$V# z)pRe_sbvZ9$oy=0rD~7PUa*#xb>ifTG>*O)6A$tK0F?Vv{KBzi+cB}yxh>kVd)qZ# z^N&cDWU@t{Q9`zCEdCpK26*Q69Xk$nIX+|`0=(lB(k5S|loq;1&+J2tNF!+qR9`5X z@=R)31Hy#Xmw~ksU#4LT0&rf@+1Xq>dt)iCmW9KIZRhipYTpAi1n&_M+k!+dZ zEu%h^Bo)z(=qjX$zvra=8Eixc`?}i#4GlBv8J%fpm|4&8OijaZ7nk^E>Qdd~7fR*! z{gTRkA}W{A7|-4nNvT5|FQA!ufG~fLivNLvH4gz{{ZIN~8Kf$qLJf&w)FtUQN3GLY z3LOm|hgPRYm5@p{)B7vJQP1J-DkAfR8E0t>;+H7N6Ep5ar_Rl|%Ouhi-S)&@MfN~Z zs+E|MXt-s}kmzVamV{#F$tOTFjv0u(@Uu|%yqQD}>^Dn)z{3vNpK{D}&_KPgDHq+h z3-t+71tUlm-4%1g318v>#Kh7BSk7og6sALIe!|To5M@^LGSsyYM-gT_{9? zxfzIb?#h_A8FKSS?SL7?T%UGtoXc$$_l>M&=d^q02U)x3vUbgL=3J5_x-)S_R?f)o z7?T>F_>XAXPldzy9B#;egm>fbQn7$Sy>sS`pcWu~ApTV}0FN@NP6-&~`1T;@7_ zpQhm3cp}Caq}p5V=BC?oK3=869Z1{iz1J-_%YWqF$F|i;*n)?qnN_XeUHb{yf;2qxpn1Xt4%4frmsd}!x)jrj1rTS*BBL#bL zE_N^ezi8%7G|>#w6shXp(FrPU(!@}NEb=AEL%8?mHC(m$ed%;B&d3M8GCLAWKAkEi zd`C}zf_?Zd;#2;AxDWqKxQny|d+1+^Kp83G|DGV7uo8r!M|=9C8&$Svky_X;HDYPN zrj!4Wx+gNpVo^||;wA!b?cUPgGuWfEsZ0TX9xLWdvZ<8zfJC`tHkImAOA>WnmS-~b zJY1k6X{zmE1Dh1R-C>`A2S=2uZJ4_lS!l!|?E^8RL1q7^+3%TgPG`Hv|29^?7;7}? zOmZ}B^xYW7j+cXqTe&E*W?Q-4Ld70wD+m9aobpgkc{r!?>W)xOw$kH@VS<>Xd_P&$dsw ztyItD+E?odN9pBbaj}K)GZkifW&C5pQKZKd(J-CSpkWJo(6Nw(!4%;rGHRIR-Ieh< zIhx;LWI=}e8y85?+aKwohY;XE|l4RH_Dv*cp5L905cT@m*1m$w_JY~M$7YOqs;f`pj{Q6J-RRKEVjGVTq}<;qa7eUv?SN0Z^%y^J=5&mjl_s+h{}#GX>;+w8CI5e|HRxjzOtso$eRIznx#PR$mWH1dh)zL zLc#)dp)K}^zI~?l?GXv!bTIhYykrHvBza1UFIiB3t#jf_;}5?6@Yf%{*7;8hgbBZr zlWY7;UW@l(!akeVs?@&L%EoK{tM2ih*N=Yv=r#X8@g6{Ixjm8*Jl%7)pT#97mwM5Z zk42jMb$mMhbrcZ?<>HcnCX%KC8RkUGB2Py13P!jf4HGe+!Pm;fE!Yc2ECgS1xdlbj zPu18l06EN3%PkXnSJUhM0N?6a!w0UcSr-L~3A;*0xI1};uNS^rC_36+&;D-S@8yN_ zHqPel3gztz=k2+WF=8G$I9hxMxe1@X@bq}#^}bj8!p^c;XH&@8bmL*c*%Wqmj95N& zI7iK|^oD2+;IeSeo{(!#*s*smBNL8#=^i-h*;C=Dm+pa44RW|QUiO3tl)?q|;k^2g zr#|d#xPEBbxk0dP5a2H9%p7gHye34SCA_jRoKJ>|VQ16z!D;74!L~8v+{h@i2I{bl z66p#l8P!&ZdZQ%m*@eg4<=4LV5z7CAl9+2kglN|nqP+yS8cNh0k9kbdATgW z^6nkiCQ6KkN_6GR=z)5;%4M5h%eU}FwcsQ;Xo}fPN z!|X$nY1OnyPgx)LWj=Rm$H|^!m}=hd3!Kvc{bGt31_pl_KhtM8Nt+zt#y-O)P`5Px zCqNUiv6IOTj=xI8jagwTv@VCp)z5H6eAMLMqV9QgUZ!b@A|k8F*(vst!)sLggm@-Q z>{LzUi$cl4WHU4WQtepnydj4|BIh6^a+MSkxtc;E*HB30(zi0dm3J-gn=7uSU;muo z+jIjFl6TCc@BDtf9H@0|$hGz@)3@x`?B7fayP6P{XyvNcf92I*39qca&^6LA>KXOR z=J2DijL#ABtEWBHf}>jSRO9D(*-MBgTr|3sGURR$!xtfV4Y{80g6sLtQ2NfrAZ-LD zt!?G9)^IrUOg|qu+1GRAWPox}A9&=zBS}J={2eCsl7Z|R!4T^DT}${~ZE;8uyA<0( zBSI%o23(0!=YU(%V2 z=R6P~^HF`fRh&r>Y#(O|^sl-2&k5QSg3ImoQf?hv5@)=61&U}+EqV=tfJU+%M5Px6#hv45kPt``X#UvP_efgl!AE>E!VGI2xwh5Nbq!K#?A3E_{&aS^y&>yto*7lLg2_(xun!ui9TryV4)Z zZ5r7G4d?E{jiCz95$WBzn)4Ks&LqJt`Z_- z7Tt9lb+pja^Es$Wxv9%QlOidD5bhoA*XG3)GoZ)3xtQ@lvrUcZv7aV^q_WRK|7Evt zGtTEwrm4cQ!mzt&Bn6qNM@z@o+)iKhp2s(l6ZVwNda6U7>g%3qPm|zi3VE7HR9uLx zSEHq0`(n~g`K;4&2OBTGon9C-Ue>f{o#0p(@~oqq`o!@*y?)po+p|V@Ac@zqU)$zU z^D2(HkfaDa(8m(w+*>F-R8Gja(AvoXx7}F<>q%Xxg7=8cM7>3 z<<@tqxE;;bcQ$Z4P1bigZfAz|T{pKg-}>%KZfB|W-EwYcgVwf+M7aPt!$K$4oUhKZ%0eO3%G`5GFMQxm$d`xs1a6p|vvi9H8Ix5Wa_*b**m_)hPQlHWfuUAT==fW;N3gJ%Z&PS$jv4kBRMnR7Mp#-L!a`0+T)`6vbu zOjl?rdu&k+$w&jipGqN#y+G8j*+jIH3-EmZKqO`R?yk?Z?YkDWK%Jm#>hbzKdux%N zZi~h;$|HKxts)HaYYvwrqpfncFA)*>0Jr!j=3t5pbX^y^!j6Iw+WxydtO6Eg^*r;F9m}}-Bn#m zHF|b2h`qaE(pcGQ8`r zMgd5C1C7cIoMgiX1g2(kB&IGQ+_xMg(o7Gg#SdaWEj4}+pF~G`6yIrtaKl%lxPdZc zeL~z&I8(iaIR<~=&RhyJ*vWzcNq@u@tr##wn?#c`EaUbXTE^`~SjINgr;51tO8eAW zE7i&7T)SLlUg%?5u4b|{SLjYsUdVF6l9U%xIiP+*UdRmB*%)5PZ-xRTDgS|NJ8pa! zb_~i~jh=!+5fHtl66iqem2`Z?&4)O*O*zfq|0){m0_jmDja6qmi%!vsk6H(~H@$Oo~5nQcDrnQE9)m)=KR)Tt^H- zlXbTL4~AU|nfZ-<1|*llccp_iO@l-w*r8{oqqn77UTAKKsn-)+NsNCq4%p9`1y7d! zA#Q(pEXadFKRpoaJ69tLjVMMHa!JBkfEf#FQsS{yp=fKGh$AO8PQ)h0w(2>VAn=eV zw2?YVY4KQ^N)0^6^!Sei$B$y_<-~Y%j`cT>us9lGm?6qFFD6rbtKnN4u5I|{#;|Ao ztY=%uvn`Ckik>}!V-HGM$%3%Gu`!28XFlHMYHKi0HgIhlwX{$Moj=37E(JQ$M+3;v zx@LzS$S7t3pe(XPKrdhdf`6NsjUF%z9RNp4e*-cz*G4Y2hfZBA)R${(RcGl7m$pxvmj z*TXia(aWS2!b03@R*1I4AJPJu!eCHYIx=OB^G;UIrM+W&1z%k_t6q+VJ?B_`-F~xJ zT0OKTkO%SQjT7sxmQ2)4d+G#7o#3g9T`S1{n(Y;bYYJzrTXMMRq9##hDtMY=$3m0RkyC z6;L29A%v~e;_JS1u+<6%V0zLsh+HRP;w^p59nz@kRtmAOj*Z+AI!x zYL5?`3e*yil=%ZE5yjPC&N$;?yL<+gx9BR8euXiJm;>hm5p(ZZKOY-iZZ!iQNgNK_ z-ofL@-p&S>uvB#%mP*$BLjs~B42U3qe(~^y!*h;uq2&Si$z-Ggtg`YiZ5i7V_N@6(F+_O|*1=D$p`pKcHb4m}S7nDFT zkkC`NNjpTguxMu4ae@SNF^BY)$(BNVqU2?WkeZx;$%XPg`Fut@Fic?!l4 z37$s881Xd1sm;?UI2vOl-WqC77?(x!{+F9u{_~jFxEYN&4^YVKV6bnXe>=|)@H!5c z>e+4hM6=uQ|1NiSO9WR*;@O23VH*L24kl;$Dn-!-sc-o;B=c~z^-?O~&Om=3NJ5Gj z0p|<2T9onqJ#~DPeqKUNalDA?BE{{V7IwspW=VkSztNli0fqw{eVl5xH__fq6IA=r z+v#mLA4Gb~OEqIP;mo4h%!*KEML4rcOm5loQp?DGW@tKI^wI{wQ7{InoFSv#_;H3f zT1F<(4&+UG^hfE?S9&QT-(_U(Z3q=M+;%rA_CvF-vXHB6##I3el}u(E(~f92ZTIqM zG*lMv<)dA2H!=@*u_;)KN59h2k!PMV7I)N}r%KjzlyJ9fHq>wBaUDh0TP0jajr~@I zm0Ie#j@AUkr3%DlY2HK;+{Fm=aJ6{oX)_S>!l1T3SZ+MGWT+SM-Hb_yk4fzU5l?)U zfu@&I03wfHZmi%>;wP9kez-99v;>FI&5`IjOrmqxW-DuiAx%YE{tyBDGTo4ne z^RyPUR$NZT0D(V}*qs$|?2d9W1q3hnB*svQ0ZV6^wo8%>InGo4O_Smne5rq|Kb%uK zn^O_WsR-v(kymh3sXlsc&YM4eS#$lJ6i>E>5qI)SI(#1q95ZUqKq6wP}j zXoLXoI$bkC1+xsH?;;yoiqI$~7Otz#x(!pp{K@G^UZ)wuhduhE4^hrEXN|- z*xx$P`*XzSXACKPXOQCf6n_DqXNRbDQEMRepq)Oan66QQ_E(52kz7Oj`i~Bjn?$H* z0FRRZc+s#}4et$2^(_hEoldSKcz*#`1N*9F%!q2P2l)&se+;{(9E!as6ja?<_ak@P zo$M8t9vge?O7rZR^`SLPV~9+O4~#wV%6=>W=mxP0+KsB9<_OO(st4H#!3~DOs;q*I zc$`1VZQo#?^c1&ObCc`c2*WUCv!OoavY~xyHP>E|HdSh+mTIoO**?`^rS=V6hsnx+ z1v9PTq2(p=yO^hCa2B$ZLqVW=3qx^{QEv*dw4%rsGF|#EF^3;S17unB^Q;vaFZ{pr(j2~x2TkbuV>pSEz0{}a?cn~S6${Q{>P2Ue)_21AX385W->^+P%LH3l$XRwTO+t%w3$Ux185+E}2`_y;cYB(7 z($;3{+zMpq>uj`6)vL`&(>h2BQFaXDT~+U^gYTrPhS6X1 zEQo#*dq{nO#VvYR&u`P-Sv8*JgfDh9L@>7}>0hMNKQ&4JVx9hJN&1)S^mioeZ$42T z-(Q*!e^U3xczi8yPSU%|EBK!PeGPMEy*-2F5#!;Av7WUow}==UBF4suu_sT zKcwOfDrC!`7^)FTp>fXyBk2bZwHf89krUH*mJ6ZxB`(r_XfvpE)H_ zn^$Pe-=Km~8#7MFlA9bxZs;kQ(;W-Df`rgwMh8hGqTi>L$W9j?7A|I{N>}QLW&R6B zgD2-w*;v_l)5|rpnQKFtYr~o4Bi1|Vt2FN84-3AHH$E>EZoi$rLy@t;T^#$u=CS6N zTgK0b&fkKknAuc}wY=PVC1-Z^y3p!%qO+LbS*P^=(o35Khi`Nl-1LoHG*SJ$g_r|h&$omz9W>rL-KF$tlMEUzhmTfq-v=Y(Ga$|r5Kn? zbM6usn#@ia{JBh-ex86VOKo~3%)_!jB%wWe8(_6wGr71%kC)fbJ(C_UFFl)vVwq0R zs*E|Lo^1D_Lz~oi6*NzWBlp`>G)9*b_iOWDtd4mE{FI-9sG8nWq^j`Cfs@dnjIQAY#xzHZhx`1k_IiZ7 zp`xCO1}Yj;)tz=mJ2la%byPG{v0l^Z&~cIz2V3pU&6@TO;LdIY%R#a;-0En;bKR6T zr`29ptsVA>fs<$Y2YV3rtatF-V0Z9rA8e!%&c$Bepcx9WUJ`e0WYmf!T;{)v+5Ove zD%w%>{R}vv?{BG~@D_dlor=Gw;vZ1(HNX|1T0cS0C1R5>CvO*M2tB9#m=U}DP?5Ag z<09yoe5&^ri!Z^Y;7c_QGCSdP(IuKP3v>&omV>s!R^ zIM#=t9o52{+69BvQ+F@JP{027Qhwhy@1z(guGknTpBH?aZ=4khciv9l6+^^kJtZMeN!YV? z)!D zU;yDMr#?iw;8I;8I7$Rh$)_^F`Y~7;lY@BJQ~RB!Kjv=q{E_YVw@kCtwINU2y%mOx z-5_AQLg~9?0#>-a4z9}F_6-RL*a1xY(gaM|Kz%~dQyKt@9Z5w=rlmww-^A0>5!DwY z5qhXCDuD7Ug$x3$Jjq zzJ`#m;d*;GcRk!ap{IhMyi>izqEZZ@pz*f532LrcZ&}ETd^8nTk4$^(1XrElt&_E1 za4F>snd{(JDy1<^#ZSTAw5T+RiiYbI?FHt^{Ni>OH@Ve@`jm@n&$CZuTd7{awU;3F zZ2|udo|=D`itkfF`(J(qO=Te!OhBzFLk z=%(Tb6#*&;i20LL`!W?{RJ=^Z6*}=HDjKLrqc$oE!O;c(6iv?={tK5K=DZ)7?A)dW zvmqsSo@#e<3b~T`PNOE_ za!4wFvcAE!LEq6HY!~@*aey5Iybt_} zZ}9j)FyQO$p{vfKay-!cI2wBfQ3S-B`;HDEmpPt*-C6snzOo}j(L0ypc^TKju7tOI zvg^(S#mCU79q6;+QyLbU*=I|RbEQvl-q0<5ZiJ^5L(-TPw1v$yI0^*GHTRwjs`E{x z1_Of+pYAx>6AWrRS~cIRM(On1gpmka!vI>g5F9ao&BkD`SAGWcNWnk$9?yw13_ry~ zPraeH_3~5XF&ZW2IDuz~J+#JOln3>^34wzUQ-N(k2sOkU(#)~*#3RNWitrM_eoC-O z(V=F3+1$);pO|g{gpuu7oBuvN-KQgnvX8O7uE$u7DH3r2o zwb;PBpo6#cnjXPO`lFpS=qm+ep%ij|lWvjm#u1}kl-+z8caJ(tN7Bge3%P2K4)CWU zc4n+L7&te`U!toh<}Cje^kz145j)J92a)P)pr7xf#!ecCW6H&do!nf&N%DjI9y+&= z3X=23a;DHx^uR=#VFfXuQF$Vy!2(67yIvzu{6maP79gJ4bOe?uoJ}terI$koIeLh+ z@1$NF-9G^tM5UyB|8Z`foYm{)^QeLsgumFthT90PSKxcS>(#ESP2qyN>+VoNg9^7=+Ik;(o#9E^2I{fZBy4U*9AY@ zR@n7S5QH!=c^QKs=E!yY5kEQIMBIW7AxW`=OG2)a ziDO|`%?L*plS3DVC^T@`Svavy)`p092%sh(q+DPVGlR;qCWc}$&=oNU6t~fF{bmw? zq8E)FbHH6Z+ACiSrY(z&l#b+(C)q);_=-9hEt%<9%!h!oFNG*;pfzGFW3t(Zv7CPv z_v1I?Szw3AKF=|Hi3rCZ?d$jRq|z3@*ID{{gq#)eADBKB{ag5jLjHjRkd$a3e@J&GMUW?1_EI2E zeI_xB{1qCA_*D&G@GtV3Hc8Z@dD9WyH+6{%R}pq-9A0z)Mtc{-}T1Yfe3pY?KXR#|rcY2#>EOFVS4g*FH2_O35hnsJZ z7P7SL*@Bj>CZubCS#_8UX{4A1ZRr}ekhD(Jvyfs1_LXc6KaV;;LIoqbZB%1umkEF3 zmazzYByw5`_^8hhFnbHTF{FA49}T;z5x~fm{YvlUHRESqfAZBQUwi86gIB*K6xN4x z8p5td`4cS%8~@w+KqM)>j}JtGxp+J)ejt+iE%kw@&tG8n`0F%KV(jqW@%@$q`1eo# z^>}eL;3i->WU9#)#(lCJdc3~+o?t@|zaUqZyv9h5hgT$#6@y!4Yl^L(e^0aet5iI% z`=PRBbaO(bHe8AvQw!&C81mb9f(rsZ^o4kl-o#i2CdDvMw>s%Ld zuDg*v?c5^Rwg}EGG7lt_c7$D>dRMFuIoIC+Gqg>xZ3{WK#e<2w{N|SlCeU?ZKEy-W z5+N((7nYAN6~;-zm0tuHP`Vp{%za`?#dS(C20d>&kF1o$l1bxBxLDDg3}IVbZs}Ug zRO={2Jp^Nt*h+>7kU_+Y7)lSzMjQ~rclR6x3)MF`#E%lBFj;&w(j0AZ5V*9jznfBP zKs8{&KoAc`<%4bpL+T-hE0V*Hx^IMF=QsvbVP~PVHSF35NJz_gWzA^d<%&>R!JWK< z%LgY)-mHA1@@j9ms3Dx!cwx^-=O~oKOf)|0DGzzd!=B2w2EViYM)6I{%`Z*b!cDuw zwNdSF$g^jjGdNbpLyx?r&%ui>2gxX~qwmrueJ8eIU-o7{8EdR#NoZCgHEAH&2|Ga@ zYdxMNoftCcpB2T@@mjZwO;O7`OBeqKfQ#Xku_kuNJ4-qoX2}o~8FQNmZnVHe!(E1o zevQEDMOM&>pP>NvC2y#h!FrIckpWB0*rEm&t=ed~W%*#Hz48MBI$||+fG+A(D&(%8 zNpHE)Hs^4?xckEH7x!P-KkKLoIcjDcb@vdecEblM;EW2K1xMlNrqOe16ZbUq6w+H3 zgK4r(v%=vtoJ(>BL6_6(XM;T?6Ck*Z1E}@BF?IZquUb za@JE5^3+^C6ZQ}Z_iPayTLjM*(lbgv9Z3AX9qPc&PK_D+E(dh54?pJ zh=tRe)JDbspeG_h!*hlOi&+9DI#d1_|0mSz7*0k~p_d|o-pRfrEI=7QO4Gm$v!2FW z@E7RkajKP4jrL=rKD>yA_^dtWW{~`&^q|ZnjBGpj z6;zNV34fGM6yuR2R^((OX;Z}3Eh@f2n0IY|Xggm;{T`u$r0jev6-{7D_(rO&r-J+m z_(N2Dk&4q)JVnJbRD2ahBqKVXP*ETU3oqKay-BAP-4<=}_#aa7V=8R)Y~@t6Qt>zy zU#8;QRNSEAPpSBGD*kUO{)viIdbTnu9--n46~yfDe?`ULQ9-#PMco!dUko<1cr9Yv zgrMqYP6oE{{~4Vzt|7P&A8FofG#Wq3Fc=FKQVd4Vj}7i08yr72*xCP1R;K^hApVb^ zX+JhN-#6s_jUoMgL+<+q-}{EO?Em8T4JE9y=6%D;_YGz58>-(oR7fpoEPUTk`o5w3 zeFJ)~c;8U>QHsH0f8SF1zNO)POA{&(s@hVYKl|mg&p-3!XGZx;XUEQ7dS>jIiF09Z zUD#UxtOb(YXVd0fInSogxxLS3u;M+lZ7wtW*}Zoic4I0OYI5N*=G?8a8Fv`(=A;=r zjdu&&#t!4%vJ~Ss!7W7e1-GUh|aW^@5Dn^r)@W^~P$sm?g$GpNxS+w&{F zykgeq2^l@(-01F!lY-F`Ha322YBaVRudZ4!&|ey?X`VIBpG3-%gcsG|*{*I`Fwo!V zMO2+fP^iQg?JygwCvXw{jb236`7fpo(ePJ#G*sswN_Ll(#=PFF|d7Te2DWoXJ=_{woUx`WwB5 zs`J1`iKi{i*i0{jKhaB2n=eRmf!(-uv}VD8KhXsNFFe`e;Rp<O~_#@w@VSZOKSm`$z3r27o`WwB5s`Dqg zgwHl-w5rx@+UWkP``DV@@UiI`qj8UsfPz(MyjF2g3u~C~O!n|@W5EP2#h>Vdpq3Ov zY&BNXm&2dv1*pvoY6-xOMj!6P<8%XT9S+Q5Ng1j f(%k_VWVw||v5RrXB-zH=s(Us)t?uvpc7MCS{q1jmyZih8s-(m%;F-PsJ7+2$5rkjR zAKFu(M&^g?f-ogWLO_sgl09Mz*x0W$;OqfHHdoyUKU@4`0BuF7OzFTK2Xo%b%?JCtYL94;thcY7GI6{+Q3>CuSa}cU>%FE zLEIPcv3LXGje$m+AUlVIO>poOKbM{1MYqNn=hhT#DNn-&{>Qi~bCauq@FmX3zF1U_yTc=+5qU(8CS=JMj`|My+kL$WpA?hhzIe=c zYQjhGgfe788j1o#cmyz^sN^0zBM+V>knuAz@G8DhB{nF><67Zk@z816cUG21<0xWK zkwbBUJ|!o{WjX4m0^&YYKlU8nCA97CzF0y=9YYBOVIT@k5K?JuQ1%UlB5~O_8jFV$ z;pb3;7CAmD4~7|4Ao$5}REmwq-P9iO*of?-;$l&MyZiXW@d;mObW{#0abE(o$-coD zQJ;{}AY;*ZB6KPO{CJTSI;4a~yGzB93LUlS?UdQRv? z9Y4{3h?Up(_%VQ16DUi|gk@!%INE!xcR)jUvZwE4|A~{qgIxnaNcCiO*OCu+^-&>p zgiuQx=s9-0=S0`Q<0pD3uap@37~|igU|&ih-bu*8cx>>joQNaEeOgWgsi}ve_^U!? zykHbP7at7`%1A62oge_jYN#iIv1l+fNbDJuqkqksd z30WBlM^S-ayU>w?)SKFaVJMJvnD-!n^tW9QUJ%X+Ck4NKGTto@#*|P3e8qVy1_nAB zj*dOAkEl3?PI&N)kJu$^K#q}d#f>&)ok6->Mz2^k0B8HtBgz@`=qew_-vnblm>b;;|UAu6}c}_Tu3jGV)zb*(1 z=j?CUzHd9{SnR!%khNBCo)nsd#orzmo_9PYjN7i;`ut9X+BIDi3=T#@;HF^O#YAGd z=y1=I!GYdmU}9I=8J8nNX=g||9ZwgW9p}HNWH5r9c0fwUsr9IP&wu)jjx&%~9ifQ) ze8=fyp89ks#|_uyZXwG>Dp~qn?I=Cc5P#_cH4CMwomT5gtkr{=F@mF zN_)fC)gb(&p{i@E!*5R)v3V^Pi^S6|s#n}^S6Tt3v{CJdIiB??oAHw_(kYp2UJAXB z5`ql~K;G@5%9qz&T6d*trl?`wUR2aM?-1N;?^5jUDxtjgV(B~ph3kUfrgYGIG+fkj zD{=}`I+44Fz{(Ju7w)@DT(x)C3Z9y2XSK2wX&I7)_<^4JPYOwr$O807fpo`!Lv{vS zk{wz}0W_FGZBR;%3xXC8h#5IfQ%+Gvj?0we&d4b+rL_hGt!zy7!CFUur)3#+$k(D+HEB4t-+*6g#K>#SC>!f2 zJ%95u%5KY|zsZDW%jdw;Y{Ju?QPT~koQ`Fbur;%$891@()2P>e1KXB?yA!xK8DnD^ zZ*I@5m4p?hRs-!fuwxnczPOtBEo!pU+-l3H)jFxwXdP*@1?GpAfo)e-o3$DE0=t)i zCtIRzG2wZ5qTSz-UPY^)<9s3Ae`*-(dRhdbVZ*O`(c8~dd>+*x6sv*s%z@TE0c(7^S@1)5R~;ufSz23pL7+erK+kt zNGpA)tm#55->_zhrwijea^0?MBxrjy&eS=ds#cZox7GJ}Bjh$tdj{0gKos{QfJRpW zZFHuj`J(-n=$;eTCPn;x^~BqOHv{jj|559|X-#g}n-ceNZA@Pr;Ze}Owp>u zoQpJitk)b??WWC$M(a4zh4SlPB%D~i9m!Fz?=JGQsv3VuPRQ91d%6Hmxvm&e74ZltUfoQ_9NErvz~p^eG1^pk>gcn_0eKmW)hXhuwx7?lMH#gCCA2 zK6Q?TrT7B$QC5EX2qH%P$Jz64flyTP+cpxNrx&J>vhq_%5M=66PHH{4a(z_*<85Lr zx3`MfSmT+K>+=ma4l`l&Scc@BtJ#pO*^m-9uJo9Th6i&ciaIwSD}RsC%P)Y!IF~86 zmNYle^v|Zi03t66FJ>6{tds^#d3?!sNf_458F*KFF4|yMgh2y#MRhWz>5#078U+1P zk<4gGrju9gQ^Fo#Li|OI*5!%oIJen(oQ z-36NKS$&wmffhtmUJZXXYQh9OeRuR)|e=M=7bR zjEmdAbKqhyl*vrpnxIB!j}^!29Bl_j!z`EmTof+ab2)k;SlHCBHFP>#Z33< zir)U?CunQ!XxB;X$@Ta37-NDpYU|JTlw(7;BPW|35qm z4GOpRB4K(40c7B> ziYsqdubJw<+&@#j;bPA%Px;G7FCCrr?oN3g{)qoE<9%$_bM$s){aj^Jva;!|?z!gO z$>!ZZUjJwQ|KR_Ro8MRdV%tpRv5SZB?wn_B(zEvU@oSEhr!7~`Q0q4VeekaRej3UrqdxNH|1|T9q{)-!#pv`c_B+}ovUe0*0iR?&5ZELO6-m&&S2vk z+xJl))84Ho5yEJ;JXr1RX~HU>0JhOu+d=>%m6$Z^S=tKRrI1}DU8tH9(jxZv64l_aI=P(oCKSMKEuiMT1g?;!!S`1>J(weQllra_^8wn=PU@>a3 z800Nrg2>+jCcrn*0?H%>TM>NfQIB}A#qSCes~%=qpu33%6=HE>Rlpw-o!`-jT{-d! zsq#Y>{m3m(YHohZlM3{&K+Qrj^0&kGR8BP=OgtZqkB1XDqLn*C)UX*M8*H*I-!zd8 z{VuhGr3J1dS<{gcx2~`ND_m&k@-A0%^LM!l^vA@&q(H4ONkxOBV?lO+BWH+~vL4m* zCpYBlN*(MbXo_3*VN0^6B_;Y-nBYRHDHtz@T-9LAbMyD1isHw=C4E@TPYMRZQJe%i zKjf*`g;v(Ua5R0?V3$(9||`5C5-0>iWsEry<~uTcT0KG^+R zj#ll~-ZOsB*=}SS4eWi`!fFP9zd$L#k3vVQSc0KdlZ8J%hRrV3_(k9un||Eluv69T^Bcc)?ft3}tG@4BzMFS@ccYqI2Cd;Fp&C2n6~#&Hae=Wd|z?IGmk-yQ5K07LbhZ(pXiy6{ByMY@+h_+UhKSDqTZms=t4BXs& zw127XZ*5V#gHkk$8nG+A$)m2isa)MdYGDmPyI;UMSv`m6nl9ow;zoN$1Z5NYn zcAF*FazUB~9Z-UhTWN(Ln*O4S32nP=%3()rOcyC+x=jh!up06P^*vCrZD)W zSj#tljSaCxsg^oqU&@S249O;^gVZ&LS=+%ua<F9r zX4Zak^D^r94}_97i-^z4h0-;tk_N14FF$n6b+KFD!8|bQ>H3XJC|x^O(vU1^_%u!n zkni~(a{P(w(G^rX3UN~{ti+;*U{F;tZm&~w^V{oGAn1XDau`%`ai()WQi$W|-+T*# zg^uRZMNCa}St%C!lyICOA?d*%%l&;_>$TE!wE?LWr=4+dDx*M|F5;WxWbae(w-Tpd z8wFb_*i1nP!Q`%dY@#E{Wnef75aKkwoflpophHJD4!+xaz4x8RW*hi12j!v;2*d!;F|d|DLoMWM`FeE7Bbdj^O5YL`K{b41RMg z6k%tSgXzLxP{M(MAeZKBK7m&SfnAtWx+vB|K`#ZzDDWakJ4Zsw*>nNj8CTssyUn!$*A)xAcPVzS zz({V8icbbEW87+q4VC?fTHQDx*-swfn9U zC4Z)u%oW(A*K5=L0wBC!V0Z1h3o^}BIdY}++V8TeJW7P1Sh>d7$%Pzt-y>SnF>5+SOhs#fpd+$>0p4;dK zj)kh&rjh%hYxUJ#v#!lV0?SrmtjhSxmEvoCS|KVp3bCk-WjApqpqjC6%HV`;j6TA( z&8lYXRikDZueR#1wra25aupjqGrgv5x~}cLAgg!}(aEqKw(<8sXa+iqHllY-70hmW zOJZ3Mcy(g^#6mmFBgMq~SqO70%oye{bBy#9&4^Mk)F}!S28%>=IUW}I>18K`M|gE$j*bx;X0m)iV`#$vr!gKx9n#m zVSpC64r(tJl7tc|;cj`x;JfnJm9qEB*c%6QKIvl3-ja~?csAO2~3&> z^oOJ`J@YghkVPW%cD25SWI%!taLG=VM(VPbRvn>)r_&{z=H|3EZCm z3=URK;nI^Y5{oepE95|e-RSql1_#F!MfJK8h3l6Pd1-NzD*^2HpTXOzvkHm4myo{; z_$N_9Vdi?JFLrWl@Qhlr59jgZQPQxv?}!1EeaPgRg849kV}S^qw>%d+3qLWZ#u6w5 z2}7|+1a45I@X(N~z=sSRsz@@c!)PdRCf@FQQr-kG<{$<%k>d-3@bO#*n6cua4x)ea zPUshzt|TsDlj?hz__U6|s)uH6_GoG6grL^NLjF^=3m7X&U&wkibq=`ogJ*bKH$52O z!!)mFJzC9}(bVc4QrNN{;~nh^ojdKqDQ#IvyJ0SVeDo0cn^5S4H{45%of>6kD&-vg z?Vu|;q)E|ADT0HUg}NX`BYrcM9FYOI1F{ z8#bTAdZhOOYN6E|I@a7)fTYK23p=;79me*6^7nWvXW_#y&Cn>}`|=CWO;R{WP@6!! zdKoWc4}jdDq&zEpP*Y)Cd;oOjuI&=qOfczRc;?}kM|IFv>S@05nnCQGw=L;yOL?~B z8N6RYkK(@Y7`%SL6qHReH!-NWd`BZ+$Etu^79_Vd>1|DU_<84*W8k+f7?`a&x!p1! zh5Ts5)&&XlC%yiZr*&ofQ~B=}1g7~2r5SAT5lT?oL8Nk@Fl|L(-CLc=A?tgd7^(`~?yQiyne{#Flk6EF@`(bq*W`znbpB4UTOPg?`Xt(1>w%rb9 zmD1)VW?pA@dOj~PW60uSORwcaRIgRMULQN1CEmLpp`?PYdt9kbdDb$%@RuvJhpzmL zg4-0_L69zH9u&9(1?88NOtZg2HR490dldVag5OZUEIyx7j0`sE64edZpo|l5K{$qD z$`QwL>&t%=It(59W7ug?o#Y-DND1v)~IQS-4~s% zihCYGEW7Wt6=5}ND{7~pje^Y-`2V3mELu0;1Q13lD^GcmN+zK^h=2M+RilR;-Zig; zuxfEIFH_D5hu@iQpgBY3r*7EpdNqHMAzCq553(=BB0GS}nNPA=4J%^_U^$c+fdaho z-LM-Qbbd$Q0Iru8;$B8@R6d0*-%-G&J&cJo*8~cZ7BK3BLD9HbUZSD`ShYu^Xf>g*%oo!1EfsV<;%WNj*bTqdM zzXJRVbzu+dLf(khruS6|(k;!5!it$RA125ha=Glq-a1TG$?C)-DUqL9>cgR>ETKo+ z1q#Wc?Dnxy8Ud{H9wzlR>lvm+b=D)7 zK5tXt{TOeT4g0b(@u!Ag63;^JNkuo@nygty85Qs=RBXISUg|Ww*}s9xW#Fv>-Vzhu zQUhY$Zu<`J z%T1GX-*TXDl~E^i4@nW`ETis?Sv_avM~=M3={&g%+y>{ZvGQcC zQ6;TDy9=CGZ?uDC@-L@B1)9l29Qn3JNPgiyON};VQET!Wr7>Z|<*;Ah*Yw`BH8w?%?)xj?(rjIcwlQpzQGf(vYw;JYobP1=O8lf0(| zwoC9_7T7V-I??LioUWre<`~=hJgmCoj&mz7yTq*7aff{T^0$xgk{X8BOfO!vd|-y6 zkZpCo9f7&ItSm}QK}2=wfq!FFL5##sle6^e zswItGyJEMkvuDf@UYmS)q25+o1xMuB%ehKKfEtR=+^;_|*FYx#-;+`ekK|GQO1b8s z;pBFXJYAvze$6OK-?o1wCGKTKeet^d=j(*SN(HK`R8f$%A0DC>e{_j0j&_F;B)8q4 zF&aZ}cRAI-8^c;-;|F`_NH*(Bj(Kv+vMTa*qgcHU7I8jz5acdoBQ?%`Sy>p&MhtEj zR_&q@pj4(PIDo*kuwVxrmcmf%2I8|IX1*F2bO&rdcw#BOl2&kN<5qbA_EOM^usRsc zV8vnVMA^c_3Rr@~3#=t_`D@7NXq~?XQVr_nq~)Tnl8x&Oy!_bM5-TAYMe4k@Cna)G zVm-be5O1ZKxFn}qxhKyJ>BaJw!fXui45(9RmxYO|v@4{HJhWr7TK&F)Pa4CXY0THO zZTG{S%-*3$V9YOLz`A?`u$2Tx1|F9%cN z20lTMdB>W_VdN|}^=aC}Lbo~+z`-A@iPK8F(2!!^Wr)N=Qs}wUS!dVP4JUp87rtRs zD89|MFq<2YW7H%*6ha%3rE{V;DSBtcdew44;%D*u_YaEcOL6Wo0V5eX-p6NGa+rWS zLCpl>*MAnKJQMny!4M{j&0aGmH)b3xW*-spwZPhivjlMVh3#T?8o+POGY_g|isR#< z(S@3YUqvxNaFg%CZDImFZ#0N1@`wc%zbN~4aFE%&sCPI3dhd|+2@rkHRMJ5*LXX(f4@tuYAi~q zf3L-78~BZBg?d)$1cqrCgrNugO`%Tu7U3pe34@!KMv1&4RXY49Mj%4R6 zgDj}URt8z%-=_xPlf6l{n{PT#mUWQ$?w}XB?zBBC!AC9~L(hf65%w`FxaDHsvf`f` zg4gZ%1KG6E`FZ)P?dy)d!=#c&WAs56jkC*H)0o|xS?T8o^2@Jm9JyRRmC5Bhk_|g< z_M{pPa?Q$8Md$l2*{CPmmsgd`RB#1G5%y0#s$Q3@IhYdpjlT>jMUBg()PrJfCxmIP zmd3(W1iuP7b!->m~Jce;zUUpaj3x2-aqr4C1-Au{7ct z>>m^HIm*o94#gf6tz~gJ8jmUQ;HeOOawK?S=dD$ zbaQ*n&3KmO@vFoOE2s?UzfwPO4OJJwQ(wRQ^^~|i4@Z6>8tP|Qz_)L+ikI+D~NSxfve?K=h5Iw@x#9p_@~qXSMGMX7C{G(JP{rE$|Jgl)Qd zJRAi-Jx5<~kl|F4WMZysW4_7*C|aMxh5@zMDlZ#;j>uN$K<;S#_3AlqXVTl5@>uwu zL-qJ%KswZvc5Q|C`T-=FcInc5wlWt83;ANRRTg-fsz+Mac>$VU?jWFQu*vBHBJ8^# zc!58Lizv%oM-@t23Rt=9);1nqBIkpoPG&t9ZR)p&)iSM)VP`1#nfHaPPK#Q77liRd zEgDAa=WmWdIl#tlbSQipiwgE#SgL}0Y&wJ}KRvjxxXEf}63+C!Nl64(3b=zjDI_Ii(C98JLR6VSFsF?8{N_i}N3;iQz zEYgkfouVijk`3q2!B!kqE?fNqxRn?MThUX>D8;BOzOv!R(bLwv!BH-kH~3pWr*dd> z?jNxrS(PJ5Bso5)SazgCM}LeLJ3@(9!VFDga*&hcL&{+xq)5~Db_-<%@mMbN@=Iqb_(d0i}DBs zG|{lVG<>8H7n{=s>h@X}Wp-0gN)TT~jD7F~AAjR+J^Cq+PQodqc`A=nj1&TePGTu! zoKl{q7@b8@LKMgpu#*ntEKf;LFhRi!6l7V!ODK&3Dkr`l)g-?lTgfHJmV3BXqPYaw zN4H3rOOSnGW&kGC6;_3c1%~Uf2C!va(%LL z{Y+)^wZdd&$Mo*LyQo&(ykIXV$iCZubh@tPJ??^|i>kVj3WxiELcq?uh!_tNv){__ z2w>+e~+!t-|5t8XDff2Rq zl~YrvFQ1-@UXD(0cxYzTuIb8Mx5~Ql8G9niQBYv^HPOvnA9&}Bkbc)~b9L)36R7O< zdfTqoJSk*&V3=OhFk=!&oMV{kvcj5SJNDNHCc}+wI&1SuMrr z{sI?ybcu-h1KQ9w3HE=qS$?B^61V$(*1GRcxDZPMc2 z5q6q+ze2nJ_+N;Bf&dr$Y_@x)qRolx(YA`82}M5>%6=h~{7k61Bh<0~SDPaKJHm!L zLh~J=>5kBHM`*hvAY}{tlgGE+5jL^^0nmC!Sa(NgXMX~0J%%C9{1keVh2FWswaLP@ z=bg8Ty%!&PdH<#TGsX4i#ao_|^F{Xys%!urbeJtSj zF3wfi#TV@boy-9eMLsl!CxOb0{W<_GCDF0VqTdY zndF;|yiGlsC+r<|@{Q@qI>B{t#BT<_qr*Y`7VtYeoW#$9Kf5EF_^se~b-0K>1N=E1 zImDj{{@jjS;@HJZOyN*&4ng^lzn$R*5S^%Min$U6*S_GlR zn$X2Y%5Ui2ArFfB_LcY+y=IiksU&GjA#Ir^ZPgj0yF8q;KM?Hn_w;ys{e1($@{m6m z3O3GQ!gjYIZaUG|c^uxB&$qSjIPm$y7<8o58-h12z8IhX$iPwfZ4U&y0zH0DFvJHs zF@oiU|3qIP><4e2}FDc;-f=^Z$6#LovIM_23C!=Cn?k9*nOn)j|7BT`&WlGXR9s@Z0P&fot(G;Ick5xK^J9 z+zcNJZl=!)uFaPLuHBajZkEpmF6Xm@>+ofP>-2HpW}k7mo$+jJ)8T%9XTaOD-yb^G z=L>ei%u^mS_z!#KsD>ZjHS5jY#{Jq;D`_%XR^*BRSQq5G*aLQ<) zo`m)rs0;0<=_jdEhUD-wlo;-&+h?#lXU>7+w(>4`#m&4o5cCJ*_5&yVd{g4K`h)#F-cVN`ePK!4nO)E{gTYt%&MIB`8EVDdNipNZ#Sm?x=P;o4M~Yt)Ol z6si|I4$e7h+U~me)bLX;ZoF-GPusF5ZKY9L>7=bPYO9>It%%xIjBgzezOieIo!43>Y!w)-S+Fm=&p>p+?tbu(_GR$%K`@^8nu<)h!b?NG3n8NuF*6 zH03WUn4dKosX@b_Q3lYnK=uadlT^^Shx$59i9w@Y>KCX#Flu7a5X-9OE&Js1U!Z)Z zKcENc?o9cM`t||Rk*Anas#%k_1^foFHc*Pr#Fa}iQ1G2obC!MT_mq~hN##hb4k^>J zJ6q00eRt=oe%Tk2J(M2W@>w;gBTtE{ctVA;x4TI8sc*Ud@(8`2AroSPOy)#Na$22H zUf9THYI?|~j)XN8IsQ8PtVHb%+4no#XZGKTJ}Z@bN`3zh_t~7Vl}hkN3>Vhwle`PB z>4z(%FsnVBB@&cIiCl$EK+!ga&B$qOtc@FL!xrKl08cRF4Gjc0hOPe7flz0k&%ZJ3 zlEP|3e*Q$D7a79ZxCv7L-OCIHe83iVb@@9(VNQ-2?mKZL;I9ocB;R<3n$qpy7r_AW z72w3PfX_SC>p@b5++UoAdhu@jV&VNji#z>stCBQsI^_+7cq9>Vrr#UtJQg?i9qER0 zt#wqg<81Rta3ylrwk4d)0r*Kl1sZXGWb-u0mid1g>JrnIAE{WWHv8 zfgK5tIt8wB^yyd6ynJT7@J4H_c4w5^DX=>QZs!zN5ZOBXjKD4$dS;3%8eJUaDh0L@ z;!m;Gi%a)<7Q@Mqa#V=J~tB&N4Y#Kcnb=3&A8o^aF<-+n^Zo%djT<$3sR@+r606XHU zyzj#Na;U8A-#|5RBwSU3tx9lJJ#;ndqXqS^Z~JcB^|sgc#tPPag5F4IzZ2Sj2Rrq? z8=Q~P_uzhx9H{jM5wPF2ZP-K6KdRZV$4PGAuu_GkfwZKPhDPR9F&L zBsil8x`;T5N0VaZp5PvnNh zDdi&Us4`O^3e2dQbZl#4JP+oD9Q-+IinUF$i=*t~SAqh&_%>TV3&B)EYj7%)*0h30 z%SMJUlsw%SC+U$H2f1xN!|^&OmN12+l?oCitA;wan~s|hGZ1tZXGmyDv$ILF{kYCh z@JMyKRwqO|0`&_b8vs~@b6weWb>F3Zm)m17mupww;#Ldn>L|CG-wY9cBdw29Zkpc+ zrHR;k8~T#R*r{!-IKox)w3Wp2LuSEZaImS_S6Z%axU}K&rWjW-dgK;YC$M!^eQ85R7YB6Tjz!rShLbT)9Z}Pm@0Xh)w}>|EbTK}VJ?L?O7u5j z^;m~Qc*XMOhVm1^qZ?}_m@n_`EAQ!pwI*K<6c}VYwQi9j(h%#qk@)#GXmWVf+?k5R z9o<@~1jwKkz?R<%sl{PL;+QgwC1e3Wo@C{e+tbR(K3!7(Tz_&IRSQ6JeH zSMnyE)lp~lXlKm1Bxb9h;@a=h2KxcS9h$ag+%cl}9!uGsYQ%B#jhz$DZ86*SDQ*`= z-YrJ{7$WaBd{*ItN9+`?NH^%^GH{54PX?Ud14$kf!6+(U8_G4Kj6>A&e-H`?egPa^TmZA*vE(jov@f|s zn-Cd*57@{gTyXeJ|Acd2%(fq+7h&`w68&R{Ui4W9oQC_GN(40Q#(e-avEaXQla_4E$)E89uT+#5+6_% zbpp8{bigUnft@(KyHxVN>oZQ?t?5RS-w*wiK(2KMr;yCTq}~=MVMDq}s2BSh4h_-^ ziNT(9)xgSoG#GQ$$GC>^%v;=gfn6_f>!lf58Ff^SZXfHqQ9a?<60>bx2!;M^OuQM~ zGziO7^YOq5*h>xVJl!AQ{l2=Ozq7B`7u>i?qL3l+bGf$*WKiX+%BAnXkR$U(XfZJ4 zjj})dgfvByvQ^l84X@T9XEuLfT82HIm6&(RbQXDcsp1~L*f3>6r{&$#Bp8LJCGzey zQD@EQ(=jKqcu0T?kpLGe^tw=?*J8x6&Ku?l=jNDg3r1dwk(Vmedg*5tdw&V~C7dsH zS~7U3z`;L-@fy>_(@<#*U?kG!FGB_?V=QuHDML076R#itbsS+CX~WWc(p49A)r~nt z%pfdlyT!E$Y@5KfNvNSZ>Zl&|jvXF9IpNq8vu&Q@HY3vBq9W}rI!GI3M}&R903I#- zuD5w4OJdF%)YbT$@r<~gr!wNOooDnY(kk)|u&cD81{-|Gm0z$zr`thng^@)E87Y`> zs)X9?0r7Tk2*Rr1Q{33!41ddm*ce4zEDCv)g|h!?5N)Lu@+3t29%K{T4h|q@*kJwg z@RwuuMMFlde~NRC9D4rr5I1ysDyL}3c)_X-fsao-|5br48vbhPY{E*CUdPS4h4%Ww z`uvy?Mn~Cz9aLm0DN?f;SiB_mDBa`lg+3?UX$S_f@6*WLk9mW~79@8UBmNL_3N8Uh z+nFz(eCe5M&&0}_1lzJ91BjPKn$Mq%IJJG&?smml578s>c;XozNyP;GHjn4&0dEgZ zPr#uykhLf~K`|%42^}0eeiu4>(P>8qnOzOJ}JiOx4{l7z|9qU9*8%gO2;InPoEW<`5?Xd+UB#ijXO!2J4$P|m^VJV&Pr#v`9_{@sp-$YdzP?MoRAq>@I7@{U)CTcPkq9((h zvAEedi>wGLCnX3XaR;6N-|s{ITHsR!$?_`ttYOfwgL(?KUe3ZEtBtX2?W}eR0yRFiLOO2(6(LE{TX&_Gc7>gxt5`;AkwatzbH3A;9KLjID+9*i^S4aT|0aEmV(4D|I9 zfp$FmF>x1cUtizxfqpT}0JFpgl5amKmic|*JP=ue=%Tk=Vt+gyZ%>cMINDT)8v4a%XJi?r8b$ z4=ZY3b-(N$3ymEUTJ{MI`(qXDLP`6--Em>&qQcF8&fJ=3d@FD1b`$e9vjN<9Xal$j zQxw6ROwm#BfM%#+?tLGO&iMv30ohm9EK}wYO^4#&Xw1Yu1iBuXIKhf2&KV zSsTq=7qhLO=5i-F*lJyj+)#xgp{V$`Te<6kINAO%dut|}(EZr;r2CJ6M~flnWOhJl zchI1+H=wU%=vF!%GLjw@M<`oZG<0Ay5f#AzE_JGX4*MCY>!3-}+?8&*}{tIUB1_y;gM2#$FD}jx0hik}iftliRB3oZ-yVmx? z-Y8cqu(jZ(ayO{uNV=~Cnvc-?9KxSW%yiX~6$OH^DC6ysi$q*ua zJtZ*+)zQ%O8DCe_!hkq6Q`DD$c$_tzWdOx1#0ADmNGjw-N%v5-mUD+1uhP_*@iBymXQrt;iqfT#dZe1KhMFr%Y2A7X zX6n}hxyRJEX5@9$&wLgi>$9p#XL3#D_gPB_XAgM>Ey!DokFF}4!~jUE4@l9Ere zi7;sCE|>FD-#%8wi4dbg{UZB(RyD?_)H79b-s+q54Cq6@ZcE+>nYV9ra+6{AA^N69 zt&8mQWvVUz9qPSoxqj-~XMaSv(#dN} zMVR#XvJ!gc6VFMqXi&<~W8+~VVM5*Y5#xn-Wi~uncOAa{A6b=8gH?1!Uz6MK&ym|t zBwct%djj8q+z752X8#PF`S=bEsx&DrR6K)<@qj#3T2iRTFsdoE%%UF0qY4#Zl?H1t zb;!<8*A4A%6L5mTzFu4!czwY*;~O~94{Cq?fzIP#A411z-XDq^ym7;kL}?8}l;@MV zm7t#odyPQY8v=f^aRXSlczc4I8cEz)Xm4->IIu*pIWF!Q-g6~%HGC;NT6y`|30v() zj=Zb3OSTB?d@ml_IxYP=c$ourH&Fc>#s;bd(F?;xZbID#A>zYSv`z2!l$ zM)BW9=L$OCLI+EUX9R+QURVP5cKX#~NLBci7>U%32s{5H{B*NKI>dhmKfQ?QjlDts zKj1HHIDZu#vZsNo2N1)NN7OSDWpN3k87@v_OcOrdg&cx5;3Ln0yoIrr+{>mMc^97< zekR714Vm6&?H9T(_73;PvPvhjYNA;+v8=iY*8Pzk)_pO1)sS(ju=u6?Yx$$uuNJ*r zbR#E{|FgnP!)-&`M{+=q^J>YZlF^l8zLApK&J}lzjI-?hqD3!Mk8J;#rE-fRl~?xM zw^P|G1^7r)MVs&CQCX!ids$MhKPzmIa^ze>{-!9mY06Qcx>!~0Ny6JL%zQDI{&XMES2%!FgtEC}bbW@(c>W5@tvd`D4a@r$3ET+|$0)I7m0 z7ue+jw|t6oTpSo4h?t%~E3hR)XYu#R;ggZPTe7mK9D{jY)Ide{K3%EVH*+T(2R=!; z+0PFOZ1K?G!`9=)yvf3bXko*+>qg6)0~3Y&W43mz2R7ZTG<{ig(Xt7ySzwz5u32sv z{fI@94oB(v*pYGHn|TwC)-(-Y9$mD2f?FZ5D+F!@mU?FROk~^h&kAhm(6e$a(hLCT z?EdQBfPpF~y*hYl@GfO=7RGZ5?$GdlaNh!;7Im)p_j?uaeKLPZG=E8I40qI7_aOKc zm=$ju4Eu|iw`t3M%Jg;y3;uUX4ebWzopQ_mTGKnVEcky)x%THre9x=+7tq zB4&S;^{2J2{hKU5T}NZgjRy2@LjT_xnD$I-0!_l@6Isj+0+j)p1V6<`k22^j$>uhR z^BFYXbrxT*8)epGRe}(sTSq)0FO)zoXy~@d^;F*?JF8CTaT>RYvBl3Sl-fH>!fCNGhHhelXuk(+LWVbQJFS7~|%CGzCa zTQL9TdrQ~G<+98GCz^YL^-Ht|d;K@p3%1fr>+f6P>pffgeD*|J_S|fawe0`I znRO@T0CpscE-iX7eA~GMnQO2qnQ)YYwP|+!huZpOV*Ronwtm1zO*$*0&WcxBCM#D* zD_7rku1PFqZAvM3u%qtRg7Y!@9^AL7-fXCr;9LUK7Q#qGu+N~f*9y)x+OQyQ2>x~D z?v?c0jg_qi`rWLe)*|}dy4=<*=G~2X;QvusdFyieN1Jn7YnUJBdX#O1Vl8(uO|mJMoC$!{vU zxv*{?RKT={%j7*R)ik^jEyKZvo9sMgqwk zdWHslAO#aK0&2waNXA5LLbfm%k_CSbLS^(|>^zZx97M&iptm;=I-~4&i0j&lq{WU- zoTm^moPg{>`YzgbP1%;-*!{0GfGl8&>;f|1zB=KoALFMTi(c%Sa8%=bYxvMr{km)H z(09LZ{R^)xnsjf7x;Mn!o1(5wf(_mKI0ePY0l72?&iVw9N3mt3Xq$!pFOIG2nYWBh z;C*X7v(03Eo3eoaZ40x_VQB|z{T_(PV+HwMbe==!JUSQA8AIp0==>=8j@wL3-Cf&015%5<>n8WelLSGzP%F*l^+FLEP?81v4v_q0}}sQ*q@+rmqDI=XHCF*$ekwV1#h1* zKy?|9YAmV-jpqy(77v<4u?}qI*NWTks<O*3VfY zka@9fVCQ21J0Gc43n|rS^1+EY0Ryy{R)toi{02?=%cb%a%&OmaGS*AjC75%|56`ek z7!%{yEONQx;zXmlqdQ(onc95^gWh6DyYB$nR2l6&a-QOxM6T(&fiO=1u=Eix&;K<9 z%^;x7n80~p-+!hY_V+=C1m|Ml-Y!E<+2H>xeB=Y*fUpe3WF8_iUe_Jacm(wR5% z^tTJZgf%<&a`n)5WQo>YS~t3Ulpn1dExP5Z7i{%{3k;b`qui3I{Gv!-EPtusSSsW% zyE@#SNCgvG0GxmF9d)q-pFg7b{gBJP_|@UBM*41n@;AFQ%ITkGcAKlC$`z}S1 z1~NBXH}Y>o${Addfggc2Zua)~`+I%Y!6wwp9|bY=@l)h&JpzXtb@l+uFA1T~iB#$! zBnlIC-2KT|I)oZ4XquiVi3Yol5d=%ztoYSA>W-PR=UqH7d|=X65w%r}?ii~X-85la zKIO_4iP_&`?F>Asw*VJ`2%rBk`b9-X(PAVD0aCrTh~iUwLg`_VnV7SN7qL=JkP%dB z+Ul4!($>6Ln#yqALEoJWDz|XRO3Zc(qHICLHFiW`3vRQ`q?QSk4-p}O1{66bLIWO) zB|`*gfJw-Bfv`cTtZX7`)RQC0dHPH$>ITHdB*K$S<7O+Mst8Z2(p(2UM3K{_SXpiE zjLFSBAo}|HV@<$A-E!E~*GJ$hp>I+Fm|iddgF0*i0E;{X|KFhMk}4Jd_u%IRbO=ab zX@ERMSVi5)05F=MiMdJ_aT1V8ZA7D-><r`ifoYEoXmY9 z)#0bmLFxrligXbqrKJ9$am0QGzrTa|q15VE&r$!~R(8*1u&A5l=#CTGUO>umf!zy@{&jOtk(1Nk3b) zy4HS``H{K5suMObp2{=o=ac$9eNM#YTSh+!1Mno5rWYrDj&hykcT#SOo{aoGL3`bj zh?b-ZQM2OOK}AZiI-z0xK|g4j2XZ{HPj|G(AJXoDZu2ItP_WH}m`Za~!Y8;6lc|e> zPbBJ6E>b<5Md8hlRs{u}ypF&v&00r}G;2-CvzAO)tP+VJ5;q!J!hZ;1e}Y*c{60s` z!X%X_=KsLh$h7irf&;s(2hop+OCq3gW55UADh$TR2}^SE%os91)lLk%7u?t}tkZvj zT!H7bI_?{d)?8RhTXUr;3qII1bhyqBT%%uLq~l$tIp?Ly%hl)4ytwUJ@P&aFz*?hP zV5Yc#wMQ(`?Z77Wp_CRw1_hXs5jHGLlvm-UyeMM3u1$_{=td zqovAzIPsBCWer0q6Z#H@OpF!j2MW(L3Us1?w$GP9rhk1bCjS#?0in?R-+@n>MIyZs ziQVc)sBa_CydOZn&&oU`OF4^@$?=4vB|R~W+q9h_5-_BSd>m_uLn_sg5V3!R-+zW+ zjgoSpiBtB%DSHlZh;}4ISqR!$MIW2sr#MMTB0}VmN&;(*3@d3aKMg*q7q*C#Sv+h+ zTOIK@8GRm08}CoC3zlM)&QbS`W}u1`ICmV01o4{dh1{14uN97OBmC-IN3DD|A%1y! z2qIWa58!cx+YwTDJ+T_1Ihj6aYij!z zHtm1OO-oonD2uC)AkH@!Ex$IzBrmm(QqMCgAEKJvfXuH7}oPy z8^I%qKmZhyA64uM`VB<%aTwU22U)Pj(TM8WVHxyONDBO(I!{p$l1QnLftpLo(Z>gl z!g+!{Dpdt3hk(zM*c4$4gXi@5R&vJ z`rbn4J~$vZB+IRLFd%@=yXZWNpUfglN7y|@f*5CI;Uban1abEq*2LxI68#Y+C2|Yq zl9b4Ceu@pl?Kfx`Y_nuIQC3m`L{OBKRDi6+=^A2EB#s3|k!>TVhgwnUR`<=#f~{g? z^Rz2(WZ?3;QRijxcY0A{K}qDuNca&_OTtA+KQQWzbPM_J1!Mx|idI{a$S@}_VjNjB z#7=>n@R+2>gfhcb=fjbM(r-yxD3mtd;u;0EG0HWHWBg4VAX_qb|3e6kvmwx<+|hTc zSL7375XOIi&PV9{3p)4Ed4NvhQh@&fhN07ggVGEh+$AzF1psl5+;gq7redb??mCuP zemBR?EWTTs3A;u`8DM%@0M2YdCDSC<5A#Ui10g^1JgPON1IhCCz7r??T-Y#lg+KIobQ`V@_CT!E3@Nd8xlLS7dW zB7g!CNqR^Y(cz7cT*pSB2>%Lj8RQ^OK%5^sa*49_ju0AyLIDt!5(0s=Bc3I0OUd^m#2w;q zpI;7<1*@1x+|VVG5S+X!0TC5^^oJ=-#Kl@HFxQ~o!^V6MGKc=T4>YB6mICKvI6BL5 zekWShWe+iUn$Hr@>=tX% zvoa4lPkL*b^cf3GpRWAObm`Ny*Y=R{?F)$yh$<`5iUj94!57QHqg_t$4*gv^nbI5v zzVfh$IRI(koQN}UhX}E2z&#C(Tt99&9$u-BI&^L$QE}UG4r)iH_c`i=^4gK+tE(@q zzPwhjHI5wwhrnFekwtAv-aKU5U}TaRn7- zPoF+bVUU<~Bc=^oq&R#gq>z!K6(=Y`ishp4q9B9|Je@X(0>jjauM;&3+lagkS}qu@ zO~6vZsZFb6T>Tq{@yhR+Z_s0BZ*dz0c7woen1;ht;Y8Kt@7wG{o#%IrY*##Y7_-k`tNb))mG<~I+&({gjUu&VtQ*DkQ_0@wZ#oJ?KW z6601Yc_|*qbVEv}U{g>zx*^6bd*k4E@Oz)Tad>?HEpDs8ZWXw#SU+uf<4@hn4?=G#&}`!j@N>cRm?a-s5AO)v5 z(T$I@a2gZ23@mOra9GrU$BvGh#Y)9l(8BWLf#|AYe4O37%hLvzgtf#o44O`>r0sEJ5#X{VXrLr(l1Qp#_@}*%EL^G2J0x6L23M|8LEI7m zB>2`HC(T!D%cL&XqR|Q;RA_3|BHhF?Q17jyl{E zj{521x)(PKjwM3=lE`MXvs#7c;1x~Us-w1Qun?7Um~u4!#qM9y#_X&Qo!Oc+Q~3o~ zPk;0D2zTi;XxV3Hfy$VE;$44z;Li`}WT35!HnU93EK4`D71JdZFCDmc;Fk=Yodv9a zwo6l?botpaE(Oj#hpZ)h>bA3XI)BmCvzN}k^0`+%FMH6Wpf%dk8q04RF@X+{b1{hR zuC`rji*!%qxIYB{o=bbaweNl=m0c$|Ywxdz-ikU`q7r>JoV5eagZo*k*C7}A?{|t| zgGNmJKu-ODoGkkmRo7ZZzg=G3+Caavv8i<}{WpzGZD#t%)QYxRP@8wQWgBlY4)EVB zV_Myon+;5Bi{<88rj53|XJ*dZzLvBuCLz)6~0NO3h0yNu=27`BZw88|>p0+6vZI$g09a zFgWt~6#kPhn?qzGmZZa5Kn)cdkyJyS($exvbC*GDRX5VL zBwgOA6eAr~xNuURCR}(-KU~4|=Js%261wgSHsaob1V=fStcxdL>!%P{bV{Eoe*gc;^=>d8h|=ePTtksmv&!0 zaOuD+t+AY?L)K}oB=Y2hqT1jpeXlz*{Xv zI|}Ky9kv~I=Is&|{O?$@c2v{vtjgNaOuuVqcH~*#&9*@JyM@e-3d_6I%#KFOyUomw zb!iaTUy)_eJZME(ut1MXkBY!_7Z9+}Q4MHe0fUQQ^5_}Js&+nlOrgw_sR#v7rGkW1 zc@LD5qm-hcAvpwYyhO=e(j(_RKQaOfB;0~ZdgSzYY@ja7sq9wumh=4IsEtbmXno7*cCY13)`~2N6S?^-Eekoa=coi{TH^ul{|PCPC3%Z zu&D{QB-}X~+PDHnj@a+Pd6nWqE1m&%e<83Fgj-(Y9Goo<=ZklS;KpSiln-izeR1|M zF?wk`ARfO-!)3;>LK43-T|EOqAj)8cESds_u>iQUn&O+#_c}U+0<%~nq}1|$0~|6W z9`O>V{ii#FfbDU|L0Te-D#T*SF)xZuB#cBGA146QAhcNqXO=P%_jHA?ljzC zDLB)jd${|2515mHkqDf%66u{lBa!7}!5AJhvUG~ei&QE)j`@>Z860deY93=i3^-mH zl~oiK5-na$x3KD6=@u5Rh~=%EaI8XVFW1VPP^+pjI?B}xY`wsNWl!a35Nvw{c8S0( zd4w|a7Dc?HrLnwaBlJ{R^$2~%hP~8zB?DKAa`_nvw7i}#DCAlw3Q@sAIgr7jVzyFdjMc7`H^WQPUw*85e>$5~;2Z zZ=&u9rQ$+WSgS5Y-~@2k<4j&^X})VVXy@T%l!b~+(a54f5o1FAO*nVuuR8V1l>0%K zQ_|00v*4LG|CG9rYmB4?o{!P*2{G4nF;nTnh3}`q+5Kt4_er?i0!NabBKxXDm;;D4 z%q|V|0@i~Ij!=sS&dXxEz|!>y+>8xJO=sZ247hjF)f+eUfyGsvImP3#R0v``!iHvz zvIw9|k{ZR$Vj?_*P1aG8kfNx^11A}Kj(`&`)fXAb-|^(y;QLSxNQ*!$RCcK>=ByYp z1DYJEiBwM5OGg(^+3Xj05APn?d7&M7{IZWg)ujb&Dg2669bAtxvg`a{-40JA^iB3K`W#}}b(}GTt2G0M* z!9r&iJ}P$F1x(r9l`dwB;qH=ZP`hb2*qCK^ONy99vuiS$ZQ{)WB$cRFz51-jT21jd{g*Fg=wAscJ&EXDiVzF%k*FsJ-XUU1?R${lELF~5SpiB5_^JRc* z_ho{c<+Fjy`Rw32{LbzwQc{A`U7PTe{P5@4C|?#FZzxYY$8*Y#Qb+C?r#m~&A=rJM z_x86BoHznU3nXl6VKT&tM6VshPtalFqqi22@u;Gn0c oe;&e$c$??Sot5qZ}A9Z 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 literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_posix.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_posix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01c26b50d9312e1a0528f46739388381c8234ec4 GIT binary patch literal 24636 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 literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_process.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_process.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5eb0a3241437464dfa4f6d47166c05d78b10f46 GIT binary patch literal 98267 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

Maq|)_thAR@!Nn^RTzF^;gI4sAr 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 Zy6rwkzxQ9TJ;n1ylWjla=$9#Q{}%uoys7{I literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_scripts.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_scripts.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a2022fe1a039aebe0dc8c676f3a86d3b66d40b9 GIT binary patch literal 14247 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 literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_sunos.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_sunos.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68994e4870dd26c045e5b03b81dc1a8c7ee10757 GIT binary patch literal 2147 zcmaJ?O>7%Q6rTO{+D@`=ow)g{;*=I=X&X}-A*GZ8Y74XyZo!4a3bb;)<7AWdt~;|1 zwPj0CRl=1xq{<;d2q_1ITBTAsaNx+Di(MlnEXk?8_Kt3+4^I#7Ym=&Z@8jK^7(C7d(aBByc;wecoj z6jZ^J1tN>8=*c3HLu!ZtP@Ibe?)Ugyd`}J}$n^j{^hjD`eGLmf0DAZ?&?8V-<9yv( z7pz7qtP-szvSllIEt|w8m^TVWKIuU0;K^-@$|#I0WIxSZ%MzjQIxahgd4rfQ<}f50 zu~ea8BC&Y38u5Th+cGQEhF}icR=yj|V@&r?XOIZX00QzPfIi1vpmI7(C|}_e-W?!E zx|Yp$4-^E<2ivI^f&A!wI=>L@GH`ZnI^%Tj4!|7A&5KKChluKXpmRTPYu#OSrpC;L zUPJ;AywqIyWt+|iyLCYa!=TRASR8et0a6zO`yECP-Dc`sov*Q%z!y^h)P#``#K#i7m1N$w3dT$sK^@V|Z_~0c01>xo`js9; zJJMxs?1o4@1KE}1BqDltq zUo|~z7hyVQ!rSQ?!%3rAXlf?RAQb=vD*k7~GV*?6*4abr~J1?^s@`s#rlHu)ls<{3XDuxszdGx_6-?YINp${5n=z33EHvFI27Tn zgT_tJSkCTvA*1G?al!^31HP|WxJ>oXZjX>uBf~1#sE34A1h2jkZ8$faFO{{tT_!cm znb1P?3!$)RgfKKp2&N5^OrmkZ+&(YcyF04K|EDH)Q75REGlcjRap-uv??i*!ln2&j z`owK{{KxEr_~W66LqAWgjkQlqKar=K(sWCnMz4@v8AWgv@^R9^JjgnFDqn)HidUF5Gr1SoPFQC_nj!q=} z5g|Jq!!XbL0CV&y2tNhATVQYtq_)7=78uzP0V{no^7Y6%6K^r`6=Ctps@Y`XZRYGh VY(H~uCDQ?fbvVFIJ*NsU+<(Yn?{NSC literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_system.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_system.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00cb641f2c3774bf1f2dfe95d63442fc51f2be02 GIT binary patch literal 54145 zcmeIbd3apMbuZZ4Z8U&xbOYT$V`pOrh@HJ;}J62kj>uJr1MTMoLc#(kz21C!9S_mc}9O>T$8S74g)bR2EM_Jgp~<#chbY zd)zFZh`6W6!{SMZr}w0@crxM{JsB)+N8H=vWpM}MnLU{-o-&wqBD*J>#hrsWCvtmo zS=@zqUQZs2ry`!;lh5L5h_CBe$Kr0peLX%F_aI)-Q^4Zsh!^%0vUmpKMLk6|Tc2#-y`c$FTkHPzuCCsllrgEjtM@?X{sUdzyQtW%t^>Odw)M8Q zAL!cK%_`cpXK!2Up56mpk9Y1pcmR2lcen2EZEHPnp!3P6@zYK}+xH#p#S1(^RnYHU zod@>pZS81%iYj$e3f`c%Yj1D+-hwL2Hz`-XwsTy0?{@x8uZC%tgI~%ax`i6ab4$?ErzNhz5A5#u?ceU^B=qVZFQhy>hTrkm_oRf7j4lnwtnqma202%U;bmjL>4>4< z^jwO;Fku`w9mv3wozrrZKiIIJm)4H)M#_o#5sA)JDwH^tC zMh5(VX9o6B`-li@y#oNYsF@z61V{Q#5A_nb)5B#)L!(ocZ3fF z14I3Rv4Xf+Q?nw=c62Rx9l;BR8_v|1N6(JF^tG#pmugzWHLZ&^?UDSBa83JM>daR! zoqX-2P}3epg0n-gbqLOmWtV5kRTOp=MO-D5+_KX{#joyMs@f5*+Ob&G8p&-7SGCT5 zb>@jn2VXlVRJDeY;A|6YZGyAyhCS`&?z7!9&6hS^+!V2wOd6MMi7#8vTF=?kh*;&j z)}^}ka9#UiU1!9%GhEj>S3jHjR{ERiLS1JV3C^8@jly5uaTwNh8h`cIxoy9?V>1-& zG6o4Ef6v{KV12*I-C?)>xZ2&3YK_4iH1`?q_zgrZ7*yO5l9p40;TV0txJ!n6Wque# z338TkkXzp}Ts3`9!anMgP@Z0lh1-@XIGrrCJ9(NaU1Mt83cwqE%cb9KLCHy7?ny&Jj@%!`j@=>Zu z?B#6zTg|`I%x^(T1sAmp4g^Dd1%B}B5k#%QVLlY_N6o=NsKUh4>J&8%27~nJeA0-e zu~`!{E@OrFAG=+sB1rQKQ_GS1YUxs1NjR+}l2#URluw#&IMb&NT^wF?*1TJK!|t3i zy|nA%x_f3*W6~Xi$(3}=U`kB7O)=KQCTXEl zLG>8pIMAl%@|!f%LuWAwDNDsMm!_B$jh{SM&GJ-fHlSD1l4CGv+R&MTe#;T#RZi}= zeCOHZNd)&p|Kxg6t3nFsP3L1Yui`Gbmqx?67cGWSLy@5#(0LjQ%G2m2QrDj`KW#W| zykhPCgeo7)5V3QOv@X>21$+Gi{h=$M2!jLuKyTpi;Xr>Vn%F;bvNtqvA`rwAt}hhg zgB3|U0g47LIlgb`NPwr$9OXv(Lj6ahj_%?8C;N{|wew|Epq!p3_xFJ`>+Pq74LB($K$9x2w)%pw%N};wdC-H9lnU8aM4zDBQ;~{ ztG|^w*?J>2{X+J1_Oh$|MsDF!Ze2LH?%lK>WWAI1{hUbdwtwKvX({(?hFsre>yocA z>}$NX4x27jLllGW5uH*QrnP}hFooEf>L|ZWdH;Czc2P&zs$4w#rF{H8y#P2~YYH#f) z=Ak1nG!XEsvxhIBhgQCCAQ%Yp-Be~Tf|vy$J7(_mv3h#_tfH~?YtEvWykDYvFCln{ z`BWRut$o+@gT!|dzn>h*-73$gjNA*)O+R;e!&1?PaM6bA=^Jl&GGEVF%Bl)yRn0#( zzilzAb`FowA${X#ESogViqXgS%ZuCBb063X+Vc}WsJ0>g z<9x2YEcwSp2^3$?wKv92KO%qFs^r0E*RD!>OqkyY+-;J<5^GK+n_LP@1~yBFDLbZ+ zPiRZJWZ;qOqMC__;*IAji4n7C)|3C~JxP{(eN1PPhQMHom;5elx+-I$u8X?wVb+eQ zT*J!g8X}UIUB}Yon0ohHG_B~M89(Q@%4_P?1Uc4hm@sL|DJ!j}UZdZ3#1!+zv3@aj zTIW0aZCNP#4Uv)7g#^Ema`?I9&$2IFCVpwKuo-Hba!e$rbtr;=tC66tu}g-{3G;)D z&baw%qCBFQ5uj1m9-ndZNm1Wzc`lR_o&1W!=|zY{lpHv-Vs#fG{_J@Yd48Bz7Y6=GdSnHcGYCGwI(+CDh_ng|V+Hs| zdS*NkwHzMoJEHLMA|sW=Rs}}Cjb-bu0_@o#R1y3!ND^TERPR!1aX7Vju5)hVQfecp zC%1RTc*%OvdMWv0@@tNWyL7I3t}NoN7wq+dyZ)Bhkk)k1YDmlc9OXWCxL~pVg zzU}~r;d(Vgtz$8@anas{ zf~!LCR@~wY8J$K{x+Wk^Z8f9vk7S6U`Jluj&rb7zhV8SCc{ zd}=mjq)bu8nORC-=KSIN^d=FdZY60ulTL4HK=5fgh9vAL{p?eVob=hJE+rZyvigJK zf?Xx%AGhq-)nNXkmQ;j)ysg!~YrFX;NvRZ;aJy;~e$v40+LG{-?cDB!gcvFni=((q zet?K}nOvLMkx_|?*fn&P4hj{Ko+b~q8+Wa>oW?LrI`xZe)~G0KsNmvguPE0$a-wg9 zkyDUF5Ld0?>!TtsqTyOr;YBn#zD`vXx_C}L>dru7ePYzPt-rM`tlYN8Y z6TTS@@@*9Cqu>Aq|A7KOf-44A7efYoF{M(l4gZ5gQ+~^E$CSYBGTuqEa7lOUHm>H5 zClNtLHkW(1#lrc-(p07&|CP%k#qPd~h$<-$nDqq4dP3%a^^B1Rml}W;PZcbYm`Mhr z9@`hL&7!F-kxA>5n6y55g|r^Jo#0q?tn9SB!gq3r4-5|U9U2VyhKGElhVWJTB=rpV zVk;l$3l0y(OdSG4Mq!=@P4sQY1;eD{Fp1i16UHNk7afp?0n5f;@;gK1zV>tTeV$v! zP2=Wq%V9HA0-U50z;~YD)}FZIoE{}BPh%(!iY$f+D{4__TRnM8NTCrap|V7Gf1)aoJ?jwjjVFRS!{|&T zg%oCXJjtIVzjx4h&Lwdk3MJ@IR%*1XGiM@s+;S{OZdJX@BSHGz@#Jy5tz5VKt|+I2 zl*erv*u=<9aQe`vsH-?xT@}ieTjy)WZO7Khztp?jr(-@fO^$^M<=8PoXK`1ufsJ7S zjmg8;?YC&Zfc|@n9G~yY6BZRvec zvu>*H*FS1jM~Wa}Ef@uNEWxOvBI8c4kZ?|!&bjqzdT;^15T8g2Hk7VNiB(t7^rR_y zcPoP5G+r>AHR>T~pj`=Juo000d031PyfTGGg(+(78yN`<`56-d9)Qv27U;VqY5fb5 zeuF6aLr|EF)oX}yT7^XI7pdu>Asg=3C^zIHFQN4y_4yxPFf7|$FF$tnF_7onMee#i zpDov){0lB9I}%8KJA5+q39UF4M*gd~9r>E)zwG*}v2X2w3^s`0Vj?2AKHBjK4M#Ky zO7fHZQ2($$@JT)r`n9i4oIV?{7ZA~shI#`u4!av*K~oGCG=bApttoDQ~g-&1Aig0z#C}&F0qd<7(UMSEoE1Rvn!Wf=`*3Z zqfnjXq?~f|e%XH(Db5bT9hJC?zArB7`U zq_6t#`YZ5bH16yRoT3;(4F5F>7)?y%BI%Yz;1>tB;b8~1;eVz2{v!oj5L9^hDvDQ7 zP)$K41)z2ejFIM_q1ZTrXySp<5s{;gT4+x26O@{8v@b}^FHZ|YG!ZKF69YrgJMhW$ z+dMKn!k?y?H8^l&sBbWuu)k{;j9wn+A3> zSK6dyIW=8mts|+Wlh!qV)o;9(@m@x_Zg<4hg>bI^)r<@I)A_T;x41XB>#3ze-R?;a zijf;JynEV`Juhs}n+eQ4dEH(ovF##rzv;T81gdl;L!XqP^kbl}uCrY)JvM2)k&(NU zQ5Mc9o8#s~?;Tyt*n2j4(lQzN*y$D({xeUUeSX%x;4Btw#e%c==Wg$$bMnj$XWCTr zOJg%ny!gCe%b$E+O8zi419A&*{>)crtFC)%1$(XFt|dW*7xOMDd!{gwn0MP`O9qaCUbjqgw37GT67Vvtw5dMDiN09GElC^j!+P z7MME_&T9xe8w6W};B2^=?82PA&@$cf%H}0^dDvZk^{KbNa^)*?dl%f8##;pUmXBhe z&$TYNs|9?rx?J2i&<#vsvG?;A=VEwOxH%zSjc`Gd`wk8U*oNs9Y1i}h#g zxksB4ezuW&%oryNYf{V{=jR@jy`Q*zzr`_-KU0@5JS8`nzP9F`bf zojx#8Ja^|G2;?-MNe?u1<=Zl7NTM!kja|oVa!kGJ>yzXfm^mITO>vhD@-S{Q9dpDz z#83747wNymeX9M}83ENkvLSNDm}*?l%!Fsr$rCPh=^B)C)*2V)S}z z_rZ8BOO$$$&M2htvc(3z4#MO$Wlbt{(@cA?kvb)rcs^}7H*;FnFj=~f{Psn` zX0yRC@B++g0KklpCD`PD6G1ekH5d%=p@HEcQ5W=WO5}orfxrl}8i<-t^qrBxm=&u; zj#w~Otu4jr(AyAWZK+~!6_9%yZcsI|2q47<)$_HpUwdg{(()nbt#%Mfj=CG(yd`gW z*jqm5Ui4NAuIjM28p;z-CNw3k^jG)KcwTvWHa(nPdeuDF@x8?P&T#prh;y@4g{Jg% zlxDOK2ntW;Ki+c~JcV}*7DpXuG#Kn8O()GXzUo}|W?wFStMZM?H_=e7;Hq8l*4|1) zuFsg^#=h+VsEGq{rIn3 zGPK|QH6*EWc+p}#V9}G(>a7uh1W4nt+r?xE*z`2+^31mzw9;C#f|s9Q*9}?2Cy!by z5~6nEQ63)%_7i}`;2Y{MA=o>c`jq0qGF@YpggOvSv2fmM_k=M*- zowZi(_i=DaVu?}16;rpkm~f)#Ao0-faEOsT(WHUkz)&#M2YclxcX)8PFT~S)gn*|G zx2Wk*D8?2ig>h|4@v$83fQS+v<>B98@8ETX3#})+p>;#~|c0SFFmSy^BANs$;$ZVLA2<-o?A$ zPl6+l6Jc9geW{^XoBEaXbA=RwIQDDDlvDH$ zau2Y6DCN|Z#BY$-n8S+Mv-D0TJqyrpl*P2-QfrrE58aARPuckB)XmbtLd0KwlM3Y_ z#rj3h+)mGo4Wq16r#voL12g6Jg4PgLc=6&>P%c;grQW~626jl_D%oh5*#@kWSRQ{NH&)>i=PXC5!DvG8#60--kb}g=SKyXZ))_0w_vvTt%^4)uI^v-)(ftB!CNn;RliaFZqd7;YaQPo6YAR* zz3qalUGTP(bYj!=Cc#~PBd286|JJ}81KMmk?SiLW$Y}=wd%q!_@q-PzwcvYfN-Qp?2_A*WLCR3iM?lYOCk zx_h?it(G@h7Clv;<`~i{!l~t0^V75cGPN90(PZ|W4p*mx`(p>!>0QNU@BhK-alvo| zGUeYfO4MIjEcK^yI3eT}FHoSsb9Aj-IiO)Ed=&Ue#&XOOPgyPWR!_{nas%)me?x6S z&rVLlZb~oobf1!@&ZxH~$*;)0p`o;dlsWsL^IU$TQwF|7(^F-3iz7mzjMa5}p%Fby z8HXXjxayVOQ%Iy6Ja(kueYY{3&0gJYTFC(_(ehV3_Aq-y%ntf@yw)*#lS$)mGkuQ_ki)V3IxEb^yBZ?Q|QS;b4x#e3qh z5H?o1VF1K*5#r*Lfbu-yCZ5<j@zhNywuXZPXGHx4H#jhK@(fRva`WCi~CSVgHQ=1UK8nu9q z9TW}Ke;>cq{*rObiQ!WLEW4w@s768$wpgtSZslerp93Ycq!##%2n&zDOXZ{Wc}YAKXeG*olD+5Veg)0 zPv%{-DKqV(^o%>4DbtJHS*a8t27Wq&HF9v#RU|j~^rEXwFaG80uF{WO-q#y1ZN0d4 zDW^J|Qys~ng?`zcd)fS!{SEt)uOaMfi1?bq?xv6RUgbKyO4jT9FFkwl*`>VNa9(XB zuRfd#>O|+gk@;hozipS*pi&-tk>D1>CUUb(gek5=H>!)Tz*S%GOy#b%f-XOTE z7&XB-ARYGqcL^{_2Edbmj{gn?sT2?d^Nf6Bs>MI1pGPTBS^rD)lY+m+|KOtt$Yc(N zakuPo+b-E3M@k#b&7Te*9N1 z04BTc(qbLY(~G;%N6py7o5G+RlZ+o-!DlCR-u^Uo{TjCafP` zsIe6LEV&ifc9~u?`MGiPYX-mdI~I9G!I+yd0yMI5h|ig`!+&IMg>dX7z3L)%nV!#)>hQZ@Z5r`pVgD+nA}! z7d38!G>$D4DW?Yb5R6uPPcVq(|BUKO>^lWF3}j!)|BQZHz!mvn$Ib@=(S+W^1A#$* zkpBZbkvO;kG;JZz4U*|QWT1f)Y)y!A%*Z~@gu@-8-R7|^i@RbUSLTfY}be{n*v70mQUYz31OlsIvAV&=7_B_tVRWADV08R_RLHH83+4j_D5{K$#FR` zwjGc6!eg((dgO*DXQqAD6Y-Qzc0zAFHHwCb@hiD1Nn`~t>652Uzw|up&|zRQww#@R zsp4YAY-slA+~N6y^SiFq3&qiVq`X(gSZy z7A;{;6hqtv^r1hkiC~JmR?VwlZwH_+V->_BiemB}2uU%wO{iH1O=%FL`2i85 z4gy>CO?AXg`gz&@{t7Fbt-OGNEIe5BnM4svk zRD|YxTn+rsDJ@FHqz%E>5swk1ibnXF%0xw6{Or|a=%%TA6ZJ?_*B4IlMN$gIsS91k zEknXO>|Rfx-RlX76d;yna4{>BnbsG-F4ziZf)QKciPYiZH62SdqBkgr zqho$TX%uXx@gayKIwqCNxLfMs3PnnU9>-83vTXP6du$Rvj6WQI0<-~%fh4`XZpRdh zV>BpT0f)37AjNM}cbGvR;e^dHTBpmOtm+$NI;6+thplo?svkD7J!$?Fgl@kRp~vq+ znC?$SnBh-D=zWOZWTH17rVq(F)+DyB4dvGTZbD3ckJ{shXd?&OlytSEKSSs3av!3; zytw+jYJENVqnQ<1(PW|++ec2WAQuVqF>L)T`_LMJt|U6sdwCMUy&x-FWKhwngZ1#K zW<#y~S+%$w1Qp?NR%=MX-Fifyo*K)qkV^b^jo4pFr7fTkM=Ru<`d0D>oiN9Xqy60T z=2J#KT_y4KL~z(}Dgo&?jiY*Q7>Xs)bwc+6+BiaV)W}K3l8-rR$$GFBziB__gINBfLRw8C&&VI-}1G6Br8;4B0NM#vcF*AFk{*M#$H z=KYKLTO!$87o6B5wN-F##ZwVYVmsBx$qwSlU*3Lp`%LX(@_KlCm>OP8EuAY|URQAG z^u^P!jZWENkeF8Xku!I8PuN)pW5Wx_r;iJTyCNC81=nuyX|DQP2?l%RU&6dGwN!HA zP?OqLX#Sv(YpaYkRa1zYzr#B`IB~P~v|{_9Bp@O%eIZ@ddg|HNs}kaG8r7-7uL~v1 zV68`3$bb$ow`9}aWa7TKag(@@c@&(7->ObQ;&;?vAO;e;cVesD|BV*LveeJoKMY?^ z*e*2?^mW6Wn%Q4hm_DKTFG`)rDjSw)L>lmaO+f(#zo39bOZ#- z9S#g1WDN_o7v)$hv9cde91OB7;y%4qAduzY3I2kpG#>tLxH6VpzOc(Tm%iw#g}bO^ z*W@;U=!MGZN+Ew!Bn=Xw%{PHOgZ?^aOKQ&{zV(;y8p zXIjrAciz*!2GygIwkCAfr}DV+tuZ+W<$__?bJy8Jx7IX9)JA3V2Te;3HP zw-nS1c;YeGj9l&3-Pf(cw1m!ptYJ<<+abhV++&40yTR2`47_&o{|&8RzbSoH=GQ|5 z`B>g$j zO=&3~rKU04o3gOCj5a;X8bjI*edhd`Dd+T=PZN>v9vNzi-g27WYg|!uo?G;85WL$3 z7i3)9;|qf&OiO`B98bZgCdAm!&pu63qCpaNe5a%>oqNBqtgV3iAf0Q=PyC>OYg-?y z^rN+(O$t(x(xkZGbdbRt;u>WdKv*LIE-BM7t$dhPX4Rn-gf$9{5htvHyGIrA0S%hR zfT$**E`<4-f?!ocHkhU8v`U-!L*VykqCd8esVlkV7LeCI~3&q3+0s@1N<3#$duU8g#HoHpOXcNWa}#$I*OJm1Zymt z=p$eSn6YOs5rmyV=@$$ixl$)N1zAR1h4^Jlp6ooA^r1I*au?+2Ge<6;TvTk^fdTs^ z@h&8~Q(p?(bI*2Sz0J&n+MC&orC)Z>9(*%9oK+b~ubS(NIP2zH7o5$4tyyq3v$}s> z$H^GI3xkpt^i8`EtBPPfd4dcp^0xtO5fWzCTBuWnmNDc7q&-k6ETQ=F{1z3RKVAF55`xh)V0D)1`3B5tZN<2Q{JrtYx}`ocbFZ2AU<0)AiUXuubQz6G5B zKO(n?z6kP|)f4Zg*dhX%Xf%cD$%bNhq_-~=HH8}DNX0y5nPz~*K&=;)EN!0-1ji~g zY>7%S>Set`AV?@2TdN$*SwulNwIGsO6mb;8UIGZ<%5^&t{b0a183%m2Yovzp0U`oNtuqTn+M2z#>KVKNH+CrThZ%KtNhIJg#lE`pcqRS_uD zJ;or=nCIUo2quX(AebDM5ID{qSW0hzB-fKZ*-2g&xpR+xnB$vDAdzm_)n>)EXW5%S zTLc}Wt7Piyw4u-oLUo!08qF;L8ueto?w&b#dH-Dcn?0m(%c`3%h`5`s85i7}1Urm= zHnF;Y{n1;ghK$C0B=UN7!;+h5(z&KZcinty*xeX$HvP!Vwrb@9N6&UH+SfzWyXpS! z9%=Psli|!mP6n-U=UTX0HQo_%FAUF>6?d(2A_K#x0uR!Jjk{DPYQI);p-1FuCNO9S z1*Z~iAg}D0w9p)~?e7*iOTb8ommVWFu1F>BlTR_hnPB^h#w|2aX7 zP6-YCzo2g3h?k2d`3K-Fk|!^|E4L7dyjcwJ!~C52b6kCT^fp|l2{MZ&U=oL&iy2E! zGH#s>Ejp_vEpQ+wTD@Mb347`$6PHucZYcW$2Ns-V%sg&-bT%vEtrT39Y@>t%;ZU4p z=gs(MJHA^xZJjbrwcW^Cx0F>A&H@t`$!d6a=TcKwxT))>g^{Mmr`o1W%X#Z&3om|Y zDrwoB2W^YHZlO()N=m_^D$bk%As>$xss5~ba8 z=9O*(&OI%$h4P%KFvGH&G>))o0a?Oyoir~*X@;oRV5w(g^bX3YjuLRpgn`HT5}?_KPM(N5K)9a>NFKq0ddF zL&v=Pa{HY5KkvTg2p8>)WbT@DET?B(c=pw2u|P?dsGakcYe&c$b?3((@9PP(mf1ig zv+C;U`HA-ek=i}s%smm$T4; zGX5fUCsIBOrlGs<8`9e9Z0~O^S6!!CkSt$7d*TvUqSYj+-j{Zdf23@Vb=opuEf znv?|9@CmF;jkQSqG2~*3H&%}A7E)M6>=xQJVNoZH->gSsk6TpsP|x~PW%%xsPnmr3 z^pWYFZ{l=<3Cq_lr;VDuMLd1@BjT7n{m58a($^-E262GIHr$5-r*Q)(sggdbNNxsY zqE_;52O8cwGQyLxJz+#lfG~rB^Uo-gjq2-#R3w^!8d<~)lP z&b&JY4i%*NmR)5_u9~o`W*MNGaVh^|{+un6RsZgWA8dbT`+H@P#z#Tlq^8|9TilRV zSR5(;bjysAp9M3}k8d`&7_KF@+05@7TTN|d^9N=QKVq=CqB+5tCovqT9yX7`(i(|@ zN`(NARs)aTXM35Se6gcFx?qN4IHvHSVj6|SGlDXaFfLdMGo%-_Ak`HrNKdP>iVG*H zt<}VTYHwD$Q(EbqQFgU>10dFNXz*~SPiP)c_`%F+L5ial@4f%uw zL!!rAoSY*Gk@r*PXAsD|sD+?2YU~BV4V;c6*d?ECYHee=T8LB-uhf=_)|X+_4LLDw z<17d}3+9>^o%KMZa4jADt!!+LCyZJgPAi`CFQzq4w!A}<+#SLrLVnnuKWDmbuY`UgrEE#uOwaVQ?^k-;lFc6^b8Vhj?E`&m z`mA?#P53L`q!24qg#yN3bg3TroygPIHn;8n**p!rw$J{mZd zsD}c2#B_6iP0t^rKnIeqNXWL3vKLW6E)kf~MLdp;C9-8SyE>6ApPcntHQZek=&XEXN_R15n#8f1NGOM7SkS?Z2vGPe=31(eJ%~VN@q{c zTju+(yU1)dcO7gZFar{41|+RA1Lm8q+gn!5fe$h=+cug%*vPdTR+$3?yIR5D=SI8x z9t3(-L`O%>Xkb?l0qeo8E~-}N6JQucYzHgM-El@t~~glSRp&49x5hix`e^MQa%Z&M%`037pHq&a(OR zMd!wNJiR`gwtl{2F>TX*;8n)qmm6n;Z??=Gn1?6YjUpaTCMdSAfa2n?y?E}>b$k6v z7*6rF)tEo1;o3IF!Z6X343D>AH0}?>THN$+ipRhFATuGRH{x7qVR+mk%?e@#6(I@U zRJs@|s3A)jM%T@$QpKvMBz9Ju+bpdHJ@^5EIvxW5iqQO?XEALj;Mto! zW&Uto;nXhd16cB`4|~?nZJh78R&?F79Z1+$^bxtGAl!U8IM?{*nR(BA@VaXYDPeLT z?e=)jcF;){xhv(|3g7kK1Hq|^Be9zlCB#clM5iT(A&!ubFxv(=B71QioT_34*G18@7iWs@AJ3nxK z-;ur$4{v8tC zaKoLEMASTU3@&7@zn1;yjt-_?uZ5F}`OuPgtL|(pzV6up@rSPnLFP7ED>{vWql4TO zrEi2t1l4|uYFXlE!6LlJ+jZ?u%lqpR+mlk>FHc1LgQN}Z9`lcrIK+SK;o9?JR~3?O zuvO(ph}>^gk!gE2-!L!YFRiK?Gg2o9;JF;|tWkvmM{D;;#3<5L1uAyl=F$EM_;!U- z9s41eDK%2dW&bQVSPYw7?ERrLy}{E1I9n(Xgc}Cp5&4rNp(bj#ta*+Q-s0q8rZOnL$vZfTEOiHlOx@taM=0lI1elGjxJ6Ca3i|t$s52F9;!&@I z!LiB}R2Qvh*hTbJ^=Lpjb2rUOQX#%z_%I_AN*-_4g|X=|p=A4lV$|eLzp!C?!_5Bk z+m>_kE^WTJc`ohkj4K%nIkkePR>-Mc&dtB{=n&i;-C3Dm+#z8kYrqffFO4Q6C-r zN3P~~Vh0q?2ln+1`F(q6Bd@QokMBRap-I+bM{V*eiraxno1(F*q_3<}ys{l7F&b=K zW9`ix3QnUT8_QaCG`nc*PO-7OlVnRQz2_l5sP}B|Y{#ObV$yWe?*7fwS73+13?`Rd zt&pvfSk>f6xeYs?v`NQQ!%L1y{^vH=tECrer)yzmnN};L)-BrV|7)^~9N_Y=Y?N(& zBl&d;?mEF<7k1ZSjkc$*Buy2Jbq<|+@WoD50jNT-*lCIBk`uSAnbgHAPQ_rtgoTO$ zQz1uLyx^RU28w|_NF%oPB*DNHu7mu2r;do<=1t1Mz+Lo47lrXPdyEqx5sCduq9<6j z3el-h3~8i3^wXCj$_;A|2`G!gX+@E=64=B*xU=!4acQrb58Ko}^yILe&olnZor~`E zf_=T=YDG+Upd2 zhXOJc5qGZ_Qp`hHm=Fvnl*7`90-P%jSA7&TP(WK&#C_=RqeL{}1Wpo!nGz4dp@Hw9 zhZz*>qS#T2K?yH;?)nRgQ4qkxAc^Q`+e;GHWxSJ_$Q9qouyejUxk;S+&Q=@OaL41o zDHt6_2e$X-uiW07KXqCtYX>MQdwLDljHyErF6*|*&OwJ@ux5)}d~Nti)u!CZOyH{S zq$P5j?qnpQ0R-MV8A;qDXg`H3xK$X}iemu|{?ZDk$`zydxr8vLNK3ccEOv8i{fN!% z5i-l?*9n|A!nNKurE~Svkv5~$6a2na6I-Ag1nc|`R-pa1iQ~u#uC>F+qT<-mi1p*Y za?wPi>+URuP~DUKkSBDQ>AVl@QXzf_07}>AKJg)<}WQF==dojY4As4qRX9AdDzC#zOod!uw%W6_>81 zPE}~cRW^dhVqaYfkueDI`zJx{C={MLcp@{QlQ%`pBgah#1uE>3)km3HcfISP?zq@*%%5 zwys0Kp=&3C-B&u}qSDuz!%B>LY8{l<89}X8$hi49t$pGSvP}8g1OD#?zbaNdSaMD1clVwd2Ue;DooB+rY?2@EYp z8ao4#Fj8Ma)alB|uZ9t~>eC^Dj<4a>B$sQVC$r0iFhE+&_MoKvurQx(psy87hX zPhWZZd(T92HcjqX&Mcekg6QDFmgz0CRZE3U;lic`cavam65LJfv11c9Fc8;FDWWZSqOO8~LW8sq!y5;ty(h&g#)^p>TMd}yTrUw&SE4p%G%4B` zEap^Scho2!;q{?Kdo_K8+9gL#*irLYFcJBGJA0e?54cuqtPGJBe?|b*AacI}SGN+7 z87ejt+@;s5!IpkvdU%FK;@>ts{%sG9B1POfy6U^KJk#{1v@k+*qIh~QzHSrUpT@{# z6y|UYAn|#`Sr9~tR%0fxgO|Y$nn@6)>S!TeLe1jEJE0ZOJqHOM_>AX#D)ShuQ^8Gd z$vLJNM#^=QF^}%@P6LfA=!OFdv%L6_ml(a==&iE!K191|VjJ2}o+OOGifz}U%_qk3 z_LpM&2R*jb^Hp@x8etyogq0Ta2QGuJHFTe^WjvRLQPO!gNTb)mvM^bG4?X_{`X%L2 z`PUz3jNm~02oi@g!2uq*kis>{J(#dQ`1nFEmLQn{mt)Kl#uM~Lh0oScKSonL?vmjU z+O+l1rrA)Zb=)SO;l?Z}6|xMLlt(};fshq1zCv@xTM-W1(BZ>EO4>1>Tu!|cDal)( zhL(&hBjGdFdf!?K)zWfI(?+qJrrwX5;1Iz0O@pSDO=5ehC8nf_OTdX^39;g0LK#e4 z{4pY0{8qZKmldZ2Q8YoUVD)@V;bBx<9pwI`p9X64^R=BU*l zI5p58U>j|sNhip2z{oIJnpzJBMTfk@Q0`!lc`K&7c7e2u0^Fg{$&tap*v3^>MHotl z2FPc=_T03if&SyYV!_{}<>N_o1j`3DDIU5saB%=mljMzxV%mv|C*V<6KHhlsd`B)G zxg1<%r{aX&l}vK76NZdt=%BD$-4Fl42Vxc^T2k4UCvg|PyfS!t-(Amwe_YuvHF3Yv z3caH=f)jOQ1QFbEpSFoKh`ZF%U$AZCxgGkZAu{6FubgaChCc9rgCLrKt*y+7MZ!Rk z0ZlZ)Kfp8J&L7}84!5IhT6l3ChAROanM&c?g(^FZ>?C%>gt)aFc#vM5&xRu2swvAoGsOOCp6eNv zbP&|$>CLaebC5{7RHQR{De7dbiVDY~g9L&j(YK~DCOqK}{y)%zJ_-&YfIB9rHlqo9 z_lra)EpSY78s(ri9U6|BJG=Hqxz2q!#w=>>Y;AAv+)rl=9|lL(+sm_|OcX7>Nnk4K zU@X)lK8Jn}QX3o_RCa_=)XL&PMoTi5i~@0Iv1pnY_cr436FMZG`ogc$Q2#L+BJZ4* z#HxEKP%8Rd*+O1(Vl-3{y;X0F0I2*(6};h zx-;eP@b&4h;~((|u9 ze+xcGTa7n785ed<@0#hjwD;m(_{GMCJZP0RUfg(T`^D{Z#c$VMsl85WByvFNc?7zo zTRDdG<~xO!MBi<@A+_LgX2i9Al9S9DD#PiO^Oo!B&38FV8q6A+arC*bT*0Xea+S%v-@m{?lbKM<-H8uaHx8(Or=G%YJ z^-k9xrT--B{j7ziM{j2O=ooW&AfFz+n*Mgqm7I42KNx&xP$+K?!@GE9-Y;R*&|&g9;!Jimt0I>r>Ev}>DyNFqv|F^~r}AADk}+2XPhIYuCVne4O5OzT=N^L7_hVSI@+72c^m>FR(&MU|BaehdBPr1ClvJheRXS*3hBGDJ0>YC6tuMVSPtj1IG) zmO(z7luw5RipiHKw_(SjNF>L> z#LBcCI6@nkW+UvJy-w1a%T3Wq}P+CEOfpm4;lz3LA6Gx{d(-9rBf)X zVcnJ`dOCu;D7KT5(E3>B(6Eo;cLjOFWGfUnvEu-Q)Wk|gUG%tDGw`CK{1H66A~`jo zPZl-9Y=Ym4)Hw4jjbKSr$*O`SO`iXQzU4S-1li$oCN|tFG+@h5N=Z1SWG=9n(!?A{ zPGXtdEciA@Y@2b^sLeCAf67BWN%O~0u{XYS=Jm$cgR_mVoq28B0!eP^ zjJ6Nm8I$clN3eIs{LOPD)Ao$bnsqIK*##*u8$fc@dr%+^vGW6``68=L;>@ z+7{e61$w*ShC5!*g_iRzlY6IIK21gypBfO{rr@)?w7$)>np%s^?-z5e)d}4d8Sw@g z1<0bYwjiv}ld6jU5)BJUPI>Y(#y>{^IcpGI@yD?h%t(XDZ`o-6I^~<9;3!p~+E?tM zGzw18F#I<>)H@k1Z|ciaxk71-JM>JhL~@6Y*sZ2m3x(Y?cA8k!ywN;X6?HVSDQmz^D;c%@_6K=#KL zg5&lkA*W$x!%|K|IHw_!(+5g?_f!IMFJGy+1*Ka>?Y* zUD_i4`K%?@u#!l42qJ%1Y`#m%%FjcrKO|{x(URulrXvRC zpj{RE7fD36s7LVlM>8&qOmme4dKzb_;9@1Ohz87h<6I%2~Z z5?3a(b;N`*`DD`ieq_5$TCYjA>$5E)fy=lg4S9>aH2QJZs(I!4tjWJ>UIj4J%&X?r z8OK%g>Wt;8d3DBf)x0`ms-G7OL85pTKpaEBlQx|nr{M4L(mZ)0tguCs7=hm#Br{+s z&IkHWMeQSf{l^2Lp!h`j#h#Ir3QonXxWTPBgvI>u2!1Efo&1dZQME?MCz9_QhCD!5Bg!-Z|r+h%ckJW5y0@)6=TO4djwVP)Tfy9@`o z&!t7^6!$X9y-G4UHD~_Ii)RZ-;OQ4qYCZ*=u{)A1o|&GF4B0G zVJKth_=NwTj>irOT|cRDpN{P{+sS_yUs{KZpG7hw57E4R1Hi;VBJ;fWEEEZLB!Algz zm@W;G!J%c-b030^vuZp_S&u0yXt4x^sv0`3A8KE%dWWE{+J9|1MQN>RPkHZ9ft-ph zpVB+TZ@7u+eW9FRy{o-=klMy|M_`<`9G#EJDxb6G@dMPU0!3H9jlM0{#zqeXC5I{XFedcVda}0CieuJ@GdFi zDKC(5i0(YljGo?C!{$7g2^ELn@J77GQ}pYR@dy+!Ti~idx1Ts#S+}R(V0r-kXRM6Q zJ4?>gb=wJ=rcCF44fW}aAba*Ip6ShWuS9G{ zJ3vE^$W=W=t!;7{^{%Zo)FG$y8Ef}c>ylURtPvPJ&FZ_t$CKZeydGLaRxX;*y0dpr zSNCJ0zpWxs3U6nm)#2VgzaM95%fFab7FOQF#J;hDUEQ4rqBhh(kq!KR26~KgQ2Lw* zY6$rWychon0+{&pofsG#WxG6K(SgbXK^!}Z#!mFX!eE~ABp%|i^R1s;cv%AEF)^WQ zDAY+2%K&yQ!#5+$I-x3T^Y zU>~{}iP&b%hQ~s;s4>VtO?8?24o7V?M9i3p!^tk8;b>BCe;<7GletRN6zq$d21bHJ zED>v?xkS4O@jO%Q7ZBa%b&HZS4Tmdzpb{cu%MeJ;L<+;s!ickY((-esTd-wK9f;Vn zCPO%fd&yH4_LN0D6_bfDsz}eFbGa|?d~5F;d*6NP2VZ&TD}uXY(ncr1t-rYbQq|c< zC)=me<5?Y;jlUY4X?|twl^t-FQ1a$6af_zluDg+$M=BvYlDzVb%B#UeZ=>L96ugaM zTGbm>@0u4OSap$jRZVlh>%Z3Y{bNF1>!PXCe!2gycG_)^z zI|NsU;O!s}uV0(~+U(J=w?=T)2wt3zDz_PWd;H4yqIav{+A7d#9Jw?5r?)S;OC#=5 zvRlZdf|n1!HT1^N0=ke_bm_T^&&@Txz2(Z5#k`Hz>X&o#F73RybGC!@VDkk+QPW~> zGfbxQTZFugA7$lT%DI?>lUJQroC{eSZ>%d>T2~)lhf=;vdoJ!l;TySyWXyrvQf^H+ zw?<4_%B>FPR^N3b__A-M7;E`k<8s6i3P8I1@TR@NN`b8^hj>ki@&z z|5ryE7_SR3(z*Q9Qb}{TqrK;k=EX zeVR$*7tY)9S&&w_KiQnWFF)Z=Ytr^rCJ6c5zVc+DlH1pu9K)i~(#Ke|?Z9kWgFQYI zB$NqH*`;UR;5UhWS`J%};OL4W?3SmyhHxT86nC+6R1{aY>_iu8on3m{rex+yB-2FW z1~6i}8<`}sUak(?aAC!)C;LlaixSKMm>s!Z4$Ud)D}B+ds@hBJj+z4}>c?z#8#Xuk zH#cm8-=&{oSlPlusnw1C>V{3--Q8pBbX0&^e8}VmYy;C+s$ARVx;pWXlEkX2>*~Zm zI6O_pi?g+jEm9hd;qOuxNLk4LK4MTq6FJPEqf}BG@_&d}oEXZdN*^Q)41d7=y+bDl z2YZ>!bF4~V0->LKn9%nO+Iqq8OB;5%FT;`h24)1dc|LUQz2aZK^MZG? z;My#BH-EG;>C@C$tt0bB`NP)KHn;f$H`kUMyH`Y+BP0O+LShUIfq3c7Ck;uY5TtT` zu6g|oekcoMTskJbch?}rZB*4Z! zQB#If4T$2(SjQPb%;uIho*+rwMa7d%`%6=A;>x~YXTWnJnG7@QH_&VOl!LeMF`~}y zuJ+!JgZuV$ez|w&!99Co5_Y%l*%?jKTh{maq9l=p_7V5B41i28owa$sHYPhgGvWwZk5N zgQm+*QPbMgjvc;L$qOsn|D;4p!uFC|36{*HNjnT5u{y%H$9K1$#gV#GB}D1?FSjqc z%LRM6;4WXTX$;#d@uYO7WY#?!5TAD}s#1ZIQhg(H-DQsKxaJN;GV6cI znNyNL)TKC?AK{X4T1h0W3|@rcG{Zj*i+Z22jq)#)yUSkj9 zx{p1GmqzCe8i6+{_!A2Ll!8SH2(mPM(h{W|r8L#z;}uGy;GY2^!QaKhIExPxGi^zUEKOx#WAIq z9cc@W!l`})GY17*kx;cqYqUX?C77(%+g8NHSD=pfqtD7E&yBl7c=GSpRO2BGGsrQ9 z#=*uzluSZNPGiD>EoSoUmhU|eWg-&W6EZPT>T!(PDiWhkB9!(AhJwRxw=ZRPrl*K8LvN-oD8Lw!m zi>L=mefnhB+kXNY-9BJokOHxULzElrI}{k?$-`IFa&+LxQMjyvSudR}tL*ou@}jT3 zXaej6c#^Gf6d8^84lEvuq1Uc>e3%A^LJDkdE~1MyRhTG8aw{;3NJl# z@tN;FcNUKKrpn?;fe*8MQyhsIO5#Ktw7DLNc)?u-PhXd|UEDSYw|v!tr&`FVrf1NE zlNB(YkrT1h-`*j3*3ayqCtzd4?rM5M+dZC$3Vw$qSZ09(*}aX(qiy01)w zLN*QjM!)HZap1luBl_Ws19qG0ihiFXFto>WN~|R_)M2cp4&5Muwpni?_%+NT7*0JP zPgJF#tFWX=YL17Z9Fn^MayXinpbVVd4UF0m5&XLzks{qvX|>^M$X-b@oY-_xc}+p-Y%u<%j5v$apdzcsJ?iUm%cJ(5TTb zGN6Bnv^WM-beyW!wO+AD=+IbW9BN&CR;Y#MX@0!S=Ep}4uaMm=WNi@a8z+ql%h~*1 ztHN@6AW*CgdurhT-t)+N*?LoNk{W!fMVcV9of?yPHZ5k;HP$R_I?Nsv z4dh`d`>9ih1N}~Gej8{0faBUy62_8XNq(p=1mB;dF;m2X&f_HvcOT>!GI=pt!}}>D z0}^=adOwBpAacSZ@@+;vCFI(isphH9D#G>|r-E~(i63`K7@qlF)X%YY-87}3M$&a? zB)*TvU8ZiX)wo>PiNX0DVH@UNM&d}}u2otyq|e6x3^^bw5A+8I1BXM=w0(QO)cIuZ zfv(3pdnHyh>WN9))Ae}QfzFPoV`TU=oNtc|o;-r>AR?>ExKzp9n^E%AzD&rAI}zjB z7m4`&D(w+FL8M;(NL?}RSrxOPNJ=R)4q}d5`WBq)1lu~nxo+8+#!~thoCSidKyVf; zC#Sr;_3T#0gc9$`B4uHF*)7iOBO2bG1c9o@dtvMJ*4cX1Ld@ZM`PsA2zW&rw9%Pp- z3l7Yqj3sxmlHrCkYo=%^yE2?zdG)}evq7*mgq;nH#EqE_%8KCw7P%j%7&9H#CY*rj zm?g)<5KfPM5yB~k;L=$|R*FWXNx24L1rp~miNqODNgSJ`0+YezaNkfoX~R|$M%XY_ zmQ2l9SXSn(^drLNe?^#^#?eQYR@|YjTig-5DjZUse?~xf1{Hy9p<`$i1wN8k|OIFV0Bld+=sWM z-Dmt?)2si20ur8U=6AdoCc`3XZZ#$ZWdS%Q(u%Z<~_2 zT4EBr!BHX#4Tjd9U&xKkoXAzmnGwB}5tmuZt1ffN-+tyB&n$7?Fz20lY{ofnXH9OS zCL5`i?Gkfp!S5CARtcF^^I|7EZ<`W0oSbQ}?u1)KL`CkCUb3Pkn$X+pAMVF~>+jQ$ z%+u?Wl!E!%;@_tJ?+bITP-rpFU9~zQBG}u2hBz{P7hvqK{ zk8T>W{>I?AX~@Vx4L5U+$ansf^po!Q_L%n=jg3>=?-}TR?<=NqW8u_S z?ip~q_mr{IVq7;>gOa%4+h^NjGUl?ZxZnGVF$~S`+xU8f8lgya5&}`AAkN^4|3d3>4$V!l9?sH ziQ}fY5EtM=hLEw>5HPT_F<@k8Q^3T|=75=<`2f$(mVkwwtpO`L+X6Orwg>Dun|f`1 z8G#Ig*k)!RlU;MLv$;33&lzyCYdrFmbdnL`n&-z%iEC83S_ao9eH2C z$MPAyj=t^IAfM$u$gdBqXL&F3 z1%U#V&qBU1P{{H=nUZ z667}qHnMyv@|yyiSiTJT&4JA{MB3X6;-E?;J_6CRg@pe#y?|eSgEg<9S z6v7?RaI#(mC7E3vFNA}G!}JRAv!JWHHyRd#gTg>(us<9Pb`JFShdZO)1N~Inc@#xm zLKt`8l~6%hr4lwl&nZsOsC-P~WPmpD}E! zu_v+!h@YUTZVh)12p!P@VI(8k-50Kl^oGNOokrFBL3DM$$su zyCwFu60Mx|NeqSOvMLcBB+()sHqBR6W4?g|-SsOGqc@nP<3hEYS0 zM4qMFaSriHjbX0F(*6lG{t1oif+=Ao(CrRK61MJsfZMT--h`!h;Aq!SzXpEj8GZO$ zx*y4J8>YB%PRJYQdgP}=aq^Hx#t4FOB$ML|RNHVmZ`g3b)GlmBJ%QT0U`Ut-2L^HC zUmOY#g(KA1spaSDpC9N8*LU=WPt+gnj@Ap$hkJv);SQl6$g{rgSh)XKeQ)=XdaTRm z2l}_xHEyYobVtLRfjwX7I2w-Bi-@(JutJ2TLCP|$TQ@kIFm(+KjBH7tQ-+Cy;!2JT zbrK?|YwjHYGK%c3!#(>jx+Oo7pP%A>l9jvQ$(gcG+TYAv@OUR$UgvM|oGfT;+}j_8GRBk}wF zUlexI{aN$?+M$h9tmz5%P-BlH!Ju#3xD5Bii!+Vq56#)PEE&!AoFx;NvGpe9ZaX+{ z&YMq8H@@-I8?G__$L_4xhNcZ)ABoxW$3_<1+4Jt=xVw0!@v6H#W-E`o%Y|KNRRjA3 zdkpsf7Mafr_M=ub&JI1i6=jSoU6xkMHb9?xy#OlNzwabht%1X;afmId(~v>EYRb4q zE@C)tG;mL&Jp*^T;DqUE?zjP~N!WzjM4+xi?(5dT52>NO#%)f^mmOC7==`14~Sew-+Bl+Dihp$ z0#X^YF`(oTGOr0pc@gQ>7!&sJiO%pKaYRA`+Ry+iVPA&gUC7{xlYMX?EEf#KkJ=9n z4~E&yUl74&Eg&d&80-*^YC!M^I*8C}KE>V0Di}M!AdwM<8jFZFGY)t34v9T7SQH?P zazZ;L1bP=tLJQ>y$FYSFA?yRJsYaTb{#S-VO|>thnaE>EFfzOA-1_q!bM}T$jRt!m z;mgLGlmm3SvI*wS-FMAdA%k4yN+4Gmb5<-zKn%I>*;;(2OFq7(a0Q5a50%sq2N@VB z3$R9yha&~nlBFEiLqsFxh|5%oG`rBroh()Uc_{S-$a;th;j^_F$|=E zqijZeQ~DaITH~O4&~oHSHV%pYHeEE&*|!lqWf44WCwR(|;pv`m*b)KMAKF?xrb`~a zC4cPzWmI#b;6h~{U(JPn&H%MG3Jc`577BaMP}Q)~1ST2cG>AGuFhVma0@7j1r9+E{ zL5r%5k+L;(bPSC}4kBR~N9NIF7y*y=nPEhD3hnCCwh>%WW11eBw&6nZh{{++jX@MP zj21G4n&h47?A0)r$~+BoN#@C{n%WwpP>&uHRuZfRhN57dmE}pem@Qe#1G7aiW|l9|V^IjV@isG4_H#+{YR z5ki;WbTyj3$KP*lugOf@6+kVP z4L2!wvyiiR?&Y#oz8krlS*ds1<;%$3T$g(JE_WOGYBsb_jmU{hi5{jCfAT@B2pvnr zq$t{okl_^$YGr67pi~CK1(h-|7Zl3S2|%3;Nk3p4w$|7ZURrVohx_aN&X9pt-bq8$Db?vA{FQQq zj8U&t6fzx^RWk3IrPW5lO_X>xcJyRRIpwVInmy~3QYj}r32R4wL!)v}>8m+acuHU0 zr?bS>z2d6p+q>WTO#`=FpjLp^F}b@g7=$D*V0F;ihGM{h;vu0w2(`pexW*w=qc~w4 zh$Jjf=0V4mF!gnufQpXk9TR4hMA*U-j!;E2HKw4EgtL9%;85rDkBM5oBrZwt{R4EL z8k=|-Hc>0nL@cfWDN$dbuJkH8jYN=~;udY$Z$2^g^yJfTJTqr2S@ihc+&A^5$uGU} z=&Yx7=E=CHc8p(idd~#L1F^hqS6Y8q@>p#)Q{3pP3LU^Jtr~WW!rG%i;Ys7Du}7A9q&nakjS_^VLz1#~ z)bOT)SDkg#Bd-^w59MxwDZz9ga(j~AnA#JD<4jVxU`=F@I+*6Ut3P2TrFnusE)c*} zp>3fX3E&CZ+Jq&10@|BMQrbvZlJ^+8BJ?&=#0?o7NFzH^PWWdvprKm7g;pXYkDuZe zyg5_Flf|>%(lHCbW1@1pY|go1rewk4JoDiAgA;A1+iAv&Z`g7sy5?*}i@uzx9g{nz zc2Dk}shsoG#B4P&cTLP!bH|EWG3N$R;`mQlE#;<5<$TNL6=wemdjIURZ{X}{N)<%{ z2Bc{C14SayQ{eOvLz{A}VTN}q90jevLy%VaaNl5b7^3753It-Xl$jM6FE@;`3rU9J zE68h5K%N!_&_o4UizBsbnN+dvZ&Ry}Akm%LYt9V|4v#XSGp*NbHEW$xL(4kTrFDEu zv35#j(gs!4Jx!?|!3!CbMH(`y2$0O~F~YGb>5Gz(C~CJD9yo=I8n{@m0~e4D*>c~sCdf$EK1VH+ z5RnEOD^rEz*SuAWo}%flIZp-PCaYS;|9|DJig~Nl^q>q)|MB{kZKg{OM@tQVDbGgv z8op(V{ZgHk^4s{9-CF38$-xcW{Mix0FG>zVCWXZTgCi>=^jNFaNrGq>(IE*E!hm#1 z2!j!YeZU%)P`Eb?B0$r?}}x zQBENK_JV;)fJ_ITRP?#oq$q8Tl-#wu3)DC<7{sY%P2P9SR>Zn!udxe36boObq=S+d zDLF~WS1Eagl2;z#ldcgx80<+ofsUz<$KeB~_%&sQ$F4EzT1 z$>5EO4_l@Acgc_4T3z}qPMpT33=MV}@gm@I!GzizlW>@xV7}=2Wd?FhzU;iVz zpTq>rtaII)C+3{~nU)1#_SE*t?b8Qee<0@YPd-3svt-`sk30R#OEj8^tWBnO`IVUC z|6ec5C>+7tB9bn=PRSdTyop5ff<%oIdjX;s$cTUCS0vW>KD8meq`Y%^bI#J44(TNw zdVP1yQ8KxkUQ*e-vo!83T^4=^cW5LB8Hdc_%Ds^a3BrKdW$4nglk~`p1-eKc3cDbp zcC|$E%WM})=||CJ4KH#FCCD?GfN_oqzB>Z94&f9^HB%vCBE|7zq*!+*lnQ@BEj)}Q zg#_5LCbrMn)=NnKA z6(Pz5{bH0`6y_BEQ`4>t7#O$%>htEU7K!48={3^eehf z%Jt+)IprLZIcbIC0pc}A3#2+dM59xVC=o(-NFdL6YTai|<)I;{Z;PYb8@7xywsG6Ux;b0k zUG5N-MGKD7A6Wm4|8vKGalC(IwqgIzOh#A6$4-}|HWCGdg6Vxpg;Dj#pnN-D-#K<* za_5~))V;|exnqIoz{CZsR3z$;KFC_%T5kGalcP0{|6u1jJh0z~uES^rB+IntbE%ON4D91OCCnP(QgH z3Pdv#JT09_!WkrcK@jvzh=xOnoM2?=2(~rl-SpPZvpZ)U0FVrN_GN&H3>gk`QYbQ;krcuLDmD|E+Z)Hb2}LX>_WJ6NQiVF3x7fY2Q$4-!~v2=e~{ejh40~-@W+%8N)Rq0N!X++2~#f& zMdm>u0FguOp*j^v;IGg^Rk2+G`vYKIg0pW(yzRfCT=I}is;orof=>4%HK~YS!~!~1 z|Av~~iFW~9Q0Sj8Y>F2)%@%Ht!(o?aF1tjiryqW+{cQVHcWum88*|snj=Xd35)lb3 zxbx@T9qcN)4 zJZ4*TXHD#U?c|K%s?tUFv@P!T$87$X+m8;XpMt6q3d@+g0s}fTGCuO!=sDr7;j_cP z`xVtt^Uu6I{_?EbKhyT@{crET>Q3UQ8#eDm1sLW9U*5d0Ebc3t^T~~+J`2^Yg`sT0 zS2XXdj{B()b7ZdxtMT{|j^*8R*v_JAxLXxJ#lQXmum0jO>g( zG7E}(lZOPzd2O(mMJi1%m-kZ9pnw>eU}8sv#>EaNdEbx0uptlu!deq((7#@Y3o$K7MriFFI^ zoHI~Vy!P@tmGhGVU^Lw+4E>0aap4Q}D|D8uUP43rY`w5Z!! z-G5{CDc93oNjOStGnYz%xqg{rmsuzB#X4$&{3J7MdMwnaF)A}idX(WVS7qe`yuOmm z@vq`3?|lwHSCzSz>FO`x-K*MC)H}kLF~WRlp(|%;$uq=YKFO=bWLDroRVr`zS#hN1 z)gW%gyZF`b8YX*>xqZaAxpQP|cOUJM^OFzVF8?#iUZ?7Yy2cu|nel|byWh_?v{W}X zG}Ju1tHwlFDFoL&c7|pqVZv6O8mI6_co~dFCY&8$k%j1E-Ju5u`okirm8q;i+y)>^7$LVHa*l%=mmkUA&-fwxD6YpebI^H0#)Q_nYAe?97Ez3XzB}-Uyx3o4Y%#QGh@tv?K}3YRR{I4D{J0W5_gr%LaXnpio2@D zO!OdSr(OCFf(w6>=~*Z$nJ=n~7uC%cHH^29?VH%Q=pg44N0D^KSmzxZ;*Jf|$KDz_ zJ2K~}O)0tNsJ)rVIlPPvz&$cmntu=PS-4CId3?eg)FHe8788W#uZ(5jX zn(n#gtU3QU3EvM)KJdmDW*XzZiu2hdb2~Q2eVa)mRy*&ki92hSBSg5*<~N)EjBnws z!Ur_UD@Zi_sLVVa184f#n5X1xPm*tRARnu{XDOU1CXMXpDTnHAnJH1KXzte0j!6Kc zp6u0`ksetrRC-a^x}@lsD_zsGz^H^|*-|6bC$N(MwTB=k<&dt4{iK8C>MVvJCclt`rK_W_yLTAa zn{Dt1yV|1AD&|Jb37dww6bQzd}i>mB-9%M7D#${RgRP!irt5*j0jkJn&x!mjt&_>v=OjQbm}`Wj;nV4FrlG0idO zt~*{d5_6U<-|>hA%Mmh4p4(H?T4ws-K7VVy=}H;jy2*N_o^Rc$bs8e;BC*p;O?XQP z{9;H)^j$_ZOs_8Ej;c0KGS=7#^_r1PH=`z&uBts&V~A1HYD&A5nzCSoCrKVEtRpKO z)7MC<9aU$|+|BXb)IMTw+_Iyt0sk6>>*yX_EF(na$P1*;VGCwI!f>$V!D!Q9I4m&Q zBr2GMzocpwu+hiDLc-G7+YK2%MZ4rVt) zFrYub>Rrnpxh}ZgJ2aQSXWTVrnXn|BzSj?b?8~3`Re+Yw`l`-1UG>$+9Q83@{leOd zRHoE=8%&h8^r)-!oMJz#tMW&UBcN((wYTC0@mb!j)VI3DQF9N;7%7J| zhofqhJKs~BQVl=#YTCI`zNcj6y1;1W)vpsS`9<3}u57AHn2GK|<86KcL{Rus46Vki z(q>!xhx&qD*eOGdb;6wzDMzH{mAI5borS-rvQH=>8Yz%7gh<6drQ8e>wNx(roJ#+Z zl3SE4AxRU-#l5vEGEFLY(SK8ms#2=760Sn{Dc~wmI)?3Q)=>znSC((8X0qmujbqkD zU)}i^=j*>1um9rvU9tKv&elIV=WCDI+GFl^7|#?knqPfBdp5sLL~S>XF1yBSV~r#_ z=G#FWKC^z$Re2dxj7Y*|bE%Any_m9OIT4o#CXR{A1nT*~n7Bl3Q z)#S~&Y2ux(n?}lIaM|nEid}(5KZeEu3WC$^3u5LQ?LWV1N@@*Mh075qX5muFUarl= zeP}9et1#(fM)H?eI|fT|5AIX72}sYiLE&Si^`~*?Fr)vJL+1{d*;=`0pMYu1JZi-1 zk^QxlTTd8I7zuu{l@8C;Y3ekR>;(gzSr3;gyAbpgqN=Mp7*uli(C_i|Au}x9suoD7 zOgY#%jW7HAbW#tvaaRF+Lr+R>2%Qrj6dsm4xL?v!KI z`u>#4_?cQios&xEoMd5LiVl=SStPh6GCK$0$|XkeN@OPKd8oTHnlNGWzi@-<&|WSg z{R4whk&S}9@JNtkhPFcq>%jxTLr+74coS6xBFqUJ#1i7Nwu|Z|5>k^Vu8Y*&E;8JDc4w*7i}(`U%5=Cwt=Ysi!6ps%NGo z?kRi2^|2>sqGPIeviF_s^JPu(vZfz&{Yn4#`e)a*&3g9z5OuyZ=h=7D%B?H7Y2&i< z&RO1aopr^*xIswR6mx8X^$uE#(=RXN7M$Dn)`7DJ;<=kH8m{Ix#ypL&+{Wwf>}&ar z;zigKn z%amB~W>b}gAJGL$5Eqe)oJNL}g(kiZ*mh+-VJ5#NsV7A4)_N=7Ks%yWP%AF*nToz~ z!RESZ%b%!3Z0rhXZ?LkJjX^IEC$MU!{AvFC)aO ze4JDf*rYK?X9V_y;!#pGhV00@hBIKAXZ}wJ97s_E_x8eUprKPV*-REagv_hPDQ?V& zSZan>Y$ss=3z@nNUpMOOUa%R_jImjyP%4N_W(CvurcU6d&8WV3`q!;>x7zf$*OCGNUhE(XxN8ePNlQFRR3^e6D@LE`Yx5zmmQ( zSYMed_m$32oc?Ql_2udJRw)0i70N^U7k zRbU1%{R*HLsL-@Z3rDB|({JmyaDER}sAnz2oRy>iuflox@0$IRt!OMUkLVv`xKf1Ci66{ zQX)#kIl9j5AKyRGJyRZESBW52jO`jhP{^pzu^Bg_JHC%1QhgG_4FY1@lV2-oOj($` zgsX-5@5li_35`e`B!bFwY;;;{oRa{Z!uPU~iMw~xaf);uae=rgyOE5J^U}px_IS6NWoG1C~=g>M{? zuyK_8x+#h);vqnk=*Kq<)~@ghLA^DCo0SzvD-~gtb<78fDLS3AOJAeX3Ppfg4Fw{W znsF$j$ow`WCk34-o1vuDcoNc)u8SqNx+~eZuKi0EGUb zL8eR=NK6NG(MS`fBLgRxMn;6LQ>R4#AGR7A^;ZKTSkrdG5Zh+16{n^M%m^kRPAz(} z-=vVoEku?Mj31b8dF#Qm56(HtA!nU=c>Ljs7r*f+TPF!)S14h59&X);>|>W&_Y3%f z0;!9bK0;n90@;!Uwmy<}J0@Y>p{LzKy%T!;vMgtb>LaO5al~Tup!Bt(t@S(9SSuR) zms8x&j3)bfC^OvNG1K+j!tv-#HCeAk(!Bd(!F$noQ26 zZ%Wj%&}z^K;#MvEQUeTr>yQxzalIJiE76s_mB@;KZ)k7=KAyn|%#jBfP7Px$v`hM2 z$ZVbl<$jCight7vbrI)2Ka)Kh?n{aFx;@Y0H4VSMQZ{EbLfA zugNEYWkL=Wy%zL+x=muRv~kB}v~JQa3+jd~3$g{%i(W%;-2@NrAlN8I4^Ic}q=G1T zJc!uH8t@J_ewQGJXcX}jN^o5brJ*;(hD1vivalFXLDb1=1z~X@TRy`;w)}twLbF8! z@vya31MdXQ*oB91U14I_&T)pGCC;T8jR?D`C8Cag$z537qo-*~+@p7wcl9o{M0o5J zw9AOvidAKIijKmF?R!291(+sWIR-BJ&z?Jeo}Y8n5k{`NWii;jSZK)@)4Q)Z6|dU4 z;)aVAbB>)UDRjdfkuO z0zQJo2_=*;BBm(X3EYKDO6ut9HcIZJ5TJVU{1Qbw8geq&4aA`p-+vOypNjNZ=keUY}JCo zTkGxohFk0GeEHHk2Y;W4Y-u=Gc+-_kc(4+@i|C$mY)@RdAa)tsNIB|@$#huji&D(do(vfnKXtLB4JjFW^7 z=JhBs>WJ}<^xzQUASSHf$LJS*#OFkma!+70ES!!^aJiA~V=s_$9%eFj3xe&j8rVe@ zf!9r{k<5k5Y{zY|lkOTRqDGZBSf{=GX9PhH;0cLG%bWL<#yzF8o^m2h&NI$&=LA+# z;i5Nx-di2_R?m8C$1Dqw57tkvpKfCj6tN*`%*jANpD6t$I#4sfdUKUa zL~&Gk*`Q~(R<)NT4M~%*1e2WZC}fhOHpnnzJS|LiAe2b5jjtI}W3QfmM4z1Wo5b?F zY8BSfzr1fHy?>=25Pxz(NI60l_1G1MrgF%jAjh(4O4<7vQkiR>1`$7Le91VRJRetd z5;oQ6W>}0j#;rq|=n$5(<(U8e{(&g=vfwipp^^2a_|!|OKQs`=N4ZeU#9@CaZYV`; zFfzI;5#woxcy~X1R-xxh@GM5H@EB^4xwyNR(U^oO5~Yw=EmUmm?8BZIL5OJROlB0T zASixiBN>rFMk=B`NWAs|rZ@6Kv_tB)ym@DF+*v&5Z=7>(xp;(8;jgogUTnRA4VI;E zRiCY%E8jN9w&2Cw+iysBypwUU;+k{kdwUkM5Lb3Kt1RXyBj3`kG3S=$JCM2Wi#c~L z-?UilO^e>bd2do**ZF61X4h54JQV*r>w%cJc^Q8YIpDntx0_hu zmf5VWF~`=Q-?nm|4Ywe;H!u3uO+?<<87tm()we6=*mcdf>tAo0aA7$@FXfw-mW`%M z#g3L8rb`?7mL}_^9sC~7df8mC*UnuocI??`y1bF!(`3EeU`5FXcKhB4~;#Cq2z~77X}^j6uS$>TevUgjlRZ=7|uZ3kQ!115kTVgA!MIAN4@hwb;Tg&w{uWMelKD>8`Kf zeqaiF_PjkmZqJ{$SH$fV=Xb{J6|?px`mg}|_|rD}us{xUW*(pFdrvp?@490&+Vc@X z$es_5D+9M8_oMKYx)ic47+DJKQT`IIsq9cr# zqPL%NNNXlZb!9!9{&p6rHT4S9uNmT*);D-YgangEqKAwAT|A}sLvcDT z5cCL3Pasj2u>SW0^?dM%X;Mt#n(c3vVZO$)%ycZN@GNcvR4@*)jP!BIL&Ap`2eYkc zkb*1;i5Cw?emtz)3C!u=G$yhJPw_Kf7_O}D&$Y{EHtOvD%$ ztf?47UiYF86I3l@RAm|2g)qsS%jInM~bPe5^mx`uk0 z-VZyVL{s90q@NC(R??l6&?YGl3g$ijxW_+}H|MDZJEuj`>1>@2Ru)(3AR7H=YBs1dyf65UplnV5M zz=kZC51MBjNkdAB_)DEeOQl~S+1HS2M=PZAo=PdFoTaCs7-~H=O1YGiu7_+2rbwT= zRBcqX~ zs#Kk7rL`jv%Q{j(2-Pos;M^~Mr_2u~#6N^)#YhP`;&w2qEp7*Un{v!(e}ZxsC^?QK zVWV%(9~U|X=^F|m7I>-`x-Z~!0x9u>OqPKoJ=n@2?nV1P-Q}T25Td_^uRBMc2p66pNco4Z_2OGKs0B=N65klDypA zua9Y|ZOEcr#DRNZMH0jMzH?{-U*Um9SsUck`)27>)nwJwrpZmy$7W8>W^KC|!NzM? zaL5+Ze8Z`&@X~D7_V-M4&gSZ+ZZewe97JUL_-nk*}+;HyLTcc-3&mX>c_?ojBqnr0u#=VuGX|baF5&bvI1tU>@ z`Fvh=Jg<5-uXci8$Ss`D-H06;v$>ludavegiFvlfbGO{Fqb}(K;GB%P}To8~97zsLG=nWqW z_X>{z(J(WFsP40gNRG!YjsA%66%lZ51ZVSbJ95-o~zjLe# z?jo`bcz^8hkRiakd>eM_d}#<5D}LnMsW*uG=AAp^WSdAC%f;}Iyv_1x-g)M|$FF(5 zK!bT;-rF39g(7`Sig`jAMC1ib)i;awxJ{S1{5@6NrTn5j1*S{eZOC7C^Lz5Fmkank z<<`r7E0t8)_iQy?-eN%dfwySy4$}ua_*Tw(#az%<%3UcgYU52;n(fGc$n$M3>xUUu z%9q;P>P#PQHXv=U$w<`>krqJsHA;e%^ie{-pTf(O{3azS>WV#7M#-PzFG3s`ZE55A zb+=qDzUh|V0YiIX7GHg<+RiuJT9?IdU8>IKn?w~6-Pb8$Ejr^jJC=y-Dk?s;U3}P7 zV-`1O5o7^I(}PpXPCvI;E2}SZiSJWO3W`*4H!Uu>PFZT8Q9?JHDL#TaMX5vmTg*$y zy;a`SFO`TJ!cLl?S(Z1|A#{Nbe=m(*;aS=4FOBd-bZ9I7mbM`I65cThP|u<2 zJo1+1tmSuYS6L`qXt~lQWf}G_qF=R+M1mCc_IU=ey3wsyk72qZ#A+;t%F57_qNVYC z{*aq+P=JZW}U@jMh1e9q=Dcgv#v*e2oOAw1i^Eb`Mizsyp6MYo5gs@arb5h zh3#<2NwH48NkgBeB!y^s0?4#RErzajB;C z2>|%Si#kQV!+*rzDdN{Z#lVDSBtT81IT8LZl0*hYUJ6Br27BS)!A@!HmgpvkPX#Dd z#Q-MCs}S+8rpg!S)l+u>0e4}nXz#ga=8Lw*i?&{DdvC{V(cbr4-hc6jC9_43%()+p z*&dC#A6@kM-^uy?T=iF8lQW-Ym;ODSN&hC5QL>wQmKB)17zLYMsd*q_4F*F4o%sGT zJ-zTPO1dfe8%qA3l37ZQQDe+{@f(z*rNNd?!lC}?kGLJ5r;CIa6RzaNVCO(z-$1|k zVKWFb} zv5<1ip^>ly`-Z&Wr(6jorIau{P)<4GpM*+EswkAG3Ha=u9+&A&k5=ZAH6KAZx$%<|tHVOtq)5LSPIXd6&F=ZO+sBxTb7n%&U6VLsO J!-+xu{{sZx@dy9_ literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_unicode.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_unicode.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b6f63559871123b9718d83d2d7b6284d8feec4d GIT binary patch literal 16033 zcmc&bZEPD?a=ToTqWCSXWPRFRS&~IsGUX3BmSx9@KCD=YP5B}@J~Ih;G)KG)A8h%JgC5Xpygoloa!BO7;u}KpS3~ehz-xWA1g`>ot8Xj8s{!BU+eYvjz_%_^+=C84BCC<1r1j*e*10iV_JFIq*b3vtK9_)Uw3=Q$1tvVkv`Nz9~y)@wGSUF zRg`-7H9iG*E8m#MeJ5~l5*qWl`*xCdyP(#sz1!C`p>yvFoHbFDH!2Gf7dRIklew`- zC>Z4hZa5I(!=cD|E)?Nn(wH0yo1cB6%VRbV$N^EtwnJicl)EU1QYaeX4tVx^_M~}n z0iKsK?~=x1v8X7waFQT&n1^LKCUvy7o)5{xW9K}<=qR9p(O4iFj{+8 z-P5+mJUA?H5%LA+gm8463&djK2~Li3y8@AkU0mm>K28+mF)@NY3UHDvA|j0ihq-`c zj)~ErAW58XNpQJaxZpVNa&u9U8#3kR~Kq7*$B1%FJ;tDDs>aWUZF8hf0^*&5aL-pf48#VhBco7h*z$M~)ye zWEu+xcG$n+v3p=xD^+0_K9%t7Bl9!=b+5Qs=9h6y4)tqMmHIVb}e ziAHvJ4s`eRsVb`I>?uXHJ;70&$)L$FyqPl8^Q>nfaxo-EBe`WbC9-Hnh0&-u;U5je zK-5{~$~4I?5M+Na8i@!&l%3>i0;6)NDZ5D&l-y5dLk=fx(&2DtE|>PmzXO=JthUt3ocMS>~mz(ufdwokRNQ>hCX0_@;Ild zB{?AH_johvO+PW8wLopmE(&AgFLlVr;3BRI%8w8%$6O~Hs z`{Q}zFpXt>_T#DZ6*q_4+!x`71#lYU0+?R`EE8B6)z6}Elvy0$hQ=ZnCct>Bvl)Ag zR*Q3uV|z0gaVc~zEV#k&UjWZQ+;ZBia3LA&bnrsjNr1^xzPCm7Y}r$;#Vp7{kDKr& ztiWcXF%HPY*`*mr1aRGo!vM#$xywK^t$0{4n&f*%19EU!kj(#%@r~C_r;38%D7r~N znXZ8zxEY8|#;P-Y-k$z51Av-{5%c#VZtm{$4;~vB?DM_=PnP4I1O8*)SNdM=M|KW; zJbCP7|I26nCp%9eeI*uoj}7{}`@P;{-GhDo-U0ty~mMFIS=2|q*}P+*3zz>oHRC<0H$*aYHOE(L}J z|L6sUn}s}y5CqA?P+2Zr2*v!-h(8d-;Kk1ik&wVcgWrLgRvFVk|Seu}!S`#Ak{SyxQSNJ*}#OgT--$YW!vx}c$^bm!^Y zOok6F7pwFmWI3chsXV51ynYfM)PNb@koDelUAk8^RZLCMZ&_rU%4LdH-;~=JoF78mH@@x~4ndkZv68XFo4T>_0@ARd_1 z?cO)o>n;|{K?-6ef-(fv0DyED}ATRD71v*2VMN;wbtS2sL&b+3zsmgZxx4yus+C%uIX%s+Eo)f+q-0{Sv8f<4lS9st~$3Z_Aa(BIa_87t0wz($5qFIanblu z*$1{IlZQ|*49=ZgGVK6<&a(R!XWUY|YAv4?maGc1yQZ2yK5|gD?H?A!Ev|s>MKHF*TY@Kan2=WS7OqsQ&fs`;;Og`fbb&2*B4Nz`<%FzYJ zEj`kMAZTxn*}!+;-}*xU@6q#=40q+aN>5SmfJspK<2rnb@~2n76l6BwN$n%xBe3wy zzuxl77PYJ}g*u%j6R*qWK0#%ff`Ys?Q2 zFQLr1X>ljq#1;UP)p>*B(cbyzP%r%ez#pfnKbUM+ixQ^lB~$H6)wUVtnti2eE4f+h z%a)pir6z7^T4lE_NXy$>6Wd#t*gY$CjkC;q_CM4$f4YBpe|KVk_flQYEc0{ws&nf? z@Mh#j!CGj9d01HfY52+%B8M$pCNT_fmK z@-D%Ci57JRz6mQPrW69!E`rY|FMN1ZXVAXn{qaXp9 z{$}2$^+qjI@4j;04wBpQ6mP14H#xL7Jw;19@6_7yB^!I?tDGoxvni6==pDdZ#^he~ z4P5VllG#WnF^H9GflO971PLTaQ?)D00n{*0j_E*4OZ>3{7jC_^;8Aqp&*k2>jlV-`EdmRDHz~6xdX>Q84u4UH18v?BLKjF*~*t~4GCMr zUE9u8YuU23E@7=(bS+t%!EVeyH}~9!=YBQv!N{lVFQakxz}E)K-n4AndB?W%8_5C; z-g~CAocX-m(7CNBXC!eH$Vk3H3##lE(lZRUYCAxm2wkLHC_^ja1|n zz~hu&?csN5TTq@VzYE};lwMz`%VR}~#L?Fwxch4U8>uxJi#dQ3ytp8I*>11JwiG}zS1MAh4IRvYCNR32-LW%*W zE4~EZCmsio(xYakOfUyVF>eg+7!_IEPU<9BzNF}w_&s=%(3ilffJ^JQ4dW$0t{ZSk zJXx8g)j2i4#J0ZxFiqXJI^)9sCY`@W-v^LDpD|W|q;i~YH^E1qu)ADlb5%+@E^`o-=f>$=_b$qTn$SZ+CzXgTuP;P3pu@h4kO-eFJ1ttS)gN%Z}@ zh;uDGHJ<-}pBj&y7EIQ(V7}wDXq{sIZ$XvFcN7{1*V8SScOl*WbAM#sYtH z_{K1(>9V6G;b=)ZJfN?jk_{g+OOA%PsUhxY$W}?P(eLlOdGN-;<(lS1P4h=RpY-4A zPu3i{!ybuSk0jV5o9H0UHDaFi|L;2Z?dEw(-$T6)M)k5GOCt>%HBzLnm7%HDqmzG} zu{KTVr|4WGspwI$X72Lz2vU{Gc1fNtxsCaHa|T2!z77CQpD0ThZJ-LNcCAEHqBP6N z+JbtNuKg{vm!<$bu_|q}Z>8+Kd(QoC^GwlKWi>NBtL)YV{=@FOtSfGH#aS09NqO!4 zEK9e-Rxbp9b~J9Sn>)IR24zCE*B+}un7P}`!OJ+i$OW!G9pFJIms$B~0RWU{vV>YG z3H&T7j}fbiX?N8YqdHb9_>EvcQS3oBl=NjK_n(4-%vigyca#kt7$M*DANs#l=K`R zdi}KgH)t?um~Ij>>LZz4xY{=@Y#qtuy|p?BhcmiArQb}y`mmg^%n%+SBnmU%ky++>Zmmxh`L z8(MhE;+z=0AVl2G?D+|~sFYcwPU#U>CO8v|#z<_EHG$DilBAV*0I`=4U~ny-2B1dN zIHe)Wg=cJ1OhgzbO!9;3GR^sDubmfyME17MWZ=GvhY`bf7t(KP02Ij+a;ddj+(ngr&3}tuUhbF?DW!b?c99+_| zW7*M^ATfDfjC0RopOgQf=F$!)OEH|uHU+t{cu$gb-+uX%Gq=tx?|wG1``OR<-$j2D zP44#IVZCvyH^F*|7yH@)U5MLU-$>{yKG#?GIGBHO7NEyO-u7U*fYRrz2I;3%7uQjE|^CfvctxD7RJdTqM1ri5828Up|Un1j9k z=G8YB`Q@Dl6FU#iyqR>gV}QPWQJ-`)0%j|l9en5YRkmvW=-kmHTffZiPq6zx?MvmVT}bj)=umfc|+Imo`Pm++MC(V z{dVo;ncJDX$7Q_^+^1%~a|8TB=3m>MRh?xz+%goRp3-Rs{He6nkh{Hw_Lqgf^}hu$ z5337Uk91*NE5oyL6*2%L86U1vLDGjVl)zF=!boP~Yc1r=uDD7i(bt}zqJxlb9H8`B z8(dnN^KBaUM9Q?oR(KK;GCX4u-d&`)4qX0%!_vKHeyjkQq5{|;bL+Pj|)8-+vdmg|v^;Ko7Uh&TmkT1tjku2oWU#~dRyzkuTO!El- z1Gq>N0H&#jR(vEj^1 zpJW#JN#_{mN#U@Tx;DwN+GtTpS|&oQwzjMBZ4t%+Fh;o9mTjvG8X9;3UWE*e8dvmC z*O{R1JnVFkqzeN!S_jcU#tbPO5UK`gPIqmKp1Q1uR1TzSGOc0Z0bU}rET3X7!Uo1Q z>l6d@4uziZ&csPmdZ19U?OMtwvuvL-Y_$Gb%BPThM-v&;U1r7ss#tyypS4#*B9xr* zKm$$;KWcmSsfH&t_ut|mG9DDAsa#VTsjAY2^B1%iVjU9kquEZGU?(<=d39w0K$GQs z_TCUlwsGJ~`DPu4u*P(yrs{jIPrD<*wt4-s(_gbn;Wxzo%)7dpncr1))fqpp zGy?khHe1(DQywb<-yv>KVf`5A7C%mws4nHaz>V(rVc8 zu(sI1t!<%<)*rw6)~m~giiDwJL4T*xwOA3~`(oTsku<#YKxZ-Z(|EYy1ieP^wd&jk z#}^)+7zJ58(6| z;kb3Z2#OWnW+;2OO>d|J51-Tb{&;2cr~RZaJvnb!(8n7OlJa)sxEuK#LU4?JpoeEg zjwnL(Bcc!vsh*^}58Ywr008W+=x7lr8JLUTn654Zr77b_~HYJ6IwZFNC(a z`Yv9>H=+Z|(sj~{eON{mat<*B@JlmF!g+Cp`ZxW-`>cEV#JZj~lqo_-iX0KEvHkjo zP_t3Qg0awoq|mN}Pz)zDbZ`tUokDnumge-S7z?l0+T#fu)s?GvpLN3ME`sCCu%+sa zMu?;bCU>UK^O4_`NMr*T4w=JYpWUEZa59)Dr#L*~+wg@XaXMKe3Fn6bV_~^KYITF- zBnmJMQvEKDQ|&p~J3y0@cTK_1Adf&91V#ncOjC6BNoDQuV4*z%*cm|2ly|Alm+VgA zm*ibp{i6$ws#*^#MK?vg24f51kwHdJNiN8&mZ`Wp@=EBFMPt}V0|Aday~hO-K$8m+ zC?Fqg;S1;E;227A?`g01tiSVRJT}k3!CKf*Ap7%0>7V%E5-&EP%oDU4nky&!s3`*+ zyd9M`_-}C@X&Vrn0g$3Er09!}+w)4lPFW^pbkRdxy0Y1?{N$(wr{gby$Noijhg{jk7?>=Mbo#M#F5^Ii2BmYn6>8f;Lo zvxgIq>}gxBZ%fp--Ep+VO>GH>vi|hCZ3WE_niNH>=uyRY5d0JY>TQ-w{TqDDGfqFp zG6YR14cwN#LVc;esq)v(50Lr-m()M3lu0e z4qxPUDMph7n;aB~#{i_vu>O~YOELszS!p`7?n)GZ8kn*w2SmtNN+QO#q6q={P!bMU z@R8VK5=xMWkjMv9jY<-*6fqJ|;`~rjghX%^_(;}!B+wu_iW>>axmlQGifmJsOR#>E z{pZCfY>^Q`cVZo}^F*GxGESL}C?_$GiXQ+S$eEJ<8^9Vv)AR%T5n7MGg`mw}QMRur z$31G>?pg1gJ!W~AMV@uowhVaFq#kuF;_uTyZt Y7Kb4YqRSrDmeL1i`~MREPfz3U+`0ScnLqhD$Neq+ zke<|-XIZc3xNDr3>*u^0uXb3|uVHU(zm~mq{W|v6_v_i)&~ISxl>QXGQ~Pk}$HKa2TO z;kWnOncoP%qu;^&Cit`avzb2){+#|C<~PId>~}K11^(RrT;@-QKd(QJ`7_|p@6Ts` zEBpoh1*srjfIvV zv{VsV{~5k5saIHeTTouv%CGBh_=4q@qui}34xb?DHJyK7zoEV}XgFfA|EN(m4?D zAtYsdk{Sk|jrRrlNzd4*=hT3Ic!H-`ZQu;$3XBh&AN5eWaeizNA*P^@AMuY41bxvI z|LM^&me@KtHhRv-2R&l}AJ32RQ-aOiVb8E{fFJdb zp02As=Nmm&H|#%IhYmV3HriNQ-%uCu2Yof;1A}J=PWu9N$%xlLd z`5aWiobB)gXoYx=yO+WlOniY9Gbnu=0yvuO^tXHg&NVJ5#|_1B8G_}cNOzR}?DBpaQHfX`d) zIys>nr=f{J(B&U>4FZB{U5^FO8Leq)E~W2co%itp^yP2`3#0Io1HNEQXU~zI z(LiuubkOI*$Tc>2jv?oM|FDl0;hJ{!`G$RiL09|m@YoI=}Qx?FrAB}ozInB8|X99Pa>IvdJw31{pK8F${(YfRghwzu5welzQyPGj1APsbT74~?89 zW9IbpyF&UL25y4{S<*vL!XPaLknv5e64tyL1|iyM%}d;O^%CR^DRak4kyfLNI zhCrzbj+@pENmNlhkdtzG^KmKt8=AH^Hi}?%(RpYi??K z`F!L8#?;nsi)t=JO)=z+xl6HEgbkm}>=dZP>i+*{-EjTPU?~$(%8}<riKq{KNDR9#;T{vtYjBqVp6WsL4G`BfwX3OXJMEkLx zK-AIu*pZIzh;&_2R_Lo*4y+shL=1vKgWqrs?l zED%ja%l-V=C}TIFNv-%W18rU@^7lT)?B& zHB$|WKCRtYo&hv6PzMK4o54ApFY?!>u1&7WL&OBO-QLPVuhI2CiU~jl+~?| z8bJj$HVlmWqv=@oMGV+8dTNZ{hj_jn&P^TPfmhTNm<)K%4Gd5Cq84!|#9uKC6k8pO zS9V}Rxl>hZ_jW?!v#2*fNE6+h!ewuYI4T84W!O=@@TlOZn=##WZi+a!3eK%zr+db- zWX+vBylAZm87l;9#mcUxFJ^%KKf|-pu2y!kSMwYzx=m>En-{0ue=v8)&9M zUm!5X2X1QlDx?7#N04$J@w8w~wXHF1!(v%9!GPFR)|?){*w(=0_HS zGhGo&<@=UOq4|-YFB4ArMozBgW=d3tVxqbyj`T!zfr$~ebsz#%YHJ)e-7(nIoR1n< zyAy$NQxY&HHgw9p;cw#U1NOstmb+`QUO8~-!1Fybn!7gpefpNA=9WnFL81BJo#scD zbqHAIw3%r$2EeQ>Ct@uYti@sL=K04Kt(75TrC_aG12%sbmHfKECR?du$SMgw^}W9w z_)M4JvlF}20Dp}7oq#_^If$^>y2iK-Qohw8@hemyEmR`$V?Q7??q6t{?TR?c-*=Sb z$ItoWh~1B=k{W{+z7GL0K;ypzf7HM#Nd&cmBv4bfV`}RL8$5~91AE~lgPCAIaJxRz z*d;V}-D%t(wjT)F4$K(tTC+vyS}@$P*4{2#v^Ix~&4RUgC49--h;fv@QTU24FNzJW z+O}15OWwFj1VF-aqw6IeNDAm|tj2DAeEvpt!@f~*MGyK%e1V%9w$=m)=wb^>a>oM6 z(2~xQui3H6gkPnOb&DM#i^wbNR)6?|h$#}Msalb(3@qhu8_bb}?^l8VL8c*&F8 zP0(vJJKZLpwiWziJ7EaCiw1=?}5c&a6iZ}7^)s*=nO3n(lmyHn&lL|VV77NRWyh{`Jo}EXL%M* zd^1Gk5Y9-naP?jtTmx-qa5_TUm{gn(7-cBOmPYdTn-XHM&5>i$5@N9Rkz>qk3uN)8 zq2zS08E%Hx0@pg3?#_s25CDjzEA5LCRwfzo{Rh_eI3kO4A z;tzDTwu^guH>fh5R}Y?w%(atB=Qyvyn<8!g)V7V%7lVJIxm-T211Cij&|I#X=7!Sa zrAWSM-Ah_;>UU-2`wb1zdv0UzRJwm0EP_-1(=M<~hb8Pg>K~l4jR$-a-mx0&uYCNe zfk9tXuQ98B^I+iY<-WAluTEsZpx7f#%pX2n-Nralk2*wF8MvqzejhJRHXfkPMeQfXg4jqTaLYu<$iT)u2u{9dgt5@T zF`|^%XfPNcQc6kNMKjs3voxOZGn2?P40a8Y^sCjL1%31e+zV@(O9p)OV%YDI!M zDuSqtgpuAZShj~P&2MT1OY4m0Ba8J#-E4cru|;rf3ERqUr9>)r3l+P=6?-EUT>}2w zy26(3yBWm~beeS6J)O>!c3%tM%Auzff&P|%_!+3ZUyqqg1FpaWDj*?)iwb2J#{-|g z46lYt;SX%E1Q{sN-?2w5K?KkZmg11|#^1_PCb2S#fNqWhI{!^17U9@TCq(|>VLzNW z5c9u@1W|L~{J?k&i}By05Ir5Wtpw{V2G$~iqCAAdHmd=4LXsOOG4OpjYlHTz=34sI z^suGq)tt!Y9m3`v;ms|P&3gp=x9njH9ucTvYiY#l7OZXp@k$2b_Wg1sITrlo4- z)Y0Qh*wnfES*8xk>D8ajm)c0v$(Q=g==ITX ze$8#2kl*lf`!Axl%p-gMWV^WwRP z(=9mNVP{pu*(f+0Z=V&M?O}VzTaPZ<_lGR|L-zfNlb4mzxM<%Ivg{D-JMLw278OP$ zO+z+$--G8DLGlm>q>H2p3Rj4E3&jP9!-yjeBbROydCL(?sbDD$ zTgqlMKXzA!?Nv83=i6u7uN`{f&_d>nF=Vg0o0Io)`HiaURWH?qb1G)cIB=TVwP-C5 z8OsH0`OhC1I7c;W&ujMf&H98bF>N2%5dQ;c!1adMtKjV^&qCA&P#O-kjjt3(hf!S+ zzIwWnw#l?%hTjT5ZJ3ExOnTI|yq~Pj%`LU5twe78bj+^JEQpK6Qdn%>ch3UR~DtmB%@kSsv28j58U}(7EzTE+6_DE>2$Yv z=D^*;vRUJjJ)a~#VmY?l)`snkw=aliZejburGk!$yqeN^|k6(ss(55 zkJ(Rgos2k}1ZUIj6L*}g2)?J)XBXXdl}B9lf~)@a)<{E_(9jijb78!kE; z$~_u&~o67H1bItwRmI(v`ai2Qb+@GcS+fRi0>+p57`M^+>tTEeQ-hWHr@+e)iU;LOWI@uy zSd>|7)8=dQG!p1#mF0b%+`1sv4e^+GNsB9rS}rfbQXGq>f2`#N^0rC_z*&2>d`8-?7)+g*Qn^bd}PbGv4G?iOs0 z6jTcZ)e9%y@Z9u-3wF;O2E{u&IiGoTdVV-k-Ynq1vpH;UxwGr&qWx&dax`Q=`jO2( z+cP(C_3(U~I70=;_ONZoot;M(ZAb3uwCNd3wj&?fizD_ef_=+^B~sBURJ6YNRAhIr zfd80@_TG@CH)QXxWXc@%@UW3C#k#dS6*-b)r zQ#gA^B)d(>ZhP}fce3{{8}!*}Kh54htNF3jHkWy$@Ot6A_fB3F_$28Wv-;-`E@khB z--IIsB!u8tPGKawO31Fdt$#mzJ96hD_x4%MlHGBw?P^=ZUMbis7n<(ax3Lng9De@r zQucN#AsZ#+rrk^9Ypffeox+(NZApH@jY>{ZS7qHA|(pUy2sHp3m${rna4T5I~8W6k*-tP<0 zf!UO{+J)nA5;2SF2YnZUQ42dCJLLf(7gSP(u_KjUU-0}Gf41E_Fph&2kfdHO{{rQ< zBBpa}bQD7Npfh{AV!P0&5d<3PMg*c3qS^+;j4>Kcj4&%BXdo4&ns~~hY?Jyai!udA z9r7k+WD*JUzm(Iw+qhgVQ2G<1@znt-7}`26l|S2 zv}DP?a^%vHxvEHhrI25F$5N#T*)HU7f8VkLKwVe?(knfCc5=}I&b-#N>%(*qTl19* z9Sf5SXG7-JWdj0#Y*As>>(MlZnfWAmUXu&A280tP7Wsv0`&_mCZ5%&KA{{|_H>-w| zRpC?Xl_i5O>ek6G(=7P**b~f@d}{kzX?98Qeo-IiGZNc`sGA{@UPyXY7r&1cDu7W5LJ9Pe2ZcA^E5g zqT@c2^NAWEgFeoD>8G$qW3oTNfst{x1Vl|Ik&y`mF(N0L9%HWiZ~_kIO^UeLNufCb zdE#_>pQIu%BYANSi9fx;s42$Hffw}9sZ#-3^V#G{IGUC#R6m-Qqs=sLej7z#-efwi zIj=fj$ctod6Ee4jGaF`7mU4^cp1E#{i!eb{xRo31^;%=XVN@ z>WF=pVBZzC?+#n{FhQ5uXTCKVGUi^IT*8%_on_pYv?kk{vgB9ONQROpB7lFMoLO>yha94`_}?Yx z-;wht^bg%)?~4wIqzel3@h6E-;SYf}tcmvs~)Dn*d@L9Q9n zZ}}#}t_L|rL*6n97x#LsErV!={18aevrONWEcfQqZ**Drr9$6)Ri&RVP!8nmB&52I z+E5nTt@}nv(7$6K2tso*fk6axFA`9f*_W=3grzz~LWg&54DCM~)A89@>aWK&f}%O% z6c*bJGW!@Lyg_jTMX-$`u|1BR=Q{_+2TuB-iiQ*-U0_8j`e4sV@rzgB4p9i@%z&IC zwi8U~Cy{@tNVJu3#UAt%9sk)Q=5oPY4h^)ByM58z!KNzlH9;AN?9CxdvtVzQnVl@K zF=S~J?2T+DOr>EHAvMpAG80CH;E$2D|J!F6mDMDU<4gyURfdh%hBj{?M9rw4J=j*M zZ2Q$EjFrqXuP)AP!?Cfx_tOICZISkHrHcr@EviRbuD9 zQ=({+3RJ`r)DT4UqYA7ZaqW<$9Ot#uS5JqEc7=1=-h5QZ**jytn^zpks}u6-!g=*G z8Av>v5wVpEw(^C7i2I0u|F$DxOYc&Km83UpvVf9pC9Ot)!GPmUcs9z@{lx|DTbE4f)4}g~0wfK>?4E^Iyo>M-B~mG@WT)d&Y++PM`L9 zqvBGi)BEbJwBuN@ZQ5+{B*Hqa`R47jIgk;S$q{mR=XB<)(4X_nOtW|=w>b5zu ztw-3_gH!))y^GePA>&cOdUWML%0&0?p}O@3k}Rhv3B!#Z$%^5DwTL<;?~23T!igS3FK zM{SPMToi|1otyi)z=Hod>cg0C#Y-{V@|fRDRULC8b+6|6*Ed6n+ zK~D~31QKble~y!u%~IL<`2WQtB5Ku1%WKeQ(X52|dZ_p46TPd<)}n;jN)sOTdrk#C z6QgHG$Ig#V)vq^pE3!O=1_Yi=nz2iH<#9Z|a?*bP@yPaGVSDeQtv7Zi*!!UZuUixG zI)e)pt*-x*;FBmEevF)PatLDh2jqN0&i_M>j$q43j)k109q`{#5^~DvLr8^%cEEZ= z(>)89Vu>qu%^jMv-nN7c&ah$keQl~CjrO^F?vd|a)*2~O49D2c7fH$cz?Nbtd|)%e zallz7N8FE78wT+wKLiByEdLgq_|De{i+GAXryjqd{Uz-Oq5nl%U@1%sER|`28JRH) z6EkL!#*A64s>$U|M_!vZ18%0*3OCDZgKPI@!gWk$x$UCyi54X=a`GzYQOqdLg!k$BMHa6rjxN`qM7W8!l4)9%=SYmsblBCq$gs5kz$s|LECL1$nORsIcy7hfy{>tXcDDh8_ zSlL{)#>Tgs>yfBI>H9b}8b!-ZZL_LyprE35wM~nKnkNeqp7iO z5p^|F_yK=_4%gZT$udHhZ`6;yhW@O7SkhdIYQ_`D5K$e~9|%nNJPl2v{8PNAApt5y zP0$cEv!1>SJ5jxtdgtoHmQB!5O1EEm`qI;6F{D-~sC_@ZZpo5W@s=W(68eQAm-O(qnQfSYi> zu%RdihwzCc3=zM3Xe>T8fCFCB z45LW?(dMY(oF8Yv?lf_)1Jj#0A*Sq>c|j0n>ACB#^Wf7I|IFydf0wfZwJHl^Bez&B|C5 z787?!1XU|m?bS3Rl+GUtxv0b@%>EiQW zn=f6omW7OEA!`}dD^tNzZr+Xb>*+697qknRq1>uVJu{uN4y<<$=UnD@nrC{iHvjCQ znVHQf5v(N-IGriyBdhbuS1)}vY%QMGAS_hbC0M(bY}xm9oUL1P&wwPN6x`c|Iqj|7 z+pRSnmAZG_8n_9ogp9v7vJV3arWuv=NZ4pS$%pvat&}5%zf_<*D9bOa$tp|vKNEJN z#Q{PH`|#ok&cqdh(JQd>3KMpZlNlc-H45D(->BCUfL+LeVW_Cb-hDj>dU{Xr#RLYl z6iI@fF))f}Q3IoR?DLer4f(OBiebE_8!gvc!j>&@tk?Q}dO6`c$Ou5Bz2jq#qPE@#ZLHBcKksPb~8#_n2sU=^`MFC;x1Y$+ib zJV1sTHK7CvaYDFW4|J)SLfZJYU(tF8D<;92mUZ6OL-9$bVPF%25TK;`!{BcZ0-`CW zeL*0esF|#Foq;jNVQ}gXP>dP!0Hd)bGiH$r`}k;H%&M8^(Z^s>)zj0v|LE}}?PP5= zYS`a?xUV~EhT({S?;tBn($ZjfFy*8VC=&Kg#d@NKfm1m2NLJ7g4-Xa10h$H!9l`0s zRQU?@sG4q;Ap8Z(eDN8jA6;@m`j<@yq>lQyk{&4 zSxfF3Z4qOkU@V+_{El%;9M6`oVfac$eCIHg*PA0id~1*cst5b%0(w@PFL7k3HcO=0 zwi=Wva?6NPtdHD;-15?{h>v+$>2$!7^hh%Vgh93xr6$CyAT4sGbn&;;iWrht!5NCL zP+^Qq`Bx(OUs&uU=Ef^qbU!^OSB-&_+>rx}&hbKwIsHOA7@xz`@Pxs^9 zy(gkJ_N(J~cl#kRgfF5*`tt+O(2Xx<>Ahop6N6_YB7Vh^B?`O^2KnLGDi<>-kwBs= zqhCTT0Xp)Pr}SKtNSKEUwnYk>go389Wjp3JsGamn=`Ws&nO zGT;pXfNk^&5Rkr8=jtlez1!;Qs?+_T)X-I(`hz+{SCVP~tsrdM@DV)g;RXdmnQ%(d zBSGtXkAH zO~h`Z8S6I)iY_8IpvaMPZU5E%bItRew{j!yeS&*m*xeC!bVeLKf}wrO_EK6lSj|3#AME?fP);_L-jb40Fo#$s-#8x3zJQB39+3b8W060g+;8xglDnk{$&uU`vX^ zPq0R4$nk)tkilojA%-d4I1>ds30d%VNR7UTWg8+Jnn{n;YU}~P5{XmyF&-f-k`al` zx1wtq3gg?zx0@WI%J~-fqG^yi?}Rn|(FvH2rT_~#7L5(#G1X`riP-S|G2S1XWbDK^ zTrw+8K1t4E9~Juu6?+Iy5(`39hlbrI-&FQW;u^t!iQEB_2m^d>m{>NYU$I`YzHE%- zRtve+3#aZFx8HTRB93~&QGdHU?AXc7#9sUA)vtz~TO&^B1y+ZhwPY=}U~X~^qM3+b zwlMx*8z7n$V5hP$Dq>ZD5(IY|>PmVNfKZKVAg5QuhY3I^<3u&7@B|=SlLTI+oftEE zhKOO7^e8|Rn@>srX%RXDNqXWytI*>sOiF~G3F#QNMtOuXJ8Y_>me$taruQ8Smr}4>;BAsrCcsfZBTCsL7>4vJ4vfw9jt=T3B zXqkF(uIC##`veqIKqK|RFf=I?XC(c0J~{!jAC!>phVUL68ygl)blAm+#K3$xHLh+* z%8R_`$u9XFh)AxAB@s+p(BJR&(k)vayim>#=pGJzJHbaDoik5!zwN#xB z+*%CY$ZJ#Ze^q#Gir7Ue;cIYK$DVKL|HAmTG0tdM4R>yb>g4THVaIM6cg}lX zJM+q!u(K}W+$A`7g`K;Zz3osQteV%}u8uSu6dDdLS`UVd2Se6_Snxx6(D1BW7>?BL z6>9e`TK9&GdqdW}A6l|QIdy^s1}Y0gF)0#stCr0aRg(Z!UMR0tu-4sfi!?qWG{y|H zg{+UT%ngF2k!3~_lDtSK$qQ2&**K~r1)`i#4tOgp^AVx($fAXGKTT;%mLng+J~(VK zyBD4kEx;*^d!o73FwcIwFj9X=s6Vu5Jrpt?3Rw?*NUbFM&7@P>B$PHSTAM<~rjWIX zH9?AL5lX=gZwVP&1Z&Gma#~(WNUnB0a$2!i{8L*>)E6>*Gd;}ei@d;yVdP4S1UNL> zwbYLwhf0`|J!*@HVj){$1xDmPtU(DSp^AN|MWnikz~lGh6GZeuvc}yP{2!|XJP2m-sc49u5FPSf=FFA9sox6JO zg$tyi2O^17hi*e-XcwscjnYGB&`4aH2SEUwg1|36YG=T|L{Qd(8u{#0k>aX3EeQ3s z&jP1qv#O%Nw7h@Rt<9AC0(26w=@ay(!K_F)!XivUdy3^_aFm!N&RbC^c%y@0z5(0a zDG;*L98+u6T!!*C6t%5-vWukYQz~TZdOzNWaz*yNCTidZMo;^|ydWYXs_`cr1X)OK z+jG(%92p3looZY`&y$_!fm5d-g&mxX-w2@0^m77sqIv-A#arf&zdRYX@4P-aw|U{w z+naAbe*2l%4}Yh9Zc?!C3|V&me9^u$K*aOEOfS{HtS=R%O5N$7ZS^Oe^4PR(kG1Ux zny=W+Sd{ISruKQXluz2)&`2=FK+yn7jLE3@Xi+sVaT~6P{uzD!W%l(Cv>H<@&QP=R zRnG@DuxS6-;ks+djaaa-*4<8#>|tV^&rX{$q2$~m+W#ol@HWBO7IyAge-}hc5ZeXe z-ogZc`$f<9|K7LeYLq%1$_>SV*pL&lBM z{)}%zCI-XK?F?f?tYl+u!5wNkC{*?=T6;pqo{$x6J`l@W1?$#@ouTcI2vu=1n#D+Y z4hWUXoq{4yk5CmS_SY#*swbXD)q=B(zX;zF)#BxbX;GxNh89P%N2ey~p?1)z#P1YD#O?Gt>=>A`%rMN5NW%<^z%1uPmEGw4V(Vk{DjMG<3@U~F2l zWX9;^hqM>R9zVN)g4SU#tj(Y$YLJm8x6pG+T37}IX1Xjc3cP(Z-)DT$O=DNK90O8w0La@{K7jbbDcvEI?sz}Dc?oJp-B-I`JOD9(oEFFcWHGx0M>IHwvt?eHzBE1e z%%as5GSX!?h|XyVWjBAy(uFeJpOWyv5tt1){Ar*S4ZE4snWp>GmX1A!x0^FN8dBde z8sUG(W<>Bi4Tg?g=69M?DP)hKGu8C2E*0VLrWrcziE_yDiu+lvNfPN`ua*SVz*T@N zKB_A-p~R#|!TecgDk{n*m`Hl!Q<|}(R1W{7sl0NEt(+i|v^jS1d>Y`%f8)0_bgjld z)XJ05_ox|0G9=I!8BTFPJ%AhpkUq(vHCu)rMYhDlUn6MEpoeZ)cC6TiBeb3X?{VZ} zhRYxbfE$UQADQbAGK%NRgp94RyHdz>yl6Lmp)_o*ozbhwso1U@ymavLBTVWflDt~391%4SlFG!04>xmrc?V6xTyi#(LlOr34syt<9#2Ym zd@nhDk&>eJvi zG-@P}lQe65Mzw=HHN(U{v)6D-zHi8zswm6Cv~kkey-FFlcgZT%LU$}l{(f6R4DMZ$ zV=~$OOIh*zmty!2mo`q7G3glI=Zb~7a4VAwMjIxEg8>&=vUMRXG zx6#li>B2CWOOURi`r}GFyHPt&CSX>w?uo!)L`E$etT?|DG(ELB>5=ebE72QZ|Zt+E8ut0N+wF9yp@Q5nL-&_+($mP(vy%?G!@oB$OO<$#a#g6 zl1_sOdRpY##Uv6S(v=`IBaQ};Dydg=VH|!sP92rbXV`9LV;DMeK2OsP3w0DxAbKlDQ znPrg7$Cv(7AAdFZ8U<{qZEM$9l$CJ969^_;OsEDe-=ir;R7W(U`+{$9B1l7V3?sq+ zK4KCUTDHI8`;9^eLY(650Ce4-;N>^PJkXph3bYz!+*!Axm0e2x1Qj>Pc(}C1lXGCA4Q0 zMe~@Do-CgS?W_~XE}9=v1DI_TTM7rd2MUS3NeW&GGDO5o6{vwsOjz$R0vj(fe_rg8 zb>+gP3v-$qrt7Ac%wg-61WZW-U_phPlcC!j%@EWy?PeV9~Ax zENWy2iBQJRgaK?SidhmGQX&*_saX$mR1wOlC1*D}k$)OKNp}pR(DEB7QDM61vk)v9)+-~IMxGywWNa4bDoxym`QqT*X3=2PJERLFCACK z>Vd8p()zB$k3#?bjv8!EhpyKnA(eQ&sM z7tV0<(q__WTTw7Kl8Cf>-S%W2U8=|91=HQb|7&!Sf=Nd@BgvL-XtyEq-58Uumy!@& z#{Uud{#SDTjGQE{+-np@PAhc@5!cUgKi1cMxM9XzS(06xC1XygxC0ZK?m5@up7Rug zVb^jheD}ylx0@f(JV-MbD(+`U)?oF9YU1|6dMteRVP{7)Dw}42X>ArvzT8wpHA4}s zxr1n_{GjRVSti*0Jlwmr?cz+P;Y@?wcin}HlVf1jGcrERl#&^s50E%gr;iVUs3gu> zfS3kPh?bUJX9mvsTn)I00Fwa<{Vsn()<8AV`qJn;@g(j|^iocY`UELF3TG@Q2L{ig zNi6>$>UBXB!gb2e2XN6iE(H&`PWlE1K@cO4G zSMfXw0LvSt43L9g=f_+kcR2tyBiWQ@!M+OvurL|`568uhV5?aX0iUmxdCmlb_hR5neBv8jJw*=}Mw>63NCv3V26xfpS%i~#A(AIHPNKmja zzm3NH>RFgoYd0@}qj{BDsLs)p*WQPK?Df`O5K*)dNqSyKPI}ZXu95gjI9QT3v(*GP zyqchVBy>v_6=kQpvC>h#H2=lwnEM41F&UL~vwQLgpYvpkNwgqL zwk2aULDYbHJoQoCsfkh1-@u}!noAI(IPnDX`Sh& zo3SQdXnoZ_|Je8P7h!eU0>jf)ceC?fZk(HVsr5(M<+xDDSvdFT)eH0W-?`BrOXYI5Lb(E$^tFDcHDuXzwe^$i zL`!)AA|c;4b(lJ9b?@3U$*nbXHm`CvybqoF&uOA7RnReMRE|78$E4Bqep<5Tq#=?w zz^03I=*&(?*d!rJI6g`E_vj~^3F3(fZo^~*^S>wPr@17ZnJ~(3L$I%)fxkzvAKWnP zZ%c%ID6`}8$N4R}+Rfl1Pz`sW0bWI=6pNQ^Ht>U=N=AgOT25*7=E(f5SXRF({srV_eZX`j(l zH7^iw)2ea$D~{hk@YAX1_O4tsyZ{ol`5;~?q$gG^tYb|Eg1!++SjbGe0&I?X>Uo+d z5Ok138j`qp8Eid}bF1Lox=70-q3PG7&=m6M zB89de@VauI{U*&tnK%>I9Y%{Ws%(a4p^_j=R}?GFvV<^o0*z#ZHnd17ivL5JWUq*m zEVt-J#`TQ(;AwlKStOPXX!1X^JUmTT-X#T+$R+73m10G z91%&ei{XNrA7$4pQyrZDER;(ATLQX4gBvGsvNuazcz4hfq`@xXqUyX5jxdUpv05bifNDUH5 zRJa7jY>_Tph9Oy54jV>e_W}2srAp%p4hWUcqNnvl+=`DHNu`KYkzF_?^BMxo*sK@W z{;T@S&x-H!xGFa$H?8kI;nqY`L5q0CeJA%z7CuRmlz$ga)H2Qlr$uW&{7VFIM5IO2 z`~m+cY|oD3DtNLX%ad%W#0RJCKvXjj)trR1DW7a3k;!3-musxE*NsUu{1w68cTg+# zxqzfyS9krZ-7}r{v}x%@cOAJA2i;jXKX|(-w=m= zZWdVI+4&Ds;rqvX2ITuRKt^`1*S2SB-_A635$0W{xSLDvZ#y`p2YC}AtfQ_8`LKxL-rhZU!Q6)lGS!5dCG1wTT2?% zVkml0mW8M%N)c19 z&Z|GI?N6EEPHDiJHuk47UkZFi5*WqBVn(=WOiqv9vxgx_vRb$5aIsjv^=1h)2$t=>smh2o!v0Q~-77QMrPIK9^TpGEqo+W*3z= zS;huL`HGvk1ELEMq6_Mzcr^wmA+Blev`%qDGu4^&C}L$47Ef|ZFIyGTV(%v-wRj2% z`7#^Z5T!vX8Og0oMQSJtB34Fi7TGK(V_Rx2KQoP6&NW;g$L_NSvu_8^CB}F#=O+WZ z$f*qky<-!>TBb@_fs?xm3LF`@5P;no*B)0xWo2W7+dY-t+4|J6Nyw3pRz5Y(-nEdu z`&3&q=+;H`xCA7CO&TkTy-6m=6xE*|9y=LLgSEBTphpeI#$a$Zsz26qtQ&^BPjnr9 z>;yJ%kms6!c`7*Wmf?&Ui;eBui+(LAp0;?@5q?xf?7!4rbupukXsSXt%T|A z{j=>$7TcAcOFfqlEm^Z)w!fM?_f$Bq67~U?oWCLX~0jkJgeh zY=t{v$(IEZPvHxGTZ2Vk2A@lS<8LEgf;B+VnkHw-oaD3Faoh7lDv%vFe*lA#vNbJ! zQ=Fl%wk+cY2)Qh5syL5)JTO|we#^1zmLum3%6tqpGqP(Pl!J7^Mo)Yc7A6X%u=rc* zIcbXs9aL=_qS22kdXWfK;nbWatyNdNOvy*Gu+!RLiS%2|&K1<;Vz>%@7T=bxR=bQB zq%y&X`9;+(_cps$aBAD&*u=2cg_BF1FOGV}b5L>OiHZkf@KXyS1Q$M$bhJKqUTAcQ!l|IE}=MZvWu_o{$35(%FpfVdw#t947Kvgm}ucrpXS|5rHC6v9}I z4dQ!8lf-$lh-gKugeTSJsLA7DS~eb!#CUpE)P?y+#7-5cBOUfd|3EnAKcWdg1I1=c zzm$3@{j&8#XFws*5J>wpyP0#rI6wczfTUbn+ z-DD8qJT=+e-Yu}fp&jAAGqm6pHCzc%^GV$2+!^WGS}J}OwMZV zzhtzKxuA#_O@g)`N7wwcZX7h)1mlRmd3$!6{E}!HzbglB7rK zYbl3UVe@zG9A2%L%O}(+pZ&&*j-R?ta?nHhm_AXoj+4soBR@M&VFSVrRR$@nlpGCu zf9357qU2m`qvUC9m?~Rse2DuiZsU;5j(V4mqF53k67P*^y0Po}t{Z!=?+sh3aFYUV zd#}H?^XkqQcF`>v^`e$Z$ja=)PhOgQ{ws6!a}6_J!x>8LE47Q(Z6V_}!Mg3AEmR9F@CkGhV#JB_Dm=f8i38x5X%kct zd!*?SXKbi}zm!Tj4MbCF%I=GU|91%~(K5%bdVq@c${Ao5k3r6R3Hmp$1^q0^CW8D1 zl(-t?ljJ(nnR&V7hWonvB}gaJbu)ss?7z#tj)SV)*F1DIRZva5qXDMtb@Yy=%><%Z zsvs(p^6z0Be)S-FkqoS8&q)T2VkVQ~fE(w+sgd{+2utgN5|rJzspJ!YY{KDq9GZz# z6NxlNxe=u%X=1zJk7|4gjM}J(+(9||`LWUUkb68?8G1f;)37>nr|SW*Lwo)ZZXB?d z%nv{}0~#73>sG*iC=~a@Y+G!<`}HPQki!UHEiap@C>NE*gZiy&IysRx5W3xeGEX@`7J|vFEI>=fYj~z`_ zG#)hKtD&5GTAe8~HYndd^fTDjk66XKsx+p|A6sx~H{PEHh^T+v*2ev@y**3!cBTfd z+or~1j9S2%4f+RT`tfXk!N>=q+<7A7cr!WB(#A4H4%u|!o#f=fiKfR$|3M$jS$NzU zp3#EH%RDxVn zDx@w`X^rBwvzp6KNGDQmOYa^UoTxq!92<`s#z|V57!(715WE5U<&CDsZZwIeQlfE$ z7;%YK&_6oiW0w&|Q*gfogvz5vuRlO)&fchLU?K?RL?9nlJYPaMrIeg4a0oGtjpL|k z6nv1=P>i5@;y+pK@=u#omlVP?MSKhDf;i_SQ>AD6kHnZA2u=hT#T`vO>4)9y;2E%I z>0>-(S1W4v`T~QzpK0*NBnYB5nD_C-E^6_Bfx|~r2Lc|i9~bxhl+YXFra9>^R!<}W0?Z~U zofXg{8WqT9B(AbLYVK5t62*x+d&y~9Y3X?uU-K-k0XF8R&15{t;x_Hmgp7Hy-w(53 zIV<9*5*$?#N6k#vlHGZ&?P}W#duF=82A=cI@3=mEtMiV1Tgb94WZ#D9xs+aZ_LYAhGy@~Q0A$s>e3HHX2 zr7>iOwNe1uiUX!i3RMuK6Wc`M@p?)s2JEL6m9!{=^guykYz{6 zzJqGApphYWt6+zOX=}*dy0UgL>u#Z_{`-x0?9CxdbI9II%3c*$D_*Fa>4d7{%-1AY z*~YMAD~Z15UYou;{o+?cwzAo;%J)G-v_s@Q*&1isKSbsAFT%`h;q1=)HC%S>O45mD zA0wT91U7||o2|CoY?Qr$WedfM&xXrUVI+ZbY?jJd8>uBDoHdc8LKjwzgRE$~%!QR& z5xT z(7$Fn#Usn?24x=mkW2c8_#4`sDYs;^19Tx=T)69t^dgvuhN04UJ(5ov(UrZHY~pR5 zc4LwIrM0_EYHj@e8QLwMimVtH?XP59%Hyq;d=k{K3;%vy$7TI?f1z;!JklN~>5;}Y z=$0a;WkO0OQ4K>?Qf&Oa!JKQ=StFJ9nd@95=SsV7v)ap#sGB$h(e}ry#=O01%+#te zUt2Y1%c?PM^_a^)T_ybQ#Bf+_p3?2zyO*ifM0F#9(^D}uxLSpkI#DMFl#4`*Jwxh% zG17@izI}#Vpczm3PvZ`keHb|YcZrUB7rtm=_wnOLk9+#Mk3Zhi+3o2%+TG{rJ$k~^ z{iUA16H&Vu+uqsP-Ph;o>hA67?!q-xz6(LVfoNAEJmdF6GFc~nJ4O69MZ84L%jD2{ z&Qyf>T8bs>9ins;qa|w)7S;2xG%49?WoE+mNHs*YgCiid#XT#31DOLvBrrlyA_W~Q zNI_7ZQ=8YO1d-$vnilv?)I=s6vLcou!BQleaEMs;36_0012dJuu6uD=q-YxmsYuaw zp=f)gs7EM*E#`ucto9f8i&SFRQ3Gp!5o@DhZ46ttuQ2O(Cw<%P4)C2KmJ-2IGT*dN z_Db6w%Qj}=??Y=Q8J8hzMDzOjiB~L(*6NV4I%KUz)%LtdR)vsNvET@2)y|~e&B&e| zn9H8mgfmLWv?WgTWYd;Qmfb&W{0E&jBaMvQ=aSvom(N9t8-?P=_cNQ8Y)(>!x?^*p z(yZ;Dq*A&xE+gwo|E2zQvqo$#!RESSD^X<5;H)_bjj3F}F%3d-!~3v-o6wl14K*g7 zHK8$L=6e~O%}JIxpZ_W}w4V50A#Rxo8EZn;nk8#a#9As?VQmo%yoD!TN7VL^aeK(R z9Z|6?j``Ey+k|^=LdM#VwKgG(W8w7ch}sb{?tuHrAq{Q-C4-Egn|Eyul z;cDHV=VTr(HvIWkBm93+vgJsM?uRLvhj$u&=rodl&z2*_y1y*RJYqNeWup=PziKfY z)~3E^FdVj;-b4>-m&umCL$U~zWf+fF-{H< z>--jSn8g~1|sswk|s4CG20c|ypM&qKh$b8rrFVD4li(5wH+FATkh%c`+?D-ao#K9 zT&2rghQ{%*x=gccw(cQ^*TZ9)$F-W8Sn(rKKOlHL!W zO6kyOswpYG?;X>w+Q^666B$%J3#Jm$X6!qKr3bd_{$jh~z~0pV4-(di;{X5v literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_aix.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_aix.py new file mode 100644 index 0000000..10934c1 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_aix.py @@ -0,0 +1,142 @@ +#!/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 psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests 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 from psutil.tests is not enough. For some reason + # we're seeing differences of ~1.2 MB. 2 MB is still a good tolerance + # when compared to GBs. + TOLERANCE_SYS_MEM = 2 * KB * KB # 2 MB + 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/.venv/lib/python3.12/site-packages/psutil/tests/test_bsd.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_bsd.py new file mode 100644 index 0000000..2786c34 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_bsd.py @@ -0,0 +1,593 @@ +#!/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 psutil.tests import HAS_BATTERY +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +if BSD: + from psutil._psutil_posix import getpagesize + + PAGESIZE = getpagesize() + # muse requires root privileges + MUSE_AVAILABLE = os.getuid() == 0 and shutil.which("muse") +else: + PAGESIZE = None + MUSE_AVAILABLE = False + + +def sysctl(cmdline): + """Expects a sysctl command with an argument and parse the result + returning only the value of interest. + """ + result = sh("sysctl " + cmdline) + if FREEBSD: + result = result[result.find(": ") + 2 :] + elif OPENBSD or NETBSD: + result = result[result.find("=") + 1 :] + try: + return int(result) + except ValueError: + return result + + +def muse(field): + """Thin wrapper around 'muse' cmdline utility.""" + out = sh('muse') + for line in out.split('\n'): + if line.startswith(field): + break + else: + raise ValueError("line not found") + return int(line.split()[1]) + + +# ===================================================================== +# --- All BSD* +# ===================================================================== + + +@pytest.mark.skipif(not BSD, reason="BSD only") +class BSDTestCase(PsutilTestCase): + """Generic tests common to all BSD variants.""" + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + @pytest.mark.skipif(NETBSD, reason="-o lstart doesn't work on NETBSD") + def test_process_create_time(self): + output = sh(f"ps -o lstart -p {self.pid}") + start_ps = output.replace('STARTED', '').strip() + start_psutil = psutil.Process(self.pid).create_time() + start_psutil = time.strftime( + "%a %b %e %H:%M:%S %Y", time.localtime(start_psutil) + ) + assert start_ps == start_psutil + + 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: + raise self.fail(f"psutil={usage.free}, df={free}") + if abs(usage.used - used) > 10 * 1024 * 1024: + raise self.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]) + + +# ===================================================================== +# --- FreeBSD +# ===================================================================== + + +@pytest.mark.skipif(not FREEBSD, reason="FREEBSD only") +class FreeBSDPsutilTestCase(PsutilTestCase): + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + @retry_on_failure() + def test_memory_maps(self): + out = sh(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) == 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_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: + raise 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 + + # --- virtual_memory(); tests against muse + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + def test_muse_vmem_total(self): + num = muse('Total') + assert psutil.virtual_memory().total == num + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_active(self): + num = muse('Active') + assert abs(psutil.virtual_memory().active - num) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_inactive(self): + num = muse('Inactive') + assert abs(psutil.virtual_memory().inactive - num) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_wired(self): + num = muse('Wired') + assert abs(psutil.virtual_memory().wired - num) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_cached(self): + num = muse('Cache') + assert abs(psutil.virtual_memory().cached - num) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_free(self): + num = muse('Free') + assert abs(psutil.virtual_memory().free - num) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_buffers(self): + num = muse('Buffer') + assert abs(psutil.virtual_memory().buffers - num) < TOLERANCE_SYS_MEM + + def test_cpu_stats_ctx_switches(self): + assert ( + abs( + 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 + ) + + # 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() + 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 + + # --- others + + def test_boot_time(self): + s = sysctl('sysctl kern.boottime') + s = s[s.find(" sec = ") + 7 :] + s = s[: s.find(',')] + btime = int(s) + assert btime == psutil.boot_time() + + # --- sensors_battery + + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_sensors_battery(self): + def secs2hours(secs): + m, _s = divmod(secs, 60) + h, m = divmod(m, 60) + return f"{int(h)}:{int(m):02}" + + out = sh("acpiconf -i 0") + fields = {x.split('\t')[0]: x.split('\t')[-1] for x in out.split("\n")} + metrics = psutil.sensors_battery() + percent = int(fields['Remaining capacity:'].replace('%', '')) + remaining_time = fields['Remaining time:'] + assert metrics.percent == percent + if remaining_time == 'unknown': + assert metrics.secsleft == psutil.POWER_TIME_UNLIMITED + else: + assert secs2hours(metrics.secsleft) == remaining_time + + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_sensors_battery_against_sysctl(self): + assert psutil.sensors_battery().percent == sysctl( + "hw.acpi.battery.life" + ) + assert psutil.sensors_battery().power_plugged == ( + sysctl("hw.acpi.acline") == 1 + ) + secsleft = psutil.sensors_battery().secsleft + if secsleft < 0: + assert sysctl("hw.acpi.battery.time") == -1 + else: + assert secsleft == sysctl("hw.acpi.battery.time") * 60 + + @pytest.mark.skipif(HAS_BATTERY, reason="has battery") + def test_sensors_battery_no_battery(self): + # If no battery is present one of these calls is supposed + # to fail, see: + # https://github.com/giampaolo/psutil/issues/1074 + with pytest.raises(RuntimeError): + sysctl("hw.acpi.battery.life") + sysctl("hw.acpi.battery.time") + sysctl("hw.acpi.acline") + assert psutil.sensors_battery() is None + + # --- sensors_temperatures + + def test_sensors_temperatures_against_sysctl(self): + num_cpus = psutil.cpu_count(True) + for cpu in range(num_cpus): + sensor = f"dev.cpu.{cpu}.temperature" + # sysctl returns a string in the format 46.0C + try: + sysctl_result = int(float(sysctl(sensor)[:-1])) + except RuntimeError: + raise pytest.skip("temperatures not supported by kernel") + assert ( + abs( + psutil.sensors_temperatures()["coretemp"][cpu].current + - sysctl_result + ) + < 10 + ) + + sensor = f"dev.cpu.{cpu}.coretemp.tjmax" + sysctl_result = int(float(sysctl(sensor)[:-1])) + assert ( + psutil.sensors_temperatures()["coretemp"][cpu].high + == sysctl_result + ) + + +# ===================================================================== +# --- OpenBSD +# ===================================================================== + + +@pytest.mark.skipif(not OPENBSD, reason="OPENBSD only") +class OpenBSDTestCase(PsutilTestCase): + def test_boot_time(self): + s = sysctl('kern.boottime') + sys_bt = datetime.datetime.strptime(s, "%a %b %d %H:%M:%S %Y") + psutil_bt = datetime.datetime.fromtimestamp(psutil.boot_time()) + assert sys_bt == psutil_bt + + +# ===================================================================== +# --- NetBSD +# ===================================================================== + + +@pytest.mark.skipif(not NETBSD, reason="NETBSD only") +class NetBSDTestCase(PsutilTestCase): + @staticmethod + def parse_meminfo(look_for): + with open('/proc/meminfo') as f: + for line in f: + if line.startswith(look_for): + return int(line.split()[1]) * 1024 + raise ValueError(f"can't find {look_for}") + + # --- virtual mem + + def test_vmem_total(self): + assert psutil.virtual_memory().total == self.parse_meminfo("MemTotal:") + + def test_vmem_free(self): + assert ( + abs(psutil.virtual_memory().free - self.parse_meminfo("MemFree:")) + < TOLERANCE_SYS_MEM + ) + + def test_vmem_buffers(self): + assert ( + abs( + psutil.virtual_memory().buffers + - self.parse_meminfo("Buffers:") + ) + < TOLERANCE_SYS_MEM + ) + + def test_vmem_shared(self): + assert ( + abs( + psutil.virtual_memory().shared + - self.parse_meminfo("MemShared:") + ) + < TOLERANCE_SYS_MEM + ) + + def test_vmem_cached(self): + assert ( + abs(psutil.virtual_memory().cached - self.parse_meminfo("Cached:")) + < TOLERANCE_SYS_MEM + ) + + # --- swap mem + + def test_swapmem_total(self): + assert ( + abs(psutil.swap_memory().total - self.parse_meminfo("SwapTotal:")) + < TOLERANCE_SYS_MEM + ) + + def test_swapmem_free(self): + assert ( + abs(psutil.swap_memory().free - self.parse_meminfo("SwapFree:")) + < TOLERANCE_SYS_MEM + ) + + def test_swapmem_used(self): + smem = psutil.swap_memory() + assert smem.used == smem.total - smem.free + + # --- others + + def test_cpu_stats_interrupts(self): + with open('/proc/stat', 'rb') as f: + for line in f: + if line.startswith(b'intr'): + interrupts = int(line.split()[1]) + break + else: + raise ValueError("couldn't find line") + assert abs(psutil.cpu_stats().interrupts - interrupts) < 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") + assert abs(psutil.cpu_stats().ctx_switches - ctx_switches) < 1000 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_connections.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_connections.py new file mode 100644 index 0000000..5ddeb85 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_connections.py @@ -0,0 +1,566 @@ +#!/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 psutil.tests import AF_UNIX +from psutil.tests import HAS_NET_CONNECTIONS_UNIX +from psutil.tests import SKIP_SYSCONS +from psutil.tests import PsutilTestCase +from psutil.tests import bind_socket +from psutil.tests import bind_unix_socket +from psutil.tests import check_connection_ntuple +from psutil.tests import create_sockets +from psutil.tests import filter_proc_net_connections +from psutil.tests import pytest +from psutil.tests import reap_children +from psutil.tests import retry_on_failure +from psutil.tests import skip_on_access_denied +from psutil.tests import tcp_socketpair +from psutil.tests import unix_socketpair +from psutil.tests import wait_for_file + + +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 exlucde 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 SUONS") + 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 + # commenteed. + # client.close() + # cons = this_proc_net_connections(kind='all') + # self.assertEqual(len(cons), 1) + # self.assertEqual(cons[0].status, psutil.CONN_CLOSE_WAIT) + finally: + server.close() + client.close() + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + 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 becayse by default the Python process + # creates a UNIX socket to '/var/run/log'. + if HAS_NET_CONNECTIONS_UNIX and not (FREEBSD or NETBSD): + cons = this_proc_net_connections(kind='unix') + 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 + from psutil.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/.venv/lib/python3.12/site-packages/psutil/tests/test_contracts.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_contracts.py new file mode 100644 index 0000000..55f3a5d --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_contracts.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Contracts tests. These tests mainly check API sanity in terms of +returned types and APIs availability. +Some of these are duplicates of tests test_system.py and test_process.py. +""" + +import platform +import signal + +import psutil +from psutil import AIX +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import SKIP_SYSCONS +from psutil.tests import PsutilTestCase +from psutil.tests import create_sockets +from psutil.tests import enum +from psutil.tests import is_namedtuple +from psutil.tests import kernel_version +from psutil.tests import pytest + + +# =================================================================== +# --- APIs availability +# =================================================================== + +# Make sure code reflects what doc promises in terms of APIs +# availability. + + +class TestAvailConstantsAPIs(PsutilTestCase): + def test_PROCFS_PATH(self): + assert hasattr(psutil, "PROCFS_PATH") == (LINUX or SUNOS or AIX) + + def test_win_priority(self): + ae = self.assertEqual + ae(hasattr(psutil, "ABOVE_NORMAL_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "BELOW_NORMAL_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "HIGH_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "IDLE_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "NORMAL_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "REALTIME_PRIORITY_CLASS"), WINDOWS) + + def test_linux_ioprio_linux(self): + ae = self.assertEqual + ae(hasattr(psutil, "IOPRIO_CLASS_NONE"), LINUX) + ae(hasattr(psutil, "IOPRIO_CLASS_RT"), LINUX) + ae(hasattr(psutil, "IOPRIO_CLASS_BE"), LINUX) + ae(hasattr(psutil, "IOPRIO_CLASS_IDLE"), LINUX) + + def test_linux_ioprio_windows(self): + ae = self.assertEqual + ae(hasattr(psutil, "IOPRIO_HIGH"), WINDOWS) + ae(hasattr(psutil, "IOPRIO_NORMAL"), WINDOWS) + ae(hasattr(psutil, "IOPRIO_LOW"), WINDOWS) + ae(hasattr(psutil, "IOPRIO_VERYLOW"), WINDOWS) + + @pytest.mark.skipif( + GITHUB_ACTIONS and LINUX, + reason="unsupported on GITHUB_ACTIONS + LINUX", + ) + def test_rlimit(self): + ae = self.assertEqual + ae(hasattr(psutil, "RLIM_INFINITY"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_AS"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_CORE"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_CPU"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_DATA"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_FSIZE"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_MEMLOCK"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_NOFILE"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_NPROC"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_RSS"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_STACK"), LINUX or FREEBSD) + + ae(hasattr(psutil, "RLIMIT_LOCKS"), LINUX) + if POSIX: + if kernel_version() >= (2, 6, 8): + ae(hasattr(psutil, "RLIMIT_MSGQUEUE"), LINUX) + if kernel_version() >= (2, 6, 12): + ae(hasattr(psutil, "RLIMIT_NICE"), LINUX) + if kernel_version() >= (2, 6, 12): + ae(hasattr(psutil, "RLIMIT_RTPRIO"), LINUX) + if kernel_version() >= (2, 6, 25): + ae(hasattr(psutil, "RLIMIT_RTTIME"), LINUX) + if kernel_version() >= (2, 6, 8): + ae(hasattr(psutil, "RLIMIT_SIGPENDING"), LINUX) + + ae(hasattr(psutil, "RLIMIT_SWAP"), FREEBSD) + ae(hasattr(psutil, "RLIMIT_SBSIZE"), FREEBSD) + ae(hasattr(psutil, "RLIMIT_NPTS"), FREEBSD) + + +class TestAvailSystemAPIs(PsutilTestCase): + def test_win_service_iter(self): + assert hasattr(psutil, "win_service_iter") == WINDOWS + + def test_win_service_get(self): + assert hasattr(psutil, "win_service_get") == WINDOWS + + 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 + ) + + +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)) + + +# =================================================================== +# --- 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) + + # TODO: remove this once 1892 is fixed + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" + ) + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_cpu_freq(self): + if psutil.cpu_freq() is None: + raise 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) + 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))) + + +class TestProcessWaitType(PsutilTestCase): + @pytest.mark.skipif(not POSIX, reason="not POSIX") + def test_negative_signal(self): + p = psutil.Process(self.spawn_testproc().pid) + p.terminate() + code = p.wait() + assert code == -signal.SIGTERM + assert isinstance(code, enum.IntEnum) diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_linux.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_linux.py new file mode 100644 index 0000000..f4342d7 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_linux.py @@ -0,0 +1,2292 @@ +#!/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 psutil.tests import AARCH64 +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import GLOBAL_TIMEOUT +from psutil.tests import HAS_BATTERY +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_GETLOADAVG +from psutil.tests import HAS_RLIMIT +from psutil.tests import PYPY +from psutil.tests import PYTEST_PARALLEL +from psutil.tests import TOLERANCE_DISK_USAGE +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import ThreadTask +from psutil.tests import call_until +from psutil.tests import pytest +from psutil.tests import reload_module +from psutil.tests import retry_on_failure +from psutil.tests import safe_rmpath +from psutil.tests import sh +from psutil.tests import skip_on_not_implemented + + +if LINUX: + from psutil._pslinux import CLOCK_TICKS + from psutil._pslinux import RootFsDeviceFinder + from psutil._pslinux import calculate_avail_vmem + from psutil._pslinux import open_binary + + +HERE = os.path.abspath(os.path.dirname(__file__)) +SIOCGIFADDR = 0x8915 +SIOCGIFHWADDR = 0x8927 +SIOCGIFNETMASK = 0x891B +SIOCGIFBRDADDR = 0x8919 +if LINUX: + SECTOR_SIZE = 512 +# ===================================================================== +# --- 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: + raise 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 +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemVirtualMemoryAgainstFree(PsutilTestCase): + 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/ + # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + # Newer versions of procps are using yet another way to compute used + # memory. + # https://gitlab.com/procps-ng/procps/commit/ + # 2184e90d2e7cdb582f9a5b706b47015e56707e4d + if get_free_version_info() < (3, 3, 12): + raise pytest.skip("free version too old") + if get_free_version_info() >= (4, 0, 0): + raise pytest.skip("free version too recent") + cli_value = free_physmem().used + psutil_value = psutil.virtual_memory().used + assert abs(cli_value - psutil_value) < TOLERANCE_SYS_MEM + + @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: + raise 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]: + raise 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 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemVirtualMemoryAgainstVmstat(PsutilTestCase): + 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/ + # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + # Newer versions of procps are using yet another way to compute used + # memory. + # https://gitlab.com/procps-ng/procps/commit/ + # 2184e90d2e7cdb582f9a5b706b47015e56707e4d + if get_free_version_info() < (3, 3, 12): + raise pytest.skip("free version too old") + if get_free_version_info() >= (4, 0, 0): + raise pytest.skip("free version too recent") + vmstat_value = vmstat('used memory') * 1024 + psutil_value = psutil.virtual_memory().used + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + @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 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemVirtualMemoryMocks(PsutilTestCase): + def test_warnings_on_misses(self): + # Emulate a case where /proc/meminfo provides few info. + # 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 +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemSwapMemory(PsutilTestCase): + @staticmethod + def meminfo_has_swap_info(): + """Return True if /proc/meminfo provides swap metrics.""" + with open("/proc/meminfo") as f: + data = f.read() + return 'SwapTotal:' in data and 'SwapFree:' in data + + def test_total(self): + free_value = free_swap().total + psutil_value = psutil.swap_memory().total + 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 + + 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(): + raise pytest.skip("/proc/meminfo has no swap metrics") + with mock.patch('psutil._pslinux.cext.linux_sysinfo') as m: + swap = psutil.swap_memory() + assert not m.called + 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 +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemCPUTimes(PsutilTestCase): + def test_fields(self): + fields = psutil.cpu_times()._fields + kernel_ver = re.findall(r'\d+\.\d+\.\d+', os.uname()[2])[0] + kernel_ver_info = tuple(map(int, kernel_ver.split('.'))) + if kernel_ver_info >= (2, 6, 11): + assert 'steal' in fields + else: + assert 'steal' not in fields + if kernel_ver_info >= (2, 6, 24): + assert 'guest' in fields + else: + assert 'guest' not in fields + if kernel_ver_info >= (3, 2, 0): + assert 'guest_nice' in fields + else: + assert 'guest_nice' not in fields + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemCPUCountLogical(PsutilTestCase): + @pytest.mark.skipif( + 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 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemCPUCountCores(PsutilTestCase): + @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 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemCPUFrequency(PsutilTestCase): + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + @pytest.mark.skipif( + AARCH64, reason="aarch64 does not always expose frequency" + ) + def test_emulate_use_second_file(self): + # https://github.com/giampaolo/psutil/issues/981 + def path_exists_mock(path): + 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, reason="aarch64 does not report mhz in /proc/cpuinfo" + ) + def test_emulate_use_cpuinfo(self): + # Emulate a case where /sys/devices/system/cpu/cpufreq* does not + # exist and /proc/cpuinfo is used instead. + 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 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemCPUStats(PsutilTestCase): + + # XXX: fails too often. + # def test_ctx_switches(self): + # vmstat_value = vmstat("context switches") + # psutil_value = psutil.cpu_stats().ctx_switches + # self.assertAlmostEqual(vmstat_value, psutil_value, delta=500) + + def test_interrupts(self): + vmstat_value = vmstat("interrupts") + psutil_value = psutil.cpu_stats().interrupts + assert abs(vmstat_value - psutil_value) < 500 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestLoadAvg(PsutilTestCase): + @pytest.mark.skipif(not HAS_GETLOADAVG, reason="not supported") + def test_getloadavg(self): + psutil_value = psutil.getloadavg() + with open("/proc/loadavg") as f: + 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 +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemNetIfAddrs(PsutilTestCase): + def test_ips(self): + for name, addrs in psutil.net_if_addrs().items(): + for addr in addrs: + if addr.family == psutil.AF_LINK: + 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) + + # 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() + # self.assertIn(name, nics) + # self.assertEqual(len(nics), found, msg="{}\n---\n{}".format( + # pprint.pformat(nics), out)) + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemNetIfStats(PsutilTestCase): + @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: + raise self.fail("no matches were found") + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemNetIOCounters(PsutilTestCase): + @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 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemNetConnections(PsutilTestCase): + @mock.patch('psutil._pslinux.socket.inet_ntop', side_effect=ValueError) + @mock.patch('psutil._pslinux.supports_ipv6', return_value=False) + def test_emulate_ipv6_unsupported(self, supports_ipv6, inet_ntop): + # see: https://github.com/giampaolo/psutil/issues/623 + try: + s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + self.addCleanup(s.close) + s.bind(("::1", 0)) + except OSError: + pass + psutil.net_connections(kind='inet6') + + def test_emulate_unix(self): + content = textwrap.dedent("""\ + 0: 00000003 000 000 0001 03 462170 @/tmp/dbus-Qw2hMPIU3n + 0: 00000003 000 000 0001 03 35010 @/tmp/dbus-tB2X8h69BQ + 0: 00000003 000 000 0001 03 34424 @/tmp/dbus-cHy80Y8O + 000000000000000000000000000000000000000000000000000000 + """) + with mock_open_content({"/proc/net/unix": content}) as m: + psutil.net_connections(kind='unix') + assert m.called + + +# ===================================================================== +# --- system disks +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemDiskPartitions(PsutilTestCase): + @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" + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemDiskIoCounters(PsutilTestCase): + def test_emulate_kernel_2_4(self): + # Tests /proc/diskstats parsing format for 2.4 kernels, see: + # 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 LINUX, reason="LINUX only") +class TestRootFsDeviceFinder(PsutilTestCase): + def setUp(self): + dev = os.stat("/").st_dev + self.major = os.major(dev) + self.minor = os.minor(dev) + + def test_call_methods(self): + finder = RootFsDeviceFinder() + if os.path.exists("/proc/partitions"): + finder.ask_proc_partitions() + else: + 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 +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestMisc(PsutilTestCase): + 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() + # self.assertRaises(OSError, psutil.pids) + with pytest.raises(OSError): + psutil.net_connections() + with pytest.raises(OSError): + psutil.net_io_counters() + with pytest.raises(OSError): + psutil.net_if_stats() + # self.assertRaises(OSError, psutil.disk_io_counters) + with pytest.raises(OSError): + psutil.disk_partitions() + with pytest.raises(psutil.NoSuchProcess): + psutil.Process() + finally: + psutil.PROCFS_PATH = "/proc" + + @retry_on_failure() + @pytest.mark.skipif(PYTEST_PARALLEL, reason="skip if pytest-parallel") + def test_issue_687(self): + # In case of thread ID: + # - pid_exists() is supposed to return False + # - Process(tid) is supposed to work + # - pids() should not return the TID + # See: https://github.com/giampaolo/psutil/issues/687 + with ThreadTask(): + p = psutil.Process() + threads = p.threads() + assert len(threads) == 2 + 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 LINUX, reason="LINUX only") +@pytest.mark.skipif(not HAS_BATTERY, reason="no battery") +class TestSensorsBattery(PsutilTestCase): + @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 + + 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 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSensorsBatteryEmulated(PsutilTestCase): + def test_it(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 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSensorsTemperatures(PsutilTestCase): + 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 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSensorsFans(PsutilTestCase): + 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 +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestProcess(PsutilTestCase): + @retry_on_failure() + def test_parse_smaps_vs_memory_maps(self): + sproc = self.spawn_testproc() + uss, pss, swap = psutil._pslinux.Process(sproc.pid)._parse_smaps() + maps = psutil.Process(sproc.pid).memory_maps(grouped=False) + 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 + + # On PYPY file descriptors are not closed fast enough. + @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") + def test_open_files_mode(self): + def get_test_file(fname): + p = psutil.Process() + 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 + + # TODO: re-enable this test. + # def test_num_ctx_switches_mocked(self): + # with mock.patch('psutil._common.open', create=True) as m: + # self.assertRaises( + # NotImplementedError, + # psutil._pslinux.Process(os.getpid()).num_ctx_switches) + # assert m.called + + def test_cmdline_mocked(self): + # see: https://github.com/giampaolo/psutil/issues/639 + p = psutil.Process() + fake_file = io.StringIO('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_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_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 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestProcessAgainstStatus(PsutilTestCase): + """/proc/pid/stat and /proc/pid/status have many values in common. + Whenever possible, psutil uses /proc/pid/stat (it's faster). + 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 +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestUtils(PsutilTestCase): + def test_readlink(self): + with mock.patch("os.readlink", return_value="foo (deleted)") as m: + assert psutil._psplatform.readlink("bar") == "foo" + assert m.called diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_memleaks.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_memleaks.py new file mode 100644 index 0000000..7f78fae --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_memleaks.py @@ -0,0 +1,487 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for detecting function memory leaks (typically the ones +implemented in C). It does so by calling a function many times and +checking whether process memory usage keeps increasing between +calls or over time. +Note that this may produce false positives (especially on Windows +for some reason). +PyPy appears to be completely unstable for this framework, probably +because of how its JIT handles memory, so tests are skipped. +""" + + +import functools +import os +import platform + +import psutil +import psutil._common +from psutil import LINUX +from psutil import MACOS +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil.tests import HAS_CPU_AFFINITY +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_ENVIRON +from psutil.tests import HAS_IONICE +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import HAS_PROC_CPU_NUM +from psutil.tests import HAS_PROC_IO_COUNTERS +from psutil.tests import HAS_RLIMIT +from psutil.tests import HAS_SENSORS_BATTERY +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import TestMemoryLeak +from psutil.tests import create_sockets +from psutil.tests import get_testfn +from psutil.tests import process_namespace +from psutil.tests import pytest +from psutil.tests import skip_on_access_denied +from psutil.tests import spawn_testproc +from psutil.tests import system_namespace +from psutil.tests import terminate + + +cext = psutil._psplatform.cext +thisproc = psutil.Process() +FEW_TIMES = 5 + + +def fewtimes_if_linux(): + """Decorator for those Linux functions which are implemented in pure + Python, and which we want to run faster. + """ + + def decorator(fun): + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + if LINUX: + before = self.__class__.times + try: + self.__class__.times = FEW_TIMES + return fun(self, *args, **kwargs) + finally: + self.__class__.times = before + else: + return fun(self, *args, **kwargs) + + return wrapper + + return decorator + + +# =================================================================== +# Process class +# =================================================================== + + +class TestProcessObjectLeaks(TestMemoryLeak): + """Test leaks of Process class methods.""" + + proc = thisproc + + def test_coverage(self): + ns = process_namespace(None) + ns.test_class_coverage(self, ns.getters + ns.setters) + + @fewtimes_if_linux() + def test_name(self): + self.execute(self.proc.name) + + @fewtimes_if_linux() + def test_cmdline(self): + self.execute(self.proc.cmdline) + + @fewtimes_if_linux() + def test_exe(self): + self.execute(self.proc.exe) + + @fewtimes_if_linux() + def test_ppid(self): + self.execute(self.proc.ppid) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @fewtimes_if_linux() + def test_uids(self): + self.execute(self.proc.uids) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @fewtimes_if_linux() + def test_gids(self): + self.execute(self.proc.gids) + + @fewtimes_if_linux() + def test_status(self): + self.execute(self.proc.status) + + 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_IONICE, reason="not supported") + def test_ionice(self): + self.execute(self.proc.ionice) + + @pytest.mark.skipif(not HAS_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)) + fun = functools.partial(cext.proc_ioprio_set, os.getpid(), -1, 0) + self.execute_w_exc(OSError, fun) + + @pytest.mark.skipif(not HAS_PROC_IO_COUNTERS, reason="not supported") + @fewtimes_if_linux() + 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) + + @fewtimes_if_linux() + def test_create_time(self): + self.execute(self.proc.create_time) + + @fewtimes_if_linux() + @skip_on_access_denied(only_if=OPENBSD) + def test_num_threads(self): + self.execute(self.proc.num_threads) + + @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") + @fewtimes_if_linux() + def test_num_fds(self): + self.execute(self.proc.num_fds) + + @fewtimes_if_linux() + def test_num_ctx_switches(self): + self.execute(self.proc.num_ctx_switches) + + @fewtimes_if_linux() + @skip_on_access_denied(only_if=OPENBSD) + def test_threads(self): + self.execute(self.proc.threads) + + @fewtimes_if_linux() + def test_cpu_times(self): + self.execute(self.proc.cpu_times) + + @fewtimes_if_linux() + @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") + def test_cpu_num(self): + self.execute(self.proc.cpu_num) + + @fewtimes_if_linux() + def test_memory_info(self): + self.execute(self.proc.memory_info) + + @fewtimes_if_linux() + def test_memory_full_info(self): + self.execute(self.proc.memory_full_info) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @fewtimes_if_linux() + def test_terminal(self): + self.execute(self.proc.terminal) + + def test_resume(self): + times = FEW_TIMES if POSIX else self.times + self.execute(self.proc.resume, times=times) + + @fewtimes_if_linux() + def test_cwd(self): + self.execute(self.proc.cwd) + + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + def test_cpu_affinity(self): + self.execute(self.proc.cpu_affinity) + + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + def test_cpu_affinity_set(self): + affinity = thisproc.cpu_affinity() + self.execute(lambda: self.proc.cpu_affinity(affinity)) + self.execute_w_exc(ValueError, lambda: self.proc.cpu_affinity([-1])) + + @fewtimes_if_linux() + def test_open_files(self): + with open(get_testfn(), 'w'): + self.execute(self.proc.open_files) + + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + @fewtimes_if_linux() + def test_memory_maps(self): + self.execute(self.proc.memory_maps) + + @pytest.mark.skipif(not LINUX, reason="LINUX only") + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit(self): + self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE)) + + @pytest.mark.skipif(not LINUX, reason="LINUX only") + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit_set(self): + limit = thisproc.rlimit(psutil.RLIMIT_NOFILE) + self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE, limit)) + self.execute_w_exc((OSError, ValueError), lambda: self.proc.rlimit(-1)) + + @fewtimes_if_linux() + # Windows implementation is based on a single system-wide + # function (tested later). + @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. + with create_sockets(): + kind = 'inet' if SUNOS else 'all' + self.execute(lambda: self.proc.net_connections(kind)) + + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + def test_environ(self): + self.execute(self.proc.environ) + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_proc_info(self): + self.execute(lambda: cext.proc_info(os.getpid())) + + +class TestTerminatedProcessLeaks(TestProcessObjectLeaks): + """Repeat the tests above looking for leaks occurring when dealing + with terminated processes raising NoSuchProcess exception. + The C functions are still invoked but will follow different code + paths. We'll check those code paths. + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.subp = spawn_testproc() + cls.proc = psutil.Process(cls.subp.pid) + cls.proc.kill() + cls.proc.wait() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + terminate(cls.subp) + + def call(self, fun): + try: + fun() + except psutil.NoSuchProcess: + pass + + if WINDOWS: + + def test_kill(self): + self.execute(self.proc.kill) + + def test_terminate(self): + self.execute(self.proc.terminate) + + def test_suspend(self): + self.execute(self.proc.suspend) + + def test_resume(self): + self.execute(self.proc.resume) + + def test_wait(self): + self.execute(self.proc.wait) + + def test_proc_info(self): + # test dual implementation + def call(): + try: + return cext.proc_info(self.proc.pid) + except ProcessLookupError: + pass + + self.execute(call) + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +class TestProcessDualImplementation(TestMemoryLeak): + def test_cmdline_peb_true(self): + self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=True)) + + def test_cmdline_peb_false(self): + self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=False)) + + +# =================================================================== +# system APIs +# =================================================================== + + +class TestModuleFunctionsLeaks(TestMemoryLeak): + """Test leaks of psutil module functions.""" + + def test_coverage(self): + ns = system_namespace() + ns.test_class_coverage(self, ns.all) + + # --- cpu + + @fewtimes_if_linux() + def test_cpu_count(self): # logical + self.execute(lambda: psutil.cpu_count(logical=True)) + + @fewtimes_if_linux() + def test_cpu_count_cores(self): + self.execute(lambda: psutil.cpu_count(logical=False)) + + @fewtimes_if_linux() + def test_cpu_times(self): + self.execute(psutil.cpu_times) + + @fewtimes_if_linux() + def test_per_cpu_times(self): + self.execute(lambda: psutil.cpu_times(percpu=True)) + + @fewtimes_if_linux() + def test_cpu_stats(self): + self.execute(psutil.cpu_stats) + + @fewtimes_if_linux() + # TODO: remove this once 1892 is fixed + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" + ) + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_cpu_freq(self): + self.execute(psutil.cpu_freq) + + @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", + ) + @fewtimes_if_linux() + def test_disk_io_counters(self): + self.execute(lambda: psutil.disk_io_counters(nowrap=False)) + + # --- proc + + @fewtimes_if_linux() + def test_pids(self): + self.execute(psutil.pids) + + # --- net + + @fewtimes_if_linux() + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") + def test_net_io_counters(self): + self.execute(lambda: psutil.net_io_counters(nowrap=False)) + + @fewtimes_if_linux() + @pytest.mark.skipif(MACOS and os.getuid() != 0, reason="need root access") + def test_net_connections(self): + # always opens and handle on Windows() (once) + psutil.net_connections(kind='all') + with create_sockets(): + self.execute(lambda: psutil.net_connections(kind='all')) + + def test_net_if_addrs(self): + # Note: verified that on Windows this was a false positive. + tolerance = 80 * 1024 if WINDOWS else self.tolerance + self.execute(psutil.net_if_addrs, tolerance=tolerance) + + def test_net_if_stats(self): + self.execute(psutil.net_if_stats) + + # --- sensors + + @fewtimes_if_linux() + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + def test_sensors_battery(self): + self.execute(psutil.sensors_battery) + + @fewtimes_if_linux() + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + def test_sensors_temperatures(self): + self.execute(psutil.sensors_temperatures) + + @fewtimes_if_linux() + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") + def test_sensors_fans(self): + self.execute(psutil.sensors_fans) + + # --- others + + @fewtimes_if_linux() + def test_boot_time(self): + self.execute(psutil.boot_time) + + def test_users(self): + self.execute(psutil.users) + + def test_set_debug(self): + self.execute(lambda: psutil._set_debug(False)) + + if WINDOWS: + + # --- win services + + def test_win_service_iter(self): + self.execute(cext.winservice_enumerate) + + def test_win_service_get(self): + pass + + def test_win_service_get_config(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_config(name)) + + def test_win_service_get_status(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_status(name)) + + def test_win_service_get_description(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_descr(name)) diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_misc.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_misc.py new file mode 100644 index 0000000..c484264 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_misc.py @@ -0,0 +1,873 @@ +#!/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 +import psutil.tests +from psutil import WINDOWS +from psutil._common import bcat +from psutil._common import cat +from psutil._common import debug +from psutil._common import isfile_strict +from psutil._common import memoize +from psutil._common import memoize_when_activated +from psutil._common import parse_environ_block +from psutil._common import supports_ipv6 +from psutil._common import wrap_numbers +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import PsutilTestCase +from psutil.tests import process_namespace +from psutil.tests import pytest +from psutil.tests import reload_module +from psutil.tests import system_namespace + + +# =================================================================== +# --- 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_testproc().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) + 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() + ): + raise self.fail(f"{name!r} not in psutil.__all__") + + # Import 'star' will break if __all__ is inconsistent, see: + # https://github.com/giampaolo/psutil/issues/656 + # 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=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 self.assertRaises(psutil.NoSuchProcess): + psutil.Process() + assert meth.called + + def test_sanity_version_check(self): + # see: https://github.com/giampaolo/psutil/issues/564 + with mock.patch( + "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 TestMemoizeDecorator(PsutilTestCase): + def setUp(self): + self.calls = [] + + tearDown = setUp + + def run_against(self, obj, expected_retval=None): + # no args + for _ in range(2): + ret = obj() + assert self.calls == [((), {})] + if expected_retval is not None: + assert ret == expected_retval + # with args + for _ in range(2): + ret = obj(1) + assert self.calls == [((), {}), ((1,), {})] + if expected_retval is not None: + assert ret == expected_retval + # with args + kwargs + for _ in range(2): + ret = obj(1, bar=2) + assert self.calls == [((), {}), ((1,), {}), ((1,), {'bar': 2})] + if expected_retval is not None: + assert ret == expected_retval + # clear cache + assert len(self.calls) == 3 + obj.cache_clear() + ret = obj() + if expected_retval is not None: + assert ret == expected_retval + assert len(self.calls) == 4 + # docstring + assert obj.__doc__ == "My docstring." + + def test_function(self): + @memoize + def foo(*args, **kwargs): + """My docstring.""" + baseclass.calls.append((args, kwargs)) + return 22 + + baseclass = self + self.run_against(foo, expected_retval=22) + + def test_class(self): + @memoize + class Foo: + """My docstring.""" + + def __init__(self, *args, **kwargs): + baseclass.calls.append((args, kwargs)) + + def bar(self): + return 22 + + baseclass = self + self.run_against(Foo, expected_retval=None) + assert Foo().bar() == 22 + + def test_class_singleton(self): + # @memoize can be used against classes to create singletons + @memoize + class Bar: + def __init__(self, *args, **kwargs): + pass + + assert Bar() is Bar() + assert id(Bar()) == id(Bar()) + assert id(Bar(1)) == id(Bar(1)) + assert id(Bar(1, foo=3)) == id(Bar(1, foo=3)) + assert id(Bar(1)) != id(Bar(2)) + + def test_staticmethod(self): + class Foo: + @staticmethod + @memoize + def bar(*args, **kwargs): + """My docstring.""" + baseclass.calls.append((args, kwargs)) + return 22 + + baseclass = self + self.run_against(Foo().bar, expected_retval=22) + + def test_classmethod(self): + class Foo: + @classmethod + @memoize + def bar(cls, *args, **kwargs): + """My docstring.""" + baseclass.calls.append((args, kwargs)) + return 22 + + baseclass = self + self.run_against(Foo().bar, expected_retval=22) + + def test_original(self): + # This was the original test before I made it dynamic to test it + # against different types. Keeping it anyway. + @memoize + def foo(*args, **kwargs): + """Foo docstring.""" + calls.append(None) + return (args, kwargs) + + calls = [] + # no args + for _ in range(2): + ret = foo() + expected = ((), {}) + assert ret == expected + assert len(calls) == 1 + # with args + for _ in range(2): + ret = foo(1) + expected = ((1,), {}) + assert ret == expected + assert len(calls) == 2 + # with args + kwargs + for _ in range(2): + ret = foo(1, bar=2) + expected = ((1,), {'bar': 2}) + assert ret == expected + assert len(calls) == 3 + # clear cache + foo.cache_clear() + ret = foo() + expected = ((), {}) + assert ret == expected + assert len(calls) == 4 + # docstring + assert foo.__doc__ == "Foo docstring." + + +class TestCommonModule(PsutilTestCase): + def test_memoize_when_activated(self): + class Foo: + @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): + self.addCleanup(supports_ipv6.cache_clear) + if supports_ipv6(): + with mock.patch('psutil._common.socket') as s: + s.has_ipv6 = False + supports_ipv6.cache_clear() + assert not supports_ipv6() + + supports_ipv6.cache_clear() + with mock.patch( + 'psutil._common.socket.socket', side_effect=OSError + ) as s: + assert not supports_ipv6() + assert s.called + + supports_ipv6.cache_clear() + with mock.patch( + 'psutil._common.socket.socket', side_effect=socket.gaierror + ) as s: + assert not supports_ipv6() + supports_ipv6.cache_clear() + assert s.called + + supports_ipv6.cache_clear() + with mock.patch( + 'psutil._common.socket.socket.bind', + side_effect=socket.gaierror, + ) as s: + assert not supports_ipv6() + supports_ipv6.cache_clear() + assert s.called + else: + with pytest.raises(OSError): + 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(): + raise 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/.venv/lib/python3.12/site-packages/psutil/tests/test_osx.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_osx.py new file mode 100644 index 0000000..050418c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_osx.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""macOS specific tests.""" + +import platform +import re +import time + +import psutil +from psutil import MACOS +from psutil import POSIX +from psutil.tests import CI_TESTING +from psutil.tests import HAS_BATTERY +from psutil.tests import TOLERANCE_DISK_USAGE +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +if POSIX: + from psutil._psutil_posix import getpagesize + + +def sysctl(cmdline): + """Expects a sysctl command with an argument and parse the result + returning only the value of interest. + """ + out = sh(cmdline) + result = out.split()[1] + try: + return int(result) + except ValueError: + return result + + +def vm_stat(field): + """Wrapper around 'vm_stat' cmdline utility.""" + out = sh('vm_stat') + for line in out.split('\n'): + if field in line: + break + else: + raise ValueError("line not found") + return int(re.search(r'\d+', line).group(0)) * getpagesize() + + +@pytest.mark.skipif(not MACOS, reason="MACOS only") +class TestProcess(PsutilTestCase): + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_process_create_time(self): + output = sh(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)) + + +@pytest.mark.skipif(not MACOS, reason="MACOS only") +class TestSystemAPIs(PsutilTestCase): + + # --- disk + + @retry_on_failure() + def test_disks(self): + # test psutil.disk_usage() and psutil.disk_partitions() + # against "df -a" + def df(path): + out = sh(f'df -k "{path}"').strip() + lines = out.split('\n') + lines.pop(0) + line = lines.pop(0) + dev, total, used, free = line.split()[:4] + if dev == 'none': + dev = '' + total = int(total) * 1024 + used = int(used) * 1024 + free = int(free) * 1024 + return dev, total, used, free + + for part in psutil.disk_partitions(all=False): + usage = psutil.disk_usage(part.mountpoint) + dev, total, used, free = df(part.mountpoint) + assert part.device == dev + assert usage.total == total + assert abs(usage.free - free) < TOLERANCE_DISK_USAGE + assert abs(usage.used - used) < TOLERANCE_DISK_USAGE + + # --- cpu + + def test_cpu_count_logical(self): + num = sysctl("sysctl hw.logicalcpu") + assert num == psutil.cpu_count(logical=True) + + def test_cpu_count_cores(self): + num = sysctl("sysctl hw.physicalcpu") + assert num == psutil.cpu_count(logical=False) + + # TODO: remove this once 1892 is fixed + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" + ) + def test_cpu_freq(self): + freq = psutil.cpu_freq() + assert freq.current * 1000 * 1000 == sysctl("sysctl hw.cpufrequency") + assert freq.min * 1000 * 1000 == sysctl("sysctl hw.cpufrequency_min") + assert freq.max * 1000 * 1000 == sysctl("sysctl hw.cpufrequency_max") + + # --- virtual mem + + def test_vmem_total(self): + sysctl_hwphymem = sysctl('sysctl hw.memsize') + assert sysctl_hwphymem == psutil.virtual_memory().total + + @pytest.mark.skipif( + CI_TESTING and MACOS and platform.machine() == 'arm64', + reason="skipped on MACOS + ARM64 + CI_TESTING", + ) + @retry_on_failure() + def test_vmem_free(self): + vmstat_val = vm_stat("free") + psutil_val = psutil.virtual_memory().free + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_active(self): + vmstat_val = vm_stat("active") + psutil_val = psutil.virtual_memory().active + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_inactive(self): + vmstat_val = vm_stat("inactive") + psutil_val = psutil.virtual_memory().inactive + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_wired(self): + vmstat_val = vm_stat("wired") + psutil_val = psutil.virtual_memory().wired + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + # --- swap mem + + @retry_on_failure() + def test_swapmem_sin(self): + vmstat_val = vm_stat("Pageins") + psutil_val = psutil.swap_memory().sin + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_swapmem_sout(self): + vmstat_val = vm_stat("Pageout") + psutil_val = psutil.swap_memory().sout + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + # --- network + + 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]) + + # --- sensors_battery + + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_sensors_battery(self): + out = sh("pmset -g batt") + percent = re.search(r"(\d+)%", out).group(1) + drawing_from = re.search(r"Now drawing from '([^']+)'", out).group(1) + power_plugged = drawing_from == "AC Power" + psutil_result = psutil.sensors_battery() + assert psutil_result.power_plugged == power_plugged + assert psutil_result.percent == int(percent) diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_posix.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_posix.py new file mode 100644 index 0000000..a784492 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_posix.py @@ -0,0 +1,488 @@ +#!/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. + +"""POSIX specific tests.""" + +import datetime +import errno +import os +import re +import shutil +import subprocess +import time +from unittest import mock + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import LINUX +from psutil import MACOS +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil.tests import AARCH64 +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import PYTHON_EXE +from psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import skip_on_access_denied +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +if POSIX: + import mmap + import resource + + from psutil._psutil_posix import getpagesize + + +def ps(fmt, pid=None): + """Wrapper for calling the ps command with a little bit of cross-platform + support for a narrow range of features. + """ + + cmd = ['ps'] + + if LINUX: + cmd.append('--no-headers') + + if pid is not None: + cmd.extend(['-p', str(pid)]) + elif SUNOS or AIX: + cmd.append('-A') + else: + cmd.append('ax') + + if SUNOS: + fmt = fmt.replace("start", "stime") + + cmd.extend(['-o', fmt]) + + output = sh(cmd) + + output = output.splitlines() if LINUX else output.splitlines()[1:] + + all_output = [] + for line in output: + line = line.strip() + + try: + line = int(line) + except ValueError: + pass + + all_output.append(line) + + if pid is None: + return all_output + else: + return all_output[0] + + +# ps "-o" field names differ wildly between platforms. +# "comm" means "only executable name" but is not available on BSD platforms. +# "args" means "command with all its arguments", and is also not available +# on BSD platforms. +# "command" is like "args" on most platforms, but like "comm" on AIX, +# and not available on SUNOS. +# so for the executable name we can use "comm" on Solaris and split "command" +# on other platforms. +# to get the cmdline (with args) we have to use "args" on AIX and +# Solaris, and can use "command" on all others. + + +def ps_name(pid): + field = "command" + if SUNOS: + field = "comm" + command = ps(field, pid).split() + return command[0] + + +def ps_args(pid): + field = "command" + if AIX or SUNOS: + field = "args" + out = ps(field, pid) + # observed on BSD + Github CI: '/usr/local/bin/python3 -E -O (python3.9)' + out = re.sub(r"\(python.*?\)$", "", out) + return out.strip() + + +def ps_rss(pid): + field = "rss" + if AIX: + field = "rssize" + return ps(field, pid) + + +def ps_vsz(pid): + field = "vsz" + if AIX: + field = "vsize" + return ps(field, pid) + + +def df(device): + try: + out = sh(f"df -k {device}").strip() + except RuntimeError as err: + if "device busy" in str(err).lower(): + raise pytest.skip("df returned EBUSY") + raise + line = out.split('\n')[1] + fields = line.split() + sys_total = int(fields[1]) * 1024 + sys_used = int(fields[2]) * 1024 + sys_free = int(fields[3]) * 1024 + sys_percent = float(fields[4].replace('%', '')) + return (sys_total, sys_used, sys_free, sys_percent) + + +@pytest.mark.skipif(not POSIX, reason="POSIX only") +class TestProcess(PsutilTestCase): + """Compare psutil results against 'ps' command line utility (mainly).""" + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc( + [PYTHON_EXE, "-E", "-O"], stdin=subprocess.PIPE + ).pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_ppid(self): + ppid_ps = ps('ppid', self.pid) + ppid_psutil = psutil.Process(self.pid).ppid() + assert ppid_ps == ppid_psutil + + def test_uid(self): + uid_ps = ps('uid', self.pid) + uid_psutil = psutil.Process(self.pid).uids().real + assert uid_ps == uid_psutil + + def test_gid(self): + gid_ps = ps('rgid', self.pid) + gid_psutil = psutil.Process(self.pid).gids().real + assert gid_ps == gid_psutil + + def test_username(self): + username_ps = ps('user', self.pid) + username_psutil = psutil.Process(self.pid).username() + assert 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: + assert p.username() == str(p.uids().real) + assert fun.called + + @skip_on_access_denied() + @retry_on_failure() + def test_rss_memory(self): + # give python interpreter some time to properly initialize + # so that the results are the same + time.sleep(0.1) + rss_ps = ps_rss(self.pid) + rss_psutil = psutil.Process(self.pid).memory_info()[0] / 1024 + assert rss_ps == rss_psutil + + @skip_on_access_denied() + @retry_on_failure() + def test_vsz_memory(self): + # give python interpreter some time to properly initialize + # so that the results are the same + time.sleep(0.1) + vsz_ps = ps_vsz(self.pid) + vsz_psutil = psutil.Process(self.pid).memory_info()[1] / 1024 + assert vsz_ps == vsz_psutil + + def test_name(self): + name_ps = ps_name(self.pid) + # remove path if there is any, from the command + name_ps = os.path.basename(name_ps).lower() + name_psutil = psutil.Process(self.pid).name().lower() + # ...because of how we calculate PYTHON_EXE; on MACOS this may + # be "pythonX.Y". + name_ps = re.sub(r"\d.\d", "", name_ps) + name_psutil = re.sub(r"\d.\d", "", name_psutil) + # ...may also be "python.X" + name_ps = re.sub(r"\d", "", name_ps) + name_psutil = re.sub(r"\d", "", name_psutil) + assert 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() + assert p.name() == "long-program-name-extended" + + def test_name_long_cmdline_ad_exc(self): + # Same as above but emulates a case where cmdline() raises + # 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() + assert p.name() == "long-program-name" + + def test_name_long_cmdline_nsp_exc(self): + # Same as above but emulates a case where cmdline() raises NSP + # 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() + with pytest.raises(psutil.NoSuchProcess): + p.name() + + @pytest.mark.skipif(MACOS or BSD, reason="ps -o start not available") + def test_create_time(self): + time_ps = ps('start', self.pid) + time_psutil = psutil.Process(self.pid).create_time() + 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") + assert time_ps in {time_psutil_tstamp, round_time_psutil_tstamp} + + def test_exe(self): + ps_pathname = ps_name(self.pid) + psutil_pathname = psutil.Process(self.pid).exe() + try: + assert ps_pathname == psutil_pathname + except AssertionError: + # certain platforms such as BSD are more accurate returning: + # "/usr/local/bin/python3.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)] + assert ps_pathname == adjusted_ps_pathname + + # On macOS the official python installer exposes a python wrapper that + # executes a python executable hidden inside an application bundle inside + # the Python framework. + # There's a race condition between the ps call & the psutil call below + # depending on the completion of the execve call so let's retry on failure + @retry_on_failure() + def test_cmdline(self): + ps_cmdline = ps_args(self.pid) + psutil_cmdline = " ".join(psutil.Process(self.pid).cmdline()) + if AARCH64 and len(ps_cmdline) < len(psutil_cmdline): + assert psutil_cmdline.startswith(ps_cmdline) + else: + assert ps_cmdline == psutil_cmdline + + # On SUNOS "ps" reads niceness /proc/pid/psinfo which returns an + # incorrect value (20); the real deal is getpriority(2) which + # returns 0; psutil relies on it, see: + # https://github.com/giampaolo/psutil/issues/1082 + # AIX has the same issue + @pytest.mark.skipif(SUNOS, reason="not reliable on SUNOS") + @pytest.mark.skipif(AIX, reason="not reliable on AIX") + def test_nice(self): + ps_nice = ps('nice', self.pid) + psutil_nice = psutil.Process().nice() + assert ps_nice == psutil_nice + + +@pytest.mark.skipif(not POSIX, reason="POSIX only") +class TestSystemAPIs(PsutilTestCase): + """Test some system APIs.""" + + @retry_on_failure() + def test_pids(self): + # Note: this test might fail if the OS is starting/killing + # other processes in the meantime + pids_ps = sorted(ps("pid")) + pids_psutil = psutil.pids() + + # on MACOS and OPENBSD ps doesn't show pid 0 + if MACOS or (OPENBSD and 0 not in pids_ps): + pids_ps.insert(0, 0) + + # There will often be one more process in pids_ps for ps itself + if len(pids_ps) - len(pids_psutil) > 1: + difference = [x for x in pids_psutil if x not in pids_ps] + [ + x for x in pids_ps if x not in pids_psutil + ] + raise self.fail("difference: " + str(difference)) + + # for some reason ifconfig -a does not report all interfaces + # returned by psutil + @pytest.mark.skipif(SUNOS, reason="unreliable on SUNOS") + @pytest.mark.skipif(not shutil.which("ifconfig"), reason="no ifconfig cmd") + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") + def test_nic_names(self): + output = sh("ifconfig -a") + for nic in psutil.net_io_counters(pernic=True): + for line in output.split(): + if line.startswith(nic): + break + else: + raise self.fail( + f"couldn't find {nic} nic in 'ifconfig -a'" + f" output\n{output}" + ) + + # @pytest.mark.skipif(CI_TESTING and not psutil.users(), + # reason="unreliable on CI") + @retry_on_failure() + def test_users(self): + out = sh("who -u") + if not out.strip(): + raise pytest.skip("no users on this system") + lines = out.split('\n') + users = [x.split()[0] for x in lines] + terminals = [x.split()[1] for x in lines] + assert len(users) == len(psutil.users()) + with self.subTest(psutil=psutil.users(), who=out): + for idx, u in enumerate(psutil.users()): + assert u.name == users[idx] + assert u.terminal == terminals[idx] + if u.pid is not None: # None on OpenBSD + psutil.Process(u.pid) + + @retry_on_failure() + def test_users_started(self): + out = sh("who -u") + if not out.strip(): + raise pytest.skip("no users on this system") + tstamp = None + # '2023-04-11 09:31' (Linux) + started = re.findall(r"\d\d\d\d-\d\d-\d\d \d\d:\d\d", out) + if started: + tstamp = "%Y-%m-%d %H:%M" + else: + # 'Apr 10 22:27' (macOS) + started = re.findall(r"[A-Z][a-z][a-z] \d\d \d\d:\d\d", out) + if started: + tstamp = "%b %d %H:%M" + else: + # 'Apr 10' + started = re.findall(r"[A-Z][a-z][a-z] \d\d", out) + if started: + tstamp = "%b %d" + else: + # 'apr 10' (sunOS) + started = re.findall(r"[a-z][a-z][a-z] \d\d", out) + if started: + tstamp = "%b %d" + started = [x.capitalize() for x in started] + + if not tstamp: + raise pytest.skip(f"cannot interpret tstamp in who output\n{out}") + + with self.subTest(psutil=psutil.users(), who=out): + for idx, u in enumerate(psutil.users()): + psutil_value = datetime.datetime.fromtimestamp( + u.started + ).strftime(tstamp) + assert psutil_value == started[idx] + + def test_pid_exists_let_raise(self): + # According to "man 2 kill" possible error values for kill + # are (EINVAL, EPERM, ESRCH). Test that any other errno + # results in an exception. + with mock.patch( + "psutil._psposix.os.kill", side_effect=OSError(errno.EBADF, "") + ) as m: + with pytest.raises(OSError): + psutil._psposix.pid_exists(os.getpid()) + assert m.called + + def test_os_waitpid_let_raise(self): + # os.waitpid() is supposed to catch EINTR and ECHILD only. + # Test that any other errno results in an exception. + with mock.patch( + "psutil._psposix.os.waitpid", side_effect=OSError(errno.EBADF, "") + ) as m: + with pytest.raises(OSError): + psutil._psposix.wait_pid(os.getpid()) + assert m.called + + def test_os_waitpid_eintr(self): + # os.waitpid() is supposed to "retry" on EINTR. + with mock.patch( + "psutil._psposix.os.waitpid", side_effect=OSError(errno.EINTR, "") + ) as m: + with pytest.raises(psutil._psposix.TimeoutExpired): + psutil._psposix.wait_pid(os.getpid(), timeout=0.01) + assert m.called + + def test_os_waitpid_bad_ret_status(self): + # Simulate os.waitpid() returning a bad status. + with mock.patch( + "psutil._psposix.os.waitpid", return_value=(1, -1) + ) as m: + with pytest.raises(ValueError): + psutil._psposix.wait_pid(os.getpid()) + assert m.called + + # AIX can return '-' in df output instead of numbers, e.g. for /proc + @pytest.mark.skipif(AIX, reason="unreliable on AIX") + @retry_on_failure() + def test_disk_usage(self): + tolerance = 4 * 1024 * 1024 # 4MB + for part in psutil.disk_partitions(all=False): + usage = psutil.disk_usage(part.mountpoint) + try: + sys_total, sys_used, sys_free, sys_percent = df(part.device) + except RuntimeError as err: + # see: + # https://travis-ci.org/giampaolo/psutil/jobs/138338464 + # https://travis-ci.org/giampaolo/psutil/jobs/138343361 + err = str(err).lower() + if ( + "no such file or directory" in err + or "raw devices not supported" in err + or "permission denied" in err + ): + continue + raise + else: + assert abs(usage.total - sys_total) < tolerance + assert abs(usage.used - sys_used) < tolerance + assert abs(usage.free - sys_free) < tolerance + assert abs(usage.percent - sys_percent) <= 1 + + +@pytest.mark.skipif(not POSIX, reason="POSIX only") +class TestMisc(PsutilTestCase): + def test_getpagesize(self): + pagesize = getpagesize() + assert pagesize > 0 + assert pagesize == resource.getpagesize() + assert pagesize == mmap.PAGESIZE diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_process.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_process.py new file mode 100644 index 0000000..9ba1ba0 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_process.py @@ -0,0 +1,1667 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for psutil.Process class.""" + +import collections +import contextlib +import errno +import getpass +import io +import itertools +import os +import signal +import socket +import stat +import string +import subprocess +import sys +import textwrap +import time +from unittest import mock + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import OSX +from psutil import POSIX +from psutil import WINDOWS +from psutil._common import open_text +from psutil.tests import CI_TESTING +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import GLOBAL_TIMEOUT +from psutil.tests import HAS_CPU_AFFINITY +from psutil.tests import HAS_ENVIRON +from psutil.tests import HAS_IONICE +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import HAS_PROC_CPU_NUM +from psutil.tests import HAS_PROC_IO_COUNTERS +from psutil.tests import HAS_RLIMIT +from psutil.tests import HAS_THREADS +from psutil.tests import MACOS_11PLUS +from psutil.tests import PYPY +from psutil.tests import PYTHON_EXE +from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import PsutilTestCase +from psutil.tests import ThreadTask +from psutil.tests import call_until +from psutil.tests import copyload_shared_lib +from psutil.tests import create_c_exe +from psutil.tests import create_py_exe +from psutil.tests import process_namespace +from psutil.tests import pytest +from psutil.tests import reap_children +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import skip_on_access_denied +from psutil.tests import skip_on_not_implemented +from psutil.tests import wait_for_pid + + +# =================================================================== +# --- psutil.Process class tests +# =================================================================== + + +class TestProcess(PsutilTestCase): + """Tests for psutil.Process class.""" + + def spawn_psproc(self, *args, **kwargs): + sproc = self.spawn_testproc(*args, **kwargs) + try: + return psutil.Process(sproc.pid) + except psutil.NoSuchProcess: + self.assertPidGone(sproc.pid) + raise + + # --- + + def test_pid(self): + p = psutil.Process() + assert p.pid == os.getpid() + with pytest.raises(AttributeError): + p.pid = 33 + + def test_kill(self): + p = self.spawn_psproc() + p.kill() + code = p.wait() + if WINDOWS: + assert code == signal.SIGTERM + else: + assert code == -signal.SIGKILL + self.assertProcessGone(p) + + def test_terminate(self): + p = self.spawn_psproc() + p.terminate() + code = p.wait() + if WINDOWS: + assert code == signal.SIGTERM + else: + assert code == -signal.SIGTERM + self.assertProcessGone(p) + + def test_send_signal(self): + sig = signal.SIGKILL if POSIX else signal.SIGTERM + p = self.spawn_psproc() + p.send_signal(sig) + code = p.wait() + if WINDOWS: + assert code == sig + else: + assert code == -sig + self.assertProcessGone(p) + + @pytest.mark.skipif(not POSIX, reason="not POSIX") + def test_send_signal_mocked(self): + sig = signal.SIGTERM + p = self.spawn_psproc() + with mock.patch('psutil.os.kill', side_effect=ProcessLookupError): + with pytest.raises(psutil.NoSuchProcess): + p.send_signal(sig) + + p = self.spawn_psproc() + with mock.patch('psutil.os.kill', side_effect=PermissionError): + with pytest.raises(psutil.AccessDenied): + p.send_signal(sig) + + def test_wait_exited(self): + # Test waitpid() + WIFEXITED -> WEXITSTATUS. + # normal return, same as exit(0) + cmd = [PYTHON_EXE, "-c", "pass"] + p = self.spawn_psproc(cmd) + code = p.wait() + assert code == 0 + self.assertProcessGone(p) + # exit(1), implicit in case of error + cmd = [PYTHON_EXE, "-c", "1 / 0"] + p = self.spawn_psproc(cmd, stderr=subprocess.PIPE) + code = p.wait() + assert code == 1 + self.assertProcessGone(p) + # via sys.exit() + cmd = [PYTHON_EXE, "-c", "import sys; sys.exit(5);"] + p = self.spawn_psproc(cmd) + code = p.wait() + assert code == 5 + self.assertProcessGone(p) + # via os._exit() + cmd = [PYTHON_EXE, "-c", "import os; os._exit(5);"] + p = self.spawn_psproc(cmd) + code = p.wait() + assert code == 5 + self.assertProcessGone(p) + + @pytest.mark.skipif(NETBSD, reason="fails on NETBSD") + def test_wait_stopped(self): + p = self.spawn_psproc() + if POSIX: + # Test waitpid() + WIFSTOPPED and WIFCONTINUED. + # Note: if a process is stopped it ignores SIGTERM. + p.send_signal(signal.SIGSTOP) + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.send_signal(signal.SIGCONT) + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.send_signal(signal.SIGTERM) + assert p.wait() == -signal.SIGTERM + assert p.wait() == -signal.SIGTERM + else: + p.suspend() + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.resume() + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.terminate() + assert p.wait() == signal.SIGTERM + assert p.wait() == signal.SIGTERM + + def test_wait_non_children(self): + # Test wait() against a process which is not our direct + # child. + child, grandchild = self.spawn_children_pair() + with pytest.raises(psutil.TimeoutExpired): + child.wait(0.01) + with pytest.raises(psutil.TimeoutExpired): + grandchild.wait(0.01) + # We also terminate the direct child otherwise the + # grandchild will hang until the parent is gone. + child.terminate() + grandchild.terminate() + child_ret = child.wait() + grandchild_ret = grandchild.wait() + if POSIX: + assert child_ret == -signal.SIGTERM + # For processes which are not our children we're supposed + # to get None. + assert grandchild_ret is None + else: + assert child_ret == signal.SIGTERM + assert child_ret == signal.SIGTERM + + def test_wait_timeout(self): + p = self.spawn_psproc() + p.name() + with pytest.raises(psutil.TimeoutExpired): + p.wait(0.01) + with pytest.raises(psutil.TimeoutExpired): + p.wait(0) + with pytest.raises(ValueError): + p.wait(-1) + + def test_wait_timeout_nonblocking(self): + p = self.spawn_psproc() + with pytest.raises(psutil.TimeoutExpired): + p.wait(0) + p.kill() + stop_at = time.time() + GLOBAL_TIMEOUT + while time.time() < stop_at: + try: + code = p.wait(0) + break + except psutil.TimeoutExpired: + pass + else: + raise self.fail('timeout') + if POSIX: + assert code == -signal.SIGKILL + else: + assert code == signal.SIGTERM + self.assertProcessGone(p) + + def test_cpu_percent(self): + p = psutil.Process() + p.cpu_percent(interval=0.001) + p.cpu_percent(interval=0.001) + for _ in range(100): + percent = p.cpu_percent(interval=None) + assert isinstance(percent, float) + assert percent >= 0.0 + with pytest.raises(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, times + assert times.system >= 0.0, times + assert times.children_user >= 0.0, times + assert times.children_system >= 0.0, times + if LINUX: + assert times.iowait >= 0.0, times + # make sure returned values can be pretty printed with strftime + for name in times._fields: + time.strftime("%H:%M:%S", time.localtime(getattr(times, name))) + + def test_cpu_times_2(self): + def waste_cpu(): + stop_at = os.times().user + 0.2 + while os.times().user < stop_at: + for x in range(100000): + x **= 2 + + waste_cpu() + a = psutil.Process().cpu_times() + b = os.times() + self.assertAlmostEqual(a.user, b.user, delta=0.1) + self.assertAlmostEqual(a.system, b.system, delta=0.1) + + @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") + def test_cpu_num(self): + p = psutil.Process() + num = p.cpu_num() + assert num >= 0 + if psutil.cpu_count() == 1: + assert num == 0 + assert p.cpu_num() in range(psutil.cpu_count()) + + def test_create_time(self): + p = self.spawn_psproc() + now = time.time() + create_time = p.create_time() + + # Use time.time() as base value to compare our result using a + # tolerance of +/- 1 second. + # It will fail if the difference between the values is > 2s. + difference = abs(create_time - now) + if difference > 2: + raise self.fail( + f"expected: {now}, found: {create_time}, difference:" + f" {difference}" + ) + + # make sure returned value can be pretty printed with strftime + time.strftime("%Y %m %d %H:%M:%S", time.localtime(p.create_time())) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_terminal(self): + terminal = psutil.Process().terminal() + if terminal is not None: + try: + tty = os.path.realpath(sh('tty')) + except RuntimeError: + # Note: happens if pytest is run without the `-s` opt. + raise pytest.skip("can't rely on `tty` CLI") + else: + assert terminal == tty + + @pytest.mark.skipif(not HAS_PROC_IO_COUNTERS, reason="not supported") + @skip_on_not_implemented(only_if=LINUX) + def test_io_counters(self): + p = psutil.Process() + # 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: + assert io2.read_count > io1.read_count + assert io2.write_count == io1.write_count + if LINUX: + assert io2.read_chars > io1.read_chars + assert io2.write_chars == io1.write_chars + else: + assert io2.read_bytes >= io1.read_bytes + assert io2.write_bytes >= io1.write_bytes + + # test writes + io1 = p.io_counters() + with open(self.get_testfn(), 'wb') as f: + f.write(bytes("x" * 1000000, 'ascii')) + io2 = p.io_counters() + assert io2.write_count >= io1.write_count + assert io2.write_bytes >= io1.write_bytes + assert io2.read_count >= io1.read_count + assert io2.read_bytes >= io1.read_bytes + if LINUX: + assert io2.write_chars > io1.write_chars + assert io2.read_chars >= io1.read_chars + + # sanity check + for i in range(len(io2)): + if BSD and i >= 2: + # On BSD read_bytes and write_bytes are always set to -1. + continue + assert io2[i] >= 0 + assert io2[i] >= 0 + + @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif(not LINUX, reason="linux only") + def test_ionice_linux(self): + def cleanup(init): + ioclass, value = init + if ioclass == psutil.IOPRIO_CLASS_NONE: + value = 0 + p.ionice(ioclass, value) + + p = psutil.Process() + if not CI_TESTING: + assert p.ionice()[0] == psutil.IOPRIO_CLASS_NONE + assert psutil.IOPRIO_CLASS_NONE == 0 + assert psutil.IOPRIO_CLASS_RT == 1 # high + assert psutil.IOPRIO_CLASS_BE == 2 # normal + assert psutil.IOPRIO_CLASS_IDLE == 3 # low + init = p.ionice() + self.addCleanup(cleanup, init) + + # low + p.ionice(psutil.IOPRIO_CLASS_IDLE) + assert tuple(p.ionice()) == (psutil.IOPRIO_CLASS_IDLE, 0) + with pytest.raises(ValueError): # accepts no value + p.ionice(psutil.IOPRIO_CLASS_IDLE, value=7) + # normal + p.ionice(psutil.IOPRIO_CLASS_BE) + assert tuple(p.ionice()) == (psutil.IOPRIO_CLASS_BE, 0) + p.ionice(psutil.IOPRIO_CLASS_BE, value=7) + assert tuple(p.ionice()) == (psutil.IOPRIO_CLASS_BE, 7) + with pytest.raises(ValueError): + p.ionice(psutil.IOPRIO_CLASS_BE, value=8) + try: + p.ionice(psutil.IOPRIO_CLASS_RT, value=7) + except psutil.AccessDenied: + pass + # errs + with pytest.raises(ValueError, match="ioclass accepts no value"): + p.ionice(psutil.IOPRIO_CLASS_NONE, 1) + with pytest.raises(ValueError, match="ioclass accepts no value"): + p.ionice(psutil.IOPRIO_CLASS_IDLE, 1) + with pytest.raises( + ValueError, match="'ioclass' argument must be specified" + ): + p.ionice(value=1) + + @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif( + not WINDOWS, reason="not supported on this win version" + ) + def test_ionice_win(self): + p = psutil.Process() + if not CI_TESTING: + assert p.ionice() == psutil.IOPRIO_NORMAL + init = p.ionice() + self.addCleanup(p.ionice, init) + + # base + p.ionice(psutil.IOPRIO_VERYLOW) + assert p.ionice() == psutil.IOPRIO_VERYLOW + p.ionice(psutil.IOPRIO_LOW) + assert p.ionice() == psutil.IOPRIO_LOW + try: + p.ionice(psutil.IOPRIO_HIGH) + except psutil.AccessDenied: + pass + else: + assert p.ionice() == psutil.IOPRIO_HIGH + # errs + with pytest.raises( + TypeError, match="value argument not accepted on Windows" + ): + p.ionice(psutil.IOPRIO_NORMAL, value=1) + with pytest.raises(ValueError, match="is not a valid priority"): + p.ionice(psutil.IOPRIO_HIGH + 1) + + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + 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) + assert value >= 0 + if name in dir(resource): + assert value == getattr(resource, name) + # XXX - On PyPy RLIMIT_INFINITY returned by + # resource.getrlimit() is reported as a very big long + # number instead of -1. It looks like a bug with PyPy. + if PYPY: + continue + assert p.rlimit(value) == resource.getrlimit(value) + else: + ret = p.rlimit(value) + assert len(ret) == 2 + assert ret[0] >= -1 + assert ret[1] >= -1 + + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit_set(self): + p = self.spawn_psproc() + p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) + assert p.rlimit(psutil.RLIMIT_NOFILE) == (5, 5) + # If pid is 0 prlimit() applies to the calling process and + # we don't want that. + if LINUX: + with pytest.raises(ValueError, match="can't use prlimit"): + psutil._psplatform.Process(0).rlimit(0) + with pytest.raises(ValueError): + p.rlimit(psutil.RLIMIT_NOFILE, (5, 5, 5)) + + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit(self): + p = psutil.Process() + testfn = self.get_testfn() + soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) + try: + p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) + with open(testfn, "wb") as f: + f.write(b"X" * 1024) + # write() or flush() doesn't always cause the exception + # but close() will. + with pytest.raises(OSError) as exc: + with open(testfn, "wb") as f: + f.write(b"X" * 1025) + assert exc.value.errno == errno.EFBIG + finally: + p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) + assert p.rlimit(psutil.RLIMIT_FSIZE) == (soft, hard) + + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit_infinity(self): + # First set a limit, then re-set it by specifying INFINITY + # and assume we overridden the previous limit. + p = psutil.Process() + soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) + try: + p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) + p.rlimit(psutil.RLIMIT_FSIZE, (psutil.RLIM_INFINITY, hard)) + with open(self.get_testfn(), "wb") as f: + f.write(b"X" * 2048) + finally: + p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) + assert p.rlimit(psutil.RLIMIT_FSIZE) == (soft, hard) + + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit_infinity_value(self): + # RLIMIT_FSIZE should be RLIM_INFINITY, which will be a really + # big number on a platform with large file support. On these + # 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) + assert hard == psutil.RLIM_INFINITY + p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) + + def test_num_threads(self): + # on certain platforms such as Linux we might test for exact + # thread number, since we always have with 1 thread per process, + # but this does not apply across all platforms (MACOS, Windows) + p = psutil.Process() + if OPENBSD: + try: + step1 = p.num_threads() + except psutil.AccessDenied: + raise pytest.skip("on OpenBSD this requires root access") + else: + step1 = p.num_threads() + + with ThreadTask(): + step2 = p.num_threads() + assert step2 == step1 + 1 + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_num_handles(self): + # a better test is done later into test/_windows.py + p = psutil.Process() + assert p.num_handles() > 0 + + @pytest.mark.skipif(not HAS_THREADS, reason="not supported") + def test_threads(self): + p = psutil.Process() + if OPENBSD: + try: + step1 = p.threads() + except psutil.AccessDenied: + raise pytest.skip("on OpenBSD this requires root access") + else: + step1 = p.threads() + + with ThreadTask(): + step2 = p.threads() + assert len(step2) == len(step1) + 1 + athread = step2[0] + # test named tuple + assert athread.id == athread[0] + assert athread.user_time == athread[1] + assert athread.system_time == athread[2] + + @retry_on_failure() + @skip_on_access_denied(only_if=MACOS) + @pytest.mark.skipif(not HAS_THREADS, reason="not supported") + def test_threads_2(self): + p = self.spawn_psproc() + if OPENBSD: + try: + p.threads() + except psutil.AccessDenied: + raise pytest.skip("on OpenBSD this requires root access") + assert ( + abs(p.cpu_times().user - sum(x.user_time for x in p.threads())) + < 0.1 + ) + assert ( + abs(p.cpu_times().system - sum(x.system_time for x in p.threads())) + < 0.1 + ) + + @retry_on_failure() + def test_memory_info(self): + p = psutil.Process() + + # step 1 - get a base value to compare our results + rss1, vms1 = p.memory_info()[:2] + percent1 = p.memory_percent() + assert rss1 > 0 + assert 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 + assert rss2 > rss1 + assert vms2 >= vms1 # vms might be equal + assert percent2 > percent1 + del memarr + + if WINDOWS: + mem = p.memory_info() + assert mem.rss == mem.wset + assert mem.vms == mem.pagefile + + mem = p.memory_info() + for name in mem._fields: + assert getattr(mem, name) >= 0 + + def test_memory_full_info(self): + p = psutil.Process() + total = psutil.virtual_memory().total + mem = p.memory_full_info() + for name in mem._fields: + value = getattr(mem, name) + assert value >= 0 + if (name == "vms" and OSX) or LINUX: + continue + assert value <= total + if LINUX or WINDOWS or MACOS: + assert mem.uss >= 0 + if LINUX: + assert mem.pss >= 0 + assert mem.swap >= 0 + + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + def test_memory_maps(self): + p = psutil.Process() + maps = p.memory_maps() + assert len(maps) == len(set(maps)) + ext_maps = p.memory_maps(grouped=False) + + for nt in maps: + if nt.path.startswith('['): + continue + if BSD and nt.path == "pvclock": + continue + assert os.path.isabs(nt.path), nt.path + + if POSIX: + try: + assert os.path.exists(nt.path) or os.path.islink( + nt.path + ), nt.path + except AssertionError: + if not LINUX: + raise + # https://github.com/giampaolo/psutil/issues/759 + with open_text('/proc/self/smaps') as f: + data = f.read() + if f"{nt.path} (deleted)" not in data: + raise + elif '64' not in os.path.basename(nt.path): + # XXX - On Windows we have this strange behavior with + # 64 bit dlls: they are visible via explorer but cannot + # be accessed via os.stat() (wtf?). + try: + st = os.stat(nt.path) + except FileNotFoundError: + pass + else: + assert stat.S_ISREG(st.st_mode), nt.path + + for nt in ext_maps: + for fname in nt._fields: + value = getattr(nt, fname) + if fname == 'path': + continue + if fname in {'addr', 'perms'}: + assert value, value + else: + assert isinstance(value, int) + assert value >= 0, value + + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + def test_memory_maps_lists_lib(self): + # Make sure a newly loaded shared lib is listed. + p = psutil.Process() + with copyload_shared_lib() as path: + + def normpath(p): + return os.path.realpath(os.path.normcase(p)) + + libpaths = [normpath(x.path) for x in p.memory_maps()] + assert normpath(path) in libpaths + + def test_memory_percent(self): + p = psutil.Process() + p.memory_percent() + with pytest.raises(ValueError): + p.memory_percent(memtype="?!?") + if LINUX or MACOS or WINDOWS: + p.memory_percent(memtype='uss') + + def test_is_running(self): + p = self.spawn_psproc() + assert p.is_running() + assert p.is_running() + p.kill() + p.wait() + assert not p.is_running() + assert not p.is_running() + + def test_exe(self): + p = self.spawn_psproc() + exe = p.exe() + try: + assert exe == PYTHON_EXE + except AssertionError: + if WINDOWS and len(exe) == len(PYTHON_EXE): + # on Windows we don't care about case sensitivity + normcase = os.path.normcase + assert normcase(exe) == normcase(PYTHON_EXE) + else: + # certain platforms such as BSD are more accurate returning: + # "/usr/local/bin/python3.7" + # ...instead of: + # "/usr/local/bin/python" + # We do not want to consider this difference in accuracy + # an error. + ver = f"{sys.version_info[0]}.{sys.version_info[1]}" + try: + assert exe.replace(ver, '') == PYTHON_EXE.replace(ver, '') + except AssertionError: + # Typically MACOS. Really not sure what to do here. + pass + + out = sh([exe, "-c", "import os; print('hey')"]) + assert out == 'hey' + + def test_cmdline(self): + cmdline = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] + p = self.spawn_psproc(cmdline) + + if NETBSD and p.cmdline() == []: + # https://github.com/giampaolo/psutil/issues/2250 + raise pytest.skip("OPENBSD: returned EBUSY") + + # XXX - most of the times the underlying sysctl() call on Net + # and Open BSD returns a truncated string. + # Also /proc/pid/cmdline behaves the same so it looks + # like this is a kernel bug. + # XXX - AIX truncates long arguments in /proc/pid/cmdline + if NETBSD or OPENBSD or AIX: + assert p.cmdline()[0] == PYTHON_EXE + else: + if MACOS and CI_TESTING: + pyexe = p.cmdline()[0] + if pyexe != PYTHON_EXE: + assert ' '.join(p.cmdline()[1:]) == ' '.join(cmdline[1:]) + return + assert ' '.join(p.cmdline()) == ' '.join(cmdline) + + @pytest.mark.skipif(PYPY, reason="broken on PYPY") + def test_long_cmdline(self): + cmdline = [PYTHON_EXE] + cmdline.extend(["-v"] * 50) + cmdline.extend( + ["-c", "import time; [time.sleep(0.1) for x in range(100)]"] + ) + p = self.spawn_psproc(cmdline) + if OPENBSD: + # XXX: for some reason the test process may turn into a + # zombie (don't know why). + try: + assert p.cmdline() == cmdline + except psutil.ZombieProcess: + raise pytest.skip("OPENBSD: process turned into zombie") + else: + ret = p.cmdline() + if NETBSD and ret == []: + # https://github.com/giampaolo/psutil/issues/2250 + raise pytest.skip("OPENBSD: returned EBUSY") + assert ret == cmdline + + def test_name(self): + p = self.spawn_psproc() + name = p.name().lower() + pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() + assert pyexe.startswith(name), (pyexe, name) + + @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") + def test_long_name(self): + pyexe = create_py_exe(self.get_testfn(suffix=string.digits * 2)) + cmdline = [ + pyexe, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] + p = self.spawn_psproc(cmdline) + if OPENBSD: + # XXX: for some reason the test process may turn into a + # zombie (don't know why). Because the name() is long, all + # UNIX kernels truncate it to 15 chars, so internally psutil + # tries to guess the full name() from the cmdline(). But the + # cmdline() of a zombie on OpenBSD fails (internally), so we + # just compare the first 15 chars. Full explanation: + # https://github.com/giampaolo/psutil/issues/2239 + try: + assert p.name() == os.path.basename(pyexe) + except AssertionError: + if p.status() == psutil.STATUS_ZOMBIE: + assert os.path.basename(pyexe).startswith(p.name()) + else: + raise + else: + assert p.name() == os.path.basename(pyexe) + + # XXX: fails too often + # @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") + # @pytest.mark.skipif(AIX, reason="broken on AIX") + # @pytest.mark.skipif(PYPY, reason="broken on PYPY") + # def test_prog_w_funky_name(self): + # # Test that name(), exe() and cmdline() correctly handle programs + # # with funky chars such as spaces and ")", see: + # # https://github.com/giampaolo/psutil/issues/628 + # pyexe = create_py_exe(self.get_testfn(suffix='foo bar )')) + # cmdline = [ + # pyexe, + # "-c", + # "import time; [time.sleep(0.1) for x in range(100)]", + # ] + # p = self.spawn_psproc(cmdline) + # assert p.cmdline() == cmdline + # assert p.name() == os.path.basename(pyexe) + # assert os.path.normcase(p.exe()) == os.path.normcase(pyexe) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_uids(self): + p = psutil.Process() + real, effective, _saved = p.uids() + # os.getuid() refers to "real" uid + assert real == os.getuid() + # os.geteuid() refers to "effective" uid + assert effective == os.geteuid() + # No such thing as os.getsuid() ("saved" uid), but we have + # os.getresuid() which returns all of them. + if hasattr(os, "getresuid"): + assert os.getresuid() == p.uids() + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_gids(self): + p = psutil.Process() + real, effective, _saved = p.gids() + # os.getuid() refers to "real" uid + assert real == os.getgid() + # os.geteuid() refers to "effective" uid + assert effective == os.getegid() + # No such thing as os.getsgid() ("saved" gid), but we have + # os.getresgid() which returns all of them. + if hasattr(os, "getresuid"): + assert os.getresgid() == p.gids() + + def test_nice(self): + def cleanup(init): + try: + p.nice(init) + except psutil.AccessDenied: + pass + + p = psutil.Process() + with pytest.raises(TypeError): + p.nice("str") + init = p.nice() + self.addCleanup(cleanup, init) + + if WINDOWS: + highest_prio = None + for prio in [ + psutil.IDLE_PRIORITY_CLASS, + psutil.BELOW_NORMAL_PRIORITY_CLASS, + psutil.NORMAL_PRIORITY_CLASS, + psutil.ABOVE_NORMAL_PRIORITY_CLASS, + psutil.HIGH_PRIORITY_CLASS, + psutil.REALTIME_PRIORITY_CLASS, + ]: + with self.subTest(prio=prio): + try: + p.nice(prio) + except psutil.AccessDenied: + pass + else: + new_prio = p.nice() + # The OS may limit our maximum priority, + # even if the function succeeds. For higher + # priorities, we match either the expected + # value or the highest so far. + if prio in { + psutil.ABOVE_NORMAL_PRIORITY_CLASS, + psutil.HIGH_PRIORITY_CLASS, + psutil.REALTIME_PRIORITY_CLASS, + }: + if new_prio == prio or highest_prio is None: + highest_prio = prio + assert new_prio == highest_prio + else: + assert new_prio == prio + else: + try: + if hasattr(os, "getpriority"): + assert ( + os.getpriority(os.PRIO_PROCESS, os.getpid()) + == p.nice() + ) + p.nice(1) + assert p.nice() == 1 + if hasattr(os, "getpriority"): + assert ( + os.getpriority(os.PRIO_PROCESS, os.getpid()) + == p.nice() + ) + # XXX - going back to previous nice value raises + # AccessDenied on MACOS + if not MACOS: + p.nice(0) + assert p.nice() == 0 + except psutil.AccessDenied: + pass + + def test_status(self): + p = psutil.Process() + assert p.status() == psutil.STATUS_RUNNING + + def test_username(self): + p = self.spawn_psproc() + username = p.username() + if WINDOWS: + domain, username = username.split('\\') + getpass_user = getpass.getuser() + if getpass_user.endswith('$'): + # When running as a service account (most likely to be + # NetworkService), these user name calculations don't produce + # the same result, causing the test to fail. + raise pytest.skip('running as service account') + assert username == getpass_user + if 'USERDOMAIN' in os.environ: + assert domain == os.environ['USERDOMAIN'] + else: + assert username == getpass.getuser() + + def test_cwd(self): + p = self.spawn_psproc() + assert p.cwd() == os.getcwd() + + def test_cwd_2(self): + cmd = [ + PYTHON_EXE, + "-c", + ( + "import os, time; os.chdir('..'); [time.sleep(0.1) for x in" + " range(100)]" + ), + ] + p = self.spawn_psproc(cmd) + call_until(lambda: p.cwd() == os.path.dirname(os.getcwd())) + + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + 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"): + assert initial == list(os.sched_getaffinity(p.pid)) + assert len(initial) == len(set(initial)) + + all_cpus = list(range(len(psutil.cpu_percent(percpu=True)))) + for n in all_cpus: + p.cpu_affinity([n]) + assert p.cpu_affinity() == [n] + if hasattr(os, "sched_getaffinity"): + assert p.cpu_affinity() == list(os.sched_getaffinity(p.pid)) + # also test num_cpu() + if hasattr(p, "num_cpu"): + assert p.cpu_affinity()[0] == p.num_cpu() + + # [] is an alias for "all eligible CPUs"; on Linux this may + # not be equal to all available CPUs, see: + # https://github.com/giampaolo/psutil/issues/956 + p.cpu_affinity([]) + if LINUX: + assert p.cpu_affinity() == p._proc._get_eligible_cpus() + else: + assert p.cpu_affinity() == all_cpus + if hasattr(os, "sched_getaffinity"): + assert p.cpu_affinity() == list(os.sched_getaffinity(p.pid)) + + with pytest.raises(TypeError): + p.cpu_affinity(1) + p.cpu_affinity(initial) + # it should work with all iterables, not only lists + p.cpu_affinity(set(all_cpus)) + p.cpu_affinity(tuple(all_cpus)) + + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + def test_cpu_affinity_errs(self): + p = self.spawn_psproc() + invalid_cpu = [len(psutil.cpu_times(percpu=True)) + 10] + with pytest.raises(ValueError): + p.cpu_affinity(invalid_cpu) + with pytest.raises(ValueError): + p.cpu_affinity(range(10000, 11000)) + with pytest.raises((TypeError, ValueError)): + p.cpu_affinity([0, "1"]) + with pytest.raises(ValueError): + p.cpu_affinity([0, -1]) + + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + def test_cpu_affinity_all_combinations(self): + p = psutil.Process() + initial = p.cpu_affinity() + assert initial, initial + self.addCleanup(p.cpu_affinity, initial) + + # All possible CPU set combinations. + if len(initial) > 12: + initial = initial[:12] # ...otherwise it will take forever + combos = [] + for i in range(len(initial) + 1): + combos.extend( + list(subset) + for subset in itertools.combinations(initial, i) + if subset + ) + + for combo in combos: + p.cpu_affinity(combo) + assert sorted(p.cpu_affinity()) == sorted(combo) + + # TODO: #595 + @pytest.mark.skipif(BSD, reason="broken on BSD") + def test_open_files(self): + p = psutil.Process() + testfn = self.get_testfn() + files = p.open_files() + assert testfn not in files + with open(testfn, 'wb') as f: + f.write(b'x' * 1024) + f.flush() + # give the kernel some time to see the new file + call_until(lambda: len(p.open_files()) != len(files)) + files = p.open_files() + filenames = [os.path.normcase(x.path) for x in files] + assert os.path.normcase(testfn) in filenames + if LINUX: + for file in files: + if file.path == testfn: + assert file.position == 1024 + for file in files: + assert os.path.isfile(file.path), file + + # another process + cmdline = ( + f"import time; f = open(r'{testfn}', 'r'); [time.sleep(0.1) for x" + " in range(100)];" + ) + p = self.spawn_psproc([PYTHON_EXE, "-c", cmdline]) + + for x in range(100): + filenames = [os.path.normcase(x.path) for x in p.open_files()] + if testfn in filenames: + break + time.sleep(0.01) + else: + assert os.path.normcase(testfn) in filenames + for file in filenames: + assert os.path.isfile(file), file + + # TODO: #595 + @pytest.mark.skipif(BSD, reason="broken on BSD") + def test_open_files_2(self): + # test fd and path fields + p = psutil.Process() + normcase = os.path.normcase + testfn = self.get_testfn() + with open(testfn, 'w') as fileobj: + for file in p.open_files(): + if ( + normcase(file.path) == normcase(fileobj.name) + or file.fd == fileobj.fileno() + ): + break + else: + raise self.fail(f"no file found; files={p.open_files()!r}") + assert normcase(file.path) == normcase(fileobj.name) + if WINDOWS: + assert file.fd == -1 + else: + assert file.fd == fileobj.fileno() + # test positions + ntuple = p.open_files()[0] + assert ntuple[0] == ntuple.path + assert ntuple[1] == ntuple.fd + # test file is gone + assert fileobj.name not in p.open_files() + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_num_fds(self): + p = psutil.Process() + testfn = self.get_testfn() + start = p.num_fds() + file = open(testfn, 'w') # noqa: SIM115 + self.addCleanup(file.close) + assert p.num_fds() == start + 1 + sock = socket.socket() + self.addCleanup(sock.close) + assert p.num_fds() == start + 2 + file.close() + sock.close() + assert p.num_fds() == start + + @skip_on_not_implemented(only_if=LINUX) + @pytest.mark.skipif( + OPENBSD or NETBSD, reason="not reliable on OPENBSD & NETBSD" + ) + def test_num_ctx_switches(self): + p = psutil.Process() + before = sum(p.num_ctx_switches()) + for _ in range(2): + time.sleep(0.05) # this shall ensure a context switch happens + after = sum(p.num_ctx_switches()) + if after > before: + return + raise self.fail("num ctx switches still the same after 2 iterations") + + def test_ppid(self): + p = psutil.Process() + if hasattr(os, 'getppid'): + assert p.ppid() == os.getppid() + p = self.spawn_psproc() + assert p.ppid() == os.getpid() + + def test_parent(self): + p = self.spawn_psproc() + assert p.parent().pid == os.getpid() + + lowest_pid = psutil.pids()[0] + assert psutil.Process(lowest_pid).parent() is None + + def test_parent_multi(self): + parent = psutil.Process() + child, grandchild = self.spawn_children_pair() + assert grandchild.parent() == child + assert child.parent() == parent + + @retry_on_failure() + def test_parents(self): + parent = psutil.Process() + assert parent.parents() + child, grandchild = self.spawn_children_pair() + assert child.parents()[0] == parent + assert grandchild.parents()[0] == child + assert grandchild.parents()[1] == parent + + def test_children(self): + parent = psutil.Process() + assert not parent.children() + assert not parent.children(recursive=True) + # On Windows we set the flag to 0 in order to cancel out the + # CREATE_NO_WINDOW flag (enabled by default) which creates + # an extra "conhost.exe" child. + child = self.spawn_psproc(creationflags=0) + children1 = parent.children() + children2 = parent.children(recursive=True) + for children in (children1, children2): + assert len(children) == 1 + assert children[0].pid == child.pid + assert children[0].ppid() == parent.pid + + def test_children_recursive(self): + # Test children() against two sub processes, p1 and p2, where + # p1 (our child) spawned p2 (our grandchild). + parent = psutil.Process() + child, grandchild = self.spawn_children_pair() + assert parent.children() == [child] + assert parent.children(recursive=True) == [child, grandchild] + # If the intermediate process is gone there's no way for + # children() to recursively find it. + child.terminate() + child.wait() + assert not parent.children(recursive=True) + + def test_children_duplicates(self): + # find the process which has the highest number of children + table = collections.defaultdict(int) + for p in psutil.process_iter(): + try: + table[p.ppid()] += 1 + except psutil.Error: + pass + # this is the one, now let's make sure there are no duplicates + pid = max(table.items(), key=lambda x: x[1])[0] + if LINUX and pid == 0: + raise pytest.skip("PID 0") + p = psutil.Process(pid) + try: + c = p.children(recursive=True) + except psutil.AccessDenied: # windows + pass + else: + assert len(c) == len(set(c)) + + def test_parents_and_children(self): + parent = psutil.Process() + child, grandchild = self.spawn_children_pair() + # forward + children = parent.children(recursive=True) + assert len(children) == 2 + assert children[0] == child + assert children[1] == grandchild + # backward + parents = grandchild.parents() + assert parents[0] == child + assert parents[1] == parent + + def test_suspend_resume(self): + p = self.spawn_psproc() + p.suspend() + for _ in range(100): + if p.status() == psutil.STATUS_STOPPED: + break + time.sleep(0.01) + p.resume() + assert p.status() != psutil.STATUS_STOPPED + + def test_invalid_pid(self): + with pytest.raises(TypeError): + psutil.Process("1") + with pytest.raises(ValueError): + psutil.Process(-1) + + def test_as_dict(self): + p = psutil.Process() + d = p.as_dict(attrs=['exe', 'name']) + assert sorted(d.keys()) == ['exe', 'name'] + + p = psutil.Process(min(psutil.pids())) + d = p.as_dict(attrs=['net_connections'], ad_value='foo') + if not isinstance(d['net_connections'], list): + assert d['net_connections'] == 'foo' + + # Test ad_value is set on AccessDenied. + with mock.patch( + 'psutil.Process.nice', create=True, side_effect=psutil.AccessDenied + ): + assert 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"), + ): + with pytest.raises(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"), + ): + assert 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() + assert 'nice' not in list(d.keys()) + # ...unless the user explicitly asked for some attr. + with pytest.raises(NotImplementedError): + p.as_dict(attrs=["nice"]) + + # errors + with pytest.raises(TypeError): + p.as_dict('name') + with pytest.raises(ValueError): + p.as_dict(['foo']) + with pytest.raises(ValueError): + p.as_dict(['foo', 'bar']) + + def test_oneshot(self): + p = psutil.Process() + with mock.patch("psutil._psplatform.Process.cpu_times") as m: + with p.oneshot(): + p.cpu_times() + p.cpu_times() + assert m.call_count == 1 + + with mock.patch("psutil._psplatform.Process.cpu_times") as m: + p.cpu_times() + p.cpu_times() + assert m.call_count == 2 + + def test_oneshot_twice(self): + # Test the case where the ctx manager is __enter__ed twice. + # The second __enter__ is supposed to resut in a NOOP. + p = psutil.Process() + with mock.patch("psutil._psplatform.Process.cpu_times") as m1: + with mock.patch("psutil._psplatform.Process.oneshot_enter") as m2: + with p.oneshot(): + p.cpu_times() + p.cpu_times() + with p.oneshot(): + p.cpu_times() + p.cpu_times() + assert m1.call_count == 1 + assert m2.call_count == 1 + + with mock.patch("psutil._psplatform.Process.cpu_times") as m: + p.cpu_times() + p.cpu_times() + assert m.call_count == 2 + + def test_oneshot_cache(self): + # Make sure oneshot() cache is nonglobal. Instead it's + # supposed to be bound to the Process instance, see: + # https://github.com/giampaolo/psutil/issues/1373 + p1, p2 = self.spawn_children_pair() + p1_ppid = p1.ppid() + p2_ppid = p2.ppid() + assert p1_ppid != p2_ppid + with p1.oneshot(): + assert p1.ppid() == p1_ppid + assert p2.ppid() == p2_ppid + with p2.oneshot(): + assert p1.ppid() == p1_ppid + assert p2.ppid() == p2_ppid + + def test_halfway_terminated_process(self): + # Test that NoSuchProcess exception gets raised in case the + # process dies after we create the Process object. + # Example: + # >>> proc = Process(1234) + # >>> time.sleep(2) # time-consuming task, process dies in meantime + # >>> proc.name() + # Refers to Issue #15 + def assert_raises_nsp(fun, fun_name): + try: + ret = fun() + except psutil.ZombieProcess: # differentiate from NSP + raise + except psutil.NoSuchProcess: + pass + except psutil.AccessDenied: + if OPENBSD and fun_name in {'threads', 'num_threads'}: + return + raise + else: + # NtQuerySystemInformation succeeds even if process is gone. + if WINDOWS and fun_name in {'exe', 'name'}: + return + raise self.fail( + f"{fun!r} didn't raise NSP and returned {ret!r} instead" + ) + + p = self.spawn_psproc() + p.terminate() + p.wait() + if WINDOWS: # XXX + call_until(lambda: p.pid not in psutil.pids()) + self.assertProcessGone(p) + + ns = process_namespace(p) + for fun, name in ns.iter(ns.all): + assert_raises_nsp(fun, name) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_zombie_process(self): + _parent, zombie = self.spawn_zombie() + self.assertProcessZombie(zombie) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_zombie_process_is_running_w_exc(self): + # Emulate a case where internally is_running() raises + # ZombieProcess. + p = psutil.Process() + with mock.patch( + "psutil.Process", side_effect=psutil.ZombieProcess(0) + ) as m: + assert p.is_running() + assert m.called + + @pytest.mark.skipif(not POSIX, reason="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: + assert p.status() == psutil.STATUS_ZOMBIE + assert m.called + + def test_reused_pid(self): + # Emulate a case where PID has been reused by another process. + subp = self.spawn_testproc() + p = psutil.Process(subp.pid) + p._ident = (p.pid, p.create_time() + 100) + + list(psutil.process_iter()) + assert p.pid in psutil._pmap + assert not p.is_running() + + # make sure is_running() removed PID from process_iter() + # internal cache + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with contextlib.redirect_stderr(io.StringIO()) as f: + list(psutil.process_iter()) + assert ( + f"refreshing Process instance for reused PID {p.pid}" + in f.getvalue() + ) + assert p.pid not in psutil._pmap + + assert p != psutil.Process(subp.pid) + msg = "process no longer exists and its PID has been reused" + ns = process_namespace(p) + for fun, name in ns.iter(ns.setters + ns.killers, clear_cache=False): + with self.subTest(name=name): + with pytest.raises(psutil.NoSuchProcess, match=msg): + fun() + + assert "terminated + PID reused" in str(p) + assert "terminated + PID reused" in repr(p) + + with pytest.raises(psutil.NoSuchProcess, match=msg): + p.ppid() + with pytest.raises(psutil.NoSuchProcess, match=msg): + p.parent() + with pytest.raises(psutil.NoSuchProcess, match=msg): + p.parents() + with pytest.raises(psutil.NoSuchProcess, match=msg): + p.children() + + def test_pid_0(self): + # Process(0) is supposed to work on all platforms except Linux + if 0 not in psutil.pids(): + with pytest.raises(psutil.NoSuchProcess): + psutil.Process(0) + # These 2 are a contradiction, but "ps" says PID 1's parent + # is PID 0. + assert not psutil.pid_exists(0) + assert psutil.Process(1).ppid() == 0 + return + + p = psutil.Process(0) + exc = psutil.AccessDenied if WINDOWS else ValueError + with pytest.raises(exc): + p.wait() + with pytest.raises(exc): + p.terminate() + with pytest.raises(exc): + p.suspend() + with pytest.raises(exc): + p.resume() + with pytest.raises(exc): + p.kill() + with pytest.raises(exc): + p.send_signal(signal.SIGTERM) + + # test all methods + ns = process_namespace(p) + for fun, name in ns.iter(ns.getters + ns.setters): + try: + ret = fun() + except psutil.AccessDenied: + pass + else: + if name in {"uids", "gids"}: + assert ret.real == 0 + elif name == "username": + user = 'NT AUTHORITY\\SYSTEM' if WINDOWS else 'root' + assert p.username() == user + elif name == "name": + assert name, name + + if not OPENBSD: + assert 0 in psutil.pids() + assert psutil.pid_exists(0) + + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + def test_environ(self): + def clean_dict(d): + exclude = ["PLAT", "HOME", "PYTEST_CURRENT_TEST", "PYTEST_VERSION"] + if MACOS: + exclude.extend([ + "__CF_USER_TEXT_ENCODING", + "VERSIONER_PYTHON_PREFER_32_BIT", + "VERSIONER_PYTHON_VERSION", + "VERSIONER_PYTHON_VERSION", + ]) + for name in exclude: + d.pop(name, None) + return { + k.replace("\r", "").replace("\n", ""): v.replace( + "\r", "" + ).replace("\n", "") + for k, v in d.items() + } + + self.maxDiff = None + p = psutil.Process() + d1 = clean_dict(p.environ()) + d2 = clean_dict(os.environ.copy()) + if not OSX and GITHUB_ACTIONS: + assert d1 == d2 + + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.skipif( + MACOS_11PLUS, + reason="macOS 11+ can't get another process environment, issue #2084", + ) + @pytest.mark.skipif( + NETBSD, reason="sometimes fails on `assert is_running()`" + ) + def test_weird_environ(self): + # environment variables can contain values without an equals sign + code = textwrap.dedent(""" + #include + #include + + char * const argv[] = {"cat", 0}; + char * const envp[] = {"A=1", "X", "C=3", 0}; + + int main(void) { + // Close stderr on exec so parent can wait for the + // execve to finish. + if (fcntl(2, F_SETFD, FD_CLOEXEC) != 0) + return 0; + return execve("/bin/cat", argv, envp); + } + """) + cexe = create_c_exe(self.get_testfn(), c_code=code) + sproc = self.spawn_testproc( + [cexe], stdin=subprocess.PIPE, stderr=subprocess.PIPE + ) + p = psutil.Process(sproc.pid) + wait_for_pid(p.pid) + assert p.is_running() + # Wait for process to exec or exit. + assert sproc.stderr.read() == b"" + if MACOS and CI_TESTING: + try: + env = p.environ() + except psutil.AccessDenied: + # XXX: fails sometimes with: + # PermissionError from 'sysctl(KERN_PROCARGS2) -> EIO' + return + else: + env = p.environ() + assert env == {"A": "1", "C": "3"} + sproc.communicate() + assert sproc.returncode == 0 + + +# =================================================================== +# --- psutil.Popen tests +# =================================================================== + + +class TestPopen(PsutilTestCase): + """Tests for psutil.Popen class.""" + + @classmethod + def tearDownClass(cls): + reap_children() + + def test_misc(self): + # XXX this test causes a ResourceWarning because + # psutil.__subproc instance doesn't get properly freed. + # Not sure what to do though. + cmd = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)];", + ] + with psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.name() + proc.cpu_times() + proc.stdin # noqa: B018 + assert dir(proc) + with pytest.raises(AttributeError): + proc.foo # noqa: B018 + proc.terminate() + if POSIX: + assert proc.wait(5) == -signal.SIGTERM + else: + assert proc.wait(5) == signal.SIGTERM + + def test_ctx_manager(self): + with psutil.Popen( + [PYTHON_EXE, "-V"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.communicate() + assert proc.stdout.closed + assert proc.stderr.closed + assert proc.stdin.closed + assert 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(0.1) for x in range(100)];", + ] + with psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.terminate() + proc.wait() + with pytest.raises(psutil.NoSuchProcess): + proc.terminate() + with pytest.raises(psutil.NoSuchProcess): + proc.kill() + with pytest.raises(psutil.NoSuchProcess): + proc.send_signal(signal.SIGTERM) + if WINDOWS: + with pytest.raises(psutil.NoSuchProcess): + proc.send_signal(signal.CTRL_C_EVENT) + with pytest.raises(psutil.NoSuchProcess): + proc.send_signal(signal.CTRL_BREAK_EVENT) + + def test__getattribute__(self): + cmd = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)];", + ] + with psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.terminate() + proc.wait() + with pytest.raises(AttributeError): + proc.foo # noqa: B018 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_process_all.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_process_all.py new file mode 100644 index 0000000..aaa3fa0 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_process_all.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Iterate over all process PIDs and for each one of them invoke and +test all psutil.Process() methods. +""" + +import enum +import errno +import multiprocessing +import os +import stat +import time +import traceback + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import OSX +from psutil import POSIX +from psutil import WINDOWS +from psutil.tests import CI_TESTING +from psutil.tests import PYTEST_PARALLEL +from psutil.tests import VALID_PROC_STATUSES +from psutil.tests import PsutilTestCase +from psutil.tests import check_connection_ntuple +from psutil.tests import create_sockets +from psutil.tests import is_namedtuple +from psutil.tests import is_win_secure_system_proc +from psutil.tests import process_namespace +from psutil.tests import pytest + + +# Cuts the time in half, but (e.g.) on macOS the process pool stays +# alive after join() (multiprocessing bug?), messing up other tests. +USE_PROC_POOL = LINUX and not CI_TESTING and not PYTEST_PARALLEL + + +def proc_info(pid): + tcase = PsutilTestCase() + + def check_exception(exc, proc, name, ppid): + tcase.assertEqual(exc.pid, pid) + if exc.name is not None: + tcase.assertEqual(exc.name, name) + if isinstance(exc, psutil.ZombieProcess): + tcase.assertProcessZombie(proc) + if exc.ppid is not None: + tcase.assertGreaterEqual(exc.ppid, 0) + tcase.assertEqual(exc.ppid, ppid) + elif isinstance(exc, psutil.NoSuchProcess): + tcase.assertProcessGone(proc) + str(exc) + repr(exc) + + def do_wait(): + if pid != 0: + try: + proc.wait(0) + except psutil.Error as exc: + check_exception(exc, proc, name, ppid) + + try: + proc = psutil.Process(pid) + except psutil.NoSuchProcess: + tcase.assertPidGone(pid) + return {} + try: + d = proc.as_dict(['ppid', 'name']) + except psutil.NoSuchProcess: + tcase.assertProcessGone(proc) + else: + name, ppid = d['name'], d['ppid'] + info = {'pid': proc.pid} + ns = process_namespace(proc) + # We don't use oneshot() because in order not to fool + # check_exception() in case of NSP. + for fun, fun_name in ns.iter(ns.getters, clear_cache=False): + try: + info[fun_name] = fun() + except psutil.Error as exc: + check_exception(exc, proc, name, ppid) + continue + do_wait() + return info + + +class TestFetchAllProcesses(PsutilTestCase): + """Test which iterates over all running processes and performs + some sanity checks against Process API's returned values. + Uses a process pool to get info about all processes. + """ + + def setUp(self): + psutil._set_debug(False) + # Using a pool in a CI env may result in deadlock, see: + # https://github.com/giampaolo/psutil/issues/2104 + if USE_PROC_POOL: + self.pool = multiprocessing.Pool() + + def tearDown(self): + psutil._set_debug(True) + if USE_PROC_POOL: + self.pool.terminate() + self.pool.join() + + def iter_proc_info(self): + # Fixes "can't pickle : it's not the + # same object as test_process_all.proc_info". + from psutil.tests.test_process_all import proc_info + + if USE_PROC_POOL: + return self.pool.imap_unordered(proc_info, psutil.pids()) + else: + ls = [proc_info(pid) for pid in psutil.pids()] + return ls + + def test_all(self): + failures = [] + for info in self.iter_proc_info(): + for name, value in info.items(): + meth = getattr(self, name) + try: + meth(value, info) + except Exception: # noqa: BLE001 + s = '\n' + '=' * 70 + '\n' + s += ( + "FAIL: name=test_{}, pid={}, ret={}\ninfo={}\n".format( + name, + info['pid'], + repr(value), + info, + ) + ) + s += '-' * 70 + s += f"\n{traceback.format_exc()}" + s = "\n".join((" " * 4) + i for i in s.splitlines()) + "\n" + failures.append(s) + else: + if value not in (0, 0.0, [], None, '', {}): + assert value, value + if failures: + raise self.fail(''.join(failures)) + + def cmdline(self, ret, info): + assert isinstance(ret, list) + for part in ret: + assert isinstance(part, str) + + def exe(self, ret, info): + assert isinstance(ret, str) + assert ret.strip() == ret + if ret: + if WINDOWS and not ret.endswith('.exe'): + return # May be "Registry", "MemCompression", ... + assert os.path.isabs(ret), ret + # Note: os.stat() may return False even if the file is there + # hence we skip the test, see: + # http://stackoverflow.com/questions/3112546/os-path-exists-lies + if POSIX and os.path.isfile(ret): + if hasattr(os, 'access') and hasattr(os, "X_OK"): + # XXX: may fail on MACOS + try: + assert os.access(ret, os.X_OK) + except AssertionError: + if os.path.exists(ret) and not CI_TESTING: + raise + + def pid(self, ret, info): + assert isinstance(ret, int) + assert ret >= 0 + + def ppid(self, ret, info): + assert isinstance(ret, int) + assert ret >= 0 + proc_info(ret) + + def name(self, ret, info): + assert isinstance(ret, str) + if WINDOWS and not ret and is_win_secure_system_proc(info['pid']): + # https://github.com/giampaolo/psutil/issues/2338 + return + # on AIX, "" processes don't have names + if not AIX: + assert ret, repr(ret) + + def create_time(self, ret, info): + assert isinstance(ret, float) + try: + assert ret >= 0 + except AssertionError: + # XXX + if OPENBSD and info['status'] == psutil.STATUS_ZOMBIE: + pass + else: + raise + # this can't be taken for granted on all platforms + # self.assertGreaterEqual(ret, psutil.boot_time()) + # make sure returned value can be pretty printed + # with strftime + time.strftime("%Y %m %d %H:%M:%S", time.localtime(ret)) + + def uids(self, ret, info): + assert is_namedtuple(ret) + for uid in ret: + assert isinstance(uid, int) + assert uid >= 0 + + def gids(self, ret, info): + assert is_namedtuple(ret) + # note: testing all gids as above seems not to be reliable for + # gid == 30 (nodoby); not sure why. + for gid in ret: + assert isinstance(gid, int) + if not MACOS and not NETBSD: + assert gid >= 0 + + def username(self, ret, info): + assert isinstance(ret, str) + assert ret.strip() == ret + assert ret.strip() + + def status(self, ret, info): + assert isinstance(ret, str) + assert ret, ret + assert ret != '?' # XXX + assert ret in VALID_PROC_STATUSES + + def io_counters(self, ret, info): + assert is_namedtuple(ret) + for field in ret: + assert isinstance(field, int) + if field != -1: + assert field >= 0 + + def ionice(self, ret, info): + if LINUX: + assert isinstance(ret.ioclass, int) + assert isinstance(ret.value, int) + assert ret.ioclass >= 0 + assert ret.value >= 0 + else: # Windows, Cygwin + choices = [ + psutil.IOPRIO_VERYLOW, + psutil.IOPRIO_LOW, + psutil.IOPRIO_NORMAL, + psutil.IOPRIO_HIGH, + ] + assert isinstance(ret, int) + assert ret >= 0 + assert ret in choices + + def num_threads(self, ret, info): + assert isinstance(ret, int) + if WINDOWS and ret == 0 and is_win_secure_system_proc(info['pid']): + # https://github.com/giampaolo/psutil/issues/2338 + return + assert ret >= 1 + + def threads(self, ret, info): + assert isinstance(ret, list) + for t in ret: + assert is_namedtuple(t) + assert t.id >= 0 + assert t.user_time >= 0 + assert t.system_time >= 0 + for field in t: + assert isinstance(field, (int, float)) + + def cpu_times(self, ret, info): + assert is_namedtuple(ret) + for n in ret: + assert isinstance(n, float) + assert n >= 0 + # TODO: check ntuple fields + + def cpu_percent(self, ret, info): + assert isinstance(ret, float) + assert 0.0 <= ret <= 100.0, ret + + def cpu_num(self, ret, info): + assert isinstance(ret, int) + if FREEBSD and ret == -1: + return + assert ret >= 0 + if psutil.cpu_count() == 1: + assert ret == 0 + assert ret in list(range(psutil.cpu_count())) + + def memory_info(self, ret, info): + assert is_namedtuple(ret) + for value in ret: + assert isinstance(value, int) + assert value >= 0 + if WINDOWS: + assert ret.peak_wset >= ret.wset + assert ret.peak_paged_pool >= ret.paged_pool + assert ret.peak_nonpaged_pool >= ret.nonpaged_pool + assert ret.peak_pagefile >= ret.pagefile + + def memory_full_info(self, ret, info): + assert is_namedtuple(ret) + total = psutil.virtual_memory().total + for name in ret._fields: + value = getattr(ret, name) + assert isinstance(value, int) + assert value >= 0 + if LINUX or (OSX and name in {'vms', 'data'}): + # On Linux there are processes (e.g. 'goa-daemon') whose + # VMS is incredibly high for some reason. + continue + assert value <= total, name + + if LINUX: + assert ret.pss >= ret.uss + + def open_files(self, ret, info): + assert isinstance(ret, list) + for f in ret: + assert isinstance(f.fd, int) + assert isinstance(f.path, str) + assert f.path.strip() == f.path + if WINDOWS: + assert f.fd == -1 + elif LINUX: + assert isinstance(f.position, int) + assert isinstance(f.mode, str) + assert isinstance(f.flags, int) + assert f.position >= 0 + assert f.mode in {'r', 'w', 'a', 'r+', 'a+'} + assert f.flags > 0 + elif BSD and not f.path: + # XXX see: https://github.com/giampaolo/psutil/issues/595 + continue + assert os.path.isabs(f.path), f + try: + st = os.stat(f.path) + except FileNotFoundError: + pass + else: + assert stat.S_ISREG(st.st_mode), f + + def num_fds(self, ret, info): + assert isinstance(ret, int) + assert ret >= 0 + + def net_connections(self, ret, info): + with create_sockets(): + assert len(ret) == len(set(ret)) + for conn in ret: + assert is_namedtuple(conn) + check_connection_ntuple(conn) + + def cwd(self, ret, info): + assert isinstance(ret, str) + assert ret.strip() == ret + if ret: + assert os.path.isabs(ret), ret + try: + st = os.stat(ret) + except OSError as err: + if WINDOWS and psutil._psplatform.is_permission_err(err): + pass + # directory has been removed in mean time + elif err.errno != errno.ENOENT: + raise + else: + assert stat.S_ISDIR(st.st_mode) + + def memory_percent(self, ret, info): + assert isinstance(ret, float) + assert 0 <= ret <= 100, ret + + def is_running(self, ret, info): + assert isinstance(ret, bool) + + def cpu_affinity(self, ret, info): + assert isinstance(ret, list) + assert ret != [] + cpus = list(range(psutil.cpu_count())) + for n in ret: + assert isinstance(n, int) + assert n in cpus + + def terminal(self, ret, info): + assert isinstance(ret, (str, type(None))) + if ret is not None: + assert os.path.isabs(ret), ret + assert os.path.exists(ret), ret + + def memory_maps(self, ret, info): + for nt in ret: + assert isinstance(nt.addr, str) + assert isinstance(nt.perms, str) + assert isinstance(nt.path, str) + for fname in nt._fields: + value = getattr(nt, fname) + if fname == 'path': + if value.startswith(("[", "anon_inode:")): # linux + continue + if BSD and value == "pvclock": # seen on FreeBSD + continue + assert os.path.isabs(nt.path), nt.path + # commented as on Linux we might get + # '/foo/bar (deleted)' + # assert os.path.exists(nt.path), nt.path + elif fname == 'addr': + assert value, repr(value) + elif fname == 'perms': + if not WINDOWS: + assert value, repr(value) + else: + assert isinstance(value, int) + assert value >= 0 + + def num_handles(self, ret, info): + assert isinstance(ret, int) + assert ret >= 0 + + def nice(self, ret, info): + assert isinstance(ret, int) + if POSIX: + assert -20 <= ret <= 20, ret + else: + priorities = [ + getattr(psutil, x) + for x in dir(psutil) + if x.endswith('_PRIORITY_CLASS') + ] + assert ret in priorities + assert isinstance(ret, enum.IntEnum) + + def num_ctx_switches(self, ret, info): + assert is_namedtuple(ret) + for value in ret: + assert isinstance(value, int) + assert value >= 0 + + def rlimit(self, ret, info): + assert isinstance(ret, tuple) + assert len(ret) == 2 + assert ret[0] >= -1 + assert ret[1] >= -1 + + def environ(self, ret, info): + assert isinstance(ret, dict) + for k, v in ret.items(): + assert isinstance(k, str) + assert isinstance(v, str) + + +class TestPidsRange(PsutilTestCase): + """Given pid_exists() return value for a range of PIDs which may or + may not exist, make sure that psutil.Process() and psutil.pids() + agree with pid_exists(). This guarantees that the 3 APIs are all + consistent with each other. See: + https://github.com/giampaolo/psutil/issues/2359 + + XXX - Note about Windows: it turns out there are some "hidden" PIDs + which are not returned by psutil.pids() and are also not revealed + by taskmgr.exe and ProcessHacker, still they can be instantiated by + psutil.Process() and queried. One of such PIDs is "conhost.exe". + Running as_dict() for it reveals that some Process() APIs + erroneously raise NoSuchProcess, so we know we have problem there. + Let's ignore this for now, since it's quite a corner case (who even + imagined hidden PIDs existed on Windows?). + """ + + def setUp(self): + psutil._set_debug(False) + + def tearDown(self): + psutil._set_debug(True) + + def test_it(self): + def is_linux_tid(pid): + try: + f = open(f"/proc/{pid}/status", "rb") # noqa: SIM115 + except FileNotFoundError: + return False + else: + with f: + for line in f: + if line.startswith(b"Tgid:"): + tgid = int(line.split()[1]) + # If tgid and pid are different then we're + # dealing with a process TID. + return tgid != pid + raise ValueError("'Tgid' line not found") + + def check(pid): + # In case of failure retry up to 3 times in order to avoid + # race conditions, especially when running in a CI + # environment where PIDs may appear and disappear at any + # time. + x = 3 + while True: + exists = psutil.pid_exists(pid) + try: + if exists: + psutil.Process(pid) + if not WINDOWS: # see docstring + assert pid in psutil.pids() + else: + # On OpenBSD thread IDs can be instantiated, + # and oneshot() succeeds, but other APIs fail + # with EINVAL. + if not OPENBSD: + with pytest.raises(psutil.NoSuchProcess): + psutil.Process(pid) + if not WINDOWS: # see docstring + assert pid not in psutil.pids() + except (psutil.Error, AssertionError): + x -= 1 + if x == 0: + raise + else: + return + + for pid in range(1, 3000): + if LINUX and is_linux_tid(pid): + # On Linux a TID (thread ID) can be passed to the + # Process class and is querable like a PID (process + # ID). Skip it. + continue + with self.subTest(pid=pid): + check(pid) diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_scripts.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_scripts.py new file mode 100644 index 0000000..de0ad2a --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_scripts.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Test various scripts.""" + +import ast +import os +import shutil +import stat +import subprocess + +import pytest + +from psutil import POSIX +from psutil import WINDOWS +from psutil.tests import CI_TESTING +from psutil.tests import HAS_BATTERY +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import HAS_SENSORS_BATTERY +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import PYTHON_EXE +from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import ROOT_DIR +from psutil.tests import SCRIPTS_DIR +from psutil.tests import PsutilTestCase +from psutil.tests import import_module_by_path +from psutil.tests import psutil +from psutil.tests import sh + + +INTERNAL_SCRIPTS_DIR = os.path.join(SCRIPTS_DIR, "internal") +SETUP_PY = os.path.join(ROOT_DIR, 'setup.py') + + +# =================================================================== +# --- Tests scripts in scripts/ directory +# =================================================================== + + +@pytest.mark.skipif( + CI_TESTING and not os.path.exists(SCRIPTS_DIR), + reason="can't find scripts/ directory", +) +class TestExampleScripts(PsutilTestCase): + @staticmethod + def assert_stdout(exe, *args, **kwargs): + kwargs.setdefault("env", PYTHON_EXE_ENV) + exe = os.path.join(SCRIPTS_DIR, exe) + cmd = [PYTHON_EXE, exe] + for arg in args: + cmd.append(arg) + try: + out = sh(cmd, **kwargs).strip() + except RuntimeError as err: + if 'AccessDenied' in str(err): + return str(err) + else: + raise + assert out, out + return out + + @staticmethod + def assert_syntax(exe): + exe = os.path.join(SCRIPTS_DIR, exe) + with open(exe, encoding="utf8") as f: + src = f.read() + ast.parse(src) + + def test_coverage(self): + # make sure all example scripts have a test method defined + meths = dir(self) + for name in os.listdir(SCRIPTS_DIR): + if name.endswith('.py'): + if 'test_' + os.path.splitext(name)[0] not in meths: + # self.assert_stdout(name) + raise self.fail( + "no test defined for" + f" {os.path.join(SCRIPTS_DIR, name)!r} script" + ) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_executable(self): + for root, dirs, files in os.walk(SCRIPTS_DIR): + for file in files: + if file.endswith('.py'): + path = os.path.join(root, file) + if not stat.S_IXUSR & os.stat(path)[stat.ST_MODE]: + raise self.fail(f"{path!r} is not executable") + + def test_disk_usage(self): + self.assert_stdout('disk_usage.py') + + def test_free(self): + self.assert_stdout('free.py') + + def test_meminfo(self): + self.assert_stdout('meminfo.py') + + def test_procinfo(self): + self.assert_stdout('procinfo.py', str(os.getpid())) + + @pytest.mark.skipif(CI_TESTING and not psutil.users(), reason="no users") + def test_who(self): + self.assert_stdout('who.py') + + def test_ps(self): + self.assert_stdout('ps.py') + + def test_pstree(self): + self.assert_stdout('pstree.py') + + def test_netstat(self): + self.assert_stdout('netstat.py') + + def test_ifconfig(self): + self.assert_stdout('ifconfig.py') + + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + def test_pmap(self): + self.assert_stdout('pmap.py', str(os.getpid())) + + def test_procsmem(self): + if 'uss' not in psutil.Process().memory_full_info()._fields: + raise pytest.skip("not supported") + self.assert_stdout('procsmem.py') + + def test_killall(self): + self.assert_syntax('killall.py') + + def test_nettop(self): + self.assert_syntax('nettop.py') + + def test_top(self): + self.assert_syntax('top.py') + + def test_iotop(self): + self.assert_syntax('iotop.py') + + def test_pidof(self): + output = self.assert_stdout('pidof.py', psutil.Process().name()) + assert str(os.getpid()) in output + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_winservices(self): + self.assert_stdout('winservices.py') + + def test_cpu_distribution(self): + self.assert_syntax('cpu_distribution.py') + + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + def test_temperatures(self): + if not psutil.sensors_temperatures(): + raise pytest.skip("no temperatures") + self.assert_stdout('temperatures.py') + + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") + def test_fans(self): + if not psutil.sensors_fans(): + raise pytest.skip("no fans") + self.assert_stdout('fans.py') + + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_battery(self): + self.assert_stdout('battery.py') + + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_sensors(self): + self.assert_stdout('sensors.py') + + +# =================================================================== +# --- Tests scripts in scripts/internal/ directory +# =================================================================== + + +@pytest.mark.skipif( + CI_TESTING and not os.path.exists(INTERNAL_SCRIPTS_DIR), + reason="can't find scripts/internal/ directory", +) +class TestInternalScripts(PsutilTestCase): + @staticmethod + def ls(): + for name in os.listdir(INTERNAL_SCRIPTS_DIR): + if name.endswith(".py"): + yield os.path.join(INTERNAL_SCRIPTS_DIR, name) + + def test_syntax_all(self): + for path in self.ls(): + with open(path, encoding="utf8") as f: + data = f.read() + ast.parse(data) + + @pytest.mark.skipif(CI_TESTING, reason="not on CI") + def test_import_all(self): + for path in self.ls(): + try: + import_module_by_path(path) + except SystemExit: + pass + + +# =================================================================== +# --- Tests for setup.py script +# =================================================================== + + +@pytest.mark.skipif( + CI_TESTING and not os.path.exists(SETUP_PY), reason="can't find setup.py" +) +class TestSetupScript(PsutilTestCase): + def test_invocation(self): + module = import_module_by_path(SETUP_PY) + with pytest.raises(SystemExit): + module.setup() + assert module.get_version() == psutil.__version__ + + @pytest.mark.skipif( + not shutil.which("python2.7"), reason="python2.7 not installed" + ) + def test_python2(self): + # There's a duplicate of this test in scripts/internal + # directory, which is only executed by CI. We replicate it here + # to run it when developing locally. + p = subprocess.Popen( + [shutil.which("python2.7"), SETUP_PY], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = p.communicate() + assert p.wait() == 1 + assert not stdout + assert "psutil no longer supports Python 2.7" in stderr + assert "Latest version supporting Python 2.7 is" in stderr diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_sunos.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_sunos.py new file mode 100644 index 0000000..b5d9d35 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_sunos.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Sun OS specific tests.""" + +import os + +import psutil +from psutil import SUNOS +from psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests import sh + + +@pytest.mark.skipif(not SUNOS, reason="SUNOS only") +class SunOSSpecificTestCase(PsutilTestCase): + def test_swap_memory(self): + out = sh(f"env PATH=/usr/sbin:/sbin:{os.environ['PATH']} swap -l") + lines = out.strip().split('\n')[1:] + if not lines: + raise ValueError('no swap device(s) configured') + total = free = 0 + for line in lines: + fields = line.split() + total = int(fields[3]) * 512 + free = int(fields[4]) * 512 + used = total - free + + psutil_swap = psutil.swap_memory() + assert psutil_swap.total == total + assert psutil_swap.used == used + assert psutil_swap.free == free + + def test_cpu_count(self): + out = sh("/usr/sbin/psrinfo") + assert psutil.cpu_count() == len(out.split('\n')) diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_system.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_system.py new file mode 100644 index 0000000..b961e1f --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_system.py @@ -0,0 +1,979 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for system APIS.""" + +import datetime +import enum +import errno +import os +import platform +import pprint +import shutil +import signal +import socket +import sys +import time +from unittest import mock + +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._common import broadcast_addr +from psutil.tests import AARCH64 +from psutil.tests import ASCII_FS +from psutil.tests import CI_TESTING +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import GLOBAL_TIMEOUT +from psutil.tests import HAS_BATTERY +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_GETLOADAVG +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import HAS_SENSORS_BATTERY +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import IS_64BIT +from psutil.tests import MACOS_12PLUS +from psutil.tests import PYPY +from psutil.tests import UNICODE_SUFFIX +from psutil.tests import PsutilTestCase +from psutil.tests import check_net_address +from psutil.tests import pytest +from psutil.tests import retry_on_failure + + +# =================================================================== +# --- System-related API tests +# =================================================================== + + +class TestProcessIter(PsutilTestCase): + def test_pid_presence(self): + assert os.getpid() in [x.pid for x in psutil.process_iter()] + sproc = self.spawn_testproc() + assert sproc.pid in [x.pid for x in psutil.process_iter()] + p = psutil.Process(sproc.pid) + p.kill() + p.wait() + assert sproc.pid not in [x.pid for x in psutil.process_iter()] + + def test_no_duplicates(self): + ls = list(psutil.process_iter()) + assert sorted(ls, key=lambda x: x.pid) == sorted( + set(ls), key=lambda x: x.pid + ) + + def test_emulate_nsp(self): + list(psutil.process_iter()) # populate cache + for x in range(2): + with mock.patch( + 'psutil.Process.as_dict', + side_effect=psutil.NoSuchProcess(os.getpid()), + ): + assert not list(psutil.process_iter(attrs=["cpu_times"])) + psutil.process_iter.cache_clear() # repeat test without cache + + def test_emulate_access_denied(self): + list(psutil.process_iter()) # populate cache + for x in range(2): + with mock.patch( + 'psutil.Process.as_dict', + side_effect=psutil.AccessDenied(os.getpid()), + ): + with pytest.raises(psutil.AccessDenied): + list(psutil.process_iter(attrs=["cpu_times"])) + psutil.process_iter.cache_clear() # repeat test without cache + + def test_attrs(self): + for p in psutil.process_iter(attrs=['pid']): + assert list(p.info.keys()) == ['pid'] + # yield again + for p in psutil.process_iter(attrs=['pid']): + assert list(p.info.keys()) == ['pid'] + with pytest.raises(ValueError): + list(psutil.process_iter(attrs=['foo'])) + with mock.patch( + "psutil._psplatform.Process.cpu_times", + side_effect=psutil.AccessDenied(0, ""), + ) as m: + for p in psutil.process_iter(attrs=["pid", "cpu_times"]): + assert p.info['cpu_times'] is None + assert 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 + ): + assert p.info['cpu_times'] is flag + assert p.info['pid'] >= 0 + assert m.called + + def test_cache_clear(self): + list(psutil.process_iter()) # populate cache + assert psutil._pmap + psutil.process_iter.cache_clear() + assert not psutil._pmap + + +class TestProcessAPIs(PsutilTestCase): + @pytest.mark.skipif( + PYPY and WINDOWS, + reason="spawn_testproc() unreliable on PYPY + WINDOWS", + ) + def test_wait_procs(self): + def callback(p): + pids.append(p.pid) + + pids = [] + sproc1 = self.spawn_testproc() + sproc2 = self.spawn_testproc() + sproc3 = self.spawn_testproc() + procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] + with pytest.raises(ValueError): + psutil.wait_procs(procs, timeout=-1) + with pytest.raises(TypeError): + psutil.wait_procs(procs, callback=1) + t = time.time() + gone, alive = psutil.wait_procs(procs, timeout=0.01, callback=callback) + + assert time.time() - t < 0.5 + assert not gone + assert len(alive) == 3 + assert not pids + for p in alive: + assert not hasattr(p, 'returncode') + + @retry_on_failure(30) + def test_1(procs, callback): + gone, alive = psutil.wait_procs( + procs, timeout=0.03, callback=callback + ) + assert len(gone) == 1 + assert len(alive) == 2 + return gone, alive + + sproc3.terminate() + gone, alive = test_1(procs, callback) + assert sproc3.pid in [x.pid for x in gone] + if POSIX: + assert gone.pop().returncode == -signal.SIGTERM + else: + assert gone.pop().returncode == 1 + assert pids == [sproc3.pid] + for p in alive: + assert not hasattr(p, 'returncode') + + @retry_on_failure(30) + def test_2(procs, callback): + gone, alive = psutil.wait_procs( + procs, timeout=0.03, callback=callback + ) + assert len(gone) == 3 + assert len(alive) == 0 + return gone, alive + + sproc1.terminate() + sproc2.terminate() + gone, alive = test_2(procs, callback) + assert set(pids) == {sproc1.pid, sproc2.pid, sproc3.pid} + for p in gone: + assert hasattr(p, 'returncode') + + @pytest.mark.skipif( + PYPY and WINDOWS, + reason="spawn_testproc() unreliable on PYPY + WINDOWS", + ) + def test_wait_procs_no_timeout(self): + sproc1 = self.spawn_testproc() + sproc2 = self.spawn_testproc() + sproc3 = self.spawn_testproc() + procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] + for p in procs: + p.terminate() + psutil.wait_procs(procs) + + def test_pid_exists(self): + sproc = self.spawn_testproc() + assert psutil.pid_exists(sproc.pid) + p = psutil.Process(sproc.pid) + p.kill() + p.wait() + assert not psutil.pid_exists(sproc.pid) + assert not psutil.pid_exists(-1) + assert psutil.pid_exists(0) == (0 in psutil.pids()) + + def test_pid_exists_2(self): + pids = psutil.pids() + for pid in pids: + try: + assert psutil.pid_exists(pid) + except AssertionError: + # in case the process disappeared in meantime fail only + # if it is no longer in psutil.pids() + time.sleep(0.1) + assert pid not in psutil.pids() + pids = range(max(pids) + 15000, max(pids) + 16000) + for pid in pids: + assert not psutil.pid_exists(pid) + + +class TestMiscAPIs(PsutilTestCase): + def test_boot_time(self): + bt = psutil.boot_time() + assert isinstance(bt, float) + assert bt > 0 + assert bt < time.time() + + @pytest.mark.skipif( + CI_TESTING and not psutil.users(), reason="unreliable on CI" + ) + def test_users(self): + users = psutil.users() + assert users + for user in users: + with self.subTest(user=user): + assert user.name + assert isinstance(user.name, str) + assert isinstance(user.terminal, (str, type(None))) + if user.host is not None: + assert isinstance(user.host, (str, type(None))) + user.terminal # noqa: B018 + user.host # noqa: B018 + assert user.started > 0.0 + datetime.datetime.fromtimestamp(user.started) + if WINDOWS or OPENBSD: + assert user.pid is None + else: + psutil.Process(user.pid) + + def test_os_constants(self): + names = [ + "POSIX", + "WINDOWS", + "LINUX", + "MACOS", + "FREEBSD", + "OPENBSD", + "NETBSD", + "BSD", + "SUNOS", + ] + for name in names: + assert isinstance(getattr(psutil, name), bool), 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 + assert [psutil.FREEBSD, psutil.OPENBSD, psutil.NETBSD].count( + True + ) == 1 + names.remove("BSD") + names.remove("FREEBSD") + names.remove("OPENBSD") + names.remove("NETBSD") + elif ( + "sunos" in sys.platform.lower() + or "solaris" in sys.platform.lower() + ): + assert psutil.SUNOS + names.remove("SUNOS") + elif "darwin" in sys.platform.lower(): + assert psutil.MACOS + names.remove("MACOS") + else: + assert psutil.WINDOWS + assert not psutil.POSIX + names.remove("WINDOWS") + + # assert all other constants are set to False + for name in names: + assert not getattr(psutil, name), name + + +class TestMemoryAPIs(PsutilTestCase): + def test_virtual_memory(self): + mem = psutil.virtual_memory() + assert mem.total > 0, mem + assert mem.available > 0, mem + assert 0 <= mem.percent <= 100, mem + assert mem.used > 0, mem + assert mem.free >= 0, mem + for name in mem._fields: + value = getattr(mem, name) + if name != 'percent': + assert isinstance(value, int) + if name != 'total': + if not value >= 0: + raise self.fail(f"{name!r} < 0 ({value})") + if value > mem.total: + raise self.fail( + f"{name!r} > total (total={mem.total}, {name}={value})" + ) + + def test_swap_memory(self): + mem = psutil.swap_memory() + assert mem._fields == ( + 'total', + 'used', + 'free', + 'percent', + 'sin', + 'sout', + ) + + assert mem.total >= 0, mem + assert mem.used >= 0, mem + if mem.total > 0: + # likely a system with no swap partition + assert mem.free > 0, mem + else: + assert mem.free == 0, mem + assert 0 <= mem.percent <= 100, mem + assert mem.sin >= 0, mem + assert mem.sout >= 0, mem + + +class TestCpuAPIs(PsutilTestCase): + def test_cpu_count_logical(self): + logical = psutil.cpu_count() + assert logical is not None + assert logical == len(psutil.cpu_times(percpu=True)) + assert logical >= 1 + + if os.path.exists("/proc/cpuinfo"): + with open("/proc/cpuinfo") as fd: + cpuinfo_data = fd.read() + if "physical id" not in cpuinfo_data: + raise pytest.skip("cpuinfo doesn't include physical id") + + def test_cpu_count_cores(self): + logical = psutil.cpu_count() + cores = psutil.cpu_count(logical=False) + if cores is None: + raise pytest.skip("cpu_count_cores() is None") + if WINDOWS and sys.getwindowsversion()[:2] <= (6, 1): # <= Vista + assert cores is None + else: + assert cores >= 1 + assert logical >= cores + + def test_cpu_count_none(self): + # https://github.com/giampaolo/psutil/issues/1085 + for val in (-1, 0, None): + with mock.patch( + 'psutil._psplatform.cpu_count_logical', return_value=val + ) as m: + assert psutil.cpu_count() is None + assert m.called + with mock.patch( + 'psutil._psplatform.cpu_count_cores', return_value=val + ) as m: + assert psutil.cpu_count(logical=False) is None + 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: + assert isinstance(cp_time, float) + assert cp_time >= 0.0 + total += cp_time + assert round(abs(total - sum(times)), 6) == 0 + str(times) + # CPU times are always supposed to increase over time + # or at least remain the same and that's because time + # 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="{} {}".format(new_t, last_t)) + # last = new + + def test_cpu_times_time_increases(self): + # Make sure time increases between calls. + t1 = sum(psutil.cpu_times()) + stop_at = time.time() + GLOBAL_TIMEOUT + while time.time() < stop_at: + t2 = sum(psutil.cpu_times()) + if t2 > t1: + return + raise self.fail("time remained the same") + + def test_per_cpu_times(self): + # Check type, value >= 0, str(). + for times in psutil.cpu_times(percpu=True): + total = 0 + sum(times) + for cp_time in times: + assert isinstance(cp_time, float) + assert cp_time >= 0.0 + total += cp_time + assert round(abs(total - sum(times)), 6) == 0 + str(times) + assert len(psutil.cpu_times(percpu=True)[0]) == len( + psutil.cpu_times(percpu=False) + ) + + # Note: in theory CPU times are always supposed to increase over + # 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="{} {}".format(lastcpu, newcpu)) + # last = new + + def test_per_cpu_times_2(self): + # Simulate some work load then make sure time have increased + # between calls. + tot1 = psutil.cpu_times(percpu=True) + giveup_at = time.time() + GLOBAL_TIMEOUT + while True: + if time.time() >= giveup_at: + return self.fail("timeout") + tot2 = psutil.cpu_times(percpu=True) + for t1, t2 in zip(tot1, tot2): + t1, t2 = psutil._cpu_busy_time(t1), psutil._cpu_busy_time(t2) + difference = t2 - t1 + if difference >= 0.05: + return None + + @pytest.mark.skipif( + CI_TESTING and OPENBSD, reason="unreliable on OPENBSD + CI" + ) + @retry_on_failure(30) + def test_cpu_times_comparison(self): + # Make sure the sum of all per cpu times is almost equal to + # base "one cpu" times. On OpenBSD the sum of per-CPUs is + # higher for some reason. + base = psutil.cpu_times() + per_cpu = psutil.cpu_times(percpu=True) + summed_values = base._make([sum(num) for num in zip(*per_cpu)]) + for field in base._fields: + with self.subTest(field=field, base=base, per_cpu=per_cpu): + assert ( + abs(getattr(base, field) - getattr(summed_values, field)) + < 2 + ) + + def _test_cpu_percent(self, percent, last_ret, new_ret): + try: + assert isinstance(percent, float) + assert percent >= 0.0 + assert percent <= 100.0 * psutil.cpu_count() + except AssertionError as err: + raise AssertionError( + "\n{}\nlast={}\nnew={}".format( + err, pprint.pformat(last_ret), pprint.pformat(new_ret) + ) + ) + + def test_cpu_percent(self): + last = psutil.cpu_percent(interval=0.001) + for _ in range(100): + new = psutil.cpu_percent(interval=None) + self._test_cpu_percent(new, last, new) + last = new + with pytest.raises(ValueError): + psutil.cpu_percent(interval=-1) + + def test_per_cpu_percent(self): + last = psutil.cpu_percent(interval=0.001, percpu=True) + assert len(last) == psutil.cpu_count() + for _ in range(100): + new = psutil.cpu_percent(interval=None, percpu=True) + for percent in new: + self._test_cpu_percent(percent, last, new) + last = new + with pytest.raises(ValueError): + psutil.cpu_percent(interval=-1, percpu=True) + + def test_cpu_times_percent(self): + last = psutil.cpu_times_percent(interval=0.001) + for _ in range(100): + new = psutil.cpu_times_percent(interval=None) + for percent in new: + self._test_cpu_percent(percent, last, new) + self._test_cpu_percent(sum(new), last, new) + last = new + with pytest.raises(ValueError): + psutil.cpu_times_percent(interval=-1) + + def test_per_cpu_times_percent(self): + last = psutil.cpu_times_percent(interval=0.001, percpu=True) + assert len(last) == psutil.cpu_count() + for _ in range(100): + new = psutil.cpu_times_percent(interval=None, percpu=True) + for cpu in new: + for percent in cpu: + self._test_cpu_percent(percent, last, new) + self._test_cpu_percent(sum(cpu), last, new) + last = new + + def test_per_cpu_times_percent_negative(self): + # see: https://github.com/giampaolo/psutil/issues/645 + psutil.cpu_times_percent(percpu=True) + zero_times = [ + x._make([0 for x in range(len(x._fields))]) + for x in psutil.cpu_times(percpu=True) + ] + with mock.patch('psutil.cpu_times', return_value=zero_times): + for cpu in psutil.cpu_times_percent(percpu=True): + for percent in cpu: + self._test_cpu_percent(percent, None, None) + + def test_cpu_stats(self): + # Tested more extensively in per-platform test modules. + infos = psutil.cpu_stats() + assert infos._fields == ( + 'ctx_switches', + 'interrupts', + 'soft_interrupts', + 'syscalls', + ) + for name in infos._fields: + value = getattr(infos, name) + assert value >= 0 + # on AIX, ctx_switches is always 0 + if not AIX and name in {'ctx_switches', 'interrupts'}: + assert value > 0 + + # TODO: remove this once 1892 is fixed + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" + ) + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_cpu_freq(self): + def check_ls(ls): + for nt in ls: + assert nt._fields == ('current', 'min', 'max') + if nt.max != 0.0: + assert nt.current <= nt.max + for name in nt._fields: + value = getattr(nt, name) + assert isinstance(value, (int, float)) + assert value >= 0 + + ls = psutil.cpu_freq(percpu=True) + if (FREEBSD or AARCH64) and not ls: + raise pytest.skip( + "returns empty list on FreeBSD and Linux aarch64" + ) + + assert ls, ls + check_ls([psutil.cpu_freq(percpu=False)]) + + if LINUX: + assert len(ls) == psutil.cpu_count() + + @pytest.mark.skipif(not HAS_GETLOADAVG, reason="not supported") + def test_getloadavg(self): + loadavg = psutil.getloadavg() + assert len(loadavg) == 3 + for load in loadavg: + assert isinstance(load, float) + assert load >= 0.0 + + +class TestDiskAPIs(PsutilTestCase): + @pytest.mark.skipif( + PYPY and not IS_64BIT, reason="unreliable on PYPY32 + 32BIT" + ) + def test_disk_usage(self): + usage = psutil.disk_usage(os.getcwd()) + assert usage._fields == ('total', 'used', 'free', 'percent') + + assert usage.total > 0, usage + assert usage.used > 0, usage + assert usage.free > 0, usage + assert usage.total > usage.used, usage + assert usage.total > usage.free, usage + assert 0 <= usage.percent <= 100, usage.percent + if hasattr(shutil, 'disk_usage'): + # py >= 3.3, see: http://bugs.python.org/issue12442 + shutil_usage = shutil.disk_usage(os.getcwd()) + tolerance = 5 * 1024 * 1024 # 5MB + assert usage.total == shutil_usage.total + assert abs(usage.free - shutil_usage.free) < tolerance + if not MACOS_12PLUS: + # see https://github.com/giampaolo/psutil/issues/2147 + assert abs(usage.used - shutil_usage.used) < tolerance + + # if path does not exist OSError ENOENT is expected across + # all platforms + fname = self.get_testfn() + with pytest.raises(FileNotFoundError): + psutil.disk_usage(fname) + + @pytest.mark.skipif(not ASCII_FS, reason="not an ASCII fs") + def test_disk_usage_unicode(self): + # See: https://github.com/giampaolo/psutil/issues/416 + with pytest.raises(UnicodeEncodeError): + psutil.disk_usage(UNICODE_SUFFIX) + + def test_disk_usage_bytes(self): + psutil.disk_usage(b'.') + + def test_disk_partitions(self): + def check_ntuple(nt): + assert isinstance(nt.device, str) + assert isinstance(nt.mountpoint, str) + assert isinstance(nt.fstype, str) + assert isinstance(nt.opts, str) + + # all = False + ls = psutil.disk_partitions(all=False) + assert ls + for disk in ls: + check_ntuple(disk) + if WINDOWS and 'cdrom' in disk.opts: + continue + if not POSIX: + assert os.path.exists(disk.device), disk + else: + # we cannot make any assumption about this, see: + # http://goo.gl/p9c43 + disk.device # noqa: B018 + # on modern systems mount points can also be files + assert os.path.exists(disk.mountpoint), disk + assert disk.fstype, disk + + # all = True + ls = psutil.disk_partitions(all=True) + assert ls + for disk in psutil.disk_partitions(all=True): + check_ntuple(disk) + if not WINDOWS and disk.mountpoint: + try: + os.stat(disk.mountpoint) + except OSError as err: + if GITHUB_ACTIONS and MACOS and err.errno == errno.EIO: + continue + # http://mail.python.org/pipermail/python-dev/ + # 2012-June/120787.html + if err.errno not in {errno.EPERM, errno.EACCES}: + raise + else: + assert os.path.exists(disk.mountpoint), disk + + # --- + + def find_mount_point(path): + path = os.path.abspath(path) + while not os.path.ismount(path): + path = os.path.dirname(path) + return path.lower() + + mount = find_mount_point(__file__) + mounts = [ + x.mountpoint.lower() + for x in psutil.disk_partitions(all=True) + if x.mountpoint + ] + assert mount in mounts + + @pytest.mark.skipif( + LINUX and not os.path.exists('/proc/diskstats'), + reason="/proc/diskstats not available on this linux version", + ) + @pytest.mark.skipif( + CI_TESTING and not psutil.disk_io_counters(), reason="unreliable on CI" + ) # no visible disks + def test_disk_io_counters(self): + def check_ntuple(nt): + assert nt[0] == nt.read_count + assert nt[1] == nt.write_count + assert nt[2] == nt.read_bytes + assert nt[3] == nt.write_bytes + if not (OPENBSD or NETBSD): + assert nt[4] == nt.read_time + assert nt[5] == nt.write_time + if LINUX: + assert nt[6] == nt.read_merged_count + assert nt[7] == nt.write_merged_count + assert nt[8] == nt.busy_time + elif FREEBSD: + assert 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 + assert len(ret) == len(set(ret)) + for key in ret: + assert key, key + check_ntuple(ret[key]) + + def test_disk_io_counters_no_disks(self): + # Emulate a case where no disks are installed, see: + # https://github.com/giampaolo/psutil/issues/1062 + with mock.patch( + 'psutil._psplatform.disk_io_counters', return_value={} + ) as m: + assert psutil.disk_io_counters(perdisk=False) is None + assert psutil.disk_io_counters(perdisk=True) == {} + assert m.called + + +class TestNetAPIs(PsutilTestCase): + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") + def test_net_io_counters(self): + def check_ntuple(nt): + assert nt[0] == nt.bytes_sent + assert nt[1] == nt.bytes_recv + assert nt[2] == nt.packets_sent + assert nt[3] == nt.packets_recv + assert nt[4] == nt.errin + assert nt[5] == nt.errout + assert nt[6] == nt.dropin + assert nt[7] == nt.dropout + assert nt.bytes_sent >= 0, nt + assert nt.bytes_recv >= 0, nt + assert nt.packets_sent >= 0, nt + 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) + assert ret != [] + for key in ret: + assert key + assert isinstance(key, str) + check_ntuple(ret[key]) + + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") + def test_net_io_counters_no_nics(self): + # Emulate a case where no NICs are installed, see: + # https://github.com/giampaolo/psutil/issues/1062 + with mock.patch( + 'psutil._psplatform.net_io_counters', return_value={} + ) as m: + assert psutil.net_io_counters(pernic=False) is None + assert 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 = {socket.AF_INET, socket.AF_INET6, psutil.AF_LINK} + for nic, addrs in nics.items(): + assert isinstance(nic, str) + assert len(set(addrs)) == len(addrs) + for addr in addrs: + assert isinstance(addr.family, int) + assert isinstance(addr.address, str) + assert isinstance(addr.netmask, (str, type(None))) + assert isinstance(addr.broadcast, (str, type(None))) + assert addr.family in families + assert isinstance(addr.family, enum.IntEnum) + if nic_stats[nic].isup: + # Do not test binding to addresses of interfaces + # that are down + if addr.family == socket.AF_INET: + with socket.socket(addr.family) as 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 + with socket.socket(af, socktype, proto) as 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: + assert addr.ptp is None + elif addr.ptp: + assert addr.broadcast is None + + # check broadcast address + if ( + addr.broadcast + and addr.netmask + and addr.family in {socket.AF_INET, socket.AF_INET6} + ): + assert addr.broadcast == broadcast_addr(addr) + + if BSD or MACOS or SUNOS: + if hasattr(socket, "AF_LINK"): + assert psutil.AF_LINK == socket.AF_LINK + elif LINUX: + assert psutil.AF_LINK == socket.AF_PACKET + elif WINDOWS: + assert 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: + assert addr.address == '06:3d:29:00:00:00' + else: + assert addr.address == '06-3d-29-00-00-00' + + def test_net_if_stats(self): + nics = psutil.net_if_stats() + assert nics, nics + all_duplexes = ( + psutil.NIC_DUPLEX_FULL, + psutil.NIC_DUPLEX_HALF, + psutil.NIC_DUPLEX_UNKNOWN, + ) + for name, stats in nics.items(): + assert isinstance(name, str) + isup, duplex, speed, mtu, flags = stats + assert isinstance(isup, bool) + assert duplex in all_duplexes + assert duplex in all_duplexes + assert speed >= 0 + assert mtu >= 0 + assert isinstance(flags, str) + + @pytest.mark.skipif( + not (LINUX or BSD or MACOS), reason="LINUX or BSD or MACOS specific" + ) + def test_net_if_stats_enodev(self): + # See: https://github.com/giampaolo/psutil/issues/1279 + with mock.patch( + 'psutil._psutil_posix.net_if_mtu', + side_effect=OSError(errno.ENODEV, ""), + ) as m: + ret = psutil.net_if_stats() + assert ret == {} + assert m.called + + +class TestSensorsAPIs(PsutilTestCase): + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + def test_sensors_temperatures(self): + temps = psutil.sensors_temperatures() + for name, entries in temps.items(): + assert isinstance(name, str) + for entry in entries: + assert isinstance(entry.label, str) + if entry.current is not None: + assert entry.current >= 0 + if entry.high is not None: + assert entry.high >= 0 + if entry.critical is not None: + assert entry.critical >= 0 + + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + def test_sensors_temperatures_fahreneit(self): + d = {'coretemp': [('label', 50.0, 60.0, 70.0)]} + with mock.patch( + "psutil._psplatform.sensors_temperatures", return_value=d + ) as m: + temps = psutil.sensors_temperatures(fahrenheit=True)['coretemp'][0] + assert m.called + assert temps.current == 122.0 + assert temps.high == 140.0 + assert temps.critical == 158.0 + + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_sensors_battery(self): + ret = psutil.sensors_battery() + assert ret.percent >= 0 + assert ret.percent <= 100 + if ret.secsleft not in { + psutil.POWER_TIME_UNKNOWN, + psutil.POWER_TIME_UNLIMITED, + }: + assert ret.secsleft >= 0 + elif ret.secsleft == psutil.POWER_TIME_UNLIMITED: + assert ret.power_plugged + assert isinstance(ret.power_plugged, bool) + + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") + def test_sensors_fans(self): + fans = psutil.sensors_fans() + for name, entries in fans.items(): + assert isinstance(name, str) + for entry in entries: + assert isinstance(entry.label, str) + assert isinstance(entry.current, int) + assert entry.current >= 0 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_testutils.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_testutils.py new file mode 100644 index 0000000..6db66e5 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_testutils.py @@ -0,0 +1,577 @@ +#!/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 testing utils (psutil.tests namespace).""" + +import collections +import errno +import os +import socket +import stat +import subprocess +import textwrap +import unittest +import warnings +from unittest import mock + +import psutil +import psutil.tests +from psutil import FREEBSD +from psutil import NETBSD +from psutil import POSIX +from psutil._common import open_binary +from psutil._common import open_text +from psutil._common import supports_ipv6 +from psutil.tests import CI_TESTING +from psutil.tests import COVERAGE +from psutil.tests import HAS_NET_CONNECTIONS_UNIX +from psutil.tests import HERE +from psutil.tests import PYTHON_EXE +from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import PsutilTestCase +from psutil.tests import TestMemoryLeak +from psutil.tests import bind_socket +from psutil.tests import bind_unix_socket +from psutil.tests import call_until +from psutil.tests import chdir +from psutil.tests import create_sockets +from psutil.tests import fake_pytest +from psutil.tests import filter_proc_net_connections +from psutil.tests import get_free_port +from psutil.tests import is_namedtuple +from psutil.tests import process_namespace +from psutil.tests import pytest +from psutil.tests import reap_children +from psutil.tests import retry +from psutil.tests import retry_on_failure +from psutil.tests import safe_mkdir +from psutil.tests import safe_rmpath +from psutil.tests import system_namespace +from psutil.tests import tcp_socketpair +from psutil.tests import terminate +from psutil.tests import unix_socketpair +from psutil.tests import wait_for_file +from psutil.tests import wait_for_pid + + +# =================================================================== +# --- Unit tests for test utilities. +# =================================================================== + + +class TestRetryDecorator(PsutilTestCase): + @mock.patch('time.sleep') + def test_retry_success(self, sleep): + # Fail 3 times out of 5; make sure the decorated fun returns. + + @retry(retries=5, interval=1, logfun=None) + def foo(): + while queue: + queue.pop() + 1 / 0 # noqa: B018 + return 1 + + queue = list(range(3)) + assert foo() == 1 + assert sleep.call_count == 3 + + @mock.patch('time.sleep') + def test_retry_failure(self, sleep): + # Fail 6 times out of 5; th function is supposed to raise exc. + @retry(retries=5, interval=1, logfun=None) + def foo(): + while queue: + queue.pop() + 1 / 0 # noqa: B018 + return 1 + + queue = list(range(6)) + with pytest.raises(ZeroDivisionError): + foo() + assert sleep.call_count == 5 + + @mock.patch('time.sleep') + def test_exception_arg(self, sleep): + @retry(exception=ValueError, interval=1) + def foo(): + raise TypeError + + with pytest.raises(TypeError): + foo() + assert sleep.call_count == 0 + + @mock.patch('time.sleep') + def test_no_interval_arg(self, sleep): + # if interval is not specified sleep is not supposed to be called + + @retry(retries=5, interval=None, logfun=None) + def foo(): + 1 / 0 # noqa: B018 + + with pytest.raises(ZeroDivisionError): + foo() + assert sleep.call_count == 0 + + @mock.patch('time.sleep') + def test_retries_arg(self, sleep): + @retry(retries=5, interval=1, logfun=None) + def foo(): + 1 / 0 # noqa: B018 + + with pytest.raises(ZeroDivisionError): + foo() + assert sleep.call_count == 5 + + @mock.patch('time.sleep') + def test_retries_and_timeout_args(self, sleep): + with pytest.raises(ValueError): + retry(retries=5, timeout=1) + + +class TestSyncTestUtils(PsutilTestCase): + def test_wait_for_pid(self): + wait_for_pid(os.getpid()) + nopid = max(psutil.pids()) + 99999 + with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): + with pytest.raises(psutil.NoSuchProcess): + wait_for_pid(nopid) + + def test_wait_for_file(self): + testfn = self.get_testfn() + with open(testfn, 'w') as f: + f.write('foo') + wait_for_file(testfn) + assert not os.path.exists(testfn) + + def test_wait_for_file_empty(self): + testfn = self.get_testfn() + with open(testfn, 'w'): + pass + wait_for_file(testfn, empty=True) + assert not os.path.exists(testfn) + + def test_wait_for_file_no_file(self): + testfn = self.get_testfn() + with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): + with pytest.raises(OSError): + wait_for_file(testfn) + + def test_wait_for_file_no_delete(self): + testfn = self.get_testfn() + with open(testfn, 'w') as f: + f.write('foo') + wait_for_file(testfn, delete=False) + assert os.path.exists(testfn) + + def test_call_until(self): + call_until(lambda: 1) + # TODO: test for timeout + + +class TestFSTestUtils(PsutilTestCase): + def test_open_text(self): + with open_text(__file__) as f: + assert f.mode == 'r' + + def test_open_binary(self): + with open_binary(__file__) as f: + assert f.mode == 'rb' + + def test_safe_mkdir(self): + testfn = self.get_testfn() + safe_mkdir(testfn) + assert os.path.isdir(testfn) + safe_mkdir(testfn) + assert os.path.isdir(testfn) + + def test_safe_rmpath(self): + # test file is removed + testfn = self.get_testfn() + open(testfn, 'w').close() + safe_rmpath(testfn) + assert not os.path.exists(testfn) + # test no exception if path does not exist + safe_rmpath(testfn) + # test dir is removed + os.mkdir(testfn) + safe_rmpath(testfn) + assert not os.path.exists(testfn) + # test other exceptions are raised + with mock.patch( + 'psutil.tests.os.stat', side_effect=OSError(errno.EINVAL, "") + ) as m: + with pytest.raises(OSError): + safe_rmpath(testfn) + assert m.called + + def test_chdir(self): + testfn = self.get_testfn() + base = os.getcwd() + os.mkdir(testfn) + with chdir(testfn): + assert os.getcwd() == os.path.join(base, testfn) + assert os.getcwd() == base + + +class TestProcessUtils(PsutilTestCase): + def test_reap_children(self): + subp = self.spawn_testproc() + p = psutil.Process(subp.pid) + assert p.is_running() + reap_children() + assert not p.is_running() + assert not psutil.tests._pids_started + assert not psutil.tests._subprocesses_started + + def test_spawn_children_pair(self): + child, grandchild = self.spawn_children_pair() + assert child.pid != grandchild.pid + assert child.is_running() + assert grandchild.is_running() + children = psutil.Process().children() + assert children == [child] + children = psutil.Process().children(recursive=True) + assert len(children) == 2 + assert child in children + assert grandchild in children + assert child.ppid() == os.getpid() + assert grandchild.ppid() == child.pid + + terminate(child) + assert not child.is_running() + assert grandchild.is_running() + + terminate(grandchild) + assert not grandchild.is_running() + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_spawn_zombie(self): + _parent, zombie = self.spawn_zombie() + assert zombie.status() == psutil.STATUS_ZOMBIE + + def test_terminate(self): + # by subprocess.Popen + p = self.spawn_testproc() + terminate(p) + self.assertPidGone(p.pid) + terminate(p) + # by psutil.Process + p = psutil.Process(self.spawn_testproc().pid) + terminate(p) + self.assertPidGone(p.pid) + terminate(p) + # by psutil.Popen + cmd = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)];", + ] + p = psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) + terminate(p) + self.assertPidGone(p.pid) + terminate(p) + # by PID + pid = self.spawn_testproc().pid + terminate(pid) + self.assertPidGone(p.pid) + terminate(pid) + # zombie + if POSIX: + parent, zombie = self.spawn_zombie() + terminate(parent) + terminate(zombie) + self.assertPidGone(parent.pid) + self.assertPidGone(zombie.pid) + + +class TestNetUtils(PsutilTestCase): + def bind_socket(self): + port = get_free_port() + with bind_socket(addr=('', port)) as s: + assert s.getsockname()[1] == port + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_bind_unix_socket(self): + name = self.get_testfn() + with bind_unix_socket(name) as sock: + assert sock.family == socket.AF_UNIX + assert sock.type == socket.SOCK_STREAM + assert sock.getsockname() == name + assert os.path.exists(name) + assert stat.S_ISSOCK(os.stat(name).st_mode) + # UDP + name = self.get_testfn() + with bind_unix_socket(name, type=socket.SOCK_DGRAM) as sock: + assert sock.type == socket.SOCK_DGRAM + + def test_tcp_socketpair(self): + addr = ("127.0.0.1", get_free_port()) + server, client = tcp_socketpair(socket.AF_INET, addr=addr) + with server, client: + # Ensure they are connected and the positions are correct. + assert server.getsockname() == addr + assert client.getpeername() == addr + assert client.getsockname() != addr + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.skipif( + NETBSD or FREEBSD, reason="/var/run/log UNIX socket opened by default" + ) + def test_unix_socketpair(self): + p = psutil.Process() + num_fds = p.num_fds() + assert not filter_proc_net_connections(p.net_connections(kind='unix')) + name = self.get_testfn() + server, client = unix_socketpair(name) + try: + assert os.path.exists(name) + assert stat.S_ISSOCK(os.stat(name).st_mode) + assert p.num_fds() - num_fds == 2 + assert ( + len( + filter_proc_net_connections(p.net_connections(kind='unix')) + ) + == 2 + ) + assert server.getsockname() == name + assert 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 + assert fams[socket.AF_INET] >= 2 + if supports_ipv6(): + assert fams[socket.AF_INET6] >= 2 + if POSIX and HAS_NET_CONNECTIONS_UNIX: + assert fams[socket.AF_UNIX] >= 2 + assert types[socket.SOCK_STREAM] >= 2 + assert types[socket.SOCK_DGRAM] >= 2 + + +@pytest.mark.xdist_group(name="serial") +class TestMemLeakClass(TestMemoryLeak): + @retry_on_failure() + def test_times(self): + def fun(): + cnt['cnt'] += 1 + + cnt = {'cnt': 0} + self.execute(fun, times=10, warmup_times=15) + assert cnt['cnt'] == 26 + + def test_param_err(self): + with pytest.raises(ValueError): + self.execute(lambda: 0, times=0) + with pytest.raises(ValueError): + self.execute(lambda: 0, times=-1) + with pytest.raises(ValueError): + self.execute(lambda: 0, warmup_times=-1) + with pytest.raises(ValueError): + self.execute(lambda: 0, tolerance=-1) + with pytest.raises(ValueError): + self.execute(lambda: 0, retries=-1) + + @retry_on_failure() + @pytest.mark.skipif(CI_TESTING, reason="skipped on CI") + @pytest.mark.skipif(COVERAGE, reason="skipped during test coverage") + def test_leak_mem(self): + ls = [] + + def fun(ls=ls): + ls.append("x" * 248 * 1024) + + try: + # will consume around 60M in total + with pytest.raises(AssertionError, match="extra-mem"): + self.execute(fun, times=100) + finally: + del ls + + def test_unclosed_files(self): + def fun(): + f = open(__file__) # noqa: SIM115 + self.addCleanup(f.close) + box.append(f) + + box = [] + kind = "fd" if POSIX else "handle" + with pytest.raises(AssertionError, match="unclosed " + kind): + self.execute(fun) + + def test_tolerance(self): + def fun(): + ls.append("x" * 24 * 1024) + + ls = [] + times = 100 + self.execute( + fun, times=times, warmup_times=0, tolerance=200 * 1024 * 1024 + ) + assert len(ls) == times + 1 + + def test_execute_w_exc(self): + def fun_1(): + 1 / 0 # noqa: B018 + + self.execute_w_exc(ZeroDivisionError, fun_1) + with pytest.raises(ZeroDivisionError): + self.execute_w_exc(OSError, fun_1) + + def fun_2(): + pass + + with pytest.raises(AssertionError): + self.execute_w_exc(ZeroDivisionError, fun_2) + + +class TestFakePytest(PsutilTestCase): + def run_test_class(self, klass): + suite = unittest.TestSuite() + suite.addTest(klass) + runner = unittest.TextTestRunner() + result = runner.run(suite) + return result + + def test_raises(self): + with fake_pytest.raises(ZeroDivisionError) as cm: + 1 / 0 # noqa: B018 + assert isinstance(cm.value, ZeroDivisionError) + + with fake_pytest.raises(ValueError, match="foo") as cm: + raise ValueError("foo") + + try: + with fake_pytest.raises(ValueError, match="foo") as cm: + raise ValueError("bar") + except AssertionError as err: + assert str(err) == '"foo" does not match "bar"' + else: + raise self.fail("exception not raised") + + def test_mark(self): + @fake_pytest.mark.xdist_group(name="serial") + def foo(): + return 1 + + assert foo() == 1 + + @fake_pytest.mark.xdist_group(name="serial") + class Foo: + def bar(self): + return 1 + + assert Foo().bar() == 1 + + def test_skipif(self): + class TestCase(unittest.TestCase): + @fake_pytest.mark.skipif(True, reason="reason") + def foo(self): + assert 1 == 1 # noqa: PLR0133 + + result = self.run_test_class(TestCase("foo")) + assert result.wasSuccessful() + assert len(result.skipped) == 1 + assert result.skipped[0][1] == "reason" + + class TestCase(unittest.TestCase): + @fake_pytest.mark.skipif(False, reason="reason") + def foo(self): + assert 1 == 1 # noqa: PLR0133 + + result = self.run_test_class(TestCase("foo")) + assert result.wasSuccessful() + assert len(result.skipped) == 0 + + def test_skip(self): + class TestCase(unittest.TestCase): + def foo(self): + fake_pytest.skip("reason") + assert 1 == 0 # noqa: PLR0133 + + result = self.run_test_class(TestCase("foo")) + assert result.wasSuccessful() + assert len(result.skipped) == 1 + assert result.skipped[0][1] == "reason" + + def test_main(self): + tmpdir = self.get_testfn(dir=HERE) + os.mkdir(tmpdir) + with open(os.path.join(tmpdir, "__init__.py"), "w"): + pass + with open(os.path.join(tmpdir, "test_file.py"), "w") as f: + f.write(textwrap.dedent("""\ + import unittest + + class TestCase(unittest.TestCase): + def test_passed(self): + pass + """).lstrip()) + with mock.patch.object(psutil.tests, "HERE", tmpdir): + with self.assertWarnsRegex( + UserWarning, "Fake pytest module was used" + ): + suite = fake_pytest.main() + assert suite.countTestCases() == 1 + + def test_warns(self): + # success + with fake_pytest.warns(UserWarning): + warnings.warn("foo", UserWarning, stacklevel=1) + + # failure + try: + with fake_pytest.warns(UserWarning): + warnings.warn("foo", DeprecationWarning, stacklevel=1) + except AssertionError: + pass + else: + raise self.fail("exception not raised") + + # match success + with fake_pytest.warns(UserWarning, match="foo"): + warnings.warn("foo", UserWarning, stacklevel=1) + + # match failure + try: + with fake_pytest.warns(UserWarning, match="foo"): + warnings.warn("bar", UserWarning, stacklevel=1) + except AssertionError: + pass + else: + raise self.fail("exception not raised") + + +class TestTestingUtils(PsutilTestCase): + def test_process_namespace(self): + p = psutil.Process() + ns = process_namespace(p) + ns.test() + fun = next(x for x in ns.iter(ns.getters) if x[1] == 'ppid')[0] + assert fun() == p.ppid() + + def test_system_namespace(self): + ns = system_namespace() + fun = next(x for x in ns.iter(ns.getters) if x[1] == 'net_if_addrs')[0] + assert fun() == psutil.net_if_addrs() + + +class TestOtherUtils(PsutilTestCase): + def test_is_namedtuple(self): + assert is_namedtuple(collections.namedtuple('foo', 'a b c')(1, 2, 3)) + assert not is_namedtuple(tuple()) diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_unicode.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_unicode.py new file mode 100644 index 0000000..d8a8c4b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_unicode.py @@ -0,0 +1,313 @@ +#!/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. + +"""Notes about unicode handling in psutil +======================================. + +Starting from version 5.3.0 psutil adds unicode support, see: +https://github.com/giampaolo/psutil/issues/1040 +The notes below apply to *any* API returning a string such as +process exe(), cwd() or username(): + +* all strings are encoded by using the OS filesystem encoding + (sys.getfilesystemencoding()) which varies depending on the platform + (e.g. "UTF-8" on macOS, "mbcs" on Win) +* no API call is supposed to crash with UnicodeDecodeError +* instead, in case of badly encoded data returned by the OS, the + following error handlers are used to replace the corrupted characters in + the string: + * sys.getfilesystemencodeerrors() or "surrogatescape" on POSIX and + "replace" on Windows. + +For a detailed explanation of how psutil handles unicode see #1040. + +Tests +===== + +List of APIs returning or dealing with a string: +('not tested' means they are not tested to deal with non-ASCII strings): + +* Process.cmdline() +* Process.cwd() +* Process.environ() +* Process.exe() +* Process.memory_maps() +* Process.name() +* Process.net_connections('unix') +* Process.open_files() +* Process.username() (not tested) + +* disk_io_counters() (not tested) +* disk_partitions() (not tested) +* disk_usage(str) +* net_connections('unix') +* net_if_addrs() (not tested) +* net_if_stats() (not tested) +* net_io_counters() (not tested) +* sensors_fans() (not tested) +* sensors_temperatures() (not tested) +* users() (not tested) + +* WindowsService.binpath() (not tested) +* WindowsService.description() (not tested) +* WindowsService.display_name() (not tested) +* WindowsService.name() (not tested) +* WindowsService.status() (not tested) +* WindowsService.username() (not tested) + +In here we create a unicode path with a funky non-ASCII name and (where +possible) make psutil return it back (e.g. on name(), exe(), open_files(), +etc.) and make sure that: + +* psutil never crashes with UnicodeDecodeError +* the returned path matches +""" + +import os +import shutil +import warnings +from contextlib import closing + +import psutil +from psutil import BSD +from psutil import POSIX +from psutil import WINDOWS +from psutil.tests import ASCII_FS +from psutil.tests import CI_TESTING +from psutil.tests import HAS_ENVIRON +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import HAS_NET_CONNECTIONS_UNIX +from psutil.tests import INVALID_UNICODE_SUFFIX +from psutil.tests import PYPY +from psutil.tests import TESTFN_PREFIX +from psutil.tests import UNICODE_SUFFIX +from psutil.tests import PsutilTestCase +from psutil.tests import bind_unix_socket +from psutil.tests import chdir +from psutil.tests import copyload_shared_lib +from psutil.tests import create_py_exe +from psutil.tests import get_testfn +from psutil.tests import pytest +from psutil.tests import safe_mkdir +from psutil.tests import safe_rmpath +from psutil.tests import skip_on_access_denied +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +def try_unicode(suffix): + """Return True if both the fs and the subprocess module can + deal with a unicode file name. + """ + sproc = None + testfn = get_testfn(suffix=suffix) + try: + safe_rmpath(testfn) + create_py_exe(testfn) + sproc = spawn_testproc(cmd=[testfn]) + shutil.copyfile(testfn, testfn + '-2') + safe_rmpath(testfn + '-2') + except (UnicodeEncodeError, OSError): + return False + else: + return True + finally: + if sproc is not None: + terminate(sproc) + safe_rmpath(testfn) + + +# =================================================================== +# FS APIs +# =================================================================== + + +class BaseUnicodeTest(PsutilTestCase): + funky_suffix = None + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.skip_tests = False + cls.funky_name = None + if cls.funky_suffix is not None: + if not try_unicode(cls.funky_suffix): + cls.skip_tests = True + else: + cls.funky_name = get_testfn(suffix=cls.funky_suffix) + create_py_exe(cls.funky_name) + + def setUp(self): + super().setUp() + if self.skip_tests: + raise pytest.skip("can't handle unicode str") + + +@pytest.mark.xdist_group(name="serial") +@pytest.mark.skipif(ASCII_FS, reason="ASCII fs") +class TestFSAPIs(BaseUnicodeTest): + """Test FS APIs with a funky, valid, UTF8 path name.""" + + funky_suffix = UNICODE_SUFFIX + + def expect_exact_path_match(self): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + return self.funky_name in os.listdir(".") + + # --- + + def test_proc_exe(self): + cmd = [ + self.funky_name, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] + subp = self.spawn_testproc(cmd) + p = psutil.Process(subp.pid) + exe = p.exe() + assert isinstance(exe, str) + if self.expect_exact_path_match(): + assert os.path.normcase(exe) == os.path.normcase(self.funky_name) + + def test_proc_name(self): + cmd = [ + self.funky_name, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] + subp = self.spawn_testproc(cmd) + name = psutil.Process(subp.pid).name() + assert isinstance(name, str) + if self.expect_exact_path_match(): + assert name == os.path.basename(self.funky_name) + + def test_proc_cmdline(self): + cmd = [ + self.funky_name, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] + subp = self.spawn_testproc(cmd) + p = psutil.Process(subp.pid) + cmdline = p.cmdline() + for part in cmdline: + assert isinstance(part, str) + if self.expect_exact_path_match(): + assert cmdline == cmd + + def test_proc_cwd(self): + dname = self.funky_name + "2" + self.addCleanup(safe_rmpath, dname) + safe_mkdir(dname) + with chdir(dname): + p = psutil.Process() + cwd = p.cwd() + assert isinstance(p.cwd(), str) + if self.expect_exact_path_match(): + assert cwd == dname + + @pytest.mark.skipif(PYPY and WINDOWS, reason="fails on PYPY + WINDOWS") + def test_proc_open_files(self): + p = psutil.Process() + start = set(p.open_files()) + with open(self.funky_name, 'rb'): + new = set(p.open_files()) + path = (new - start).pop().path + assert isinstance(path, str) + if BSD and not path: + # XXX - see https://github.com/giampaolo/psutil/issues/595 + raise pytest.skip("open_files on BSD is broken") + if self.expect_exact_path_match(): + assert os.path.normcase(path) == os.path.normcase(self.funky_name) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_proc_net_connections(self): + name = self.get_testfn(suffix=self.funky_suffix) + sock = bind_unix_socket(name) + with closing(sock): + conn = psutil.Process().net_connections('unix')[0] + assert isinstance(conn.laddr, str) + assert conn.laddr == name + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.skipif( + not HAS_NET_CONNECTIONS_UNIX, reason="can't list UNIX sockets" + ) + @skip_on_access_denied() + def test_net_connections(self): + def find_sock(cons): + for conn in cons: + if os.path.basename(conn.laddr).startswith(TESTFN_PREFIX): + return conn + raise ValueError("connection not found") + + name = self.get_testfn(suffix=self.funky_suffix) + sock = bind_unix_socket(name) + with closing(sock): + cons = psutil.net_connections(kind='unix') + conn = find_sock(cons) + assert isinstance(conn.laddr, str) + assert 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) + + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + @pytest.mark.skipif(PYPY, reason="unstable on PYPY") + def test_memory_maps(self): + with copyload_shared_lib(suffix=self.funky_suffix) as funky_path: + + def normpath(p): + return os.path.realpath(os.path.normcase(p)) + + libpaths = [ + normpath(x.path) for x in psutil.Process().memory_maps() + ] + # ...just to have a clearer msg in case of failure + libpaths = [x for x in libpaths if TESTFN_PREFIX in x] + assert normpath(funky_path) in libpaths + for path in libpaths: + assert isinstance(path, str) + + +@pytest.mark.skipif(CI_TESTING, reason="unreliable on CI") +class TestFSAPIsWithInvalidPath(TestFSAPIs): + """Test FS APIs with a funky, invalid path name.""" + + funky_suffix = INVALID_UNICODE_SUFFIX + + def expect_exact_path_match(self): + return True + + +# =================================================================== +# Non fs APIs +# =================================================================== + + +class TestNonFSAPIS(BaseUnicodeTest): + """Unicode tests for non fs-related APIs.""" + + funky_suffix = UNICODE_SUFFIX + + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + @pytest.mark.skipif(PYPY and WINDOWS, reason="segfaults on PYPY + WINDOWS") + def test_proc_environ(self): + # Note: differently from others, this test does not deal + # with fs paths. + env = os.environ.copy() + env['FUNNY_ARG'] = self.funky_suffix + sproc = self.spawn_testproc(env=env) + p = psutil.Process(sproc.pid) + env = p.environ() + for k, v in env.items(): + assert isinstance(k, str) + assert isinstance(v, str) + assert env['FUNNY_ARG'] == self.funky_suffix diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/test_windows.py b/.venv/lib/python3.12/site-packages/psutil/tests/test_windows.py new file mode 100644 index 0000000..c5c536b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/test_windows.py @@ -0,0 +1,914 @@ +#!/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. + +"""Windows specific tests.""" + +import datetime +import glob +import os +import platform +import re +import shutil +import signal +import subprocess +import sys +import time +import warnings +from unittest import mock + +import psutil +from psutil import WINDOWS +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import HAS_BATTERY +from psutil.tests import IS_64BIT +from psutil.tests import PYPY +from psutil.tests import TOLERANCE_DISK_USAGE +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +if WINDOWS and not PYPY: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + import win32api # requires "pip install pywin32" + import win32con + import win32process + import wmi # requires "pip install wmi" / "make install-pydeps-test" + +if WINDOWS: + from psutil._pswindows import convert_oserror + + +cext = psutil._psplatform.cext + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +@pytest.mark.skipif(PYPY, reason="pywin32 not available on PYPY") +class WindowsTestCase(PsutilTestCase): + pass + + +def powershell(cmd): + """Currently not used, but available just in case. Usage: + + >>> powershell( + "Get-CIMInstance Win32_PageFileUsage | Select AllocatedBaseSize") + """ + if not shutil.which("powershell.exe"): + raise pytest.skip("powershell.exe not available") + cmdline = ( + "powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive " + f"-NoProfile -WindowStyle Hidden -Command \"{cmd}\"" # noqa: Q003 + ) + return sh(cmdline) + + +def wmic(path, what, converter=int): + """Currently not used, but available just in case. Usage: + + >>> wmic("Win32_OperatingSystem", "FreePhysicalMemory") + 2134124534 + """ + out = sh(f"wmic path {path} get {what}").strip() + data = "".join(out.splitlines()[1:]).strip() # get rid of the header + if converter is not None: + if "," in what: + return tuple(converter(x) for x in data.split()) + else: + return converter(data) + else: + return data + + +# =================================================================== +# System APIs +# =================================================================== + + +class TestCpuAPIs(WindowsTestCase): + @pytest.mark.skipif( + 'NUMBER_OF_PROCESSORS' not in os.environ, + reason="NUMBER_OF_PROCESSORS env var is not available", + ) + def test_cpu_count_vs_NUMBER_OF_PROCESSORS(self): + # Will likely fail on many-cores systems: + # https://stackoverflow.com/questions/31209256 + num_cpus = int(os.environ['NUMBER_OF_PROCESSORS']) + assert num_cpus == psutil.cpu_count() + + def test_cpu_count_vs_GetSystemInfo(self): + # Will likely fail on many-cores systems: + # https://stackoverflow.com/questions/31209256 + sys_value = win32api.GetSystemInfo()[5] + psutil_value = psutil.cpu_count() + assert sys_value == psutil_value + + def test_cpu_count_logical_vs_wmi(self): + w = wmi.WMI() + procs = sum( + proc.NumberOfLogicalProcessors for proc in w.Win32_Processor() + ) + assert psutil.cpu_count() == procs + + def test_cpu_count_cores_vs_wmi(self): + w = wmi.WMI() + cores = sum(proc.NumberOfCores for proc in w.Win32_Processor()) + assert psutil.cpu_count(logical=False) == cores + + def test_cpu_count_vs_cpu_times(self): + assert psutil.cpu_count() == len(psutil.cpu_times(percpu=True)) + + def test_cpu_freq(self): + w = wmi.WMI() + proc = w.Win32_Processor()[0] + assert proc.CurrentClockSpeed == psutil.cpu_freq().current + assert proc.MaxClockSpeed == psutil.cpu_freq().max + + +class TestSystemAPIs(WindowsTestCase): + def test_nic_names(self): + out = sh('ipconfig /all') + nics = psutil.net_io_counters(pernic=True).keys() + for nic in nics: + if "pseudo-interface" in nic.replace(' ', '-').lower(): + continue + if nic not in out: + raise self.fail( + f"{nic!r} nic wasn't found in 'ipconfig /all' output" + ) + + def test_total_phymem(self): + w = wmi.WMI().Win32_ComputerSystem()[0] + assert int(w.TotalPhysicalMemory) == psutil.virtual_memory().total + + def test_free_phymem(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] + assert ( + abs(int(w.AvailableBytes) - psutil.virtual_memory().free) + < TOLERANCE_SYS_MEM + ) + + def test_total_swapmem(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] + assert ( + int(w.CommitLimit) - psutil.virtual_memory().total + == psutil.swap_memory().total + ) + if psutil.swap_memory().total == 0: + assert psutil.swap_memory().free == 0 + assert psutil.swap_memory().used == 0 + + def test_percent_swapmem(self): + if psutil.swap_memory().total > 0: + w = wmi.WMI().Win32_PerfRawData_PerfOS_PagingFile(Name="_Total")[0] + # calculate swap usage to percent + percentSwap = int(w.PercentUsage) * 100 / int(w.PercentUsage_Base) + # exact percent may change but should be reasonable + # assert within +/- 5% and between 0 and 100% + assert psutil.swap_memory().percent >= 0 + assert abs(psutil.swap_memory().percent - percentSwap) < 5 + assert psutil.swap_memory().percent <= 100 + + # @pytest.mark.skipif(wmi is None, reason="wmi module is not installed") + # def test__UPTIME(self): + # # _UPTIME constant is not public but it is used internally + # # as value to return for pid 0 creation time. + # # WMI behaves the same. + # w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + # p = psutil.Process(0) + # wmic_create = str(w.CreationDate.split('.')[0]) + # psutil_create = time.strftime("%Y%m%d%H%M%S", + # time.localtime(p.create_time())) + + # Note: this test is not very reliable + @retry_on_failure() + def test_pids(self): + # Note: this test might fail if the OS is starting/killing + # other processes in the meantime + w = wmi.WMI().Win32_Process() + wmi_pids = {x.ProcessId for x in w} + psutil_pids = set(psutil.pids()) + assert wmi_pids == psutil_pids + + @retry_on_failure() + def test_disks(self): + ps_parts = psutil.disk_partitions(all=True) + wmi_parts = wmi.WMI().Win32_LogicalDisk() + for ps_part in ps_parts: + for wmi_part in wmi_parts: + if ps_part.device.replace('\\', '') == wmi_part.DeviceID: + if not ps_part.mountpoint: + # this is usually a CD-ROM with no disk inserted + break + if 'cdrom' in ps_part.opts: + break + if ps_part.mountpoint.startswith('A:'): + break # floppy + try: + usage = psutil.disk_usage(ps_part.mountpoint) + except FileNotFoundError: + # usually this is the floppy + break + assert usage.total == int(wmi_part.Size) + wmi_free = int(wmi_part.FreeSpace) + assert usage.free == wmi_free + # 10 MB tolerance + if abs(usage.free - wmi_free) > 10 * 1024 * 1024: + raise self.fail(f"psutil={usage.free}, wmi={wmi_free}") + break + else: + raise self.fail(f"can't find partition {ps_part!r}") + + @retry_on_failure() + def test_disk_usage(self): + for disk in psutil.disk_partitions(): + if 'cdrom' in disk.opts: + continue + sys_value = win32api.GetDiskFreeSpaceEx(disk.mountpoint) + psutil_value = psutil.disk_usage(disk.mountpoint) + assert abs(sys_value[0] - psutil_value.free) < TOLERANCE_DISK_USAGE + assert ( + abs(sys_value[1] - psutil_value.total) < TOLERANCE_DISK_USAGE + ) + assert psutil_value.used == psutil_value.total - psutil_value.free + + def test_disk_partitions(self): + sys_value = [ + x + '\\' + for x in win32api.GetLogicalDriveStrings().split("\\\x00") + if x and not x.startswith('A:') + ] + psutil_value = [ + x.mountpoint + for x in psutil.disk_partitions(all=True) + if not x.mountpoint.startswith('A:') + ] + assert 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) + assert ( + ps_names & wmi_names + ), f"no common entries in {ps_names}, {wmi_names}" + + def test_boot_time(self): + wmi_os = wmi.WMI().Win32_OperatingSystem() + wmi_btime_str = wmi_os[0].LastBootUpTime.split('.')[0] + 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()) + assert diff <= 5 + + def test_boot_time_fluctuation(self): + # https://github.com/giampaolo/psutil/issues/1007 + with mock.patch('psutil._pswindows.cext.boot_time', return_value=5): + assert psutil.boot_time() == 5 + with mock.patch('psutil._pswindows.cext.boot_time', return_value=4): + assert psutil.boot_time() == 5 + with mock.patch('psutil._pswindows.cext.boot_time', return_value=6): + assert psutil.boot_time() == 5 + with mock.patch('psutil._pswindows.cext.boot_time', return_value=333): + assert psutil.boot_time() == 333 + + +# =================================================================== +# sensors_battery() +# =================================================================== + + +class TestSensorsBattery(WindowsTestCase): + def test_has_battery(self): + if win32api.GetPwrCapabilities()['SystemBatteriesPresent']: + assert psutil.sensors_battery() is not None + else: + assert psutil.sensors_battery() is None + + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_percent(self): + w = wmi.WMI() + battery_wmi = w.query('select * from Win32_Battery')[0] + battery_psutil = psutil.sensors_battery() + assert ( + abs(battery_psutil.percent - battery_wmi.EstimatedChargeRemaining) + < 1 + ) + + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_power_plugged(self): + w = wmi.WMI() + battery_wmi = w.query('select * from Win32_Battery')[0] + battery_psutil = psutil.sensors_battery() + # Status codes: + # https://msdn.microsoft.com/en-us/library/aa394074(v=vs.85).aspx + assert battery_psutil.power_plugged == (battery_wmi.BatteryStatus == 2) + + def test_emulate_no_battery(self): + with mock.patch( + "psutil._pswindows.cext.sensors_battery", + return_value=(0, 128, 0, 0), + ) as m: + assert psutil.sensors_battery() is None + assert m.called + + def test_emulate_power_connected(self): + with mock.patch( + "psutil._pswindows.cext.sensors_battery", return_value=(1, 0, 0, 0) + ) as m: + assert ( + 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: + assert ( + 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: + assert ( + psutil.sensors_battery().secsleft == psutil.POWER_TIME_UNKNOWN + ) + assert m.called + + +# =================================================================== +# Process APIs +# =================================================================== + + +class TestProcess(WindowsTestCase): + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_issue_24(self): + p = psutil.Process(0) + with pytest.raises(psutil.AccessDenied): + p.kill() + + def test_special_pid(self): + p = psutil.Process(4) + assert p.name() == 'System' + # use __str__ to access all common Process properties to check + # that nothing strange happens + str(p) + p.username() + assert p.create_time() >= 0.0 + try: + rss, _vms = p.memory_info()[:2] + except psutil.AccessDenied: + # expected on Windows Vista and Windows 7 + if platform.uname()[1] not in {'vista', 'win-7', 'win7'}: + raise + else: + assert rss > 0 + + def test_send_signal(self): + p = psutil.Process(self.pid) + with pytest.raises(ValueError): + p.send_signal(signal.SIGINT) + + def test_num_handles_increment(self): + p = psutil.Process(os.getpid()) + before = p.num_handles() + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() + ) + after = p.num_handles() + assert after == before + 1 + win32api.CloseHandle(handle) + assert p.num_handles() == before + + def test_ctrl_signals(self): + p = psutil.Process(self.spawn_testproc().pid) + p.send_signal(signal.CTRL_C_EVENT) + p.send_signal(signal.CTRL_BREAK_EVENT) + p.kill() + p.wait() + with pytest.raises(psutil.NoSuchProcess): + p.send_signal(signal.CTRL_C_EVENT) + with pytest.raises(psutil.NoSuchProcess): + p.send_signal(signal.CTRL_BREAK_EVENT) + + def test_username(self): + name = win32api.GetUserNameEx(win32con.NameSamCompatible) + if name.endswith('$'): + # When running as a service account (most likely to be + # NetworkService), these user name calculations don't produce the + # same result, causing the test to fail. + raise pytest.skip('running as service account') + assert psutil.Process().username() == name + + def test_cmdline(self): + sys_value = re.sub(r"[ ]+", " ", win32api.GetCommandLine()).strip() + psutil_value = ' '.join(psutil.Process().cmdline()) + if sys_value[0] == '"' != psutil_value[0]: + # The PyWin32 command line may retain quotes around argv[0] if they + # were used unnecessarily, while psutil will omit them. So remove + # the first 2 quotes from sys_value if not in psutil_value. + # A path to an executable will not contain quotes, so this is safe. + sys_value = sys_value.replace('"', '', 2) + assert 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() + assert 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() + assert sys_value['PeakWorkingSetSize'] == psutil_value.peak_wset + assert sys_value['WorkingSetSize'] == psutil_value.wset + assert ( + sys_value['QuotaPeakPagedPoolUsage'] + == psutil_value.peak_paged_pool + ) + assert sys_value['QuotaPagedPoolUsage'] == psutil_value.paged_pool + assert ( + sys_value['QuotaPeakNonPagedPoolUsage'] + == psutil_value.peak_nonpaged_pool + ) + assert ( + sys_value['QuotaNonPagedPoolUsage'] == psutil_value.nonpaged_pool + ) + assert sys_value['PagefileUsage'] == psutil_value.pagefile + assert sys_value['PeakPagefileUsage'] == psutil_value.peak_pagefile + + assert psutil_value.rss == psutil_value.wset + assert psutil_value.vms == psutil_value.pagefile + + 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) + assert 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() + assert 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() + assert psutil_value.read_count == sys_value['ReadOperationCount'] + assert psutil_value.write_count == sys_value['WriteOperationCount'] + assert psutil_value.read_bytes == sys_value['ReadTransferCount'] + assert psutil_value.write_bytes == sys_value['WriteTransferCount'] + assert psutil_value.other_count == sys_value['OtherOperationCount'] + assert psutil_value.other_bytes == sys_value['OtherTransferCount'] + + def test_num_handles(self): + import ctypes + import ctypes.wintypes + + PROCESS_QUERY_INFORMATION = 0x400 + handle = ctypes.windll.kernel32.OpenProcess( + PROCESS_QUERY_INFORMATION, 0, self.pid + ) + self.addCleanup(ctypes.windll.kernel32.CloseHandle, handle) + + hndcnt = ctypes.wintypes.DWORD() + ctypes.windll.kernel32.GetProcessHandleCount( + handle, ctypes.byref(hndcnt) + ) + sys_value = hndcnt.value + psutil_value = psutil.Process(self.pid).num_handles() + assert psutil_value == sys_value + + def test_error_partial_copy(self): + # https://github.com/giampaolo/psutil/issues/875 + exc = OSError() + exc.winerror = 299 + with mock.patch("psutil._psplatform.cext.proc_cwd", side_effect=exc): + with mock.patch("time.sleep") as m: + p = psutil.Process() + with pytest.raises(psutil.AccessDenied): + p.cwd() + assert m.call_count >= 5 + + def test_exe(self): + # NtQuerySystemInformation succeeds if process is gone. Make sure + # it raises NSP for a non existent pid. + pid = psutil.pids()[-1] + 99999 + proc = psutil._psplatform.Process(pid) + with pytest.raises(psutil.NoSuchProcess): + proc.exe() + + +class TestProcessWMI(WindowsTestCase): + """Compare Process API results with WMI.""" + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_name(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + assert p.name() == w.Caption + + # This fail on github because using virtualenv for test environment + @pytest.mark.skipif( + GITHUB_ACTIONS, reason="unreliable path on GITHUB_ACTIONS" + ) + def test_exe(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + # Note: wmi reports the exe as a lower case string. + # Being Windows paths case-insensitive we ignore that. + assert p.exe().lower() == w.ExecutablePath.lower() + + def test_cmdline(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + assert ' '.join(p.cmdline()) == w.CommandLine.replace('"', '') + + def test_username(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + domain, _, username = w.GetOwner() + username = f"{domain}\\{username}" + assert p.username() == username + + @retry_on_failure() + def test_memory_rss(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + rss = p.memory_info().rss + assert rss == int(w.WorkingSetSize) + + @retry_on_failure() + def test_memory_vms(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + vms = p.memory_info().vms + # http://msdn.microsoft.com/en-us/library/aa394372(VS.85).aspx + # ...claims that PageFileUsage is represented in Kilo + # bytes but funnily enough on certain platforms bytes are + # returned instead. + wmi_usage = int(w.PageFileUsage) + if vms not in {wmi_usage, wmi_usage * 1024}: + raise self.fail(f"wmi={wmi_usage}, psutil={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()) + ) + assert wmic_create == psutil_create + + +# --- + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +class TestDualProcessImplementation(PsutilTestCase): + """Certain APIs on Windows have 2 internal implementations, one + based on documented Windows APIs, another one based + NtQuerySystemInformation() which gets called as fallback in + case the first fails because of limited permission error. + Here we test that the two methods return the exact same value, + see: + https://github.com/giampaolo/psutil/issues/304. + """ + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_memory_info(self): + mem_1 = psutil.Process(self.pid).memory_info() + with mock.patch( + "psutil._psplatform.cext.proc_memory_info", + side_effect=PermissionError, + ) as fun: + mem_2 = psutil.Process(self.pid).memory_info() + assert len(mem_1) == len(mem_2) + for i in range(len(mem_1)): + assert mem_1[i] >= 0 + assert mem_2[i] >= 0 + assert abs(mem_1[i] - mem_2[i]) < 512 + assert fun.called + + def test_create_time(self): + ctime = psutil.Process(self.pid).create_time() + with mock.patch( + "psutil._psplatform.cext.proc_times", + side_effect=PermissionError, + ) as fun: + assert psutil.Process(self.pid).create_time() == ctime + assert fun.called + + def test_cpu_times(self): + cpu_times_1 = psutil.Process(self.pid).cpu_times() + with mock.patch( + "psutil._psplatform.cext.proc_times", + side_effect=PermissionError, + ) as fun: + cpu_times_2 = psutil.Process(self.pid).cpu_times() + assert fun.called + assert abs(cpu_times_1.user - cpu_times_2.user) < 0.01 + assert abs(cpu_times_1.system - cpu_times_2.system) < 0.01 + + def test_io_counters(self): + io_counters_1 = psutil.Process(self.pid).io_counters() + with mock.patch( + "psutil._psplatform.cext.proc_io_counters", + side_effect=PermissionError, + ) as fun: + io_counters_2 = psutil.Process(self.pid).io_counters() + for i in range(len(io_counters_1)): + assert abs(io_counters_1[i] - io_counters_2[i]) < 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=PermissionError, + ) as fun: + assert psutil.Process(self.pid).num_handles() == num_handles + assert fun.called + + def test_cmdline(self): + for pid in psutil.pids(): + try: + a = cext.proc_cmdline(pid, use_peb=True) + b = cext.proc_cmdline(pid, use_peb=False) + except OSError as err: + err = convert_oserror(err) + if not isinstance( + err, (psutil.AccessDenied, psutil.NoSuchProcess) + ): + raise + else: + assert a == b + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +class RemoteProcessTestCase(PsutilTestCase): + """Certain functions require calling ReadProcessMemory. + This trivially works when called on the current process. + Check that this works on other processes, especially when they + have a different bitness. + """ + + @staticmethod + def find_other_interpreter(): + # find a python interpreter that is of the opposite bitness from us + code = "import sys; sys.stdout.write(str(sys.maxsize > 2**32))" + + # XXX: a different and probably more stable approach might be to access + # the registry but accessing 64 bit paths from a 32 bit process + for filename in glob.glob(r"C:\Python*\python.exe"): + proc = subprocess.Popen( + args=[filename, "-c", code], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + output, _ = proc.communicate() + proc.wait() + if output == str(not IS_64BIT): + return filename + + test_args = ["-c", "import sys; sys.stdin.read()"] + + def setUp(self): + super().setUp() + + other_python = self.find_other_interpreter() + if other_python is None: + raise pytest.skip( + "could not find interpreter with opposite bitness" + ) + if IS_64BIT: + self.python64 = sys.executable + self.python32 = other_python + else: + self.python64 = other_python + self.python32 = sys.executable + + env = os.environ.copy() + env["THINK_OF_A_NUMBER"] = str(os.getpid()) + self.proc32 = self.spawn_testproc( + [self.python32] + self.test_args, env=env, stdin=subprocess.PIPE + ) + self.proc64 = self.spawn_testproc( + [self.python64] + self.test_args, env=env, stdin=subprocess.PIPE + ) + + def tearDown(self): + super().tearDown() + self.proc32.communicate() + self.proc64.communicate() + + def test_cmdline_32(self): + p = psutil.Process(self.proc32.pid) + assert len(p.cmdline()) == 3 + assert p.cmdline()[1:] == self.test_args + + def test_cmdline_64(self): + p = psutil.Process(self.proc64.pid) + assert len(p.cmdline()) == 3 + assert p.cmdline()[1:] == self.test_args + + def test_cwd_32(self): + p = psutil.Process(self.proc32.pid) + assert p.cwd() == os.getcwd() + + def test_cwd_64(self): + p = psutil.Process(self.proc64.pid) + assert p.cwd() == os.getcwd() + + def test_environ_32(self): + p = psutil.Process(self.proc32.pid) + e = p.environ() + assert "THINK_OF_A_NUMBER" in e + assert e["THINK_OF_A_NUMBER"] == str(os.getpid()) + + def test_environ_64(self): + p = psutil.Process(self.proc64.pid) + try: + p.environ() + except psutil.AccessDenied: + pass + + +# =================================================================== +# Windows services +# =================================================================== + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +class TestServices(PsutilTestCase): + def test_win_service_iter(self): + valid_statuses = { + "running", + "paused", + "start", + "pause", + "continue", + "stop", + "stopped", + } + valid_start_types = {"automatic", "manual", "disabled"} + valid_statuses = { + "running", + "paused", + "start_pending", + "pause_pending", + "continue_pending", + "stop_pending", + "stopped", + } + for serv in psutil.win_service_iter(): + data = serv.as_dict() + assert isinstance(data['name'], str) + assert data['name'].strip() + assert isinstance(data['display_name'], str) + assert isinstance(data['username'], str) + assert data['status'] in valid_statuses + if data['pid'] is not None: + psutil.Process(data['pid']) + assert isinstance(data['binpath'], str) + assert isinstance(data['username'], str) + assert isinstance(data['start_type'], str) + assert data['start_type'] in valid_start_types + assert data['status'] in valid_statuses + assert isinstance(data['description'], str) + pid = serv.pid() + if pid is not None: + p = psutil.Process(pid) + assert p.is_running() + # win_service_get + s = psutil.win_service_get(serv.name()) + # test __eq__ + assert serv == s + + def test_win_service_get(self): + ERROR_SERVICE_DOES_NOT_EXIST = ( + psutil._psplatform.cext.ERROR_SERVICE_DOES_NOT_EXIST + ) + ERROR_ACCESS_DENIED = psutil._psplatform.cext.ERROR_ACCESS_DENIED + + name = next(psutil.win_service_iter()).name() + with pytest.raises(psutil.NoSuchProcess) as cm: + psutil.win_service_get(name + '???') + assert cm.value.name == name + '???' + + # test NoSuchProcess + service = psutil.win_service_get(name) + exc = OSError(0, "msg", 0) + exc.winerror = ERROR_SERVICE_DOES_NOT_EXIST + with mock.patch( + "psutil._psplatform.cext.winservice_query_status", side_effect=exc + ): + with pytest.raises(psutil.NoSuchProcess): + service.status() + with mock.patch( + "psutil._psplatform.cext.winservice_query_config", side_effect=exc + ): + with pytest.raises(psutil.NoSuchProcess): + service.username() + + # test AccessDenied + exc = OSError(0, "msg", 0) + exc.winerror = ERROR_ACCESS_DENIED + with mock.patch( + "psutil._psplatform.cext.winservice_query_status", side_effect=exc + ): + with pytest.raises(psutil.AccessDenied): + service.status() + with mock.patch( + "psutil._psplatform.cext.winservice_query_config", side_effect=exc + ): + with pytest.raises(psutil.AccessDenied): + service.username() + + # test __str__ and __repr__ + assert service.name() in str(service) + assert service.display_name() in str(service) + assert service.name() in repr(service) + assert service.display_name() in repr(service) diff --git a/assets/questions.txt b/assets/questions.txt index d893efe..144859f 100644 --- a/assets/questions.txt +++ b/assets/questions.txt @@ -1,68 +1,20 @@ -Which command is used to temporarily change a SELinux boolean?|semanage boolean|setsebool|getsebool|2 -How do you enable a systemd service at boot?|systemctl enable|systemctl start|systemctl active|1 -Which command lists all active SELinux booleans?|getsebool -a|semanage boolean -l|setsebool|2 -How do you force a container to stop?|podman kill|podman stop|podman rm|1 -Which systemctl command switches to another target?|systemctl isolate|systemctl switch|systemctl target|1 -How do you check the status of a SELinux boolean?|semanage boolean|getsebool|setsebool -P|2 -How do you create a new partition with parted?|parted /dev/vdb mkpart|mkfs.ext4 /dev/vdb|fdisk /dev/vdb|1 -Which command lists all configured SELinux ports?|semanage port -l|getsebool -a|setsebool|1 -Which command lists all systemd targets?|systemctl list-units --type=target --all|systemctl list-targets|systemctl list-active-targets|1 -How do you persistently change a SELinux boolean?|setsebool -P|getsebool -P|semanage boolean -P|1 -Which command shows the partition table?|fdisk -l|parted print|lsblk|1 -How do you restart a systemd service?|systemctl restart|systemctl reload|service restart|1 -Which command shows all running systemd services?|systemctl list-units --type=service|systemctl status --all|systemctl active-services|1 -How do you schedule a one-time job with cron?|at|crontab -e|cron add-job|1 -How do you list installed packages?|rpm -qa|yum list installed|dnf list installed|1 -How do you add a new user?|useradd|adduser|usermod|1 -How do you change file ownership?|chown user:group file|chmod user file|ls -l file|1 -Which command shows disk usage of a directory?|du -sh /dir|df -h /dir|ls -lh /dir|1 -How do you create a new logical volume?|lvcreate -L 10G -n volume group|vgcreate -L 10G|lvextend -L +10G volume|1 -How do you deactivate a user?|passwd -l user|usermod -L user|userdel user|2 -How do you view all available network interfaces?|ip addr|ifconfig|netstat -i|1 -How do you display the system uptime?|uptime|top|vmstat|1 -How do you view swap usage?|free -h|swapon -s|cat /proc/swaps|1 -How do you reload firewalld rules?|firewall-cmd --reload|iptables reload|firewallctl restart|1 -How do you list kernel modules?|lsmod|modprobe -l|dmesg|1 -How do you analyze CPU usage?|top|htop|vmstat|1 -Which command lists active TCP connections?|netstat -tulpn|ss -tulpn|lsof -i|1 -How do you stop a systemd service?|systemctl stop|systemctl disable|systemctl restart|1 -How do you check the DNS cache?|systemd-resolve --statistics|systemctl dns|dns cache|1 -What command is used to list all files in a directory?|ls -l|ls -a|ls -la|ls -lh|2 -What command is used to change the current directory?|cd|chdir|change|dir|1 -What command is used to display the current working directory?|pwd|cwd|dir|ls|1 -What command is used to create a new directory?|mkdir|newdir|createdir|makedir|1 -What command is used to remove a file?|rm|delete|remove|erase|1 -What command is used to copy files?|cp|copy|duplicate|move|1 -What command is used to move files?|mv|move|transfer|shift|1 -What command is used to display the contents of a file?|cat|show|display|view|1 -What command is used to search for a pattern in a file?|grep|search|find|look|1 -What command is used to display the first few lines of a file?|head|top|start|begin|1 -What command is used to display the last few lines of a file?|tail|end|finish|bottom|1 -What command is used to display the manual page for a command?|man|help|info|guide|1 -What command is used to change file permissions?|chmod|chperm|chown|chattr|1 -What command is used to change file ownership?|chown|chperm|chmod|chattr|1 -What command is used to display disk usage?|du|df|disk|usage|1 -What command is used to display free disk space?|df|du|disk|free|1 -What command is used to display system information?|uname|sysinfo|info|system|1 -What command is used to display running processes?|ps|proc|process|run|1 -What command is used to terminate a process?|kill|terminate|end|stop|1 -What command is used to display network configuration?|ifconfig|netconfig|network|config|1 -What command is used to display the routing table?|route|netstat|traceroute|path|1 -What command is used to display open network connections?|netstat|conn|connections|open|1 -What command is used to display the hostname?|hostname|host|name|display|1 -What command is used to change the hostname?|hostnamectl|hostctl|namectl|changehost|1 -What command is used to display the current date and time?|date|time|datetime|now|1 -What command is used to set the system date and time?|date -s|time -s|datetime -s|setdate|1 -What command is used to display the system uptime?|uptime|time|sysuptime|up|1 -What command is used to reboot the system?|reboot|restart|shutdown -r|boot|1 -What command is used to shut down the system?|shutdown|halt|poweroff|stop|1 -What command is used to display the contents of a directory?|ls|dir|list|show|1 -What command is used to display the current user's ID?|id|userid|uid|whoami|1 -What command is used to switch to another user?|su|switch|user|login|1 -What command is used to display the system's kernel version?|uname -r|kernel|version|sysver|1 -What command is used to display the system's architecture?|uname -m|arch|architecture|sysarch|1 -What command is used to display the system's release information?|uname -a|release|info|sysinfo|1 -What command is used to display the system's hardware information?|lshw|hwinfo|hardware|sysinfo|1 -What command is used to display the system's memory usage?|free|mem|memory|usage|1 -What command is used to display the system's CPU usage?|top|cpu|usage|syscpu|1 -What command is used to display the system's load average?|uptime|load|average|sysload|1 \ No newline at end of file +Which command creates a compressed tar archive?|gzip dir|tar czf archive.tar.gz dir|bzip2 dir|2 +How do you append output to a file in Bash?|>|&>|>>|3 +Which command finds all files named "passwd" under /etc?|find /etc -name passwd|grep passwd /etc|ls /etc/passwd|1 +How do you switch to the root user?|sudo su -|su -|sudo root|2 +Which command shows the SELinux context of a file?|ls -Z|ls -l|ls --context|1 +How do you create a new user?|usermod|useradd|adduser|2 +Which command lists all running containers?|podman ps|podman list|docker ps|1 +How do you mount an NFS share?|mount -t nfs server:/share /mnt|mount -t ext4 /dev/sda1 /mnt|mount -o loop image.iso /mnt|1 +How do you schedule a one-time job for 5 minutes from now?|at now + 5 minutes|cron 5|crontab -e|1 +Which command sets a file's permissions to rwxr-xr--?|chmod 754 file|chmod 644 file|chmod 777 file|1 +How do you check the status of firewalld?|firewall-cmd --state|firewallctl status|systemctl firewall|1 +How do you extend a logical volume by 1G?|lvextend -L +1G /dev/vg/lv|lvcreate -L 1G /dev/vg/lv|lvresize -L 1G /dev/vg/lv|1 +How do you display the last 10 lines of a file?|tail file|head file|less file|1 +How do you change a user's password aging policy?|chage|passwd|usermod|1 +How do you list all available systemd targets?|systemctl list-units --type=target --all|systemctl list-targets|systemctl list-active-targets|2 +How do you display the current runlevel?|runlevel|who -r|systemctl get-default|1 +How do you set a service to start at boot?|systemctl enable|systemctl start|systemctl boot|1 +How do you transfer a file securely to a remote host?|scp file user@host:/path|ftp file user@host:/path|rsync file user@host:/path|1 +How do you display the UUID of a filesystem?|blkid|lsblk|uuidgen|1 +How do you reload the systemd daemon?|systemctl daemon-reload|systemctl reload|systemctl restart|1 \ No newline at end of file diff --git a/assets/session.log b/assets/session.log index d5be17b..5d13b9c 100644 --- a/assets/session.log +++ b/assets/session.log @@ -436,3 +436,6 @@ Session start: 2025-06-17 23:05:53 XP and level updated: 215 XP, Level 3 Session start: 2025-06-17 23:06:31 XP and level updated: 225 XP, Level 3 +Session start: 2025-06-17 23:23:35 +Session duration: 22 minutes and 43 seconds +Session end: 2025-06-17 23:29:19 diff --git a/assets/total_time.txt b/assets/total_time.txt index b7f71a9..cecd0ea 100644 --- a/assets/total_time.txt +++ b/assets/total_time.txt @@ -1 +1 @@ -27592 \ No newline at end of file +28955 \ No newline at end of file diff --git a/import-hosts-netbox.py b/import-hosts-netbox.py deleted file mode 100644 index 0009902..0000000 --- a/import-hosts-netbox.py +++ /dev/null @@ -1,49 +0,0 @@ -import subprocess -import xml.etree.ElementTree as ET -import pynetbox -import requests -from mac_vendor_lookup import MacLookup - -# Config -nmap_range = "192.168.1.0/24" -netbox_url = "https://http://192.168.1.144" -netbox_token = "4a8593d7bd6beb95f9574c575e95783d49b6b611" -discord_webhook = "https://discord.com/api/webhooks/1362175717827285022/v8ASU7JLvH7Y4effLy_xajDIc0IluGQKJn5fCOr4oJqJB3ynYBhr-bM-NeKp9hMciQov" - -nb = pynetbox.api(netbox_url, token=netbox_token) -mac_lookup = MacLookup() - -def send_discord_message(content): - requests.post(discord_webhook, json={"content": content}) - -# Scan network -result = subprocess.run(["nmap", "-sn", "-oX", "-", nmap_range], capture_output=True, text=True) -root = ET.fromstring(result.stdout) - -for host in root.findall("host"): - ip_elem = host.find("address[@addrtype='ipv4']") - mac_elem = host.find("address[@addrtype='mac']") - - if ip_elem is not None: - ip = ip_elem.attrib["addr"] - mac = mac_elem.attrib["addr"] if mac_elem is not None else "Unknown" - vendor = "Unknown" - - if mac != "Unknown": - try: - vendor = mac_lookup.lookup(mac) - except Exception: - vendor = "Lookup failed" - - existing = nb.dcim.devices.filter(name=ip) - if not existing: - nb.dcim.devices.create( - name=ip, - device_type=1, # Replace with actual device_type ID - device_role=1, # Replace with actual device_role ID - site=1, # Replace with actual site ID - description=f"Discovered via script. MAC: {mac}, Vendor: {vendor}" - ) - send_discord_message(f"New device discovered: {ip}\nMAC: {mac}\nVendor: {vendor}") - -print("Scan completed.") diff --git a/main.py b/main.py index 39feebd..33d8974 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,7 @@ import tkinter as tk from tkinter import messagebox import random import subprocess -from datetime import datetime +from datetime import datetime, timedelta # Constants SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -14,12 +14,13 @@ XP_FILE = os.path.join(ASSETS_DIR, 'xp.txt') LEVEL_FILE = os.path.join(ASSETS_DIR, 'level.txt') SESSION_LOG = os.path.join(ASSETS_DIR, 'session.log') QUESTIONS_FILE = os.path.join(ASSETS_DIR, 'questions.txt') +QUESTIONS_SHUFFLED_FILE = os.path.join(ASSETS_DIR, 'questions_shuffled.txt') TOTAL_TIME_FILE = os.path.join(ASSETS_DIR, 'total_time.txt') URL = "https://rol.redhat.com" TIMER_MINUTES = 60 # Set the ROL subscription expiration date -SUBSCRIPTION_END_DATE = datetime(2026, 2, 23) # Adjust this date based on your actual ROL subscription end +SUBSCRIPTION_END_DATE = datetime.now() + timedelta(days=343) # Utility Functions def initialize_assets(): @@ -140,19 +141,50 @@ def show_timer(minutes, questions_file, session_log, total_time, xp, level): def on_closing(): end_session() + # Load & parse questions + with open(questions_file, 'r') as f: + lines = [l.strip() for l in f if l.strip()] + question_line = random.choice(lines) + parts = question_line.split('|') + question_text = parts[0] + answers = parts[1:-1] + correct_idx = int(parts[-1]) - 1 # zero-based + + # Build a list of (text, is_correct) and shuffle + opts = [] + for idx, ans in enumerate(answers): + opts.append({'text': ans, 'correct': (idx == correct_idx)}) + random.shuffle(opts) + + # Display question + question_label = tk.Label(root, text=f"Question: {question_text}", font=("Helvetica", 14)) + question_label.pack(pady=10) + + answer_var = tk.StringVar(value="") + radio_buttons = [] + for opt in opts: + rb = tk.Radiobutton(root, text=opt['text'], + variable=answer_var, value=opt['text'], + font=("Helvetica", 12)) + rb.pack(anchor='w') + radio_buttons.append(rb) + def check_answer(): - selected_answer = answer_var.get() - if selected_answer == correct_answer: - messagebox.showinfo("Correct!", "You selected the correct answer.") + sel = answer_var.get() + correct = any(o['text'] == sel and o['correct'] for o in opts) + if correct: + messagebox.showinfo("Correct!", "You chose the right answer.") new_xp, new_level = update_xp(True) else: - messagebox.showinfo("Incorrect", "You selected the wrong answer.") + messagebox.showinfo("Incorrect", "Sorry, that’s not correct.") new_xp, new_level = update_xp(False) - for button in radio_buttons: - button.config(state=tk.DISABLED) + for b in radio_buttons: b.config(state=tk.DISABLED) xp_label.config(text=f"XP: {new_xp}") level_label.config(text=f"Level: {new_level}") + submit_btn = tk.Button(root, text="Submit", command=check_answer) + submit_btn.pack(pady=5) + def periodic_kill_steam(): kill_steam() root.after(10000, periodic_kill_steam) @@ -180,28 +212,6 @@ def show_timer(minutes, questions_file, session_log, total_time, xp, level): total_time_label = tk.Label(root, text=f"Total session time: {total_time // 3600} hours, {(total_time % 3600) // 60} minutes, and {total_time % 60} seconds", font=("Helvetica", 14)) total_time_label.pack(pady=5) - # Load Questions - with open(questions_file, 'r') as f: - questions = f.readlines() - random.shuffle(questions) - - question = random.choice(questions).strip() - question_text, *answers, correct_answer = question.split('|') - - question_label = tk.Label(root, text=f"Question: {question_text}", font=("Helvetica", 14)) - question_label.pack(pady=10) - - answer_var = tk.StringVar(value="") - radio_buttons = [] - - for idx, answer in enumerate(answers, start=1): - radio_button = tk.Radiobutton(root, text=answer, variable=answer_var, value=str(idx), font=("Helvetica", 12)) - radio_button.pack(anchor='w') - radio_buttons.append(radio_button) - - submit_button = tk.Button(root, text="Submit", command=check_answer, font=("Helvetica", 12)) - submit_button.pack(pady=10) - root.after(1000, update_timer) root.after(1000, update_rol_timer) root.after(10000, periodic_kill_steam) @@ -220,15 +230,31 @@ def open_firefox(url): else: print("No graphical environment detected. Skipping browser opening.") +def shuffle_questions(input_file, output_file): + """Shuffle questions and answers, updating the correct answer index.""" + with open(input_file) as f: + lines = [l.strip() for l in f if l.strip()] + + with open(output_file, 'w') as f: + for line in lines: + parts = line.split('|') + q, answers, correct = parts[0], parts[1:-1], int(parts[-1]) + zipped = list(zip(answers, range(1, len(answers)+1))) + random.shuffle(zipped) + new_answers, old_indices = zip(*zipped) + new_correct = old_indices.index(correct) + 1 + f.write(f"{q}|{'|'.join(new_answers)}|{new_correct}\n") + def main(): initialize_assets() log_session("start") total_time, xp, level = display_summary() kill_steam() open_firefox(URL) - show_timer(TIMER_MINUTES, QUESTIONS_FILE, SESSION_LOG, total_time, xp, level) + show_timer(TIMER_MINUTES, QUESTIONS_SHUFFLED_FILE, SESSION_LOG, total_time, xp, level) log_session("end") if __name__ == "__main__": + shuffle_questions(QUESTIONS_FILE, QUESTIONS_SHUFFLED_FILE) main()

kQf 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 C`7LSy literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/__pycache__/_psosx.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/__pycache__/_psosx.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16871053c265b9d489d69b824f53630e8e32636d GIT binary patch literal 21532 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 U=lf|MMULu3+h4Y|^*B6#1<=Wc%m4rY literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/__pycache__/_pssunos.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/__pycache__/_pssunos.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8290c1ba6c908d72f7addc03feaa0b8929cb238d GIT binary patch literal 30380 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 @memoize + ... def foo() + ... return 1 + ... + >>> foo() + 1 + >>> foo.cache_clear() + >>> + + It supports: + - functions + - classes (acts as a @singleton) + - staticmethods + - classmethods + + It does NOT support: + - methods + """ + + @functools.wraps(fun) + def wrapper(*args, **kwargs): + key = (args, frozenset(sorted(kwargs.items()))) + try: + return cache[key] + except KeyError: + try: + ret = cache[key] = fun(*args, **kwargs) + except Exception as err: # noqa: BLE001 + raise err from None + return ret + + def cache_clear(): + """Clear cache.""" + cache.clear() + + cache = {} + wrapper.cache_clear = cache_clear + return wrapper + + +def memoize_when_activated(fun): + """A memoize decorator which is disabled by default. It can be + activated and deactivated on request. + For efficiency reasons it can be used only against class methods + accepting no arguments. + + >>> class Foo: + ... @memoize + ... def foo() + ... print(1) + ... + >>> f = Foo() + >>> # deactivated (default) + >>> foo() + 1 + >>> foo() + 1 + >>> + >>> # activated + >>> foo.cache_activate(self) + >>> foo() + 1 + >>> foo() + >>> foo() + >>> + """ + + @functools.wraps(fun) + def wrapper(self): + try: + # case 1: we previously entered oneshot() ctx + ret = self._cache[fun] + except AttributeError: + # case 2: we never entered oneshot() ctx + try: + return fun(self) + except Exception as err: # noqa: BLE001 + raise err from None + except KeyError: + # case 3: we entered oneshot() ctx but there's no cache + # for this entry yet + try: + ret = fun(self) + except Exception as err: # noqa: BLE001 + 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(proc): + """Activate cache. Expects a Process instance. Cache will be + stored as a "_cache" instance attribute. + """ + proc._cache = {} + + def cache_deactivate(proc): + """Deactivate and clear cache.""" + try: + del proc._cache + except AttributeError: + pass + + wrapper.cache_activate = cache_activate + wrapper.cache_deactivate = cache_deactivate + return wrapper + + +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. + """ + try: + st = os.stat(path) + except PermissionError: + raise + except OSError: + return False + else: + return stat.S_ISREG(st.st_mode) + + +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. + """ + try: + os.stat(path) + 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: + with socket.socket(AF_INET6, socket.SOCK_STREAM) as sock: + sock.bind(("::1", 0)) + return True + except OSError: + return False + + +def parse_environ_block(data): + """Parse a C environ block of environment variables into a dictionary.""" + # The block is usually raw data from the target process. It might contain + # trailing garbage and lines that do not look like assignments. + ret = {} + pos = 0 + + # localize global variable to speed up access. + WINDOWS_ = WINDOWS + while True: + next_pos = data.find("\0", pos) + # nul byte at the beginning or double nul byte means finish + if next_pos <= pos: + break + # there might not be an equals sign + equal_pos = data.find("=", pos, next_pos) + if equal_pos > pos: + key = data[pos:equal_pos] + value = data[equal_pos + 1 : next_pos] + # Windows expects environment variables to be uppercase only + if WINDOWS_: + key = key.upper() + ret[key] = value + pos = next_pos + 1 + + return ret + + +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. + """ + try: + return socket.AddressFamily(num) + except ValueError: + return num + + +def socktype_to_enum(num): + """Convert a numeric socket type value to an IntEnum member. + If it's not a known member, return the numeric value itself. + """ + try: + return socket.SocketKind(num) + except ValueError: + return num + + +def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None): + """Convert a raw connection tuple to a proper ntuple.""" + if fam in {socket.AF_INET, AF_INET6}: + if laddr: + laddr = addr(*laddr) + if raddr: + raddr = addr(*raddr) + if type_ == socket.SOCK_STREAM and fam in {AF_INET, AF_INET6}: + status = status_map.get(status, CONN_NONE) + else: + status = CONN_NONE # ignore whatever C returned to us + fam = sockfam_to_enum(fam) + type_ = socktype_to_enum(type_) + if pid is None: + return pconn(fd, fam, type_, laddr, raddr, status) + else: + return sconn(fd, fam, type_, laddr, raddr, status, pid) + + +def 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. + """ + + def outer(fun): + 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=DeprecationWarning, stacklevel=2) + return getattr(self, replacement)(*args, **kwargs) + + return inner + + return outer + + +class _WrapNumbers: + """Watches numbers so that they don't overflow and wrap + (reset to zero). + """ + + def __init__(self): + self.lock = threading.Lock() + self.cache = {} + self.reminders = {} + self.reminder_keys = {} + + def _add_dict(self, input_dict, name): + assert name not in self.cache + assert name not in self.reminders + assert name not in self.reminder_keys + self.cache[name] = input_dict + 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()) + for gone_key in gone_keys: + for remkey in self.reminder_keys[name][gone_key]: + del self.reminders[name][remkey] + del self.reminder_keys[name][gone_key] + + def run(self, input_dict, name): + """Cache dict and sum numbers which overflow and wrap. + Return an updated copy of `input_dict`. + """ + if name not in self.cache: + # This was the first call. + self._add_dict(input_dict, name) + return input_dict + + self._remove_dead_reminders(input_dict, name) + + old_dict = self.cache[name] + new_dict = {} + for key in input_dict: + input_tuple = input_dict[key] + try: + old_tuple = old_dict[key] + except KeyError: + # The input dict has a new key (e.g. a new disk or NIC) + # which didn't exist in the previous call. + new_dict[key] = input_tuple + continue + + bits = [] + for i in range(len(input_tuple)): + input_value = input_tuple[i] + old_value = old_tuple[i] + remkey = (key, i) + if input_value < old_value: + # it wrapped! + self.reminders[name][remkey] += old_value + self.reminder_keys[name][key].add(remkey) + bits.append(input_value + self.reminders[name][remkey]) + + new_dict[key] = tuple(bits) + + self.cache[name] = input_dict + return new_dict + + def cache_clear(self, name=None): + """Clear the internal cache, optionally only for function 'name'.""" + with self.lock: + if name is None: + self.cache.clear() + self.reminders.clear() + self.reminder_keys.clear() + else: + self.cache.pop(name, None) + self.reminders.pop(name, None) + self.reminder_keys.pop(name, None) + + def cache_info(self): + """Return internal cache dicts as a tuple of 3 elements.""" + with self.lock: + return (self.cache, self.reminders, self.reminder_keys) + + +def wrap_numbers(input_dict, name): + """Given an `input_dict` and a function `name`, adjust the numbers + which "wrap" (restart from zero) across different calls by adding + "old value" to "new value" and return an updated dict. + """ + with _wn.lock: + return _wn.run(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 +# ===================================================================== + + +@memoize +def term_supports_colors(file=sys.stdout): # pragma: no cover + if os.name == 'nt': + return True + try: + import curses + + assert file.isatty() + curses.setupterm() + assert curses.tigetnum("colors") > 0 + except Exception: # noqa: BLE001 + return False + else: + return True + + +def hilite(s, color=None, bold=False): # pragma: no cover + """Return an highlighted version of 'string'.""" + if not term_supports_colors(): + return s + attr = [] + colors = dict( + blue='34', + brown='33', + darkgrey='30', + green='32', + grey='37', + lightblue='36', + red='91', + violet='35', + yellow='93', + ) + colors[None] = '29' + try: + color = colors[color] + except KeyError: + msg = f"invalid color {color!r}; choose amongst {list(colors.keys())}" + 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 not term_supports_colors(): + print(s, file=file) + elif POSIX: + print(hilite(s, color, bold), file=file) + else: + import ctypes + + DEFAULT_COLOR = 7 + GetStdHandle = ctypes.windll.Kernel32.GetStdHandle + SetConsoleTextAttribute = ( + ctypes.windll.Kernel32.SetConsoleTextAttribute + ) + + colors = dict(green=2, red=4, brown=6, yellow=6) + colors[None] = DEFAULT_COLOR + try: + color = colors[color] + except KeyError: + msg = ( + f"invalid color {color!r}; choose between" + f" {list(colors.keys())!r}" + ) + raise ValueError(msg) from None + if bold and color <= 7: + color += 8 + + handle_id = -12 if file is sys.stderr else -11 + GetStdHandle.restype = ctypes.c_ulong + handle = GetStdHandle(handle_id) + SetConsoleTextAttribute(handle, color) + try: + print(s, file=file) + finally: + SetConsoleTextAttribute(handle, DEFAULT_COLOR) + + +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/.venv/lib/python3.12/site-packages/psutil/_psaix.py b/.venv/lib/python3.12/site-packages/psutil/_psaix.py new file mode 100644 index 0000000..ba2725f --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/_psaix.py @@ -0,0 +1,565 @@ +# 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 platform implementation.""" + +import functools +import glob +import os +import re +import subprocess +import sys +from collections import namedtuple + +from . import _common +from . import _psposix +from . import _psutil_aix as cext +from . import _psutil_posix as cext_posix +from ._common import NIC_DUPLEX_FULL +from ._common import NIC_DUPLEX_HALF +from ._common import NIC_DUPLEX_UNKNOWN +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import conn_to_ntuple +from ._common import get_procfs_path +from ._common import memoize_when_activated +from ._common import usage_percent + + +__extra__all__ = ["PROCFS_PATH"] + + +# ===================================================================== +# --- globals +# ===================================================================== + + +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 = cext_posix.getpagesize() +AF_LINK = cext_posix.AF_LINK + +PROC_STATUSES = { + cext.SIDL: _common.STATUS_IDLE, + cext.SZOMB: _common.STATUS_ZOMBIE, + cext.SACTIVE: _common.STATUS_RUNNING, + cext.SSWAP: _common.STATUS_RUNNING, # TODO what status is this? + cext.SSTOP: _common.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, +} + +proc_info_map = dict( + ppid=0, + rss=1, + vms=2, + create_time=3, + 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']) + + +# ===================================================================== +# --- memory +# ===================================================================== + + +def virtual_memory(): + total, avail, free, _pinned, inuse = cext.virtual_mem() + percent = usage_percent((total - avail), total, round_=1) + return svmem(total, avail, percent, inuse, free) + + +def swap_memory(): + """Swap system memory as a (total, used, free, sin, sout) tuple.""" + 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) + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +def cpu_times(): + """Return system-wide CPU times as a named tuple.""" + ret = cext.per_cpu_times() + return scputimes(*[sum(x) for x in zip(*ret)]) + + +def per_cpu_times(): + """Return system per-CPU times as a list of named tuples.""" + ret = cext.per_cpu_times() + return [scputimes(*x) for x in ret] + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + try: + return os.sysconf("SC_NPROCESSORS_ONLN") + except ValueError: + # mimic os.cpu_count() behavior + return None + + +def cpu_count_cores(): + cmd = ["lsdev", "-Cc", "processor"] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + stdout, stderr = (x.decode(sys.stdout.encoding) for x in (stdout, stderr)) + if p.returncode != 0: + msg = f"{cmd!r} command error\n{stderr}" + raise RuntimeError(msg) + processors = stdout.strip().splitlines() + return len(processors) or None + + +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 + ) + + +# ===================================================================== +# --- disks +# ===================================================================== + + +disk_io_counters = cext.disk_io_counters +disk_usage = _psposix.disk_usage + + +def disk_partitions(all=False): + """Return system disk partitions.""" + # TODO - the filtering logic should be better checked so that + # it tries to reflect 'df' as much as possible + retlist = [] + partitions = cext.disk_partitions() + for partition in partitions: + device, mountpoint, fstype, opts = partition + if device == 'none': + device = '' + if not all: + # 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: + continue + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + retlist.append(ntuple) + return retlist + + +# ===================================================================== +# --- network +# ===================================================================== + + +net_if_addrs = cext_posix.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). + """ + families, types = _common.conn_tmap[kind] + rawlist = cext.net_connections(_pid) + 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 + 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 = {x[0] for x in net_if_addrs()} + ret = {} + for name in names: + mtu = cext_posix.net_if_mtu(name) + flags = cext_posix.net_if_flags(name) + + # 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, + ) + stdout, stderr = p.communicate() + stdout, stderr = ( + x.decode(sys.stdout.encoding) for x in (stdout, stderr) + ) + if p.returncode == 0: + re_result = re.search( + r"Running: (\d+) Mbps.*?(\w+) Duplex", stdout + ) + if re_result is not None: + speed = int(re_result.group(1)) + duplex = re_result.group(2) + + output_flags = ','.join(flags) + isup = 'running' in flags + duplex = duplex_map.get(duplex, NIC_DUPLEX_UNKNOWN) + ret[name] = _common.snicstats(isup, duplex, speed, mtu, output_flags) + return ret + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +def boot_time(): + """The system boot time expressed in seconds since the epoch.""" + return cext.boot_time() + + +def users(): + """Return currently connected users as a list of namedtuples.""" + retlist = [] + rawlist = cext.users() + localhost = (':0.0', ':0') + 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 localhost: + hostname = 'localhost' + nt = _common.suser(user, tty, hostname, tstamp, pid) + retlist.append(nt) + return retlist + + +# ===================================================================== +# --- processes +# ===================================================================== + + +def pids(): + """Returns a list of PIDs currently running on the system.""" + return [int(x) for x in os.listdir(get_procfs_path()) if x.isdigit()] + + +def pid_exists(pid): + """Check for the existence of a unix pid.""" + return os.path.exists(os.path.join(get_procfs_path(), str(pid), "psinfo")) + + +def wrap_exceptions(fun): + """Call callable into a try/except clause and translate ENOENT, + EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. + """ + + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + pid, ppid, name = self.pid, self._ppid, self._name + try: + return fun(self, *args, **kwargs) + except (FileNotFoundError, ProcessLookupError) as err: + # ENOENT (no such file or directory) gets raised on open(). + # ESRCH (no such process) can get raised on read() if + # process is gone in meantime. + if not pid_exists(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: + """Wrapper class around underlying C implementation.""" + + __slots__ = ["_cache", "_name", "_ppid", "_procfs_path", "pid"] + + def __init__(self, pid): + self.pid = pid + self._name = None + self._ppid = None + self._procfs_path = get_procfs_path() + + def oneshot_enter(self): + self._proc_basic_info.cache_activate(self) + self._proc_cred.cache_activate(self) + + def oneshot_exit(self): + self._proc_basic_info.cache_deactivate(self) + self._proc_cred.cache_deactivate(self) + + @wrap_exceptions + @memoize_when_activated + def _proc_basic_info(self): + return cext.proc_basic_info(self.pid, self._procfs_path) + + @wrap_exceptions + @memoize_when_activated + def _proc_cred(self): + return cext.proc_cred(self.pid, self._procfs_path) + + @wrap_exceptions + def name(self): + if self.pid == 0: + return "swapper" + # 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 + 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) + ): + 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 + ): + return possible_exe + return '' + + @wrap_exceptions + def cmdline(self): + return cext.proc_args(self.pid) + + @wrap_exceptions + def environ(self): + return cext.proc_environ(self.pid) + + @wrap_exceptions + def create_time(self): + return self._proc_basic_info()[proc_info_map['create_time']] + + @wrap_exceptions + def num_threads(self): + return self._proc_basic_info()[proc_info_map['num_threads']] + + if HAS_THREADS: + + @wrap_exceptions + def threads(self): + rawlist = cext.proc_threads(self.pid) + retlist = [] + for thread_id, utime, stime in rawlist: + ntuple = _common.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 + # an empty list means there were no connections for process or + # process is no longer active so we force NSP in case the PID + # is no longer there. + if not retlist: + # will raise NSP if process is gone + os.stat(f"{self._procfs_path}/{self.pid}") + return retlist + + @wrap_exceptions + 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 + # an empty list means there were no connections for process or + # process is no longer active so we force NSP in case the PID + # is no longer there. + if not ret: + # will raise NSP if process is gone + os.stat(f"{self._procfs_path}/{self.pid}") + return ret + + @wrap_exceptions + def nice_get(self): + return cext_posix.getpriority(self.pid) + + @wrap_exceptions + def nice_set(self, value): + return cext_posix.setpriority(self.pid, value) + + @wrap_exceptions + def ppid(self): + self._ppid = self._proc_basic_info()[proc_info_map['ppid']] + return self._ppid + + @wrap_exceptions + def uids(self): + real, effective, saved, _, _, _ = self._proc_cred() + return _common.puids(real, effective, saved) + + @wrap_exceptions + def gids(self): + _, _, _, real, effective, saved = self._proc_cred() + return _common.puids(real, effective, saved) + + @wrap_exceptions + def cpu_times(self): + t = cext.proc_cpu_times(self.pid, self._procfs_path) + return _common.pcputimes(*t) + + @wrap_exceptions + def terminal(self): + ttydev = self._proc_basic_info()[proc_info_map['ttynr']] + # convert from 64-bit dev_t to 32-bit dev_t and then map the device + ttydev = ((ttydev & 0x0000FFFF00000000) >> 16) | (ttydev & 0xFFFF) + # try to match rdev of /dev/pts/* files ttydev + for dev in glob.glob("/dev/**/*"): + if os.stat(dev).st_rdev == ttydev: + return dev + return None + + @wrap_exceptions + def cwd(self): + procfs_path = self._procfs_path + try: + result = os.readlink(f"{procfs_path}/{self.pid}/cwd") + return result.rstrip('/') + 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() + rss = ret[proc_info_map['rss']] * 1024 + vms = ret[proc_info_map['vms']] * 1024 + return pmem(rss, vms) + + memory_full_info = memory_info + + @wrap_exceptions + def status(self): + code = self._proc_basic_info()[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, + ) + stdout, stderr = p.communicate() + stdout, stderr = ( + x.decode(sys.stdout.encoding) for x in (stdout, stderr) + ) + if "no such process" in stderr.lower(): + raise NoSuchProcess(self.pid, self._name) + procfiles = re.findall(r"(\d+): S_IFREG.*name:(.*)\n", stdout) + retlist = [] + for fd, path in procfiles: + path = path.strip() + if path.startswith("//"): + path = path[1:] + if path.lower() == "cannot be retrieved": + continue + retlist.append(_common.popenfile(path, int(fd))) + return retlist + + @wrap_exceptions + def num_fds(self): + if self.pid == 0: # no /proc/0/fd + return 0 + 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)) + + @wrap_exceptions + def wait(self, timeout=None): + return _psposix.wait_pid(self.pid, timeout, self._name) + + if HAS_PROC_IO_COUNTERS: + + @wrap_exceptions + def io_counters(self): + try: + 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 _common.pio(rc, wc, rb, wb) diff --git a/.venv/lib/python3.12/site-packages/psutil/_psbsd.py b/.venv/lib/python3.12/site-packages/psutil/_psbsd.py new file mode 100644 index 0000000..13bd926 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/_psbsd.py @@ -0,0 +1,971 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""FreeBSD, OpenBSD and NetBSD platforms implementation.""" + +import contextlib +import errno +import functools +import os +from collections import defaultdict +from collections import namedtuple +from xml.etree import ElementTree # noqa: ICN001 + +from . import _common +from . import _psposix +from . import _psutil_bsd as cext +from . import _psutil_posix as cext_posix +from ._common import FREEBSD +from ._common import NETBSD +from ._common import OPENBSD +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import debug +from ._common import memoize +from ._common import memoize_when_activated +from ._common import usage_percent + + +__extra__all__ = [] + + +# ===================================================================== +# --- globals +# ===================================================================== + + +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, + } +elif OPENBSD: + PROC_STATUSES = { + cext.SIDL: _common.STATUS_IDLE, + cext.SSLEEP: _common.STATUS_SLEEPING, + cext.SSTOP: _common.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, + # 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. + # SONPROC indicates that the process is actually executing on + # 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, + } +elif NETBSD: + PROC_STATUSES = { + cext.SIDL: _common.STATUS_IDLE, + cext.SSLEEP: _common.STATUS_SLEEPING, + cext.SSTOP: _common.STATUS_STOPPED, + cext.SZOMB: _common.STATUS_ZOMBIE, + cext.SRUN: _common.STATUS_WAKING, + cext.SONPROC: _common.STATUS_RUNNING, + } + +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, +} + +PAGESIZE = cext_posix.getpagesize() +AF_LINK = cext_posix.AF_LINK + +HAS_PER_CPU_TIMES = hasattr(cext, "per_cpu_times") +HAS_PROC_NUM_THREADS = hasattr(cext, "proc_num_threads") +HAS_PROC_OPEN_FILES = hasattr(cext, 'proc_open_files') +HAS_PROC_NUM_FDS = hasattr(cext, 'proc_num_fds') + +kinfo_proc_map = dict( + ppid=0, + status=1, + 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 +# ===================================================================== + + +# fmt: off +# psutil.virtual_memory() +svmem = namedtuple( + 'svmem', ['total', 'available', 'percent', 'used', 'free', + 'active', 'inactive', 'buffers', 'cached', 'shared', 'wired']) +# psutil.cpu_times() +scputimes = namedtuple( + 'scputimes', ['user', 'nice', 'system', 'idle', 'irq']) +# psutil.Process.memory_info() +pmem = namedtuple('pmem', ['rss', 'vms', 'text', 'data', 'stack']) +# psutil.Process.memory_full_info() +pfullmem = pmem +# psutil.Process.cpu_times() +pcputimes = namedtuple('pcputimes', + ['user', 'system', 'children_user', 'children_system']) +# psutil.Process.memory_maps(grouped=True) +pmmap_grouped = namedtuple( + 'pmmap_grouped', 'path rss, private, ref_count, shadow_count') +# psutil.Process.memory_maps(grouped=False) +pmmap_ext = namedtuple( + 'pmmap_ext', 'addr, perms path rss, private, ref_count, shadow_count') +# psutil.disk_io_counters() +if FREEBSD: + sdiskio = namedtuple('sdiskio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes', + 'read_time', 'write_time', + 'busy_time']) +else: + sdiskio = namedtuple('sdiskio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes']) +# fmt: on + + +# ===================================================================== +# --- memory +# ===================================================================== + + +def virtual_memory(): + mem = cext.virtual_mem() + if NETBSD: + total, free, active, inactive, wired, cached = mem + # On NetBSD buffers and shared mem is determined via /proc. + # The C ext set them to 0. + with open('/proc/meminfo', 'rb') as f: + for line in f: + if line.startswith(b'Buffers:'): + buffers = int(line.split()[1]) * 1024 + elif line.startswith(b'MemShared:'): + shared = int(line.split()[1]) * 1024 + # Before avail was calculated as (inactive + cached + free), + # same as zabbix, but it turned out it could exceed total (see + # #2233), so zabbix seems to be wrong. Htop calculates it + # differently, and the used value seem more realistic, so let's + # match htop. + # https://github.com/htop-dev/htop/blob/e7f447b/netbsd/NetBSDProcessList.c#L162 + # https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/netbsd/memory.c#L135 + used = active + wired + avail = total - used + else: + total, free, active, inactive, wired, cached, buffers, shared = mem + # matches freebsd-memory CLI: + # * https://people.freebsd.org/~rse/dist/freebsd-memory + # * https://www.cyberciti.biz/files/scripts/freebsd-memory.pl.txt + # matches zabbix: + # * https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/freebsd/memory.c#L143 + avail = inactive + cached + free + used = active + wired + cached + + percent = usage_percent((total - avail), total, round_=1) + return svmem( + total, + avail, + percent, + used, + free, + active, + inactive, + buffers, + cached, + shared, + wired, + ) + + +def swap_memory(): + """System swap memory as (total, used, free, sin, sout) namedtuple.""" + total, used, free, sin, sout = cext.swap_mem() + percent = usage_percent(used, total, round_=1) + return _common.sswap(total, used, free, percent, sin, sout) + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +def cpu_times(): + """Return system per-CPU times as a namedtuple.""" + user, nice, system, idle, irq = cext.cpu_times() + return scputimes(user, nice, system, idle, irq) + + +if HAS_PER_CPU_TIMES: + + def per_cpu_times(): + """Return system CPU times as a namedtuple.""" + ret = [] + for cpu_t in cext.per_cpu_times(): + user, nice, system, idle, irq = cpu_t + item = scputimes(user, nice, system, idle, irq) + ret.append(item) + return ret + +else: + # XXX + # Ok, this is very dirty. + # On FreeBSD < 8 we cannot gather per-cpu information, see: + # https://github.com/giampaolo/psutil/issues/226 + # If num cpus > 1, on first call we return single cpu times to avoid a + # crash at psutil import time. + # Next calls will fail with NotImplementedError + def per_cpu_times(): + """Return system CPU times as a namedtuple.""" + if cpu_count_logical() == 1: + return [cpu_times()] + if per_cpu_times.__called__: + msg = "supported only starting from FreeBSD 8" + raise NotImplementedError(msg) + per_cpu_times.__called__ = True + return [cpu_times()] + + per_cpu_times.__called__ = False + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + return cext.cpu_count_logical() + + +if OPENBSD or NETBSD: + + def cpu_count_cores(): + # OpenBSD and NetBSD do not implement this. + return 1 if cpu_count_logical() == 1 else None + +else: + + def cpu_count_cores(): + """Return the number of CPU cores in the system.""" + # From the C module we'll get an XML string similar to this: + # http://manpages.ubuntu.com/manpages/precise/man4/smp.4freebsd.html + # We may get None in case "sysctl kern.sched.topology_spec" + # is not supported on this BSD version, in which case we'll mimic + # os.cpu_count() and return None. + ret = None + s = cext.cpu_topology() + if s is not None: + # get rid of padding chars appended at the end of the string + index = s.rfind("") + if index != -1: + s = s[: index + 9] + root = ElementTree.fromstring(s) + try: + ret = len(root.findall('group/children/group/cpu')) or None + finally: + # needed otherwise it will memleak + root.clear() + if not ret: + # If logical CPUs == 1 it's obvious we' have only 1 core. + if cpu_count_logical() == 1: + return 1 + return ret + + +def cpu_stats(): + """Return various CPU stats as a named tuple.""" + 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 (?). + # + # 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() + ) + 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) + + +if FREEBSD: + + def cpu_freq(): + """Return frequency metrics for CPUs. As of Dec 2018 only + CPU 0 appears to be supported by FreeBSD and all other cores + match the frequency of CPU 0. + """ + ret = [] + num_cpus = cpu_count_logical() + for cpu in range(num_cpus): + try: + current, available_freq = cext.cpu_freq(cpu) + except NotImplementedError: + continue + if available_freq: + try: + min_freq = int(available_freq.split(" ")[-1].split("/")[0]) + except (IndexError, ValueError): + min_freq = None + try: + max_freq = int(available_freq.split(" ")[0].split("/")[0]) + except (IndexError, ValueError): + max_freq = None + ret.append(_common.scpufreq(current, min_freq, max_freq)) + return ret + +elif OPENBSD: + + def cpu_freq(): + curr = float(cext.cpu_freq()) + return [_common.scpufreq(curr, 0.0, 0.0)] + + +# ===================================================================== +# --- disks +# ===================================================================== + + +def disk_partitions(all=False): + """Return mounted disk partitions as a list of namedtuples. + 'all' argument is ignored, see: + https://github.com/giampaolo/psutil/issues/906. + """ + retlist = [] + partitions = cext.disk_partitions() + for partition in partitions: + device, mountpoint, fstype, opts = partition + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + retlist.append(ntuple) + return retlist + + +disk_usage = _psposix.disk_usage +disk_io_counters = cext.disk_io_counters + + +# ===================================================================== +# --- network +# ===================================================================== + + +net_io_counters = cext.net_io_counters +net_if_addrs = cext_posix.net_if_addrs + + +def net_if_stats(): + """Get NIC stats (isup, duplex, speed, mtu).""" + names = net_io_counters().keys() + ret = {} + for name in names: + try: + mtu = cext_posix.net_if_mtu(name) + flags = cext_posix.net_if_flags(name) + duplex, speed = cext_posix.net_if_duplex_speed(name) + except OSError as err: + # https://github.com/giampaolo/psutil/issues/1279 + if err.errno != errno.ENODEV: + raise + else: + if hasattr(_common, 'NicDuplex'): + duplex = _common.NicDuplex(duplex) + output_flags = ','.join(flags) + isup = 'running' in flags + ret[name] = _common.snicstats( + isup, duplex, speed, mtu, output_flags + ) + return ret + + +def net_connections(kind): + """System-wide network connections.""" + families, types = conn_tmap[kind] + ret = set() + 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 + nt = conn_to_ntuple( + fd, fam, type, laddr, raddr, status, TCP_STATUSES, pid + ) + ret.add(nt) + return list(ret) + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +if FREEBSD: + + def sensors_battery(): + """Return battery info.""" + try: + percent, minsleft, power_plugged = cext.sensors_battery() + except NotImplementedError: + # See: https://github.com/giampaolo/psutil/issues/1074 + return None + power_plugged = power_plugged == 1 + if power_plugged: + secsleft = _common.POWER_TIME_UNLIMITED + elif minsleft == -1: + secsleft = _common.POWER_TIME_UNKNOWN + else: + secsleft = minsleft * 60 + return _common.sbattery(percent, secsleft, power_plugged) + + def sensors_temperatures(): + """Return CPU cores temperatures if available, else an empty dict.""" + ret = defaultdict(list) + num_cpus = cpu_count_logical() + for cpu in range(num_cpus): + try: + current, high = cext.sensors_cpu_temperature(cpu) + if high <= 0: + high = None + name = f"Core {cpu}" + ret["coretemp"].append( + _common.shwtemp(name, current, high, high) + ) + except NotImplementedError: + pass + + return ret + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +def boot_time(): + """The system boot time expressed in seconds since the epoch.""" + return cext.boot_time() + + +def users(): + """Return currently connected users as a list of namedtuples.""" + 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) + retlist.append(nt) + return retlist + + +# ===================================================================== +# --- processes +# ===================================================================== + + +@memoize +def _pid_0_exists(): + try: + Process(0).name() + except NoSuchProcess: + return False + except AccessDenied: + return True + else: + return True + + +def pids(): + """Returns a list of PIDs currently running on the system.""" + ret = cext.pids() + if OPENBSD and (0 not in ret) and _pid_0_exists(): + # On OpenBSD the kernel does not return PID 0 (neither does + # ps) but it's actually querable (Process(0) will succeed). + ret.insert(0, 0) + return ret + + +if NETBSD: + + def pid_exists(pid): + exists = _psposix.pid_exists(pid) + if not exists: + # We do this because _psposix.pid_exists() lies in case of + # zombie processes. + return pid in pids() + else: + return True + +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 + + +def is_zombie(pid): + try: + st = cext.proc_oneshot_info(pid)[kinfo_proc_map['status']] + return PROC_STATUSES.get(st) == _common.STATUS_ZOMBIE + except OSError: + return False + + +def wrap_exceptions(fun): + """Decorator which translates bare OSError exceptions into + NoSuchProcess and AccessDenied. + """ + + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + pid, ppid, name = self.pid, self._ppid, self._name + try: + return fun(self, *args, **kwargs) + except ProcessLookupError as err: + if 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 OSError as err: + if pid == 0 and 0 in pids(): + raise AccessDenied(pid, name) from err + raise + + 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 (ProcessLookupError, FileNotFoundError) as err: + # ENOENT (no such file or directory) gets raised on open(). + # ESRCH (no such process) can get raised on read() if + # process is gone in meantime. + if is_zombie(inst.pid): + raise ZombieProcess(pid, name, ppid) from err + else: + raise NoSuchProcess(pid, name) from err + except PermissionError as err: + raise AccessDenied(pid, name) from err + + +class Process: + """Wrapper class around underlying C implementation.""" + + __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 + + def oneshot_enter(self): + self.oneshot.cache_activate(self) + + def oneshot_exit(self): + self.oneshot.cache_deactivate(self) + + @wrap_exceptions + def name(self): + name = self.oneshot()[kinfo_proc_map['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(f"/proc/{self.pid}/exe") + else: + # OpenBSD: exe cannot be determined; references: + # 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 shutil.which(cmdline[0]) or "" + else: + return "" + + @wrap_exceptions + 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 + # 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: + pid, name, ppid = self.pid, self._name, self._ppid + if is_zombie(self.pid): + raise ZombieProcess(pid, name, ppid) from err + if not pid_exists(self.pid): + raise NoSuchProcess(pid, name, ppid) from err + # XXX: this happens with unicode tests. It means the C + # routine is unable to decode invalid unicode chars. + debug(f"ignoring {err!r} and returning an empty list") + return [] + else: + raise + else: + 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']] + tmap = _psposix.get_terminal_map() + try: + return tmap[tty_nr] + except KeyError: + return None + + @wrap_exceptions + def ppid(self): + self._ppid = self.oneshot()[kinfo_proc_map['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']], + ) + + @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']], + ) + + @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']], + ) + + if FREEBSD: + + @wrap_exceptions + def cpu_num(self): + return self.oneshot()[kinfo_proc_map['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 + + @wrap_exceptions + def create_time(self): + return self.oneshot()[kinfo_proc_map['create_time']] + + @wrap_exceptions + def num_threads(self): + if HAS_PROC_NUM_THREADS: + # FreeBSD + 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']], + ) + + @wrap_exceptions + def threads(self): + # Note: on OpenSBD this (/dev/mem) requires root access. + rawlist = cext.proc_threads(self.pid) + retlist = [] + for thread_id, utime, stime in rawlist: + ntuple = _common.pthread(thread_id, utime, stime) + retlist.append(ntuple) + if OPENBSD: + self._assert_alive() + return retlist + + @wrap_exceptions + def net_connections(self, kind='inet'): + families, types = conn_tmap[kind] + ret = [] + + if NETBSD: + 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) + + for item in rawlist: + 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) + + self._assert_alive() + return ret + + @wrap_exceptions + def wait(self, timeout=None): + return _psposix.wait_pid(self.pid, timeout, self._name) + + @wrap_exceptions + def nice_get(self): + return cext_posix.getpriority(self.pid) + + @wrap_exceptions + def nice_set(self, value): + return cext_posix.setpriority(self.pid, value) + + @wrap_exceptions + def status(self): + code = self.oneshot()[kinfo_proc_map['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, + ) + + @wrap_exceptions + def cwd(self): + """Return process current working directory.""" + # sometimes we get an empty string, in which case we turn + # it into None + if OPENBSD and self.pid == 0: + return "" # ...else it would raise EINVAL + elif NETBSD or HAS_PROC_OPEN_FILES: + # FreeBSD < 8 does not support functions based on + # kinfo_getfile() and kinfo_getvmmap() + return cext.proc_cwd(self.pid) + else: + raise NotImplementedError( + "supported only starting from FreeBSD 8" if FREEBSD else "" + ) + + nt_mmap_grouped = namedtuple( + '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 + + # FreeBSD < 8 does not support functions based on kinfo_getfile() + # and kinfo_getvmmap() + if HAS_PROC_OPEN_FILES: + + @wrap_exceptions + def open_files(self): + """Return files opened by process as a list of namedtuples.""" + rawlist = cext.proc_open_files(self.pid) + return [_common.popenfile(path, fd) for path, fd in rawlist] + + else: + open_files = _not_implemented + + # FreeBSD < 8 does not support functions based on kinfo_getfile() + # and kinfo_getvmmap() + if HAS_PROC_NUM_FDS: + + @wrap_exceptions + def num_fds(self): + """Return the number of file descriptors opened by this process.""" + ret = cext.proc_num_fds(self.pid) + if NETBSD: + self._assert_alive() + return ret + + else: + num_fds = _not_implemented + + # --- FreeBSD only APIs + + if FREEBSD: + + @wrap_exceptions + def cpu_affinity_get(self): + return cext.proc_cpu_affinity_get(self.pid) + + @wrap_exceptions + def cpu_affinity_set(self, cpus): + # Pre-emptively check if CPUs are valid because the C + # function has a weird behavior in case of invalid CPUs, + # see: https://github.com/giampaolo/psutil/issues/586 + allcpus = set(range(len(per_cpu_times()))) + for cpu in cpus: + if cpu not in 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: + # 'man cpuset_setaffinity' about EDEADLK: + # <> + if err.errno in {errno.EINVAL, errno.EDEADLK}: + for cpu in cpus: + if cpu not in 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/.venv/lib/python3.12/site-packages/psutil/_pslinux.py b/.venv/lib/python3.12/site-packages/psutil/_pslinux.py new file mode 100644 index 0000000..8cc64e9 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/_pslinux.py @@ -0,0 +1,2295 @@ +# Copyright (c) 2009, Giampaolo 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 platform implementation.""" + + +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 warnings +from collections import defaultdict +from collections import namedtuple + +from . import _common +from . import _psposix +from . import _psutil_linux as cext +from . import _psutil_posix as cext_posix +from ._common import ENCODING +from ._common import NIC_DUPLEX_FULL +from ._common import NIC_DUPLEX_HALF +from ._common import NIC_DUPLEX_UNKNOWN +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import bcat +from ._common import cat +from ._common import debug +from ._common import decode +from ._common import get_procfs_path +from ._common import isfile_strict +from ._common import memoize +from ._common import memoize_when_activated +from ._common import open_binary +from ._common import open_text +from ._common import parse_environ_block +from ._common import path_exists_strict +from ._common import supports_ipv6 +from ._common import usage_percent + + +# fmt: off +__extra__all__ = [ + 'PROCFS_PATH', + # io prio constants + "IOPRIO_CLASS_NONE", "IOPRIO_CLASS_RT", "IOPRIO_CLASS_BE", + "IOPRIO_CLASS_IDLE", + # connection status constants + "CONN_ESTABLISHED", "CONN_SYN_SENT", "CONN_SYN_RECV", "CONN_FIN_WAIT1", + "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", + "CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING", +] + +if hasattr(resource, "prlimit"): + __extra__all__.extend( + [x for x in dir(cext) if x.startswith('RLIM') and x.isupper()] + ) +# fmt: on + + +# ===================================================================== +# --- globals +# ===================================================================== + + +POWER_SUPPLY_PATH = "/sys/class/power_supply" +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 = cext_posix.getpagesize() +BOOT_TIME = None # set later +LITTLE_ENDIAN = sys.byteorder == 'little' + +# "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 + + +# ioprio_* constants http://linux.die.net/man/2/ioprio_get +class IOPriority(enum.IntEnum): + IOPRIO_CLASS_NONE = 0 + IOPRIO_CLASS_RT = 1 + IOPRIO_CLASS_BE = 2 + IOPRIO_CLASS_IDLE = 3 + + +globals().update(IOPriority.__members__) + +# See: +# https://github.com/torvalds/linux/blame/master/fs/proc/array.c +# ...and (TASK_* constants): +# https://github.com/torvalds/linux/blob/master/include/linux/sched.h +PROC_STATUSES = { + "R": _common.STATUS_RUNNING, + "S": _common.STATUS_SLEEPING, + "D": _common.STATUS_DISK_SLEEP, + "T": _common.STATUS_STOPPED, + "t": _common.STATUS_TRACING_STOP, + "Z": _common.STATUS_ZOMBIE, + "X": _common.STATUS_DEAD, + "x": _common.STATUS_DEAD, + "K": _common.STATUS_WAKE_KILL, + "W": _common.STATUS_WAKING, + "I": _common.STATUS_IDLE, + "P": _common.STATUS_PARKED, +} + +# 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, +} + + +# ===================================================================== +# --- named tuples +# ===================================================================== + + +# fmt: off +# psutil.virtual_memory() +svmem = namedtuple( + 'svmem', ['total', 'available', 'percent', 'used', 'free', + 'active', 'inactive', 'buffers', 'cached', 'shared', 'slab']) +# psutil.disk_io_counters() +sdiskio = namedtuple( + 'sdiskio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes', + 'read_time', 'write_time', + 'read_merged_count', 'write_merged_count', + 'busy_time']) +# psutil.Process().open_files() +popenfile = namedtuple( + 'popenfile', ['path', 'fd', 'position', 'mode', 'flags']) +# psutil.Process().memory_info() +pmem = namedtuple('pmem', 'rss vms shared text lib data dirty') +# psutil.Process().memory_full_info() +pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', 'pss', 'swap')) +# psutil.Process().memory_maps(grouped=True) +pmmap_grouped = namedtuple( + 'pmmap_grouped', + ['path', 'rss', 'size', 'pss', 'shared_clean', 'shared_dirty', + 'private_clean', 'private_dirty', 'referenced', 'anonymous', 'swap']) +# psutil.Process().memory_maps(grouped=False) +pmmap_ext = namedtuple( + 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) +# psutil.Process.io_counters() +pio = namedtuple('pio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes', + 'read_chars', 'write_chars']) +# psutil.Process.cpu_times() +pcputimes = namedtuple('pcputimes', + ['user', 'system', 'children_user', 'children_system', + 'iowait']) +# fmt: on + + +# ===================================================================== +# --- utils +# ===================================================================== + + +def readlink(path): + """Wrapper around os.readlink().""" + 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 + # bytes, not str" errors when the string is passed to other + # fs-related functions (os.*, open(), ...). + # Apparently everything after '\x00' is garbage (we can have + # ' (deleted)', 'new' and possibly others), see: + # https://github.com/giampaolo/psutil/issues/717 + path = path.split('\x00')[0] + # Certain paths have ' (deleted)' appended. Usually this is + # bogus as the file actually exists. Even if it doesn't we + # don't care. + if path.endswith(' (deleted)') and not path_exists_strict(path): + path = path[:-10] + return path + + +def file_flags_to_mode(flags): + """Convert file's open() flags into a readable string. + Used by Process.open_files(). + """ + modes_map = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'} + mode = modes_map[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)] + if flags & os.O_APPEND: + mode = mode.replace('w', 'a', 1) + mode = mode.replace('w+', 'r+') + # possible values: r, w, a, r+, a+ + return mode + + +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. + """ + # Re-adapted from iostat source code, see: + # https://github.com/sysstat/sysstat/blob/ + # 97912938cd476645b267280069e83b1c8dc0e1c7/common.c#L208 + # Some devices may have a slash in their name (e.g. cciss/c0d0...). + 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) + + +@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(f"{procfs_path}/stat") 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) + + +try: + set_scputimes_ntuple("/proc") +except Exception as err: # noqa: BLE001 + # Don't want to crash at import time. + debug(f"ignoring exception on import: {err!r}") + scputimes = namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0) + + +# ===================================================================== +# --- system memory +# ===================================================================== + + +def calculate_avail_vmem(mems): + """Fallback for kernels < 3.14 where /proc/meminfo does not provide + "MemAvailable", see: + https://blog.famzah.net/2014/09/24/. + + This code reimplements the algorithm outlined here: + https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/ + commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773 + + 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 + """ + # Note about "fallback" value. According to: + # https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/ + # commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773 + # ...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 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(f"{get_procfs_path()}/zoneinfo") + except OSError: + return fallback # kernel 2.6.13 + + watermark_low = 0 + with f: + for line in f: + line = line.strip() + if line.startswith(b'low'): + watermark_low += int(line.split()[1]) + watermark_low *= PAGESIZE + + avail = free - watermark_low + pagecache = lru_active_file + lru_inactive_file + pagecache -= min(pagecache / 2, watermark_low) + avail += pagecache + avail += slab_reclaimable - min(slab_reclaimable / 2.0, watermark_low) + return int(avail) + + +def virtual_memory(): + """Report virtual memory stats. + This implementation mimics procps-ng-3.3.12, aka "free" CLI tool: + https://gitlab.com/procps-ng/procps/blob/ + 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c#L778-791 + The returned values are supposed to match both "free" and "vmstat -s" + CLI tools. + """ + missing_fields = [] + mems = {} + with open_binary(f"{get_procfs_path()}/meminfo") as f: + for line in f: + fields = line.split() + mems[fields[0]] = int(fields[1]) * 1024 + + # /proc doc states that the available fields in /proc/meminfo vary + # by architecture and compile options, but these 3 values are also + # returned by sysinfo(2); as such we assume they are always there. + total = mems[b'MemTotal:'] + free = mems[b'MemFree:'] + try: + buffers = mems[b'Buffers:'] + except KeyError: + # https://github.com/giampaolo/psutil/issues/1010 + buffers = 0 + missing_fields.append('buffers') + try: + cached = mems[b"Cached:"] + except KeyError: + cached = 0 + missing_fields.append('cached') + else: + # "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 + cached += mems.get(b"SReclaimable:", 0) # since kernel 2.6.19 + + try: + shared = mems[b'Shmem:'] # since kernel 2.6.32 + except KeyError: + try: + shared = mems[b'MemShared:'] # kernels 2.4 + except KeyError: + shared = 0 + missing_fields.append('shared') + + try: + active = mems[b"Active:"] + except KeyError: + active = 0 + missing_fields.append('active') + + try: + inactive = mems[b"Inactive:"] + except KeyError: + try: + inactive = ( + mems[b"Inact_dirty:"] + + mems[b"Inact_clean:"] + + mems[b"Inact_laundry:"] + ) + except KeyError: + inactive = 0 + missing_fields.append('inactive') + + try: + slab = mems[b"Slab:"] + 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. + # - free and htop available memory differs as per: + # http://askubuntu.com/a/369589 + # http://unix.stackexchange.com/a/65852/168884 + # - MemAvailable has been introduced in kernel 3.14 + try: + 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') + elif avail > total: + # If avail is greater than total or our calculation overflows, + # that's symptomatic of running within a LCX container where such + # values will be dramatically distorted over those of the host. + # https://gitlab.com/procps-ng/procps/blob/ + # 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c#L764 + avail = free + + percent = usage_percent((total - avail), total, round_=1) + + # Warn about missing metrics which are set to 0. + if missing_fields: + 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, stacklevel=2) + + return svmem( + total, + avail, + percent, + used, + free, + active, + inactive, + buffers, + cached, + shared, + slab, + ) + + +def swap_memory(): + """Return swap memory metrics.""" + mems = {} + with open_binary(f"{get_procfs_path()}/meminfo") as f: + for line in f: + fields = line.split() + mems[fields[0]] = int(fields[1]) * 1024 + # We prefer /proc/meminfo over sysinfo() syscall so that + # psutil.PROCFS_PATH can be used in order to allow retrieval + # for linux containers, see: + # https://github.com/giampaolo/psutil/issues/1015 + try: + total = mems[b'SwapTotal:'] + free = mems[b'SwapFree:'] + except KeyError: + _, _, _, _, total, free, unit_multiplier = cext.linux_sysinfo() + total *= unit_multiplier + free *= unit_multiplier + + used = total - free + percent = usage_percent(used, total, round_=1) + # get pgin/pgouts + try: + 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 " + f"be determined and were set to 0 ({err})" + ) + warnings.warn(msg, RuntimeWarning, stacklevel=2) + sin = sout = 0 + else: + with f: + sin = sout = None + for line in f: + # values are expressed in 4 kilo bytes, we want + # bytes instead + if line.startswith(b'pswpin'): + sin = int(line.split(b' ')[1]) * 4 * 1024 + elif line.startswith(b'pswpout'): + sout = int(line.split(b' ')[1]) * 4 * 1024 + if sin is not None and sout is not None: + break + else: + # 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 " + 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) + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +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. + """ + procfs_path = get_procfs_path() + set_scputimes_ntuple(procfs_path) + with open_binary(f"{procfs_path}/stat") as f: + values = f.readline().split() + fields = values[1 : len(scputimes._fields) + 1] + fields = [float(x) / CLOCK_TICKS for x in fields] + return scputimes(*fields) + + +def per_cpu_times(): + """Return a list of namedtuple 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(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) + cpus.append(entry) + return cpus + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + try: + return os.sysconf("SC_NPROCESSORS_ONLN") + except ValueError: + # as a second fallback we try to parse /proc/cpuinfo + num = 0 + with open_binary(f"{get_procfs_path()}/cpuinfo") as f: + for line in f: + if line.lower().startswith(b'processor'): + num += 1 + + # unknown format (e.g. amrel/sparc architectures), see: + # https://github.com/giampaolo/psutil/issues/200 + # try to parse /proc/stat as a last resort + if num == 0: + search = re.compile(r'cpu\d') + with open_text(f"{get_procfs_path()}/stat") as f: + for line in f: + line = line.split(' ')[0] + if search.match(line): + num += 1 + + if num == 0: + # mimic os.cpu_count() + return None + return num + + +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(f"{get_procfs_path()}/cpuinfo") as f: + for line in f: + line = line.strip().lower() + if not line: + # new section + try: + mapping[current_info[b'physical id']] = current_info[ + b'cpu cores' + ] + except KeyError: + pass + current_info = {} + elif line.startswith((b'physical id', b'cpu cores')): + # ongoing section + key, value = line.split(b'\t:', 1) + current_info[key] = int(value) + + 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(f"{get_procfs_path()}/stat") as f: + ctx_switches = None + interrupts = None + soft_interrupts = None + for line in f: + if line.startswith(b'ctxt'): + ctx_switches = int(line.split()[1]) + elif line.startswith(b'intr'): + 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 + ): + break + syscalls = 0 + return _common.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" +): + + def cpu_freq(): + """Return frequency metrics for all CPUs. + Contrarily to other OSes, Linux updates these values in + real-time. + """ + cpuinfo_freqs = _cpu_get_cpuinfo_freq() + paths = glob.glob( + "/sys/devices/system/cpu/cpufreq/policy[0-9]*" + ) or glob.glob("/sys/devices/system/cpu/cpu[0-9]*/cpufreq") + paths.sort(key=lambda x: int(re.search(r"[0-9]+", x).group())) + ret = [] + pjoin = os.path.join + 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 = bcat(pjoin(path, "cpuinfo_cur_freq"), fallback=None) + if curr is None: + online_path = f"/sys/devices/system/cpu/cpu{i}/online" + # if cpu core is offline, set to all zeroes + if cat(online_path, fallback=None) == "0\n": + ret.append(_common.scpufreq(0.0, 0.0, 0.0)) + continue + msg = "can't find current frequency file" + raise NotImplementedError(msg) + curr = int(curr) / 1000 + max_ = int(bcat(pjoin(path, "scaling_max_freq"))) / 1000 + min_ = int(bcat(pjoin(path, "scaling_min_freq"))) / 1000 + ret.append(_common.scpufreq(curr, min_, max_)) + return ret + +else: + + def cpu_freq(): + """Alternate implementation using /proc/cpuinfo. + min and max frequencies are not available and are set to None. + """ + return [_common.scpufreq(x, 0.0, 0.0) for x in _cpu_get_cpuinfo_freq()] + + +# ===================================================================== +# --- network +# ===================================================================== + + +net_if_addrs = cext_posix.net_if_addrs + + +class _Ipv6UnsupportedError(Exception): + pass + + +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". + + Note: in case of UNIX sockets we're only able to determine the + local endpoint/path, not the one it's connected to. + According to [1] it would be possible but not easily. + + [1] http://serverfault.com/a/417946 + """ + + 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) + udp6 = ("udp6", socket.AF_INET6, socket.SOCK_DGRAM) + unix = ("unix", socket.AF_UNIX, None) + self.tmap = { + "all": (tcp4, tcp6, udp4, udp6, unix), + "tcp": (tcp4, tcp6), + "tcp4": (tcp4,), + "tcp6": (tcp6,), + "udp": (udp4, udp6), + "udp4": (udp4,), + "udp6": (udp6,), + "unix": (unix,), + "inet": (tcp4, tcp6, udp4, udp6), + "inet4": (tcp4, udp4), + "inet6": (tcp6, udp6), + } + self._procfs_path = None + + def get_proc_inodes(self, pid): + inodes = defaultdict(list) + for fd in os.listdir(f"{self._procfs_path}/{pid}/fd"): + try: + inode = readlink(f"{self._procfs_path}/{pid}/fd/{fd}") + except (FileNotFoundError, ProcessLookupError): + # ENOENT == file which is gone in the meantime; + # os.stat(f"/proc/{self.pid}") will be done later + # to force NSP (if it's the case) + continue + except OSError as err: + if err.errno == errno.EINVAL: + # not a link + continue + if err.errno == errno.ENAMETOOLONG: + # file name too long + debug(err) + continue + raise + else: + if inode.startswith('socket:['): + # the process is using a socket + inode = inode[8:][:-1] + inodes[inode].append((pid, int(fd))) + return inodes + + def get_all_inodes(self): + inodes = {} + for pid in pids(): + try: + inodes.update(self.get_proc_inodes(pid)) + 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 + # and fd set to None anyway. + # 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. + continue + return inodes + + @staticmethod + def decode_address(addr, family): + """Accept an "ip:port" address as displayed in /proc/net/* + and convert it into a human readable form, like: + + "0500000A:0016" -> ("10.0.0.5", 22) + "0000000000000000FFFF00000100007F:9E49" -> ("::ffff:127.0.0.1", 40521) + + The IP address portion is a little or big endian four-byte + hexadecimal number; that is, the least significant byte is listed + first, so we need to reverse the order of the bytes to convert it + to an IP address. + The port is represented as a two-byte hexadecimal number. + + Reference: + http://linuxdevcenter.com/pub/a/linux/2000/11/16/LinuxAdmin.html + """ + ip, port = addr.split(':') + port = int(port, 16) + # this usually refers to a local socket in listen mode with + # no end-points connected + if not port: + return () + ip = ip.encode('ascii') + if family == socket.AF_INET: + # see: https://github.com/giampaolo/psutil/issues/201 + if LITTLE_ENDIAN: + ip = socket.inet_ntop(family, base64.b16decode(ip)[::-1]) + else: + ip = socket.inet_ntop(family, base64.b16decode(ip)) + else: # IPv6 + 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)), + ) + else: + ip = socket.inet_ntop( + socket.AF_INET6, + struct.pack('<4I', *struct.unpack('<4I', ip)), + ) + except ValueError: + # see: https://github.com/giampaolo/psutil/issues/623 + if not supports_ipv6(): + raise _Ipv6UnsupportedError from None + raise + return _common.addr(ip, port) + + @staticmethod + def process_inet(file, family, type_, inodes, filter_pid=None): + """Parse /proc/net/tcp* and /proc/net/udp* files.""" + if file.endswith('6') and not os.path.exists(file): + # IPv6 not supported + return + with open_text(file) as f: + f.readline() # skip the first line + for lineno, line in enumerate(f, 1): + try: + _, laddr, raddr, status, _, _, _, _, _, inode = ( + line.split()[:10] + ) + except ValueError: + 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("ambiguous inode with multiple " + # "PIDs references") + pid, fd = inodes[inode][0] + else: + pid, fd = None, -1 + if filter_pid is not None and filter_pid != pid: + continue + else: + if type_ == socket.SOCK_STREAM: + status = TCP_STATUSES[status] + else: + status = _common.CONN_NONE + try: + laddr = NetConnections.decode_address(laddr, family) + raddr = NetConnections.decode_address(raddr, family) + except _Ipv6UnsupportedError: + continue + yield (fd, family, type_, laddr, raddr, status, pid) + + @staticmethod + def process_unix(file, family, inodes, filter_pid=None): + """Parse /proc/net/unix files.""" + with open_text(file) as f: + f.readline() # skip the first line + for line in f: + tokens = line.split() + try: + _, _, _, _, type_, _, inode = tokens[0:7] + except ValueError: + if ' ' not in line: + # see: https://github.com/giampaolo/psutil/issues/766 + continue + 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] + else: + pairs = [(None, -1)] + for pid, fd in pairs: + if filter_pid is not None and filter_pid != pid: + continue + else: + path = tokens[-1] if len(tokens) == 8 else '' + type_ = _common.socktype_to_enum(int(type_)) + # XXX: determining the remote endpoint of a + # UNIX socket on Linux is not possible, see: + # https://serverfault.com/questions/252723/ + raddr = "" + status = _common.CONN_NONE + yield (fd, family, type_, path, raddr, status, pid) + + def retrieve(self, kind, pid=None): + self._procfs_path = get_procfs_path() + if pid is not None: + inodes = self.get_proc_inodes(pid) + if not inodes: + # no connections for this process + return [] + else: + inodes = self.get_all_inodes() + ret = set() + 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( + path, family, type_, inodes, filter_pid=pid + ) + else: + 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 + ) + else: + conn = _common.sconn( + fd, family, type_, laddr, raddr, status, bound_pid + ) + ret.add(conn) + return list(ret) + + +_net_connections = NetConnections() + + +def net_connections(kind='inet'): + """Return system-wide open connections.""" + 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(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, # 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, + } + names = net_io_counters().keys() + ret = {} + for name in names: + try: + mtu = cext_posix.net_if_mtu(name) + flags = cext_posix.net_if_flags(name) + duplex, speed = cext.net_if_duplex_speed(name) + except OSError as err: + # https://github.com/giampaolo/psutil/issues/1279 + if err.errno != errno.ENODEV: + raise + debug(err) + else: + output_flags = ','.join(flags) + isup = 'running' in flags + ret[name] = _common.snicstats( + isup, duplex_map[duplex], speed, mtu, output_flags + ) + return ret + + +# ===================================================================== +# --- disks +# ===================================================================== + + +disk_usage = _psposix.disk_usage + + +def disk_io_counters(perdisk=False): + """Return disk I/O statistics for every disk installed on the + system as a dict of raw tuples. + """ + + def read_procfs(): + # OK, this is a bit confusing. The format of /proc/diskstats can + # have 3 variations. + # On Linux 2.4 each line has always 15 fields, e.g.: + # "3 0 8 hda 8 8 8 8 8 8 8 8 8 8 8" + # On Linux 2.6+ each line *usually* has 14 fields, and the disk + # name is in another position, like this: + # "3 0 hda 8 8 8 8 8 8 8 8 8 8 8" + # ...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 + 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.""" + fstypes = set() + 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(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 not device or fstype not in fstypes: + continue + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + retlist.append(ntuple) + + return retlist + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +def sensors_temperatures(): + """Return hardware (CPU and others) temperatures as a dict + including hardware name, label, current, max and critical + temperatures. + + Implementation notes: + - /sys/class/hwmon looks like the most recent interface to + retrieve this info, and this implementation relies on it + only (old distros will probably use something else) + - lm-sensors on Ubuntu 16.04 relies on /sys/class/hwmon + - /sys/class/thermal/thermal_zone* is another one but it's more + difficult to parse + """ + ret = collections.defaultdict(list) + basenames = glob.glob('/sys/class/hwmon/hwmon*/temp*_*') + # CentOS has an intermediate /device directory: + # 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({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(bcat(path)) / 1000.0 + path = os.path.join(os.path.dirname(base), 'name') + unit_name = cat(path).strip() + except (OSError, ValueError): + # A lot of things can go wrong here, so let's just skip the + # whole entry. Sure thing is Linux's /sys/class/hwmon really + # is a stinky broken mess. + # https://github.com/giampaolo/psutil/issues/1009 + # https://github.com/giampaolo/psutil/issues/1101 + # https://github.com/giampaolo/psutil/issues/1129 + # https://github.com/giampaolo/psutil/issues/1245 + # https://github.com/giampaolo/psutil/issues/1323 + continue + + high = bcat(base + '_max', fallback=None) + critical = bcat(base + '_crit', fallback=None) + label = cat(base + '_label', fallback='').strip() + + 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((label, current, high, critical)) + + # Indication that no sensors were detected in /sys/class/hwmon/ + if not basenames: + basenames = glob.glob('/sys/class/thermal/thermal_zone*') + basenames = sorted(set(basenames)) + + for base in basenames: + try: + path = os.path.join(base, 'temp') + current = float(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(): + """Return hardware fans info (for CPU and other peripherals) as a + dict including hardware label and current speed. + + Implementation notes: + - /sys/class/hwmon looks like the most recent interface to + retrieve this info, and this implementation relies on it + only (old distros will probably use something else) + - lm-sensors on Ubuntu 16.04 relies on /sys/class/hwmon + """ + ret = collections.defaultdict(list) + basenames = glob.glob('/sys/class/hwmon/hwmon*/fan*_*') + if not basenames: + # CentOS has an intermediate /device directory: + # https://github.com/giampaolo/psutil/issues/971 + basenames = glob.glob('/sys/class/hwmon/hwmon*/device/fan*_*') + + basenames = sorted({x.split("_")[0] for x in basenames}) + for base in basenames: + try: + current = int(bcat(base + '_input')) + except OSError as err: + debug(err) + continue + unit_name = cat(os.path.join(os.path.dirname(base), 'name')).strip() + label = cat(base + '_label', fallback='').strip() + ret[unit_name].append(_common.sfan(label, current)) + + return dict(ret) + + +def sensors_battery(): + """Return battery information. + 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. + """ + null = object() + + 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 = bcat(path, fallback=null) + if ret != null: + 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') 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, min(bats)) + + # Base metrics. + energy_now = multi_bcat(root + "/energy_now", root + "/charge_now") + power_now = multi_bcat(root + "/power_now", root + "/current_now") + energy_full = multi_bcat(root + "/energy_full", root + "/charge_full") + 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 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)) + if percent == -1: + return None + + # Is AC power cable plugged in? + # Note: AC0 is not always available and sometimes (e.g. CentOS7) + # it's called "AC". + power_plugged = None + online = multi_bcat( + os.path.join(POWER_SUPPLY_PATH, "AC0/online"), + os.path.join(POWER_SUPPLY_PATH, "AC/online"), + ) + if online is not None: + power_plugged = online == 1 + else: + status = cat(root + "/status", fallback="").strip().lower() + if status == "discharging": + power_plugged = False + 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 + elif energy_now is not None and power_now is not None: + try: + secsleft = int(energy_now / power_now * 3600) + except ZeroDivisionError: + secsleft = _common.POWER_TIME_UNKNOWN + elif time_to_empty is not None: + secsleft = int(time_to_empty * 60) + if secsleft < 0: + secsleft = _common.POWER_TIME_UNKNOWN + else: + secsleft = _common.POWER_TIME_UNKNOWN + + return _common.sbattery(percent, secsleft, power_plugged) + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +def users(): + """Return currently connected users as a list of namedtuples.""" + retlist = [] + rawlist = cext.users() + for item in rawlist: + user, tty, hostname, tstamp, pid = item + nt = _common.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 = 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 + msg = f"line 'btime' not found in {path}" + raise RuntimeError(msg) + + +# ===================================================================== +# --- processes +# ===================================================================== + + +def pids(): + """Returns a list of PIDs currently running on the system.""" + path = get_procfs_path().encode(ENCODING) + return [int(x) for x in os.listdir(path) if x.isdigit()] + + +def pid_exists(pid): + """Check for the existence of a unix PID. Linux TIDs are not + supported (always return False). + """ + if not _psposix.pid_exists(pid): + return False + else: + # Linux's apparently does not distinguish between PIDs and TIDs + # (thread IDs). + # listdir("/proc") won't show any TID (only PIDs) but + # os.stat("/proc/{tid}") will succeed if {tid} exists. + # os.kill() can also be passed a TID. This is quite confusing. + # In here we want to enforce this distinction and support PIDs + # only, see: + # https://github.com/giampaolo/psutil/issues/687 + try: + # Note: already checked that this is faster than using a + # regular expr. Also (a lot) faster than doing + # 'return pid in pids()' + path = f"{get_procfs_path()}/{pid}/status" + with open_binary(path) as f: + for line in f: + if line.startswith(b"Tgid:"): + tgid = int(line.split()[1]) + # If tgid and pid are the same then we're + # dealing with a process PID. + return tgid == pid + msg = f"'Tgid' line not found in {path}" + raise ValueError(msg) + except (OSError, ValueError): + return pid in pids() + + +def ppid_map(): + """Obtain a {pid: ppid, ...} dict for all running processes in + one shot. Used to speed up Process.children(). + """ + ret = {} + procfs_path = get_procfs_path() + for pid in pids(): + try: + with open_binary(f"{procfs_path}/{pid}/stat") as f: + data = f.read() + except (FileNotFoundError, ProcessLookupError): + # Note: we should be able to access /stat for all processes + # aka it's unlikely we'll bump into EPERM, which is good. + pass + else: + rpar = data.rfind(b')') + dset = data[rpar + 2 :].split() + ppid = int(dset[1]) + ret[pid] = ppid + return ret + + +def wrap_exceptions(fun): + """Decorator which translates bare OSError and 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 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: + """Linux process implementation.""" + + __slots__ = ["_cache", "_name", "_ppid", "_procfs_path", "pid"] + + def __init__(self, pid): + self.pid = pid + self._name = None + self._ppid = None + self._procfs_path = get_procfs_path() + + def _is_zombie(self): + # Note: most of the times Linux is able to return info about the + # process even if it's a zombie, and /proc/{pid} will exist. + # There are some exceptions though, like exe(), cmdline() and + # memory_maps(). In these cases /proc/{pid}/{file} exists but + # it's empty. Instead of returning a "null" value we'll raise an + # exception. + try: + data = bcat(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}") + + @wrap_exceptions + @memoize_when_activated + def _parse_stat_file(self): + """Parse /proc/{pid}/stat file and return a dict with various + process info. + Using "man proc" as a reference: where "man proc" refers to + position N always 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. + """ + data = bcat(f"{self._procfs_path}/{self.pid}/stat") + # Process name is between parentheses. It can contain spaces and + # other parentheses. This is taken into account by looking for + # the first occurrence of "(" and the last occurrence of ")". + rpar = data.rfind(b')') + name = data[data.find(b'(') + 1 : rpar] + fields = data[rpar + 2 :].split() + + ret = {} + ret['name'] = name + ret['status'] = fields[0] + ret['ppid'] = fields[1] + ret['ttynr'] = fields[4] + ret['utime'] = fields[11] + ret['stime'] = fields[12] + ret['children_utime'] = fields[13] + ret['children_stime'] = fields[14] + ret['create_time'] = fields[19] + ret['cpu_num'] = fields[36] + 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(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(f"{self._procfs_path}/{self.pid}/smaps") as f: + return f.read().strip() + + def oneshot_enter(self): + 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) + self._read_status_file.cache_deactivate(self) + self._read_smaps_file.cache_deactivate(self) + + @wrap_exceptions + def name(self): + # XXX - gets changed later and probably needs refactoring + return decode(self._parse_stat_file()['name']) + + @wrap_exceptions + def exe(self): + try: + return readlink(f"{self._procfs_path}/{self.pid}/exe") + except (FileNotFoundError, ProcessLookupError): + self._raise_if_zombie() + # no such file error; might be raised also if the + # path actually exists for system processes with + # low pids (about 0-20) + if os.path.lexists(f"{self._procfs_path}/{self.pid}"): + return "" + raise + + @wrap_exceptions + def cmdline(self): + 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 + # some processes may change their cmdline after being started + # (via setproctitle() or similar), they are usually not + # compliant with this rule and use spaces instead. Google + # Chrome process is an example. See: + # https://github.com/giampaolo/psutil/issues/1179 + sep = '\x00' if data.endswith('\x00') else ' ' + if data.endswith(sep): + data = data[:-1] + cmdline = data.split(sep) + # Sometimes last char is a null byte '\0' but the args are + # separated by spaces, see: https://github.com/giampaolo/psutil/ + # issues/1179#issuecomment-552984549 + if sep == '\x00' and len(cmdline) == 1 and ' ' in data: + cmdline = data.split(' ') + return cmdline + + @wrap_exceptions + def environ(self): + 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()['ttynr']) + tmap = _psposix.get_terminal_map() + try: + return tmap[tty_nr] + except KeyError: + return None + + # May not be available on old kernels. + if os.path.exists(f"/proc/{os.getpid()}/io"): + + @wrap_exceptions + def io_counters(self): + 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: + try: + name, value = line.split(b': ') + except ValueError: + # https://github.com/giampaolo/psutil/issues/1004 + continue + else: + fields[name] = int(value) + if not fields: + msg = f"{fname} file was empty" + raise RuntimeError(msg) + try: + return pio( + fields[b'syscr'], # read syscalls + fields[b'syscw'], # write syscalls + fields[b'read_bytes'], # read bytes + fields[b'write_bytes'], # write bytes + fields[b'rchar'], # read chars + fields[b'wchar'], # write chars + ) + except KeyError as err: + 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['utime']) / CLOCK_TICKS + stime = float(values['stime']) / CLOCK_TICKS + children_utime = float(values['children_utime']) / CLOCK_TICKS + children_stime = float(values['children_stime']) / CLOCK_TICKS + iowait = float(values['blkio_ticks']) / CLOCK_TICKS + return pcputimes(utime, stime, children_utime, children_stime, iowait) + + @wrap_exceptions + def cpu_num(self): + """What CPU the process is on.""" + return int(self._parse_stat_file()['cpu_num']) + + @wrap_exceptions + def wait(self, timeout=None): + return _psposix.wait_pid(self.pid, timeout, self._name) + + @wrap_exceptions + def create_time(self): + ctime = float(self._parse_stat_file()['create_time']) + # According to documentation, starttime is in field 21 and the + # unit is jiffies (clock ticks). + # We first divide it for clock ticks and then add uptime returning + # seconds since the epoch. + # Also use cached value if available. + bt = BOOT_TIME or boot_time() + return (ctime / CLOCK_TICKS) + bt + + @wrap_exceptions + def memory_info(self): + # ============================================================ + # | FIELD | DESCRIPTION | AKA | TOP | + # ============================================================ + # | rss | resident set size | | RES | + # | vms | total program size | size | VIRT | + # | shared | shared pages (from shared mappings) | | SHR | + # | text | text ('code') | trs | CODE | + # | lib | library (unused in Linux 2.6) | lrs | | + # | data | data + stack | drs | DATA | + # | dirty | dirty pages (unused in Linux 2.6) | dt | | + # ============================================================ + with open_binary(f"{self._procfs_path}/{self.pid}/statm") as f: + vms, rss, shared, text, lib, data, dirty = ( + int(x) * PAGESIZE for x in f.readline().split()[:7] + ) + return pmem(rss, vms, shared, text, lib, data, dirty) + + if HAS_PROC_SMAPS_ROLLUP or HAS_PROC_SMAPS: + + 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 _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. + # + # You might be tempted to calculate USS by subtracting + # the "shared" value from the "resident" value in + # /proc//statm. But at least on Linux, statm's "shared" + # value actually counts pages backed by files, which has + # little to do with whether the pages are actually shared. + # /proc/self/smaps on the other hand appears to give us the + # correct information. + smaps_data = self._read_smaps_file() + # Note: smaps file can be empty for certain processes. + # The code below will not crash though and will result to 0. + uss = sum(map(int, _private_re.findall(smaps_data))) * 1024 + pss = sum(map(int, _pss_re.findall(smaps_data))) * 1024 + swap = sum(map(int, _swap_re.findall(smaps_data))) * 1024 + return (uss, pss, swap) + + @wrap_exceptions + def memory_full_info(self): + if HAS_PROC_SMAPS_ROLLUP: # faster + try: + uss, pss, swap = self._parse_smaps_rollup() + except (ProcessLookupError, FileNotFoundError): + uss, pss, swap = self._parse_smaps() + else: + uss, pss, swap = self._parse_smaps() + basic_mem = self.memory_info() + return pfullmem(*basic_mem + (uss, pss, swap)) + + else: + memory_full_info = memory_info + + if HAS_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: 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: + fields = line.split(None, 5) + if not fields[0].endswith(b':'): + # new block section + yield (current_block.pop(), data) + current_block.append(line) + else: + try: + data[fields[0]] = int(fields[1]) * 1024 + except ValueError: + if fields[0].startswith(b'VmFlags:'): + # see issue #369 + continue + 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 or for + # zombies. + if not data: + self._raise_if_zombie() + return [] + lines = data.split(b'\n') + ls = [] + first_line = lines.pop(0) + current_block = [first_line] + for header, data in get_blocks(lines, current_block): + hfields = header.split(None, 5) + try: + addr, perms, _offset, _dev, _inode, path = hfields + except ValueError: + addr, perms, _offset, _dev, _inode, path = hfields + [''] + if not path: + path = '[anon]' + else: + path = decode(path) + path = path.strip() + if path.endswith(' (deleted)') and not path_exists_strict( + path + ): + path = path[:-10] + 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), + data.get(b'Shared_Dirty:', 0), + data.get(b'Private_Clean:', 0), + data.get(b'Private_Dirty:', 0), + data.get(b'Referenced:', 0), + data.get(b'Anonymous:', 0), + data.get(b'Swap:', 0), + ) + ls.append(item) + return ls + + @wrap_exceptions + def cwd(self): + return readlink(f"{self._procfs_path}/{self.pid}/cwd") + + @wrap_exceptions + 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: + 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 _common.pctxsw(int(ctxsw[0]), int(ctxsw[1])) + + @wrap_exceptions + def num_threads(self, _num_threads_re=re.compile(br'Threads:\t(\d+)')): + # 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(f"{self._procfs_path}/{self.pid}/task") + thread_ids.sort() + retlist = [] + hit_enoent = False + for thread_id in thread_ids: + fname = f"{self._procfs_path}/{self.pid}/task/{thread_id}/stat" + try: + with open_binary(fname) as f: + st = f.read().strip() + 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 :] + values = st.split(b' ') + utime = float(values[11]) / CLOCK_TICKS + stime = float(values[12]) / CLOCK_TICKS + ntuple = _common.pthread(int(thread_id), utime, stime) + retlist.append(ntuple) + if hit_enoent: + self._raise_if_not_alive() + return retlist + + @wrap_exceptions + def nice_get(self): + # 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) + + @wrap_exceptions + def nice_set(self, value): + return cext_posix.setpriority(self.pid, value) + + # starting from CentOS 6. + if HAS_CPU_AFFINITY: + + @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: + 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 HAS_PROC_IO_PRIORITY: + + @wrap_exceptions + def ionice_get(self): + ioclass, value = cext.proc_ioprio_get(self.pid) + ioclass = IOPriority(ioclass) + return _common.pionice(ioclass, value) + + @wrap_exceptions + def ionice_set(self, ioclass, value): + if value is None: + value = 0 + if value and ioclass in { + IOPriority.IOPRIO_CLASS_IDLE, + IOPriority.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 hasattr(resource, "prlimit"): + + @wrap_exceptions + 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: + msg = "can't use prlimit() against PID 0 process" + raise ValueError(msg) + try: + if limits is None: + # get + return resource.prlimit(self.pid, resource_) + else: + # set + if len(limits) != 2: + 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: + # I saw this happening on Travis: + # https://travis-ci.org/giampaolo/psutil/jobs/51368273 + self._raise_if_zombie() + raise + + @wrap_exceptions + def status(self): + 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(f"{self._procfs_path}/{self.pid}/fd") + hit_enoent = False + for fd in files: + file = f"{self._procfs_path}/{self.pid}/fd/{fd}" + try: + path = readlink(file) + except (FileNotFoundError, ProcessLookupError): + # ENOENT == file which is gone in the meantime + hit_enoent = True + continue + except OSError as err: + if err.errno == errno.EINVAL: + # not a link + continue + 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. + # A regular file is always supposed to be have an + # absolute path though. + if path.startswith('/') and isfile_strict(path): + # Get file position and flags. + 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 (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 + ) + retlist.append(ntuple) + if hit_enoent: + self._raise_if_not_alive() + return retlist + + @wrap_exceptions + 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(f"{self._procfs_path}/{self.pid}/fd")) + + @wrap_exceptions + def ppid(self): + 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)) + + @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)) diff --git a/.venv/lib/python3.12/site-packages/psutil/_psosx.py b/.venv/lib/python3.12/site-packages/psutil/_psosx.py new file mode 100644 index 0000000..620497b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/_psosx.py @@ -0,0 +1,544 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""macOS platform implementation.""" + +import errno +import functools +import os +from collections import namedtuple + +from . import _common +from . import _psposix +from . import _psutil_osx as cext +from . import _psutil_posix as cext_posix +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import isfile_strict +from ._common import memoize_when_activated +from ._common import parse_environ_block +from ._common import usage_percent + + +__extra__all__ = [] + + +# ===================================================================== +# --- globals +# ===================================================================== + + +PAGESIZE = cext_posix.getpagesize() +AF_LINK = cext_posix.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, +} + +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, +} + +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 +# ===================================================================== + + +# fmt: off +# psutil.cpu_times() +scputimes = namedtuple('scputimes', ['user', 'nice', 'system', 'idle']) +# psutil.virtual_memory() +svmem = namedtuple( + 'svmem', ['total', 'available', 'percent', 'used', 'free', + 'active', 'inactive', 'wired']) +# psutil.Process.memory_info() +pmem = namedtuple('pmem', ['rss', 'vms', 'pfaults', 'pageins']) +# psutil.Process.memory_full_info() +pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', )) +# fmt: on + + +# ===================================================================== +# --- memory +# ===================================================================== + + +def virtual_memory(): + """System virtual memory as a namedtuple.""" + total, active, inactive, wired, free, speculative = cext.virtual_mem() + # This is how Zabbix calculate avail and used mem: + # https://github.com/zabbix/zabbix/blob/master/src/libs/zbxsysinfo/osx/memory.c + # Also see: https://github.com/giampaolo/psutil/issues/1277 + avail = inactive + free + used = active + wired + # This is NOT how Zabbix calculates free mem but it matches "free" + # cmdline utility. + free -= speculative + percent = usage_percent((total - avail), total, round_=1) + return svmem(total, avail, percent, used, free, active, inactive, wired) + + +def swap_memory(): + """Swap system memory as a (total, used, free, sin, sout) tuple.""" + total, used, free, sin, sout = cext.swap_mem() + percent = usage_percent(used, total, round_=1) + return _common.sswap(total, used, free, percent, sin, sout) + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +def cpu_times(): + """Return system CPU times as a namedtuple.""" + user, nice, system, idle = cext.cpu_times() + return scputimes(user, nice, system, idle) + + +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 = cpu_t + item = scputimes(user, nice, system, idle) + ret.append(item) + return ret + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + return cext.cpu_count_logical() + + +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 = ( + cext.cpu_stats() + ) + return _common.scpustats( + ctx_switches, interrupts, soft_interrupts, syscalls + ) + + +def cpu_freq(): + """Return CPU frequency. + On macOS per-cpu frequency is not supported. + Also, the returned frequency never changes, see: + https://arstechnica.com/civis/viewtopic.php?f=19&t=465002. + """ + curr, min_, max_ = cext.cpu_freq() + return [_common.scpufreq(curr, min_, max_)] + + +# ===================================================================== +# --- disks +# ===================================================================== + + +disk_usage = _psposix.disk_usage +disk_io_counters = cext.disk_io_counters + + +def disk_partitions(all=False): + """Return mounted disk partitions as a list of namedtuples.""" + retlist = [] + partitions = cext.disk_partitions() + for partition in partitions: + device, mountpoint, fstype, opts = partition + if device == 'none': + device = '' + if not all: + if not os.path.isabs(device) or not os.path.exists(device): + continue + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + retlist.append(ntuple) + return retlist + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +def sensors_battery(): + """Return battery information.""" + try: + percent, minsleft, power_plugged = cext.sensors_battery() + except NotImplementedError: + # no power source - return None according to interface + return None + power_plugged = power_plugged == 1 + if power_plugged: + secsleft = _common.POWER_TIME_UNLIMITED + elif minsleft == -1: + secsleft = _common.POWER_TIME_UNKNOWN + else: + secsleft = minsleft * 60 + return _common.sbattery(percent, secsleft, power_plugged) + + +# ===================================================================== +# --- network +# ===================================================================== + + +net_io_counters = cext.net_io_counters +net_if_addrs = cext_posix.net_if_addrs + + +def net_connections(kind='inet'): + """System-wide network connections.""" + # Note: on macOS this will fail with AccessDenied unless + # the process is owned by root. + ret = [] + for pid in pids(): + try: + 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)) + return ret + + +def net_if_stats(): + """Get NIC stats (isup, duplex, speed, mtu).""" + names = net_io_counters().keys() + ret = {} + for name in names: + try: + mtu = cext_posix.net_if_mtu(name) + flags = cext_posix.net_if_flags(name) + duplex, speed = cext_posix.net_if_duplex_speed(name) + except OSError as err: + # https://github.com/giampaolo/psutil/issues/1279 + if err.errno != errno.ENODEV: + raise + else: + if hasattr(_common, 'NicDuplex'): + duplex = _common.NicDuplex(duplex) + output_flags = ','.join(flags) + isup = 'running' in flags + ret[name] = _common.snicstats( + isup, duplex, speed, mtu, output_flags + ) + return ret + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +def boot_time(): + """The system boot time expressed in seconds since the epoch.""" + return cext.boot_time() + + +def users(): + """Return currently connected users as a list of namedtuples.""" + retlist = [] + rawlist = cext.users() + for item in rawlist: + user, tty, hostname, tstamp, pid = item + if tty == '~': + continue # reboot or shutdown + if not tstamp: + continue + nt = _common.suser(user, tty or None, hostname or None, tstamp, pid) + retlist.append(nt) + return retlist + + +# ===================================================================== +# --- processes +# ===================================================================== + + +def pids(): + ls = cext.pids() + if 0 not in ls: + # 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.insert(0, 0) + except NoSuchProcess: + pass + except AccessDenied: + ls.insert(0, 0) + return ls + + +pid_exists = _psposix.pid_exists + + +def is_zombie(pid): + try: + st = cext.proc_kinfo_oneshot(pid)[kinfo_proc_map['status']] + return st == cext.SZOMB + except OSError: + return False + + +def wrap_exceptions(fun): + """Decorator which translates bare OSError exceptions into + NoSuchProcess and AccessDenied. + """ + + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + pid, ppid, name = self.pid, self._ppid, self._name + try: + return fun(self, *args, **kwargs) + except ProcessLookupError as err: + if 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 + + return wrapper + + +class Process: + """Wrapper class around underlying C implementation.""" + + __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): + # 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 + + @wrap_exceptions + @memoize_when_activated + def _get_pidtaskinfo(self): + # Note: should work for PIDs owned by user only. + ret = cext.proc_pidtaskinfo_oneshot(self.pid) + assert len(ret) == len(pidtaskinfo_map) + return ret + + def oneshot_enter(self): + self._get_kinfo_proc.cache_activate(self) + self._get_pidtaskinfo.cache_activate(self) + + def oneshot_exit(self): + self._get_kinfo_proc.cache_deactivate(self) + self._get_pidtaskinfo.cache_deactivate(self) + + @wrap_exceptions + def name(self): + name = self._get_kinfo_proc()[kinfo_proc_map['name']] + return name if name is not None else cext.proc_name(self.pid) + + @wrap_exceptions + def exe(self): + return cext.proc_exe(self.pid) + + @wrap_exceptions + def cmdline(self): + return cext.proc_cmdline(self.pid) + + @wrap_exceptions + def environ(self): + return parse_environ_block(cext.proc_environ(self.pid)) + + @wrap_exceptions + def ppid(self): + self._ppid = self._get_kinfo_proc()[kinfo_proc_map['ppid']] + return self._ppid + + @wrap_exceptions + def cwd(self): + 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']], + ) + + @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']], + ) + + @wrap_exceptions + def terminal(self): + tty_nr = self._get_kinfo_proc()[kinfo_proc_map['ttynr']] + tmap = _psposix.get_terminal_map() + try: + return tmap[tty_nr] + except KeyError: + return None + + @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']], + ) + + @wrap_exceptions + def memory_full_info(self): + basic_mem = self.memory_info() + uss = cext.proc_memory_uss(self.pid) + return pfullmem(*basic_mem + (uss,)) + + @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, + ) + + @wrap_exceptions + def create_time(self): + return self._get_kinfo_proc()[kinfo_proc_map['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) + + @wrap_exceptions + def num_threads(self): + return self._get_pidtaskinfo()[pidtaskinfo_map['numthreads']] + + @wrap_exceptions + def open_files(self): + if self.pid == 0: + return [] + files = [] + rawlist = cext.proc_open_files(self.pid) + for path, fd in rawlist: + if isfile_strict(path): + ntuple = _common.popenfile(path, fd) + files.append(ntuple) + return files + + @wrap_exceptions + def net_connections(self, kind='inet'): + families, types = conn_tmap[kind] + rawlist = cext.proc_net_connections(self.pid, families, types) + ret = [] + for item in rawlist: + fd, fam, type, laddr, raddr, status = item + nt = conn_to_ntuple( + fd, fam, type, laddr, raddr, status, TCP_STATUSES + ) + ret.append(nt) + return ret + + @wrap_exceptions + def num_fds(self): + if self.pid == 0: + return 0 + return cext.proc_num_fds(self.pid) + + @wrap_exceptions + def wait(self, timeout=None): + return _psposix.wait_pid(self.pid, timeout, self._name) + + @wrap_exceptions + def nice_get(self): + return cext_posix.getpriority(self.pid) + + @wrap_exceptions + def nice_set(self, value): + return cext_posix.setpriority(self.pid, value) + + @wrap_exceptions + def status(self): + code = self._get_kinfo_proc()[kinfo_proc_map['status']] + # XXX is '?' legit? (we're not supposed to return it anyway) + return PROC_STATUSES.get(code, '?') + + @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) + retlist.append(ntuple) + return retlist diff --git a/.venv/lib/python3.12/site-packages/psutil/_psposix.py b/.venv/lib/python3.12/site-packages/psutil/_psposix.py new file mode 100644 index 0000000..88703fd --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/_psposix.py @@ -0,0 +1,207 @@ +# Copyright (c) 2009, Giampaolo 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 posix systems.""" + +import enum +import glob +import os +import signal +import time + +from ._common import MACOS +from ._common import TimeoutExpired +from ._common import memoize +from ._common import sdiskusage +from ._common import usage_percent + + +if MACOS: + from . import _psutil_osx + + +__all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map'] + + +def pid_exists(pid): + """Check whether pid exists in the current process table.""" + if pid == 0: + # According to "man 2 kill" PID 0 has a special meaning: + # it refers to <> so we don't want to go any further. + # If we get here it means this UNIX platform *does* have + # a process with id 0. + return True + try: + os.kill(pid, 0) + 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 + + +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 + + +def wait_pid( + pid, + timeout=None, + proc_name=None, + _waitpid=os.waitpid, + _timer=getattr(time, 'monotonic', time.time), # noqa: B008 + _min=min, + _sleep=time.sleep, + _pid_exists=pid_exists, +): + """Wait for a process PID to terminate. + + If the process terminated normally by calling exit(3) or _exit(2), + or by returning from main(), the return value is the positive integer + passed to *exit(). + + If 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* != None and process is still alive raise TimeoutExpired. + timeout=0 is also possible (either return immediately or raise). + """ + if pid <= 0: + # see "man waitpid" + msg = "can't wait for PID 0" + raise ValueError(msg) + interval = 0.0001 + flags = 0 + if timeout is not None: + flags |= os.WNOHANG + stop_at = _timer() + timeout + + def sleep(interval): + # Sleep for some time and return a new increased interval. + if timeout is not None: + if _timer() >= stop_at: + raise TimeoutExpired(timeout, pid=pid, name=proc_name) + _sleep(interval) + return _min(interval * 2, 0.04) + + # See: https://linux.die.net/man/2/waitpid + while True: + try: + retpid, status = os.waitpid(pid, flags) + except InterruptedError: + interval = sleep(interval) + except ChildProcessError: + # This has two meanings: + # - PID is not a child of os.getpid() in which case + # we keep polling until it's gone + # - PID never existed in the first place + # In both cases we'll eventually return None as we + # can't determine its exit status code. + while _pid_exists(pid): + interval = sleep(interval) + return None + else: + if retpid == 0: + # WNOHANG flag was used and PID is still running. + interval = sleep(interval) + continue + + if os.WIFEXITED(status): + # Process terminated normally by calling exit(3) or _exit(2), + # or by returning from main(). The return value is the + # positive integer passed to *exit(). + return os.WEXITSTATUS(status) + elif os.WIFSIGNALED(status): + # Process exited due to a signal. Return the negative value + # of that signal. + return negsig_to_enum(-os.WTERMSIG(status)) + # elif os.WIFSTOPPED(status): + # # Process was stopped via SIGSTOP or is being traced, and + # # waitpid() was called with WUNTRACED flag. PID is still + # # alive. From now on waitpid() will keep returning (0, 0) + # # until the process state doesn't change. + # # It may make sense to catch/enable this since stopped PIDs + # # ignore SIGTERM. + # interval = sleep(interval) + # continue + # elif os.WIFCONTINUED(status): + # # Process was resumed via SIGCONT and waitpid() was called + # # with WCONTINUED flag. + # interval = sleep(interval) + # continue + else: + # Should never happen. + msg = f"unknown process exit status {status!r}" + raise ValueError(msg) + + +def disk_usage(path): + """Return disk usage associated with path. + Note: UNIX usually reserves 5% disk space which is not accessible + by user. In this function "total" and "used" values reflect the + total and used disk space whereas "free" and "percent" represent + the "free" and "used percent" user disk space. + """ + st = os.statvfs(path) + # Total space which is only available to root (unless changed + # at system level). + total = st.f_blocks * st.f_frsize + # Remaining free space usable by root. + avail_to_root = st.f_bfree * st.f_frsize + # Remaining free space usable by user. + avail_to_user = st.f_bavail * st.f_frsize + # Total space being used in general. + used = total - avail_to_root + if MACOS: + # see: https://github.com/giampaolo/psutil/pull/2152 + used = _psutil_osx.disk_usage_used(path, used) + # Total space which is available to user (same as 'total' but + # for the user). + total_user = used + avail_to_user + # User usage percent compared to the total amount of space + # the user can use. This number would be higher if compared + # to root's because the user has less space (usually -5%). + usage_percent_user = usage_percent(used, total_user, round_=1) + + # 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 + ) + + +@memoize +def get_terminal_map(): + """Get a map of device-id -> path as a dict. + Used by Process.terminal(). + """ + ret = {} + ls = glob.glob('/dev/tty*') + glob.glob('/dev/pts/*') + for name in ls: + assert name not in ret, name + try: + ret[os.stat(name).st_rdev] = name + except FileNotFoundError: + pass + return ret diff --git a/.venv/lib/python3.12/site-packages/psutil/_pssunos.py b/.venv/lib/python3.12/site-packages/psutil/_pssunos.py new file mode 100644 index 0000000..78d941c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/_pssunos.py @@ -0,0 +1,734 @@ +# Copyright (c) 2009, Giampaolo 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 Solaris platform implementation.""" + +import errno +import functools +import os +import socket +import subprocess +import sys +from collections import namedtuple +from socket import AF_INET + +from . import _common +from . import _psposix +from . import _psutil_posix as cext_posix +from . import _psutil_sunos as cext +from ._common import AF_INET6 +from ._common import ENCODING +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import debug +from ._common import get_procfs_path +from ._common import isfile_strict +from ._common import memoize_when_activated +from ._common import sockfam_to_enum +from ._common import socktype_to_enum +from ._common import usage_percent + + +__extra__all__ = ["CONN_IDLE", "CONN_BOUND", "PROCFS_PATH"] + + +# ===================================================================== +# --- globals +# ===================================================================== + + +PAGE_SIZE = cext_posix.getpagesize() +AF_LINK = cext_posix.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, +} + +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 +} + +proc_info_map = dict( + ppid=0, + rss=1, + vms=2, + create_time=3, + nice=4, + num_threads=5, + status=6, + ttynr=7, + 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) +) + + +# ===================================================================== +# --- memory +# ===================================================================== + + +def virtual_memory(): + """Report virtual memory metrics.""" + # we could have done this with kstat, but IMHO this is good enough + total = os.sysconf('SC_PHYS_PAGES') * PAGE_SIZE + # note: there's no difference on Solaris + 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) + + +def swap_memory(): + """Report swap memory metrics.""" + 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 + # ...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', + 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: + msg = f"'swap -l' failed (retcode={p.returncode})" + raise RuntimeError(msg) + + lines = stdout.strip().split('\n')[1:] + if not lines: + msg = 'no swap device(s) configured' + raise RuntimeError(msg) + total = free = 0 + for line in lines: + line = line.split() + 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 + ) + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +def cpu_times(): + """Return system-wide CPU times as a named tuple.""" + ret = cext.per_cpu_times() + return scputimes(*[sum(x) for x in zip(*ret)]) + + +def per_cpu_times(): + """Return system per-CPU times as a list of named tuples.""" + ret = cext.per_cpu_times() + return [scputimes(*x) for x in ret] + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + try: + return os.sysconf("SC_NPROCESSORS_ONLN") + except ValueError: + # mimic os.cpu_count() behavior + return None + + +def cpu_count_cores(): + """Return the number of CPU cores in the system.""" + return cext.cpu_count_cores() + + +def cpu_stats(): + """Return various CPU stats as a named tuple.""" + ctx_switches, interrupts, syscalls, _traps = cext.cpu_stats() + soft_interrupts = 0 + return _common.scpustats( + ctx_switches, interrupts, soft_interrupts, syscalls + ) + + +# ===================================================================== +# --- disks +# ===================================================================== + + +disk_io_counters = cext.disk_io_counters +disk_usage = _psposix.disk_usage + + +def disk_partitions(all=False): + """Return system disk partitions.""" + # TODO - the filtering logic should be better checked so that + # it tries to reflect 'df' as much as possible + retlist = [] + partitions = cext.disk_partitions() + for partition in partitions: + device, mountpoint, fstype, opts = partition + if device == 'none': + device = '' + if not all: + # 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. + 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) + retlist.append(ntuple) + return retlist + + +# ===================================================================== +# --- network +# ===================================================================== + + +net_io_counters = cext.net_io_counters +net_if_addrs = cext_posix.net_if_addrs + + +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). + Only INET sockets are returned (UNIX are not). + """ + families, types = _common.conn_tmap[kind] + rawlist = cext.net_connections(_pid) + ret = set() + for item in rawlist: + fd, fam, type_, laddr, raddr, status, pid = item + if fam not in families: + continue + if type_ not in types: + continue + # TODO: refactor and use _common.conn_to_ntuple. + if fam in {AF_INET, AF_INET6}: + if laddr: + laddr = _common.addr(*laddr) + 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) + ret.add(nt) + return list(ret) + + +def net_if_stats(): + """Get NIC stats (isup, duplex, speed, mtu).""" + 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, '') + return ret + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +def boot_time(): + """The system boot time expressed in seconds since the epoch.""" + return cext.boot_time() + + +def users(): + """Return currently connected users as a list of namedtuples.""" + retlist = [] + rawlist = cext.users() + localhost = (':0.0', ':0') + 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 localhost: + hostname = 'localhost' + nt = _common.suser(user, tty, hostname, tstamp, pid) + retlist.append(nt) + return retlist + + +# ===================================================================== +# --- processes +# ===================================================================== + + +def pids(): + """Returns a list of PIDs currently running on the system.""" + path = get_procfs_path().encode(ENCODING) + return [int(x) for x in os.listdir(path) if x.isdigit()] + + +def pid_exists(pid): + """Check for the existence of a unix pid.""" + return _psposix.pid_exists(pid) + + +def wrap_exceptions(fun): + """Call callable into a try/except clause and translate ENOENT, + EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. + """ + + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + pid, ppid, name = self.pid, self._ppid, self._name + try: + return fun(self, *args, **kwargs) + except (FileNotFoundError, ProcessLookupError) as err: + # ENOENT (no such file or directory) gets raised on open(). + # ESRCH (no such process) can get raised on read() if + # process is gone in meantime. + if not pid_exists(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: + """Wrapper class around underlying C implementation.""" + + __slots__ = ["_cache", "_name", "_ppid", "_procfs_path", "pid"] + + def __init__(self, pid): + self.pid = pid + self._name = None + 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) + self._proc_basic_info.cache_activate(self) + self._proc_cred.cache_activate(self) + + def oneshot_exit(self): + self._proc_name_and_args.cache_deactivate(self) + self._proc_basic_info.cache_deactivate(self) + self._proc_cred.cache_deactivate(self) + + @wrap_exceptions + @memoize_when_activated + def _proc_name_and_args(self): + return cext.proc_name_and_args(self.pid, self._procfs_path) + + @wrap_exceptions + @memoize_when_activated + def _proc_basic_info(self): + if self.pid == 0 and not os.path.exists( + f"{self._procfs_path}/{self.pid}/psinfo" + ): + raise AccessDenied(self.pid) + ret = cext.proc_basic_info(self.pid, self._procfs_path) + assert len(ret) == len(proc_info_map) + return ret + + @wrap_exceptions + @memoize_when_activated + def _proc_cred(self): + return cext.proc_cred(self.pid, self._procfs_path) + + @wrap_exceptions + def name(self): + # note: max len == 15 + return self._proc_name_and_args()[0] + + @wrap_exceptions + def exe(self): + try: + return os.readlink(f"{self._procfs_path}/{self.pid}/path/a.out") + except OSError: + pass # continue and guess the exe name from the cmdline + # Will be guessed later from cmdline but we want to explicitly + # invoke cmdline here in order to get an AccessDenied + # exception if the user has not enough privileges. + self.cmdline() + return "" + + @wrap_exceptions + def cmdline(self): + return self._proc_name_and_args()[1].split(' ') + + @wrap_exceptions + def environ(self): + return cext.proc_environ(self.pid, self._procfs_path) + + @wrap_exceptions + def create_time(self): + return self._proc_basic_info()[proc_info_map['create_time']] + + @wrap_exceptions + def num_threads(self): + return self._proc_basic_info()[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']] + + @wrap_exceptions + def nice_set(self, value): + 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) + + @wrap_exceptions + def ppid(self): + self._ppid = self._proc_basic_info()[proc_info_map['ppid']] + return self._ppid + + @wrap_exceptions + 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']] + saved = None + return _common.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']] + saved = None + return _common.puids(real, effective, saved) + + @wrap_exceptions + def cpu_times(self): + try: + times = cext.proc_cpu_times(self.pid, self._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. + # Error originates from read() and also tools like "cat" + # fail in the same way (!). + # Since there simply is no way to determine CPU times we + # return 0.0 as a fallback. See: + # https://github.com/giampaolo/psutil/issues/857 + times = (0.0, 0.0, 0.0, 0.0) + else: + raise + return _common.pcputimes(*times) + + @wrap_exceptions + def cpu_num(self): + return cext.proc_cpu_num(self.pid, self._procfs_path) + + @wrap_exceptions + def terminal(self): + procfs_path = self._procfs_path + hit_enoent = False + tty = wrap_exceptions(self._proc_basic_info()[proc_info_map['ttynr']]) + if tty != cext.PRNODEV: + for x in (0, 1, 2, 255): + try: + return os.readlink(f"{procfs_path}/{self.pid}/path/{x}") + except FileNotFoundError: + hit_enoent = True + continue + if hit_enoent: + 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: https://groups.google.com/g/comp.unix.solaris/c/tcqvhTNFCAs + procfs_path = self._procfs_path + try: + return os.readlink(f"{procfs_path}/{self.pid}/path/cwd") + 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() + rss = ret[proc_info_map['rss']] * 1024 + vms = ret[proc_info_map['vms']] * 1024 + return pmem(rss, vms) + + memory_full_info = memory_info + + @wrap_exceptions + def status(self): + code = self._proc_basic_info()[proc_info_map['status']] + # XXX is '?' legit? (we're not supposed to return it anyway) + return PROC_STATUSES.get(code, '?') + + @wrap_exceptions + def threads(self): + procfs_path = self._procfs_path + ret = [] + 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 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. + # Error originates from read() and also tools like "cat" + # fail in the same way (!). + # Since there simply is no way to determine CPU times we + # return 0.0 as a fallback. See: + # https://github.com/giampaolo/psutil/issues/857 + continue + # ENOENT == thread gone in meantime + if err.errno == errno.ENOENT: + hit_enoent = True + continue + raise + else: + nt = _common.pthread(tid, utime, stime) + ret.append(nt) + if hit_enoent: + self._assert_alive() + return ret + + @wrap_exceptions + def open_files(self): + retlist = [] + hit_enoent = False + procfs_path = self._procfs_path + 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 FileNotFoundError: + hit_enoent = True + continue + else: + if isfile_strict(file): + retlist.append(_common.popenfile(file, int(fd))) + if hit_enoent: + 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", str(pid)] + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + stdout, stderr = p.communicate() + 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) + msg = f"{cmd!r} command error\n{stderr}" + raise RuntimeError(msg) + + lines = stdout.split('\n')[2:] + for i, line in enumerate(lines): + line = line.lstrip() + if line.startswith('sockname: AF_UNIX'): + path = line.split(' ', 2)[2] + type = lines[i - 2].strip() + if type == 'SOCK_STREAM': + type = socket.SOCK_STREAM + elif type == 'SOCK_DGRAM': + type = socket.SOCK_DGRAM + else: + type = -1 + yield (-1, socket.AF_UNIX, type, path, "", _common.CONN_NONE) + + @wrap_exceptions + 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 + # an empty list means there were no connections for process or + # process is no longer active so we force NSP in case the PID + # is no longer there. + if not ret: + # will raise NSP if process is gone + 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) + ]) + return ret + + nt_mmap_grouped = namedtuple('mmap', 'path rss anon locked') + nt_mmap_ext = namedtuple('mmap', 'addr perms path rss anon locked') + + @wrap_exceptions + def memory_maps(self): + def toaddr(start, end): + return "{}-{}".format( + hex(start)[2:].strip('L'), hex(end)[2:].strip('L') + ) + + procfs_path = self._procfs_path + retlist = [] + try: + rawlist = cext.proc_memory_maps(self.pid, 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. + # Error originates from read() and also tools like "cat" + # fail in the same way (!). + # Since there simply is no way to determine CPU times we + # return 0.0 as a fallback. See: + # https://github.com/giampaolo/psutil/issues/857 + return [] + else: + raise + hit_enoent = False + for item in rawlist: + addr, addrsize, perm, name, rss, anon, locked = item + addr = toaddr(addr, addrsize) + if not name.startswith('['): + try: + 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 inconsistency with /proc similar + # to: http://goo.gl/55XgO + name = f"{procfs_path}/{self.pid}/path/{name}" + hit_enoent = True + else: + raise + retlist.append((addr, perm, name, rss, anon, locked)) + if hit_enoent: + self._assert_alive() + return retlist + + @wrap_exceptions + def num_fds(self): + 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) + ) + + @wrap_exceptions + def wait(self, timeout=None): + return _psposix.wait_pid(self.pid, timeout, self._name) diff --git a/.venv/lib/python3.12/site-packages/psutil/_psutil_linux.abi3.so b/.venv/lib/python3.12/site-packages/psutil/_psutil_linux.abi3.so new file mode 100755 index 0000000000000000000000000000000000000000..eea093ccedafb46f9725ac501fa34e6d395e18e5 GIT binary patch literal 115336 zcmeFacYIaF`Zv1P-YIMEm0c1-3Uvbk6GKUWAXO3wkVt?4p{QU8A%S2>LJH;BLWvq< z6s)M6BX+EZWA8>0EJu%6j&iK%5#13L?A_e&cV^bk%9i}@``-7Td+*1U&#alJ%rnnC z^GsQ@_L`hGtzf!C*EQ>MYA0)iGEyWaTM?drq?0FG8>_jrY;BM>Q1C`5p5sM&`BVmHt+&sw?UikHVY?GiEQ)F2Atomzh78&+7a7r_GUHuUuKz z^ZoJEC6;sqilJ#8rFV{t^=Qp0{?{&SjJbRpP=cO^_!I)TW+BW$VAIY=peYt2@LGhh z7~yn;B?!EZynfVs^KSQoM_Rw~YWF|?(60Tp<6q8upf&D+lz;rzv3KCVA1;sce|K=t zjt2LF39D9=^_=l-d!I|5y7ZSCa8(NM?3uSO?0o}6Rd$hi#&H~LJ0oa6ELft-L{ zL6x2feU7K+^N@c${QJmvJbYs}_5D{j{Fj3NczWK^jU8rnqtC={@Sbk!^)vX7SMELC z;P3AS|9Lm{jq1k!eY0jB6{*QK3?zV31{9ZS5KIp~{9o_Ipc2loq-Lzv)H~3-Q=-;;+{g-r8 zZdEsO`gDUI*NvQay5S$wjXuA^-;UQ`yxrJsb~kn&(M^9$>c*efb)(O>-RM)_jr=FN z(dS0^;qmJA_ipSq7W@H(@avRr_^$!u@#N0~eqgu|z`G!ab7%NecMJ3BC z%Ovgil;YA2B@|FnS@D-Lt){M_zM`_YaO1Sv+T!_T_0wys*UXs@psIRNMg6Mz8|z9- zDl4Hz8BxWR)uko%71dQ*U41Pu)wNn(O>IS0eYpk+8%m2sR$vP^&RM>?thBy(DsoS6 zs45kiLZwKehGL6z%fL}xs?DyR-%z@$u(rCitgcSWDP@#fR#j29!qTyHRaxno#WfWx zifc=%R+gcRoZ6Meg(bChWkn4&tTe>Uh6(1^*EW>aD~%SGR5p~MK0+zPtRxsJs#aQZ z>&oh3#^ntwJGIWQE2?ck3G){emq!mDP3Bu6RmAMdb=nK6UA|FOpT&LR*%iw59UXODgLu7+S%y)wLD%8(CUa z$(pi?s`6?`DnN4=&n{a}bVYf|iWRkW7B;7*rmSj(R$g0HW>LTtl2f>GK~+U*^@_4$ zHgi`ikgsHASsk6C4y|5YRc=+8CNJB7QtPxcE8q;uXKG6->dK1Ct83XNtRdQeUPD!V z#hNbmo?czMri87L57#X&R+($6>nb(?Ijed_LuFa<)Y`I=`m&Loh@2HG@~i5nR#(CE ztC+i>x@x6Rn~5%b%9+9nsH~`J*kGwz1Gg%!sHP!`QPJa}Vec;J(wc_il5%#m`i+0c z(}ifuxuT*D9*VA7L4U8a1UA%_p^a3{tKe=G<;5%LvKxx)YRWo2Tx-6#+y;1A zJ=<9(I@>R|jLkTGer{QLNke75hE|s>Y&zLt>(F0T6hl+lza$7zR;AFVLo9R{*i`i3 zb!D~XmDTI*U1oOmtg7AsYa)QMH_3Be@XRWrcjk2aa~De z@%)l?WksvtQY$o2*Hj5BD%!lVy87x`iwt`xGA!817}i%g5>BtEEMq6sDl3-P)C=Lm z>#B!m5H4kEj5ebnf6CP2k;6w0&kCha3Z+JMriPCWrNw+DCm-IOc*0gwk(vRlnV8Ss z4)L7sz>bTZi~OB=tQ0wzjuOx7@~J(l3!mhaTv8&_`v1nCCU`GbYqS7LYv@&h#fVe8 z8#rD%biGN@2`5%mc&N)G3fDuxQQBSw*C#W7r1r9c69hd%`;UU9d`C#Wwf|%7*Bnyw z_8zsrW8rrx{KH{*3(l*hcA94K&*Fj(*RnADE(Ke$#ecfu-yO#9SL>0sF#Jlz|9lv} zakwn^P#C^W@dv~3Z6`{8txm?4K8=ds2*Yb5Bwrv5->Udi!|=;eC4Y7p{(8lq7lz-X z)-}t*@OLQwRblvSwE%iP41ZYh9}2?<)B?+jt@?hW__Gz=!aL_n{g;K|dnkNk7(Pwm zcZK06D|}lRevZN)3d1i@culRREqy8!J`jdqukhJn_zM(%TeYl56e4tx(6z&cw?BXH zvf<-G+*r5U@I7q!JvMxQ8~$M%et-?%X2TD%;h(qR2ix!mY&mm?sgrdT+VEDNA#Je@Z#~-)zs!bLuMCLRR@v|| z0wZrVHasSx(6zya_lB^V)@Z}~Z1@Xpc>DAHHXAZ1^@CKG}wU-iGgG!ymBWd)x2_ZFufETh}2QzORKu9JJxpt0O@@X2S<; z{6E_818sP1tpw;J_Ho9r;nmbG82vW9dgK@QBpY5$Q34;Z;Zp@h-coINHAM+Rh7E5` zy(Ema;jL$V;WF+3=Y*e2oo1(uUt) z!=GZqH`?%XZ1@Xp_)#|eHXA<6hTmbst0j@3?y}+47L&m5w&A%)XI*=2c(rvSNDtfa z;)RvU&}PF=u!JCf-iB9OH-dV=hF4os0)NnkKiMYdkPWZ4<^(lp!)M$0kJ<23Z1^8- z_^CF$Rx1JezkOaaY^fTFsJ_ZZ34cRFsJ47`&XYpYS08pGG*A z@BsnOBOFJ#O~9uR=90O6kASBT<`mz)OTgm^a|&yV3U1#c;AaSP>TTa8;718_ z$=|+B!1og76x`k@;5!I&>TRzP@Qs8y<+d*q@U?_F)wUN3_%gzrV%xI?d=X(zq3sz0 z-b|QNXL~@ve<92%v)wP?TEfE#YXV+Pcm&~Nzl!!J%qg?|kbqAkoJshAfaeh&Nw`hG zrx4~;*S<%S_-N_~%)GIc2r`1^hK(PF3xifIlP5DXRV0FC86u zt&{TSVuWaooWf|_kM%tvCaX@+hECWnV1rM z0cK(X67rh&2d|}-n!c!y&fBs^)9VxRHa*LdGTWBsEXfJ!m)CqWSV+#U)NMIMdCfmV z`W29#*|uQ5(lfvLT@o7h}6d;^;s(Qc@q7R`rJbeLi#kaoKDMhtIyc~ zK_ARlNqNmbz(Sv(4`;UJwHBqs1QSqi%w7Xqa#LcO+BTdBkNO7gk+((DF_$In?Q{U- z5D3d5zqIT6?}*@E zkp4b5C6HY|HLoS>TP$Xw`A9S_G#?()+-m>eOe)+Gc{BN-|6P!8*B>IbOA$xWL=bT# zR6$X{PJcXA`A0ztMkhMSCKMK`WB(TTDJ`Gb5!{V3sA~_DTR`EKoBCk?mdIqK$^ft( z`Lw*#ce(z`YwGZA{Q-@av#9x4&cZE+e&C2E#)%}1GFyIhG%VTjyW?R_@K17;g5lCP z?HHGN(=uO}mf5yie?$jkUh`+c8rHS>^QJc6)>d-1L_UeFE}_k)XDN+iLC(^gGjfXI zep|tU0)CHecr(}c;Cgdb)A#i+?L*N~)O@wu?`z5ch;sgcYQPwd{N{c6&Ck(-c`Xe_Zu8f(nt#e! zkl%bzOcj%=v37xuuYb>pOaf% zJ#aHEUarw79DFtaYg%caWP^uNUAKIE??(;E!KJ z>!x>nqOR??b)mEB2SFE&x}Ev2>OwDQ?g%~%di!}mi}45zJ}vW$X_+0F@6ZA0416$X zH<%Ch2+=Jn9kKm0I$KNRtQ<}2%>6M(*i#@KtnK(|F7ji3_-;N2(csr#AqQu3Om1%U zDH!=VwkQ&N(Z5E0Ya2}mU?h#fNcukVH1BV5HnlY*V7PcaMRdhH*azN`bo$c094}tT zrpJs8zRCh5y3@Wn3ow2ZQ41)M9UMj;OrfdhLShuh=!IoqYgX|fOr?K^U=-C`dGIp$ z=(Nm_Ib9SqACk+vE>qc4&{<4nzO6-~BVzm)b6RNn^`N{bIhi#dwN1ab0V~ET{t6Ec zK-l+CCFZp6_rW`Vgi(-k_?k|J21rHtnzqW6;cFU?JiuWbYm}*guW3KVJm9i@O)(K7 z)#z)QhgA6w`{?dht0W}8tCA2oAW||w=J}9BUI=|hB_TBcsl-C;H1fjBn~%aP&=Nlc zV6J2IL-YkP5EI|LpNrU2QJbc=yq0O2uW1qT$pM29{h591q+{@)TZDfxLn^_`0n#5^ zCk?0UDOx?_R}yEW#>0j_CLYI&h*Yv2z!#UWj}14^dTN@gSj?& zeLLmd9g^43&?&1~$-0!X&Zex;dKvnS9te5gqLP+=FS9Smx-adNH@H*YR3&dR<&Eho zFI0gGAcYm^MW(+1u=M`{59`63X`>Ya+lYG8PG1v$G4RS=O}I$&Q8;XH5kz$H+AjLP z!+fJ{`g=O{|0{A@_ATMW5mK*Nsdv_wvpO>6G^yxGnvjh9Nuko22Bu^9AH_CxD*yF9Jat+*;qjZv935ZSp;x z^8ad+U(!W>5KL^IvncW%0HON-2b(Lwk5SQ3{jtKe>Ti=5+eO|@N?tnUU1gV7NO@P- z;D^=g#UjId3=%&>9>vb&9%wv+bQpEo4mDM>{*yT&Fjvx~eN zmAsjhccoq4KPc~hRLrtpYp1-vHhCYSQl0fZSIPV3Gss&DfTTPz^vx3aj|LBslnuc4 zOz{*NUu$27(o}u0%nc?&Cv=1%Pz1w-fx2Y2wSNs{`L?Ex^I|gFxOuRZ!wl%38=2n+ z4}*wjorf{RXnX`cF*pc?wcvTgS>I>Nk9vJFCgN=DQgI^A1LOTs zx1yyg+HTUgvksZpC|Zo7T}+zTBx=oSRkUNDvhEG_k9uXEO$vWd;+Ke35@fAc_%?~p zebkivYZQLB#1GAT?m2o@@F2>>J_gruy^(#fV%r3_JK|RUfoTwHHg1E3rfxBxcKXBS z@$iRXXgbQNdGv8K9E5F#Fy#xGve*Tpt!q^weIzp5YRhtkkC6D(M?0kMrz!mBp9mZH zgIl1T)JW*jt$tmfca7j#5MT`y{P-hy8dUDARPZ`s4EfZ@eS`8X6QUd1^IFdu8q5YK zpZ4hQ{M|W_Mlf9<_Y34tKsKILFy7bnZ{%z|YgPuGT7Z;yc=w+cGsG4?CvKUw`d8aZ3?o;gMgA5a?|w%ym7C!i6>!xQo&_XdVrY6}Iv< zT?3SS8hlfvhU2;LeX_|%!cPQ}56?{rXm<9B-w&htZ%20ZoZI9DLD(~RAMC^dprAQu zdurTP&^on%zbQoJCvC!8m4fE|-`wiEx~-446-xCLa-Rz(bXKov0B~p>so0$%#qOgF zY;KxLyFGyMkX9cpR$uk?;yJc{aWEZv;E71Q%)&Uv=dCV;B4w z!l_gu3VaxEZ}K|#Q4LzFwi*e+?Y8=T~WNpYUMETf>dJ>MUVL$4LY{uM~ zqlSF04c0&xjCa2Fe}H6-L;4r_rPw(;Z*p)G$g`S%&42pyC_FXgKmD;&bc~C@jE8LO zragn*G~qwZd)Z^QgTNO1391M0{|NezP4G4KfT44?e5p6w0+m0>1WRjHf2aoUpz?j7 zJdN138^~ouR0Dd)55Y$uigIs&T($c$0zx6+$UZ(#-wvUim3>VwK!f15V8;M-Ky#HAo9UM=qn_fd7@P<$@kGIatQvybpG7g9ou<=Ys*yLebGhkrPM-8z z;VbdMTv0{e*8N0tk0p5HAy(SA^~DfILs@G0yzp6K8%2&<>)tgclx$I zjdbQaIYQ?nCo8|p-_p@hzdSgJl|qv&&fnD0fhm7|&w}P3F=FR7|0LSt^^fvhMQ9FK zHhU-SFH6jV(<+t&}ld>D+UAdbtJP#SKe{cEA`ksZNz$Ou!N;i}m4@xcg3UJ&gE6T}xZfA(Y}1O%TGrf3)e<`^=g zuO7KEuchZJ!vVBrB~g~x0&W=}Ob4&nFFeu+_9w`JZm{nIv`i(OgYUQG3zc(XHEjU~ zcJ$W`V(TZ^2Gwct$7n$NE0s@CS+%e85@^SHO_2rpoCBYnF7|fvo8d{hDcQbQ+^AZh zfbUu4mJfC{mqCh#c$JE?Ia-prFnAp#abTM6d#EU7iSMD@lmg#_^(nKT{>bloMzpgp z?*Lr!4)`TZR)FeCpQvAkMp%Q~=tj%r;B{ zg#{Rtaid|7%Hvk@SiYmm2sT3UX{S;P+w(?C{2mzD zklZ(?ark-$?kG0)&c~au;F~Ck4BUhd<_nL(i#@oKiKS^lYu1JE4S0L7igj&1is3F; z2cg{8-TE1<*V(_t`bBWbCD-Gia`bpZ;N*T4*FbqKSZh56q_qGWC6F`y1uauCp*@39 z6GIak^eom=?b^RWVe-K;9%yLXB;>Q-)eq)`T|Z3jayN7^*9XBb-i4p90uXv$gHs#? zlFSXSb3O1PAZDt6g}R^JSzJ>t@`?I@<4BJ34~+lm*7$$Ds9Yp}T_N5ovS-a?ow)hl z5W_V}CGzq;jp$gMb`IIPzg)Kj557YU#5VsOkSOYr$X5doQLl!7up%kc;kI&&*B!wm zrazGSP`@2k{)Y28@caa@eWR#zHuUNn3-uluL{(dRUXIolqxy4bShmZ%kQF|vUqF|* zkA|>D^>T2v$3Y~@$9_BuNCL#tS_tF=fIPk;!V+^Y%KG#A(*l25;7<$uX@Ng2@TUd- zw7{Ph_|pRaZ?wSRxWuv9(+hHD%umyB z?#A-k>XH?uC3W>$Wp#DU@)DdXP*V-5)#9#IRoCNGib|fgP*q)0Tce?3YbxqWHJpL4 z2InMHXlsxc3e;8NhyYEXgtVIK+WNY(O0A@!zIsjBii#3##YTMXUs0;Tvg%BQKsiOP z(B>87&&n?<&Y5qerp}o+EtD=?U?p>NigK*<^!fRxg?MI7n^l0STC~}7rsw0lA0>8n z;k-FhEnwdKP$Bb+ayqjxdx$b?{*1W`rY-1{kUuq)o>x>jFMm!*Tv1W}tPpK}{*1zD zvvc!j&k*$D{Mpm5)1o3g=v)kAWIM3TM^G3#W2au0Xf04~SDgFc_fwRhHDQEDMxXRX40$C33LJ z@~TrObOgYH0imisP`054=k}DX7#dg!DTC)(ozvbWrxoQD&6!h>rk!3t)n9bukxNn%%6RJi#QkY&98TK)F9saMn}iZ#3OD)9PwsHM~V~t|LN#x zM4bC}N5}n)v7hxL;v&TSO;tVO6vS^Jo`^UXTZCsLF2d&UPQ>3LejM>uyqt=``=$uI zQ5uSPE8i?3z8|muu0WiNSJNLOz8P=ihvMCBJbqWOgfT0OOaIF}P2b?xd&fi@Jj*Ku zIM#phiya+)$k)97>E5JMeCB$iQJdUr!il3&1_G;1M95|0m5vT>+h|j~{#M5nZ_>ri zTyNm~h#B5phGVtc8<^@%0%?lZ$TR2IAm-nSunhUnSNSh?%3_e%6ugtD#+2BhsMtTDRAjX->!e6qEE6&!o>&^;l~G2hWMoexKtU)^@lQYy~`X+y{ihmHH*Ega=go&mHKf@$ul9B z_Sga06VXT3AuVkO{kOoUG92^W-XvHPHk@M4x02aldkJ|iM4s>9*U}G>=VJO1{czbv zco7h?Iyt8EZigK)(-ddd`Pm1t1?z?gUhE(2r`f>qCoQnEzYy(9{6ygMfERvIcH*z-Y0$95tF-O(sKGS6!i0QCmw8Q5$VXUP5EwM(`$lT87`hBf#UHiamU<4+C7 z9Wh4J&laJyX|O$1q95jhegO21%s~1Q(9x240GEL_8n0W#IeJ2u+4RFa`bb_^Kg>nv zxa_#9cYp_$+@%(Lu_mJ-R61>s%#y!=v1>trl)z08RluC0f9z-UU+Aqy0An35YrOuiVzfF3$0uHj4INd&N~}DD9VlxZ#GkzJ!f!{!6;J{! z{Ev9S$jj0*Rh8$GXfYl#`Fn@de}m%3iyToOtK4G>ejINAdEwWSg5F<(|DV5G=h%l= zWK2L}rmJ|Nipy2JPQ~Y|_$n3Os^SM#ykEs{s`wKXf3M=G0h+iHR6In*V^lm{#S2wj zuHtnnK3~OGsrXhEKd9pUDt=SNpQ!kI6-NzJ<*RszipQvUx{4R7xLn2SRD8aQuTt@? zDt=JK`&Im=ia$~D_bQGWq{>(E5EYM6@pKh0RB^eA*Qxk?6)bnwj(#LA>%2P)|Gyhs=i^!D;pt%ltn=`!^Y6mx z*7%2Sbe7kVEb-tZ-o?STII?v8Jzb>3^onL32R~JqXtIi+5|5bkL+&fEv zW&eO!3|iR#{n$Dm&&nCDk9AI7cXaE#yzc1Mxq02ut@HD`qg&_bbw>}Kr>7Xg^|#K| z>n^`_zFv29>zuvr=oY@ac*fMJ;{&Pq4;pEK%nTd=ni&|GF)}kFb5uqkbsjck^GfO! zVOVBnnpnm;77)aUO@E&lLhw2#kUf+sp@pFbRn9hO1Lzpc{Z4?mVX zj9sgQC8er{{t|pChQFH*fa`b-;V_~%gXGYSzcO+}f5<2z`T~KB;x>%K5y}6o;*5yI zUbGgmh$tg+FN8*J0N!y)GEtG-!E!v%yAdU6!%E}#|me84?c)T z8A=d;^B2j-mx$%D+%^Z$-)15)~1#QcxmhAd0>MHzR7SVIZfGj$71t7llcC zMokbv1WGsroQqu1JiEjnz1ly4StbF9uJrFmq!|q)uSw1(L34B4CCVUI8EA-4M=c#VyKU2CGaj^+!;tWdd~r;?vHdi ztz+nYJOIHx%HiD%`ct6#SuxG`EpE|69kCSgCebWIi@lYF%!~WN!btA86M;7S3uYIR zf?1GyXz>L=L>tbyJ{CcX-HhzU?6^@DMhf?h2Rde*-j_uchG^8M2R@Sr0-G$NPHRZq z47@7C_c+p?YjjbGtt5(;=y@3gc}IHa015c1qo&`b>!gIA4X^Kf1(_1Jod6oH-N=@t zFK{{g7D|MsCEtmZ{x_Yfg`YUD@2q!fS}$SRzx5%9)kk|Rhgkh@Ly4OT0c`W$m*Po5 zzurfjWBaN;8#H~F@D*-;&_DZ*S8CA6{01MYIKR=0lJp{IG!GoOlJvRAto6%Nylgow z`BD~lu~0JnIDUO+PiQ5Tym%lBe;7ILN03ThJd(H{0DpzhpXZJ1+sylrc5JWg`{EEJ za8ae7!c@9>zN872Dz(G}r4qeH=)A4diu^!FKTBFsi9BEC*7Tix^616K9Q~|bZ1fpm zo{G??Y(uH0pDiqQKHx@#{%h)1YWg{D=5cO6v_8;=43D9P1J+Q$=1A7&b%a+C1|9=$ zz`jm`2X>;LChky55P+E-VsozV6osV^1|gFTfHFj7mZ{31wrG?ETHoc0na>_SL1l-6BsbnjsB zBaJ4~3^9^nQ>zib7SF<~d!M=Q_}@L@*u@3+M+2Ea_cxk881aB8RDK}cE zxa=mMSPf!|~9f<3*a58E|9^Y%zgJ?7O& z$C{%dC(e8j@_U#r4EgcqXK0b0=2}QjFyBLoiDnaYNHPQ1N=i0wLatusbCA&6{1K(} zF^eI&ubG4z_A`4TSATOacmn1gXf?nr0Dho(5wsd)W*Qhj%nOiru(=<3hnRD)Y(2sJ z8*-(ZuftwxrVg{5Xby!A>E^#+hoR;i}gY*RRN7QSgITW>?XkGye zOfs*4#ZER8V6n;O6(~2`Y=pEN^FPQn#Y{kYsyPbOTyrlZOfz4G{ORVGNY600Kti6m z6k6q*7XW{X`2uP&(|i!=0&@iFILkZ}mYHqxJ;EIGd6Zjda)WHHnT)dLnS)UG`R41u z6q#=1T3}8`Ef$)yP~sx?$zdduzZbqGRj(Ox{$Zl^uZ2w=1-_~z4;d8H<$^) zuQP8#P1l=!ao=E$LQOZC8&Su4y=6KxyW-djE*P8*z*=0Tt4R0`;k?TgY z9Jam5`~h{p*?bmuxWya`3*2gc5B}RsZei{=Yarova|7^qm`><(r}+eQyUXvD)JsRZ%6u=`FBviGIv1! z*JcZ9_lRnV zT5NHx4RHy9g4Oi|tmdAq`_C1;TXB!Qlzf63#|4-hk7-ofdnr7##}M$Fae5+rx zHRCdwc6^IY5EIw)Fc@RTp@{-9V~l9Hk>?c{Gv=hYe^N2ed&m(pHue$ZFg!~^ju{ur z8u~qbktb$?U`x^*ln^ti&pzU4FtdE42bp& z_p&jPJjbB7XM~5A4QTOJg4{*(L+4P$Tu^78SM?9&&*@;p1y^*nR^Y`;QJCI!qh=@W}RZdP=D4BKoOl&2}9jY`~m@M+n5OA;&9 z<13K8dqleDWORPHQ1JDSYJ+(5FglNBj)9eQvk7U3`75g8H2n}0VLp!PN1B&FgJ^Ru zx>mG#7pmhjF9S8oJPdOh<}WD3ZH@(xX~u&ZVKD%tZW#wnR2HYsW{q?;R|s&2jo z3p&hv*u-fjqFY3mXF^b<`5KCgGRJ}HPRv6)v5At7KssW_3#dn2A29~RL_n~=$F)Ws zHC_*f9y=o`U-NKz+~cqA4FK>za%Lh*z7O~dk?1v!3@|mZ1~J|XSnTCUMH{>ueryc^FEIgVz2O(1zU-o-8E2<+gGKW3ysU(Zv>7XP)o<(&nj3e{)ZlSp#Fvy`Kge|yD=O>Vh*5<`own_tpIwz3=78V zx)j;_OE^HhLzgmp^FJ=eJ9Y02G;Kuht4NE`dtQwqjNXN4^!P|gOVT38K~}WEyKxzA z-V;z-=Di{E z@^y1Jyv|{c!ac%l2ih=uz?uB!FxV_$zK^EPHdmqr8k7FRuBs=Ufo9MgNy&8Qh@@pG zS2L3S0sqkaNpsP^wLsER+4V09C;Ju_Pdzq ztX3C1S&H8ix3=Np(lu|GERD5(!9i341_kv2THzygT_{j&l1JwKqp#FTgn zOY$tnJ#nxA0-h}N{jhK=;cb@}OI!uL33hZF4>GlbH1#lOH>3$$X9^ zR<&U=MUbMLIhiGMND#17i@6#m_Q$_r+zXti7Pt8KT;mWyiI*WaDN%Tq<4mBE`y`Vq zW)Q3t-z&x#g{+?G@Z?^eSh}C)IYPkeeiB>`&n4_azGyafgy(4#+siN549|bj0KH=U z*`T;R7ot|Z;sjuN4xyQP_3(2fjM4gYXSla7ZZ-HClQ~av#i}Y2cmieV<<9tQ;9O4Y z?PP#UE4{(ln^V8b8K^~GTm$%Dsr_ffP?>93W6odx?@1cSjoS!p+>-VnX}gi|{}Z=C z+Q4kMQ=eNLf_Q|)0Z`5F!otsg581*}l%b_WpzOqHp5MqXd}+W-sD9!UA*BcUgP%9J z1c?I%Vtnr_7fW1)+ZK%jhk)MiNk<}A1T#saR@z|hSi7-feTK!Z4ai5E^c^jh3-!cu zv7`++6WaGr71$OlJ1TV{>JU{TYkVW|LhS*oVbMUEQ1VF&D_$E7(gs|I9zCE|(B2Qx zm`&xaXMbBR=)aR5vah0Z1HONMu_Ek+6^5*&wDEwC(1ZHV6twYOXmVvdpacR3#)(=K zl3q?_7`3D0NLWw!pVI#nT2S;Kc~3>oomx ziMj&dWe90KRCZeFuPEU}zB#Zu#)*27K1xd)rFdxH=Ohm{{6KU$E9Z*B0nF9MX=!t8 zoL_b2Txa7Pt)+>h?va-=d&5L1*OIx}#+jj|ZMDgq)|Io>#yL_;+hybAu^Tp-*Vs5S zwY0}=oY!>ayw%1zR!ci*fw zV~VYu$tpL0Es8-{-|P?_$@=NXJF;fzNWLU&F^U&sn;j|aYkWoX5W+C=W^A)#G?708 z@hw8&DNIlL7Kh0h!nyt7^M>DykyPL65D%C0u!@_GkXnmxEA%Fbd>e>25k^#$*J}Ft zjySHT9NV)qxcjSL>(JBAN|oAh*qoLqs=>EuqVKhIT7kuBX;&#`j?dhJL7nxH?8CUt z)zOviR}6H(Es_DHkqCRgsE8c%Z&bu}kWu7xLt;mPs4>IwoNKd0h^0BcufJGjiHx=T#?K?ee_dpH0AB zNI!yXm*@2nL@q*@g&^DI1+2=E*|~kUYL^#v(Jt?UgY&O!mw!s+Em$VpfFRrDC0(?O zbNin2hChND>M!eJSp6?J-a#0CJA6+6m+s>#c7}mXVg%WQ^$vaHGWf<2Ou)nUQTYZ( zB$c}pEH@#f?alxe7boyoa7jPa(jB8Z@d~GnV#SM823^|&!TKo?_0p@Bztq3Q*0^ks^J5k=4HvqWZSj8fwAIs`m>NJ z*M4T!|AH_A*@hvEJ9ZqdF<${$1nNA5^lNNYWtAvgOIxO}qhWw#)I_+#yt%5J^!19I zEWJd|5i;v|nq!ZmJ3^^Kl~q5cd&lE}QkZ5V6j+ZSOmiC&w;%|s@ZtPLgt4^Bzm+r? zM_e~Tp$W8*w8969JV22(h!<8EiN#R{g0Mn3m$1SjP!}Mif2U-y!D$7)^}&Rsu(U#d zNEKE%T&T)Pe_fHgT7g-G5wuLnh-H*YSvmFZb?@Xp$_Qtp{J$UwBiw<+ZUkWjzD{}> zK^S4Ml3^L)Ht1tBLb@UkR?0;}yf8u*hPzP+!U*A9!U(5onN6F}FgcyaT5mF#k z7$Im?C4G=0cVmP?MRwrCA*q^`Q~y=>=E8G@C!B}!&p{AIxEqN(5rh#AB5?pg7@5z@CS8C{LAL}6)!G)NUjXqm5! zonEKNe>8#^9k!_s?9lv}SM*$mcRoIo5f$HzGWpU&RGiP(_aJ1EP1OD!K(8Z+(IFey zM_DEM{sc@)+|5dV@Hh&K(cvjoDjY>zNhniTV;QoY!R#Cz!nuSsnn2w`;r|XBdd{Jw zw4Dk|Yn%kB!WwyVWieX%eTw`?Ysik_A1OI?%}RxQ!r|~P?ypR92Nc+iAWU-*i8m31 zRh&Le`vrjTi7X|}@`*MmWb=v1ifoMz{UBagp%g+&5QG)Nxr7xi1oeD`^aV;rSD)yk zu(ZM?NEKH2+Nw(WC`JCG6~ySUOpzU$m9lc`hQqrwpp0-g%D)pq7~vfx{(~Tl5aq`Y z=m7{LG%0D85%xhJn-MNmWXlMHAYK@u970wg2qT1Z2_v+C+KiBXi;~gR2$c#;BTRu* zVT4}_Wdm#JTNJq)BkWOR%o8egipr^vba@;9|*z-A0qJqf-pi%9CkwxWOq<9EF-)E zeQZXUtjNHMYZ$}}BUD1bY6RIG!nlMHwu5>pLiz$l?P`QR3QHptK&mjpn7OK)^ihi3 zjS-e9GP;9Gg^X~r!&^B>8R0>ce?NjS!pBH_gdmLI>w#q!g6s}ThGm2|ppVT6mnyPl zgpm+0j8F{$YY=322;&k)xB}G65Ylf^)UHOTR9G5eE~E-0WQUBfMUlHP!X8CNcTlO2 z5xfrX+7xAkM|$9a2SFI&QzQ-}2qVPd3D=DvyTd4Hd&>y_fj%}Pe4xmd5l(`5VT4); zs6mk3A&g5H;c8H?L`eTmQM(%9afPK37DB2pLUhOouPbslM$kq}wH>ISOofaP@9?g} z+%Cq1$5H;H2*L=TArV9nM(EiS+nNZ%2!ls=HNyMQ$7X~PifkESBE$N`;It#Nlm9RYur{^7kSLBYcCzR|vug{jgt> zfFO*}GfSG@GQt} z6-EezjL<`6>c$A!itNy=RLBTJ9o~!5lo6gs`TG%s5xz&_TLfW*0eCV_Mi53QQ_?IW zaI?f_gla{$j8F*i!U#Mf|2za?gm5llgj+$q86mw%$>?f?0)?d~R70vTLR!cOrHcGV zBc$~hA=~W?Psb}%RbZ=#lU^27(5w3 zJMONiji_rBl^ap%k}8Xm8&S6_atp{)Lu4)OrA(>#vzVusOT5eZq}6(9AKS>AEb^-s zIsGR^rc|gSb{Hn+yRhcFU)M_^S=ZCC_!j#=_v_-F%cN`Zun2O-KS4g(McxPH&d_9T znVh_jxgOVD-0#t{PreJ0{**4Zf3hV}K_Cy^1CB4e;y5rrUkye_tOysVwbR=K&g92|=g%^7yY56rpd;|22Y669nmn`<*-H`W07Df~ z?L^`qSf&k8WHwc9h^(cJRrq?~OC{drym`JXNXwjN%a&QB0PaL;LvO(drVo?Gx4s}0 zr)Yqv@BAXuGX5~R-585?y$3-Wf0*2EoDIY&2!Sa*k!iTzldtc%FPmCXUP44aA}$5T z1qj1mgOd6PY5ebiI6|cGu1x7etFhs*0%7<#${Q(>uLAK9k@wQuMoHw4cu@ZcVR-Lo zAV*8&cX&2^4`FyMo?i7ViF60hA@RI6{2mNQ`WT7K#prV)Le5jH?M7W60o(B!$?qKw z4+Fb!-D>H{r(%IS7a@yOxu>ZpPXqf1Li#}^U?%E_hK<<1>pXUWk&VbPYPcn@M#>wE zwc-GTEK;RBMdANqI~^hYNLP9Mu{ose0sp3uw6#*&HQ>LJ(nyuk6y{zpu8#8vjPyJ3!WH9`lvXFDZ36!} zlt!wQrYMgA`w*oK43j1{K}KeSe;%cY5#?+-g!};hub5|R*E|u};fA$G)*ur{(&1~M zkbaKLbPASZxd>T%LMkZA)xchkkomAp9K;*~^dz)1udZ#TMQGZqA!(bWv`X;vzmjKVfGTUMD7ON8Bc<&Q(@kz% z{Tnj?uQSjGf{+Rua2owWq51Y-$G8Hte0MVoqa-g5USlP}<$Rz}A~7*K&IK>Ovp7*- z?5M;2RJvMPrOL*M{4|+OH~=PQ+3nK)r8qc>)e4Rx$kNh|+VB_I@NuepkpIR`eC8B| z=kFZSCaF3Tcup{h88fX&;hFgZTjq7j;$pa+;^c7qp?JBYZy1S=bRDhnVJd$zGgSm5TW;NhDEj6UH@`3Xu)Va>1FjdOhVM~B@fPp} z@e**Vc=`8&c=wkpzBGH${0y0=@qN1fPpM&mkD~|?c&mRUIvkQLc!ZpxePc2uY4CFPbhl(SmOF(zDq{POdcb% zosCs;{%{4*>EMYj=k&Qss&)z={q-ICHpNn*2>Q->$T?Hgdx!WwW(CA9L=b01={tt( z24_MV-eV$U-k`D{i^EpU0}9{@?RO|m=f@I4OM6NYC^`WF1o*gC@T$V|2N--Zwld`Z zqzK$75^K=Yl;Y?qLZmk9@&xF=OfKD6+1WznWyx%+T=5dNL>hO_RgvJFsaLaxN-i8x zT>8#?ku~3~(END@bkJwxcMIIYw7&K)39;XXh4tuNAbo|}e}ORiE&v%PA|JwB{>vGB z`8fAk5m$~mfVdHavk~S|;x=TFmz6@(hzssmVhMj3><=Oc;fIiTA7KK8PoU%p6q|Xy z5+(XvTCsw-e7hWSlqrSrZksQQ;4rg&6^EJam0J1+mE(QnV5KwgK9T);kHU+i%#cyv zSqk$M{SK18pvaA)^`f~6h_IwzKVcyC8P3~y-g0>>1T9G8@CDwOB&i#ToWpQa2osn; zbA!qwJ#nUjenl-EC=f0PZ8MiDG%K$ys=&P%0gH^E(UcKdZ8Tm{%Wv`fVF4Tfllb)+ z;_V2$vanG8Kl>Knh_ZcwZ+&~e0ivS~uIr-IxA&DutoHLiYr7@^h^qA8j7W3w zW0R;g$^8%+n#qnBZICO9ADcu~IlmGZ9*`JqOvF7V12-(?R z6B1*%mnKef9Rtc3Axss}{4`s%!TeDaW@I?WlP*3XF(&#S1K{E(B*r9pe1eOgkQgVM zthd3>kFb0+PVt`%7S}QI&5XYQc~BinEAX*ysV;s(V$2dihKrw&7_)^oV_p1&#F!)U zXS;qwhcF8JX2JWYvD7;ulZAEppT z{7Q)G!NrqXix)qc?H9iYDEtDj9!OmA;uo`7y+uWxyXqvi=beaMXPC6kJ>a)_S5JP$ z?^-HIFNBfW;CODAAn_|@d#+nRG9`)spUO^J4N{CG^@`|9S`U&(lCr``pM&I0@=?RW zFw!iN5(VkZFw!L?`333xFw(zC@(I!nT}k3_23K+9A{eD7e|YLE5T5yRh;1tJPb zCx3YAyDEu#@#B1txG^W%h(=CyVRX}{>SPEX;PjnI;%A+?LJKF_jnA^lgT&euPh^nC zvk!Z5C1*8NFYy!3k?~|Rp7_*Zr zZJf_`<$Te`nW-gjwQ(Np%K2s|r^D#bdbcW8S{WZVqNhh_@8GUR|0*^5?@?^xR827c zgyE?BFT*3U!SX+S9v-QVW?4l(c{Gdlc{owl2jT1%>dWtl%aM-a?e7TDRL3b{Zlnmf z2S$o(0a#yyUqBu;aj5UW&mwsedTivgx-46K2QK7B!v}<%{+jrjF{&teA0>48n$aaD znCP)aKDgbSV4}ynC`q2d7d=4$5zu4+XePN_VuCSN`_E<;PB2ELpM_~|PB6xrjKfefyho{t63MDi2}AVTy022!K$8gJ}CmvG-o#)%#&tT!1axeg*%g!>dy zPBwl3V7OZeOg63ukm}|xnJZgRhPp2&kYikfQVV?P6fh-$su$=!KcPI6N)+n8H^?|E zl2s_ueb*71Z&HUPx-W-Nk-^5U)U_z)*bRzmct6KhuP?3|=*HM@!RyKO#EpV<&&fSs z0L?I1=^p%bzJXuoe}p*xa$vNc{E5GNTrAt{Hw&vs{E5Dqnnb>MB;)B>30mUY5Fax- zI+6Hv3$MlA!%}lRJ^6Eg_h^xeM@qztrcL5Fs{3^23GX}l&N~bYB;py;C-N0ya4A4Y z7SE8rROV_0+>Fqx0l#4N$FWie0PaWVEp{$rePo-7W#?ps-eTi2PT)QS{4#M_P*Lw8 zaLSBGSc6|(w|h6sd#BuZJ5a4L`s5C&ZvCJXyP(2KvIeJ~_z zoRD=n*e|6l5~VCjcp1nSDC_PpS?h!>F~=~AsN;Acizi4q5e7Df^pb=eASWRVeoeJ3 z>llIDtmoM%qb;lq;Gl0dIL>7mYXIrlk+kDE5VM|TkSO()gqMKa&oaKZ$y%k#fGr0N zok*ebIGIlSHiF~DP#HO*jNW)s>4`9~EmVdioC#zZ!XR<%v`v<@?=i3^V5FD&P7$&m z2mAe$l|jNj9vkZ4p;{EV|~99F;+XvD#q z)mK4zQ)Y0~4b=wswBf(B@P;;crVZ~w*Fga;=T-ArXIz6fDKf{&lP&TtMNYw?MCTy~ z_exW+i2+Lu-k~yZxLqPM;1{>#gbxjQOHrxnkS`SQ0QEjmRhy&kr81wh?j>+FC<3{L z9JB!#)n`!08XtUDJBnqXTCg1GFCSO4RYSI6_~6BNIo&T<{-?Fd@s6^Yhw;yOaWbbL z$Au`-*(wjO*#JG&5jS0rwTU+F0k3t64bu+(*d`()c|X#|XV_mn82P0CWf-;9f#DO&3tA`v4(bLK*Iz zl;@DpSa&pOP6=hZHxr7KP@elI%8!yzp?ephXbCNLn}l3?!i~^(nR_K=ntD7Z=vD5g z33+s(e~tSA%8Zpzy?X?q9=eC!YJN@$P!1wx~A zHz$>c-A_v`y74SZe^xX9LuIb>8u?&KioC&KwO!w1-jBRdm_T!_X7-ze z1q@ddbKfQm;Kzdok9LYS*v>I*cpjqXk-aurG`PUNVPSiK_W`hZiLsWlJAv?h4Y3h- zB#UK`Q3{NGaT~Qf_7Z^-3tF*&jV?<505ZBRVC4>pG1fR0eNF6;80t66U{zRnZ4vA|`@^*6JtO1~+4RVPEtgFho0%?40i+gM%RB_>u z3j#i#uHe2U=_aN>hH>40m+7Gaq#u)Mo>Az2TBPMEPu@?!?wAcLnCpAffN_mDt0o>D z+{Ir{n~h$kVdbyD$7B12m3^AS1*zsH=Wjqq67PG1TpsZaU=OY&W7LPO@$!fSJvOQo zY(2k0qv~41L}d3g2<|OFC(_P|^qNG1s2|@}CEW^U_d=m=!UUwfiDGqnCv&i#T5=VX z^bL>bOXxWZohS^M+#fl7aS?q9y>HR9WPTUv)^s8D7Ym(8&*-HkuZ1Oiu84$E2#>=g zFN^8THLnY6-oYVM0@u8L`B`hWg_Yb1C9zO7P86_^Bo1j>Lhk@9_L8XbT4FnEo51C+ zf3(QJL!_kseR$TRdtTh%Leio!F@Y{8S~Oh(Kbh?zKbhTZVO5U4-22eC>tw8% z%px%;q1wFkod+FfpkL;rRh-+~&JBoH zY?q5yY;S;m0AYZ5uX2SMfqc&G_n#Z6(DW-sXN*UW@FEO6iBhiYK~Gu%_zZ;pLy5b} z$I|cu^xQ$a(AiIdY6IVgLG|4|NO%B@cT<2kbn-4Q?UamJIu>D&A2aNISUC(_!)o2_ zVurIoXh28_fH-hpC*egPpG6paMmCVR=$%8vjyixrhfw&dl(m^!-RELE^~BuXgY)|d z&TQOqHNy(26SGBHV=ww1S%zRKQ3rg1!e0qI{~Lu8enOeTGxOlX3ZR=Jqsw{BTG?o+ zJ10xQY~wSf0KUl*Y+oq0A=sjUiHGn=#)r?^K+yVeo<6*gKFRk?;=Q7*dQJ#-v!gZ5VS$dV5_U>TYdV2eWU1$H)Z3876^lO zftVh0ymMk{vG^SL8p7a26vE4LymOHr#J;#Rl%vtZ923BO5_1?V5tltj>WxYa2hFpP z6K-bIX+|K$z%MTbKcc7{Xz76G(98I?ABPp7PW+T!c9)AHS=4$IvlbyGn-$&%HeC|- z0r@m_C<)Wy94eGr1H{0qjoAQiRKCgq*y;vIb?`V3RRj;W@Ua zE>Zv|m3{wLckcoo*Hzt%pV8=yXY{ruznw&W#C9H*Uy1Xwljvnzjx3Eel43h{j%8_N zDUmdaW|Y_|C8dhG(awJX_BTTO<#eQM*{^4mzGC>w$Ku2X(^B9 z{?^)S?Q`bL8AW#a_v`ncPnxs#I%}=H_S$Q&{W^2DA;OGiF7f~SVFu|;h6PY`h}nhCe$Kk{30D*k?(=&MK`cQ)=3j-&N>fQ>mY`T4RX z>W^)Od17tO#r;A!214W=Lu9*vmabzvxtTtXMns=e=7MJWv081WJJ(RnzlzHJDgI3} z-MNi$*SDeR;@>o@UEHiH9voS7=pqQdo?Go(ar@2qzo@a9aQu1&>=v&Bw+$nYgb9@fepTYm7Kd-~1XQEd{GyEPW@=Qak%jhi@we1)n;Qz7}NTdPuP@)cr zI{_5%e{qe}HT9!p?&MElPkMr8)!M`!zrEI>hBK<}D8}nTz=9@6)yPS=&wF7|$ z|7+NMm$Uiaf%LBtj%|23+c4gVpak6I+hKtb(I%!qqpG#J?zf=VWoZ!M>`3Pz{x9o5 z;$8r+$N$B539Gf)&(vmz12+3CgamCCXu)Ksjqeg#8mfHX(5B-0$bhrqLvowVxakox z<4S(&B2L=3+_8)h{N9Sqca7x$uN+1JTG*>rt|Qpl^mXD|Vz|ip4cC$Y*AnJ(wtVCQ zyay5gP0p6j;qnOv@$N&KbH9MfmpS+UTy5su4t$(G2V&+ry#^loi+X)*cs~7R?SEznLUqmA?ul;Yku!`Pq1AYsku< zBz*(PDxxxzB&_o9B9>i1W0n6!qULR6tn#-Jm2M*wm9FwH2D~7-7vDAhYuXksOZ+cj zYENN&9;@ZowAFSkhN89nnzmYgOIbQ*=5TikUO&ejW2vg0kY5OUl1xfm@ zaw~x3*R;`NmJ5>fUA2Oc{F=6l8A;z&n+ZujY;~_?Bz;$P5RzWg=Jqm@zN-!qlHRoD zjxdtGtELFauSPq?Ncyh2kC6Oow6`*nzN=^hq57}T`MU36Bz;$XfROxZw5J(K-&HRV zl3$JXDMr$F)fWlLuSWX@Bk8;9dkm8Cs_2DNcv}!*D`2_>$;AZLGDy>es$~q`K;T*i z`w47eaGbzv7<@f}9SqWLaaAvaG`X)j%pfh3Rb?4`i9mW@b3yVu1pX4h3cSk3scu?& zBkG7>4^=I%hjIzOVo4A1i)v~%q`m^;)YpNm*;r3V>=!p9aS10XQ{SUImzuXhITfqc zAaz;%{oq;DP_rp@G5p3bw*;9t2bs4R=2Ro`Zsj{QPQ~X*?zZ@MNK$3Z6{!J|lqTer zOz)&#A~kk!0&gpPFQu-EZw1++Wi{8N{*>r1BKm73=XI30PI9iK%yx1jnMzS&m&rL* zM~Uk>(a<#Ho0O#I?qUD8tH{_SD}~hQCi*bTBi_`qz}| zjMEmxqNbWXspYPc>@_5*2T4fR0=iLY8qOsEx#3{pAxn4nRs4wflQrF`L&X08A@?sb zRZ&fe9!^x^89qw&#;*ifl3zJj`@ZBK5)Zv{uJ-+AhdOl!LPG8Da3YzaSI*Tw$z^J2 zq*u<>exTt-2l+LQ;FWW=ALK-1ie5Qa`yoy=IZeshk5$kch!;PF3~QfmV)zAupINvL z_$8w=PVKXGmlDa%FlX)WE!slxR)U{fvV-7pfoTp@~h;F$g!dJBgtO|U+QEG@;)kNeHoog?f>CKW$F{Ch1x%? zBtzin$%fibRlJ=Ppx5QqekMt#cbb;fzEp7yjI@|uk5~J-Mv7RAk0Y?ue%`?Jdc4}d zGBCX!ul5TDUPv@wY-0M=WY{mIjzHjIdOcq4U-KH?V*2@h?UxNq%lEZkS(rtxi|O@v zwf~njSWK_StNp5h>GgQEe^W(^oQvN|%Kz<>y=0o?8GLW7{qy7ygruk?*M7E=3h$)o zRe7~vtE7T8pf7=8F0UI(c4Oq$I6OJx)E!Sg1_~$j6WpmA=0s(ReoIg{!ii++0c2E{ zO+Ey&np77mr*3q?88z&Z}#wc++CmK`# zMu`c|rz!O)B_=tsEcH=JOmU{GQin)%j(ILj-9?FMPF$2~rNkY~)13M?@ysL-pls_L zmrT(>Wg9}8+fB?_N^5eBvL?5T-jG%=Z%DfzMEnAIX9ca_G;CZ(TjY-t_MHy4bevVR z;hZ^pdW?jYQEkp4RUFXC4pU03}ENQgi9V9RAnvJCj-$ zQ5%atc<((Iy+AZ=%c%P3$7IdCCEZ?6dOiyJhw;Dm<4`bGi0y2{6`yrf$Y-SJu{3(< z*v(5kTrPe)Zu|@!Kf%AVzFA1f!f(Ot4Q9f-ftGOeEle9|ZRW?Xch+Ac#8gQiG{muO zAf>MZyzO+0Q2aJB`3n(;65ux8!?gara`y@Ff7jf_lo$fyRlDEhtpB{+CdaFS0O*p6 z-=&k@_1~1+Q@Fjx+~x`Oj|5Mixqg+DpZu&xW!z$AsCnr2q~0FU+n4MXZps3=lX&OS z*=87TZR|O~@yEe)*^gEe;tGNhT`6u{t`BvhgJ?tTRyrLBQ8HqugYqE4IVQqK0MqBI z7o*qvPaUxIWDF9_p-TElCuy8W{xZH@d8G3;z{QVr^5IE%r1Jr~Q}jruIXnrEbiRyR ze0Y*R(uqPsq(0J_NRl;DL}d<7!Xuqap-hs-;&`O<-ME;;lkiCAD!QSEClQrP4_4xl z&hHa(at#vIe^P<3_bzLpyY%|a1xuG*hr2F~uNJIWLY8yiz6NsY>L~)apCq`RSDhDp z3T=HlwIE4{b0pt_8>!@?gAn8-3zSGT@{Qzul&ESP!JX#hKT)EZ6C0BsphOKPwj|#} ziCRuvo%{+V>Ns&@@((Fd&xyUsDM~aXZ-lgT&GgWSlUm4$zU1%H9gGNY=V0<~N;Gof zSn^wxXi9eD&RC5%>ZBGgXs^H{H%=`P6Ie&_5DyYjEoP9GpJ4G5IJI(A)ItizRU6q> zcY$WnX%II$3u!UE`V~h18llOS&O$Oy%?11zERD?<(HD$9jhjvM&|<7EMucJC#HmET z40NonneMLNix!9f#mB)NtJ^}jVWd6$FPT{lTzwNsq+qplq76CVLrT!_oDZP6_a0~I z<8tE|Zd_$AVYr1ZhPttu?JEe7@PcHo#)PsCjQinZ4%l zeE8kaQ0K!0>;CeyfbIS=O|2sLmmei6-Cw4J-Cw4J-Cw4JzrRc=e}9=${{Awh{QYH0 z`TNV1^7of1|g{_^`kR!10J zE;$2)?k`gwp+%D;xQ&I9&(WReg_7&gc;;LvX#_zXQQ3u(DPoa@l7EL}yHJuRD&0*c zDqSeq4R}HF6k!&x1$|%By8tir3VFMuOiysv|6l>4(NoI^HqTwueH37F{aUzd^_9t$ zmEb*qMD+T+4}NsUK0oto<~n?x2Jjbge7eQC<|> zQEpsIWHo>{ENGmFIkg5sT}x}QwwzqOJKPvawNVYYu`QS_EglMCH* zmIn7-m%6z3ECe)tn)6zA1bhu2Swz#jCi;TWe-*bI_W)nr#P8wf*O@W%43Uv;QjA90Y_7e6ReFH#v7fA<2hn2K$Kp|9Yw<$qIet~ z!lU?K$sI+*HZD5Se~tfD?|>e$g?tv!_AU&g@W1M9gj>`^_3|jvXYhZ)i-c=rH=rMo zuBxMSQ$5MLd>=;X_+NED;TALQOGy7Q;rOAO{ z#Z4p2+_U9b& z<^atTf~RQJ{40UP>i0^5Lfn1A@p_WZc^n{vt$t2uC^-J6(7cXmQ0&!B9Y$@+^BF(S z)f)wW1@M3Ayg@ zD4D1SEddJ}WAVCb^ni3H21K$pKzKFUQ|c-fP}KHd^+Y-EPJpG@|RmC z$-0PXg&Qb-#nRtxLuKHJao;3V;8j2*wE~3b0?mzzcOepv$r_KG2WY|ETpb%&N<^!R z#G0(u&`mV-Sgfm&Xt0dl)kxjO#bvdI#iAUj*hkcf;7^slKe@)A*n!|a>PJcAB`FcX z&VkWJW$gNDax~w+syD(g(j z7%U<>#-OHTljdM)$ZJ-ItI&A%8jD8Oe(PBa4riUc7ic@aD=>qRvOcPPY_RMiy2#Zx zLP07Zmsq^`lAsq~8fZ0_=?x5`T3~m>N9q>I+$7B>D6^%=5XR5(%7Q@NDC(9f)5gGv zpGEu4XPdT`?H8aW_r;!Wa|f0ltUl!4%r$$3>uhZl-LEulXU7#j>Z)omnbvkSfut)r z1l+{R0emQv%(@Pv)s@>|8!oXAD^;V9wd&QfNd^TO3sVfEf;dsbFlN*imIdYPCke z8b1ownr^bLINEZUBuh7FxLX)>1PU;VG=^cD2%Q8lO+gY0Ig7&;V1mVxs9>=)OlvC( zaWm+X;u6a>B9N9V0-<81-+IrnXOiKK-$5m`hzqocN$sRkVVhTlZC-6`UZXazZ7ho4 zbu7U3d$al?0q7(eg61TPFD$kj4VM-bqi7(5X|gpX$6aIeCR5Mlssy!#6R>nt#C$p` z!kTRH*vHlknI3zpet?j=SrJiR#iJM#hF3{=LJ&azR*i9liH{csOOC$H+~Khc`Y{d3 zI#^LhKz|i*gNqeSO0GX|pYWZb+-N222&IirDwaDXYK_%?JI2^WeXLe}tj;)2eb5MI z?9rgUq>Yh9ntO|i-K%KiLe7IT1;c}t#X5fS2eQ&L7?;6<0CfS&qI}FB#uoX?3i6|s z)?xUb5w^-AIS+%ppconI&GA#r64xpk=TMBHaZb3qprNgg1JRnGEyYRKsvoU09k+ED zxJ$r~Hu$cyG3Zj41bz2XjsDdd{mJVt3ycmfw-n^$=;x(N*;L$*ntrS$Sa7^xskmF) zLiO>Q{fX~mREOs_0Do982)MlEB zN56}M!4fmEAUOhAu)4|l{Sa2%52ZTU5b$hrXPG71aW@vGq8t*Q?6*W3g3reT=N!=# zyXP-p4FOJP*ARlh#2w&EhY7e z7pM8wqnKrv2B=Dk76<$)ImfZff75;n9afUZDM1rZi}c4RVnbu*l2J;a2cZ{O&vT4o zge{M1yCsd?RvuNx%82Tt<(6<$y`TsO#V+)lB@bB4tgqRx&(*>>n3*H2NPFR#tzaKL;RH}AYGXt zxRSqcVl!hJ>H9I)#U-BjUHU#vUvx8WXyOT?i9hD9OH4tJ#wGxDu6x8?_ZHWEIKlb5 zZt`0$)=B1{e`7HJCnf*47UyqhB>yMfwTaJ~{MVZN*Zvphe>LU5WU+g7B7Q2i&#h`N zxXEWxfYxB+6IiDHW40AL#_uxKig0@OBA0DBx34yGrw2wNjX znB2(hL^g;CL>EYy$>wvjGs6^xpkE+`obtKh+p`6aG&Q#gF*GtV5r9$yGO-$^#0x9ensTpX4XVjY>q8xDt$*8!8su`iM z2Qd?{S+H0z;&5)HsH8=a5oI0E4HqWBZHs2$iQLqQprrixiK!u?LoOOJQJ9^_XU8WB zo5tm*ckXrs(5E&?0DW_u0%&6++V1v5{LYwL9k1HwZjax2hM~vYhFxxQ6Pn&FZVfl8 zXb&08w-BNe@xP3r zO(SoFO{DHd-f2YeyODExj(l$^&G#(jTTS`SQogqwVii+~S&lKWWFL^93*`@vy9;tZ z&FcKl>qc1}(x!HwTisenF0#eH)ve@^?%{kT#eL?Zaq?|MDh)chOV2Q`~YHE z6NJTYO`btGj3F7n^&oOz8*Ah=3320#U;L{PdLpGX!cr}#B7_sYaEDoPW8I(OAJ_sUD%Reyk zkxVyL(Zm49o09w7g``d5fyNIsx~av;rQTg}9a!2l!>Wo6F)C#NmqI`}`Xu{cZ9+XMMU8wX`)w?K zL9zwRF&=rx5>~^rBhc0pNgA8Sv2#M-KjiN^{(cR+C(ba{%k=FJ+C!#3XQIRp zX$VU_-48j)HvACa7o@X^w|)UpJ9pi6o7_|f8d5!Xr>S_=PKy5j`g+sX0j_u5x4YGc zzX9H{M1xy>z1x6sF7>guatm`4pCpmglXbH>V_zIaN7G_E8jPWvMiG?bZ>+%VfMRhY zQnl`??e3MAxU1d+#zRf4m7BPqx@L^3F$+q>-yPc*izS{+pu?y^UlQ9z$gjTsMb5=d z*slE@QC+Nw_V1W&lFmM!Iw*Pk)_?N)Vc|cwD@DFo{2y>WhTGI6z4Nz3lJrSWx+#oS zXzF(xO|N6IFC??B79>6QocR>Tt80y3UOq#)u>lf73nilK6+`my2&5F54$C zsgA{;JTKmVX5Q21%4l5*>I7l_R*JC-L=B9&gY}T#maxX-izX!_^jZ2nufN1DOwyP+>#&3I! z?oQCWr)eK8G3-JNZgi73Bad5XGGxk*y-1jv`VsZKZ6im~lV4BhYJFjZT?&6{v@v76fC#%{pP3lh&Eo!sT7 zy4=`iATBzHff(j#&Q&r`Lu;mTt%Cf;o1tbBI@0hjzK8~A$qOJ0N#sjwEes~n*;*KL z5(@|M|5^Bfh`howdjd4&;P@hpUzhUe-%Xe~7G@`jm*elc=M1@sv$GGJO$XK-Eaxwb#nwt znyt|aL~Y`%yAsz_;yaT0+iBGyNeeFD6AW?yMGg9H$|&&$`i{uS$5on=f-W>EHJb;- z8-V_cmM7veVZ$R&=*h_ZC{*=n)K+}W7Y?Br#;aoPW~hcmnWH@Usv||QrbXY$f_ph# zTVRX(#bO^|`z(sj^5lQ*P9DQ@X|SXYV&5RK*C=DjWSw*^zhYBD9^Rgz+=CXT`xRUe zvj%~sG#|55C+v6dG_dSkkn9kiva@p)O@fIiG>MF2l*%CL`7aGTi8%>vHJQ4iBE%Bc z8_czoFhgX@&dibz0g4aMSJ^_z1b1FD=1@6I*ez~ zD~Myi&T;+!{{K9#b4NPQu#PPw-bO{V)!hQv=(Qt!@kU2}cq~hMEC~1|0|3Nqy^T!5 z`vHLsRrCho-XgF=5ZuQJcOv24gR9X)=o`1=mv9f!h;6@mESTR=c-F3$qw9~K;GqPT zc9T5RUXJksq7wE6(5cj5AX~f1t=aN6w>In6T<^xvlO*0k3m{lnejy?;^G4=9svzn)WmPz=hnq9`GQ-s6Cia{@yl#}yjBnKPaE-=CituL zO_(cS&(f{(-0S0~&bU>{U!uiqOre+jkXPZW6Y(5IHt0nf-A3r&FzYtnOy3J>8T%$G z3iU7#Q$O-DO%_s#H{54B73$$0;`_Cj4KyXbc5I)! zsDXQK7fbEbF}5ULcDLfX0V{IItv0rk6*!nku69?`oTrxOLDYjj#1o%}C*6gc-G=Lc zz+wT2D@bJ%Z%G~{c)e>YLy*i@*v+KrdtV~H5Y{n;OFTwJ@?zW{H=ABQ5nnnALerl> z+;PsRV%;L*PNImo#^W0?481-%$<9IU5vsoLnzgR`G2KG_)2_su#44W+7Q z_%Y1wX!jgWzM^6yt&~t%c`V*Tel`1IB1u)8N~|EdiYA2Tam0_fL+>j4Z!_#Z|G8Mv zJ$_ zH}P2-fmR=Mchbly5gVN0qZzKNpnEodXyEi-K^5#Jiaib=dT z_98WvKf}}(@eu1Y2yW67%_0~tqb4=>BCXBvVo&35xlLHZPsFn?a@MhU3;MNq-#*B0 z*@;+%!2K+(zM4=i>#kUHhpfiacvD~u$JJ9~%gu8v^cv53{g*V8(&Y`E$@SOvM>sr>lVC_UZ<*Do4%r^u< zTLTX+q3}aXA^xhz;zaCB5rUQ{T4;>XD&vH6UY{_7#Paj&z|`3v>m+F4b#6Rn?In?< zecO1VfxmesI{zsz@leL+wT+$_HH#f&YScqDwn7u}WoOI|G+OrY(^R~8l17ha(vJ0Y zGlaZ|@9^-Mq0m>^8GiANfFX;QBx1B85yRs*Jc-9th4)!cVW1cs*)t6>23Jz%u7=Rk zh}iB~Dqw;}oY*3#P-mwPh0`=ePr$=)69eiaRM3jRfI7Y!Q!`7i(|P!I19NgjEZpsl z>)4aBMc8VTv%Z@ao zJK^AScWx?+B~HN(j}6TP^zufs$7fH_5C{XA+ziVNh)rh(`n$Tlj?T7$Jx+cMk2M#| zPoL`L$C|yaeE-aBRt2X|bEyE$qmU!n8gY*2p-@vfZz4B5 z#3D5#+_C1IotT)+p9mO`?6#+8pt*hPRn1$sv}|kH+PrPcwyj&XZr{?pu@8pbJ5-Px zo3?JfqCT6oJ0aXJkydw*13Z-oDP(4lmu;)r+s* z{?7J(2cwo99=!H08oem{EOOe<8#iA2Ocp)iHqw9|EM+Q`X=Xe(GhR65-8D8oJVwQX zE`yzYnXcX*@Oo`{R&#_c#&>$Uy&lqT(lZ-Gc?Y$wPklQ1iJTFTIn>j>x39OS>u_g> z*VP-=xv#7L5c1jE*)h=F)w9P)MmgF$GnwALjMv-K-Q(nkypf^85VATxl+PLwUHdzG z2l_L1=EB&v)(mO~YNxxpyxvTg*WMc-@n)wCuQ6Gsb$6$?d!VPi9|g?to!0)&gI)b> zbe$1-Qz#fDX8K|0{aPQhUVdWePTTs-$PwmL8yY20Cn%qm9PwkiFwJ)ccX(ah>FwLR z-rc*mZ)23yVdIznb|A2jdkVQB*< zh3X;QQB^2dKh>6B6qMMTnHZlOFCY$@2bP_#-Mc*~(4Xn-4l32riP8_W_xJXBUHzSX zt-it1U1?+RwsxkwF&v=>A$7gaw zBertkd)=fevfb0u+D&!W*WI-rzS`Z}ep3c516gNfV-`=zV}R>nv0!p2e>*D)Yjw4Q zJKfpdwHw~m=gcAo!2t`H1F^DzFrD(?P&3kSTL-Ddc4%JkBFfOOJE{RTep}{Bn5wO7 z57`PA>LP4IrCQb?Le!bg{oY_}_dutuYP9~=KEGj24w*KRAq!igZQcMp%|z|K*(vJt zP28q(tKq~X?Y(`S93&~Yv5M?J(lp?y6ef&ht*Wb;EZy7P%_9jsaqmn{AcPJf{Ik}F zGW}4zqqV=4nb9!ixqWYBLaLDt%CgncLotk5w^0?fc6WAk?q(m{KY*HSZ_RL+_C|*$ z$0trPq4m}6oa{%m9JJ+U-`nbek(>`b1Eizo=qF03In*}B6Qb$)wVks(Zpx^wqZH zb?oVD-S6a0j|D}^r)hvigxkDyTgF41Y`=;0L4%Rt0pCTJ7pMRV8(wRFe_vPIK)(c# zR_$AQ2Qu2V>?T~G8sPPQRgF&#%^2qjwFjSM=;XF5jqgb7p^!L`4qAn@Kz9J0Z4Bt^ z+|zlm$j$e6q8g}FC}l@yck3Z)y~fK4)!o~(2f@!o8fp?f_%gk{b*%Ax&hN5YM@D3b zWqWa;x~2;4xhb?+3{^<1L{09hOhYa98+5UqR%s0-)WxV9YEoLwgs_5v`sbwJ1ZQodyF;qV3259lX(i0wv1P-hGqTFV@TK?W9&o z{oqWlkQ>e!haNClqKz;||KXlighv@Jp=YG=0*MNDsDzWs@%mbO_Lv~vI+)&jDC4DD z_mFR?6@n2D-9ee{7`kWBHW(@f+UyI>xK$m zW*|+yE}}A;^-W$!JKl>&w~IO))8;8`UoBSRHj&}TWcrL?#OI=D9~B&LP$T7R((qvF z>pU}-C0ljvIWW)}49)4*n|riPnn4a`Rz-sxX-P7&LqjM4Lt80&yPb*8p=zM zG+Dy^*+py{%VO3eOHn$}(O;ZVo~;A@z56>mx>}uy3GRy#zqn%_9>OHbR?${eSs*SG zes6E4-?J`B7*uDVu6-x#%b1p`13(HaJdPs;`UZM>w2>h~w5Io>Z!`1*!9h{`-mdNr z(*)Id_h(FH!a4)yX&aB`hB65uje5j@+Hy0<2~k!?-)!{O?r!d{pziT3k7sCVG0SFU zgOHql4B?B4S``1yz}b<$xz#9-7TDW&NGB|2f?*~nREKRe%AYjOKpB|8(cX6`-H+0= z+xcp7XEC1lW@e|RXo|&-HkG~03;@!nyxl|i)uc?*M3db%5OR;E!B_&OR4CI>MfCKx z_L)Z6PH`Q>m~PCpNbyN;d}NpsSK%Fj_8Z%X6S8k~+LJB0KBU`bp9fMt%MIORn zWH;sm+A{k3X|~LaMbo5%W+3a2`91!5A|eO|S5w27c^h?GI}m(s#>@^EV`svfKx~JI zOlut14&F@@FKX_6y=?=C6h+f_l7-0JX%;1$+Wom$t zyN%f6MRWL|`MP@ccA`>Do;-lfkOSj4tzIjSZ}|>llkW-W@G$A@&x39yF z)7U@kAV7RuR~q)KqxP7w)1-$t2Hc6~UCj(+o0qwn=d4t$h?(v{PuIaB<7;L3}$9drakwy z-fS#nmMw~wWhSjJnZU<{m?^_PH(Bcyjc|jtUer185W#O_DCM5c{ylvj#?-+HA2(?o zM%osQ2@HZ;k-W}>={^s`TC+mHk%N4^*LXTvk>_Hvo`glb-G2S|_4E9nn~mwtc!>pZ z67g2+kH--_CdqUjkU^8SS6Zg>s|~Bf8k#Vs%ASPl<S?$h2~*^DQqa)KC@n|Id}(}CS1V-nc2lQxa%N$CcW!19PC@Fl z_4T%5P=RSbWh3O$Ay{ufOGw`wYzgE%Je6Vp!{T!rw`1dedt^jnX3_iMikQBzc&y-I zHHK=~#!Xo%lZ^d2Y->jc4WKzp_GIs(u7MYcwQJx>24$+v+&EIWwzyE-%@=t>J62S@ zn>)Mq?CtkEf72^=_U!e#wq9-4D-Ei>HnVWC)s=x?>2W($KE+NsCeC1wiaw9})%LZMmuus5EgNg{2-DYdl!5$bkMdim+B-D2|i zH!7YyL80_PF#!!2U8El65gHhp-@upxZPwAE{t^bUhl*jQ2G|lnVg$cjAT6>EVs?Sn zKnt^Zr*J3q#{kO9Z}Vv6Eb{zc+2Bzx`aKJiY$Xv0n)n{eA*|s_HRs*AgFs&4g`(Jh z!%7Vh#7R+*Xhjn&ip@^&%>r!->71=2ORwPAok4pXqWc(rs}!szLj?8)oGf)V%$vv3 z8g1nv4<1u_`K_!x(o6zV8g`TpN(_Uwke$KSpxLhTyxb_ZHytdeAwmz0V8;SOJM3U$ z*p)t&$=)%Wof^)1d$NVDLUz*e^0YyDe0J2q@E9h;*KiK2vmS<}kboPq5Nt=%*p(YE zppkg|0vd009G1l>#=k#3TYy2qjE%V4p@~`Y?M3fK8(L<~*96y|-2Uuj4$iMx(agjf zo}Ixi*~IK*k-{0?g~&~eQ2C6<@XV$)&+{6RfK9~PQ7NNvT-u!@6zghCkf%cO1I^SP zv~4g|9>Y8>rlEitcsZWSKqbKbU+*w9!>f55GZ@(Bcvt{~A&?Clnw7VCQVMpO#KN0d z$-*78B-hK0`R2X6+h@8h%!43})#t92`&DWqQ?rxUrY-Oi3Ot2SRQ7!K4)zJ}&cYBj zzSShys{jFS8JOlvu1`;R6W(l$23(+>b~!Vo{?;P{wgE?X_has`-{lujg31%jnl)Ebh767XdWvB`B?|y6d@OD1BTk0zH^vf zAe5cC6P`0PTfk;BX5m983oIH-=GeIKW-%3^4Uya|qV1T!!d}>Z%~l6r+nnKXjOs*z zfa1=?G@=v@9I;-8={?$Z4zEfAP{980w1;wIDKnr8mO8MZ9JuQZ7iK2tF*<07od!4$ zpon@z^hY(SDXdJR7>ZARY3znmT>XTpgBfgA<7t81G*!8iC4)(2z$jBxNPKOhukBcb z%@yFo*k?O6H4H&B<3kg^JSxkb=$)t(7~W6ihqL1-zGHeNTHc`cVW@cFMXHvBIpm9Z z=rJ^s2T3+NHhyBvDdY-66Ej1T&g?WlhvIn%B2(E3-V296B5aJpV?mI|V<6cPf|!T7 zNlgAO+VE1g*$G;3!Q_MF@-5nx!vkidRyIrQrTq{{U|qCuydVsY-akeWB|}fUQFmdU zZleZ(D)KUjR)&Bcd-F!dXDCDn4Pi#}+#v~hAD$M;%>+57(Yz3{i4>3jalo0Rm_LoY z3W&xO(k13)%;P9%aOPo@-4yL)uWs~Ot5p;t((mpGFw$OX` z@VQE^F=s%*{~@SK=bvK(XAN2STiLI-?EMOMS=4}nL5ckl5)TT?uw}*3Hl-i>r7(T| zOs>#bK{|zsK6B};?4P#m{R;lIMGYtzlo+KmOejnTd}amdS>e}GhfYkU&pnnst>C*X zYVZ*O>4anY#GF9`_}^6Yd*$}IbKWnUZ}BsjN1l}XM~f$++D8kIk77B#q6KstJvJ}8lb;}$h|n}BcjQHH$udxhM-*pj77cP5k#N43`_S_#BnR4U%~q=YEZ$)1w~T(&uZ!;KB0oa?90>wp0@Qr zs)q7&U1;JZD?hE^zgyIxg4OsX4SkqZ6DtJ?k5=%2MGX!LxDPKWEss!wNj2`XuKSa<+ z=tmm(pmaYMP;Ca33RGJJY;dVK7Enfl|4lKS8kl-ZKZ10naJHOP_)kSY zIG=)_v#3D@wG_lznstehfWm782h%CI*`kgrs45Ys)j@y?N2&JtjA5e0e26L>mGGcQ zf(ibvya~e8bC^I$JrPU=QFz9xomTLdEb6F&s#3HGlMpn_P~hTC+7fb|x2R6$iK$^>yoi|z;bqow+)D2*;Lr-Ufs%!3Gl zMgseL|JI_8DyS+&n?R|4;xm?-U>-yj zj!Nj1VH8ZTrF?4%Q_o=nC9N01R1k&N*fCpLK{`;4KJaG+Ri$VXD3uOIA{)Xyh+>Wo zW8UkVU`IIv6#QF@I;xsUGne{lXh4 z;mm`m!chs|5=k(@qVir4rk=wDO8OxYOa)Q+1*>*i!OvLKQ3X|{XcH*aKlqHLCYT3N zg`*O#lwl4`u&lfZ!qjt^KuKQ>m_Xr0k{P@}!HX>FsDi3evd zf-va{;C94#4 z=Qg=LYsu23!er4=mnM=)O1Ut@6!g!C= z8zwS11(84Z%N3}H1pFf(H7UA1DOXUb%+l*2$>nEO&0Cj9hJMDkYR322Ty8DR_}0>l zb>AYC@1>I2PMc|3LA5C+^W~)nJSa$%4P|hu6xA?`bxVOE%Q%6#h*=YXyJL zq7EzgqM(rRDir>WCJ8qxGI6 z3aUeqF!d<43nHoGgsliY73uHuuV!u`YPOGOPW^D zFMLJ|f1NFSMnNrbbm22vc=ZV)Q&_DKbF6=;6<{L;=Q0RhX`%)p(*(CltViU<0F?77 zfxlyo;whL77)9X;-zW;c&Z0bRHol!awbOoUCr`l-3yRdx2BgM`;Yls2Pqoc!*h$+C zktwVOi#hXZM{b2!%(tL4HFc*b%l*Jw?x=#(0n3%z_+Mux+O46F22O26u(E<0Op1c!23wB93Tm8SIwRHp*7p4Wbv`cMbQO+*2tVOi z7BPZM;l+OE$LV5-5jGYYy@n9A3sAm243xsDcIU@Q<+XJ@C2PiH+w$IKk)~lU z;OhldemYai+#khUemX-MIL_B3qgQ+#6nxd9G77r#w2)CtQ!DK=L>UEF7ZGZz-6vGA ztBBC(H1~I@#{H_XW91JjSZPrM3aZ*vi2k#hT4f0b6>PSs0R^=rhF4P^mT*wPy%u#q z!C+CNtL2Og`uwK@2Ne8)&Z_2k*i+db6I!ehDEL8(8c;AOFgANbNtXsZO6z z!C>~$x?{OQ*p^uKw1OKfs$Vm?-beY`8z|-|(G5ZrUFuT0GD{BkN)vy?HuV7ozbL5a z97vzXgyKDxIj!K67Im1 zie`Wcf5+EU!FtgJ!yyG#dE!*<-y~#&Q~2#ZtCr~JK1#vGQVinM$K-e0QXaWYz}E%H zL`!?&Tl5lp{%8Sp>_dVT9uXe6w1Tf#l&4@+0ks5}0w@^NTw%X5N=7_9@lD`mvGkGU z7u9u4lptLdew*N+tAanVs9P0`(sj&QIjx{y7q_bVU$RuUDyXH1u8UjeQy1LM)#dDR zla}+bKsgou6I-aXf?Ab`pbAEn^OvOrq^`p1e6X5=e%S}L?EhfPKB(Z_WgnbR*=1?N zxkE~{-Rif^Hk4-qIMv3_dfQoT)tX<0v+b;ivloq}nvX`WN+c}Z4|7ml#q;M6E#*{Z& zQ(X~3<^Qfh%^cA*feIIfMl;&b&yBMZJ(V^)pR=U##GH}^|0Gr+&X1E;1hfhGd6qbB z#887)1kR6>^3+)G=XvDkQXT585ffy}^ZYm|gvXuI7VDLE37$3$?Ghp?*@d`-6{l?u zgkgsVO2y3)x@i@`O}t$U@EdYPMQcZORC*_%R4ctwa0>e%1&XAj@_hAfRrR#gfh<(b z2=}7W(gQE5m@ja!RN$cM>I)oHUDX6pI#UVXEhYbewLp|K)+X>p>eV~S<12ThRJkMb z)#SsX!Q9dUN-$^)Rp}^KV9cdntply-G1rw+&wl-sjJYVIEz%QQDOU`76?|TT_gsTs zWxw9C_bYgdMGYtzlvp|vCOZvFo^O_G+)3Vh_DM-7J0sajfiF{B-5dPhk~z7D!k3B| zjD=OaN{${LL;8Ip=htmv(hBAN~6TFl>C9) zE4W^&V=lK>_BzYnui$2j8c;AOvDEFYF3z6{tB&*Fm0P71>yLWLpA~+m+ymLi1^lFs zS}w)V%%fdgax_M8oNFZgq%Fiz1wSgNxN}eI+3!mNrwxhujw)C!D5lfI3ZG8FOM-Oo zk;Jn;oq}34mZ#;@!Xfmj>i!kFRfwDjlO7416vJ<|= k0|)CpyJN!?m4Ro-||OP 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 literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/_psutil_posix.abi3.so b/.venv/lib/python3.12/site-packages/psutil/_psutil_posix.abi3.so new file mode 100755 index 0000000000000000000000000000000000000000..70bd2966a422d59e8750d4c1d12e833add3d065b GIT binary patch literal 71640 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 0: + percentswap = cext.swap_percent() + used = int(0.01 * percentswap * total) + else: + percentswap = 0.0 + used = 0 + + free = total - used + percent = round(percentswap, 1) + return _common.sswap(total, used, free, percent, 0, 0) + + +# ===================================================================== +# --- disk +# ===================================================================== + + +disk_io_counters = cext.disk_io_counters + + +def disk_usage(path): + """Return disk usage associated with path.""" + 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 + percent = usage_percent(used, total, round_=1) + return _common.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] + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +def cpu_times(): + """Return system CPU times as a named tuple.""" + user, system, idle = cext.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 + ) + + +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) + ret.append(item) + return ret + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + return cext.cpu_count_logical() + + +def cpu_count_cores(): + """Return the number of CPU cores in the system.""" + return cext.cpu_count_cores() + + +def cpu_stats(): + """Return CPU statistics.""" + ctx_switches, interrupts, _dpcs, syscalls = cext.cpu_stats() + soft_interrupts = 0 + return _common.scpustats( + ctx_switches, interrupts, soft_interrupts, syscalls + ) + + +def cpu_freq(): + """Return CPU frequency. + On Windows per-cpu frequency is not supported. + """ + curr, max_ = cext.cpu_freq() + min_ = 0.0 + return [_common.scpufreq(float(curr), min_, float(max_))] + + +_loadavg_inititialized = False + + +def getloadavg(): + """Return the number of processes in the system run queue averaged + over the last 1, 5, and 15 minutes respectively as a tuple. + """ + global _loadavg_inititialized + + if not _loadavg_inititialized: + cext.init_loadavg_counter() + _loadavg_inititialized = True + + # Drop to 2 decimal points which is what Linux does + raw_loads = cext.getloadavg() + return tuple(round(load, 2) for load in raw_loads) + + +# ===================================================================== +# --- network +# ===================================================================== + + +def net_connections(kind, _pid=-1): + """Return socket connections. If pid == -1 return system-wide + connections (as opposed to connections opened by one process only). + """ + families, types = conn_tmap[kind] + rawlist = cext.net_connections(_pid, families, types) + ret = set() + for item in rawlist: + fd, fam, type, laddr, raddr, status, pid = item + nt = conn_to_ntuple( + fd, + fam, + type, + laddr, + raddr, + status, + TCP_STATUSES, + pid=pid if _pid == -1 else None, + ) + ret.add(nt) + return list(ret) + + +def net_if_stats(): + """Get NIC stats (isup, duplex, speed, mtu).""" + ret = {} + rawdict = cext.net_if_stats() + for name, items in rawdict.items(): + isup, duplex, speed, mtu = items + if hasattr(_common, 'NicDuplex'): + duplex = _common.NicDuplex(duplex) + ret[name] = _common.snicstats(isup, duplex, speed, mtu, '') + return ret + + +def net_io_counters(): + """Return network I/O statistics for every network interface + installed on the system as a dict of raw tuples. + """ + return cext.net_io_counters() + + +def net_if_addrs(): + """Return the addresses associated to each NIC.""" + return cext.net_if_addrs() + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +def sensors_battery(): + """Return battery information.""" + # For constants meaning see: + # 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) + charging = bool(flags & 8) + + if no_battery: + return None + if power_plugged or charging: + secsleft = _common.POWER_TIME_UNLIMITED + elif secsleft == -1: + secsleft = _common.POWER_TIME_UNKNOWN + + return _common.sbattery(percent, secsleft, power_plugged) + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +_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 + + +def users(): + """Return currently connected users as a list of namedtuples.""" + retlist = [] + rawlist = cext.users() + for item in rawlist: + user, hostname, tstamp = item + nt = _common.suser(user, None, hostname, tstamp, None) + retlist.append(nt) + return retlist + + +# ===================================================================== +# --- Windows services +# ===================================================================== + + +def win_service_iter(): + """Yields a list of WindowsService instances.""" + for name, display_name in cext.winservice_enumerate(): + yield WindowsService(name, display_name) + + +def win_service_get(name): + """Open a Windows service and return it as a WindowsService instance.""" + service = WindowsService(name, None) + service._display_name = service._query_config()['display_name'] + return service + + +class WindowsService: # noqa: PLW1641 + """Represents an installed Windows service.""" + + def __init__(self, name, display_name): + self._name = name + self._display_name = display_name + + def __str__(self): + details = f"(name={self._name!r}, display_name={self._display_name!r})" + return f"{self.__class__.__name__}{details}" + + def __repr__(self): + return f"<{self.__str__()} at {id(self)}>" + + def __eq__(self, other): + # 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): + return not self == other + + def _query_config(self): + with self._wrap_exceptions(): + display_name, binpath, username, start_type = ( + cext.winservice_query_config(self._name) + ) + # XXX - update _self.display_name? + return dict( + display_name=display_name, + binpath=binpath, + username=username, + start_type=start_type, + ) + + def _query_status(self): + with self._wrap_exceptions(): + status, pid = cext.winservice_query_status(self._name) + if pid == 0: + pid = None + return dict(status=status, pid=pid) + + @contextlib.contextmanager + def _wrap_exceptions(self): + """Ctx manager which translates bare OSError and WindowsError + exceptions into NoSuchProcess and AccessDenied. + """ + try: + yield + 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): + """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): + """The service display name. The value is cached when this class + is instantiated. + """ + return self._display_name + + def binpath(self): + """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): + """The name of the user that owns this service.""" + return self._query_config()['username'] + + def start_type(self): + """A string which can either be "automatic", "manual" or + "disabled". + """ + return self._query_config()['start_type'] + + # status query + + def pid(self): + """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): + """Service status as a string.""" + return self._query_status()['status'] + + def description(self): + """Service long description.""" + return cext.winservice_query_descr(self.name()) + + # utils + + def as_dict(self): + """Utility method retrieving all the information above as a + dictionary. + """ + d = self._query_config() + d.update(self._query_status()) + d['name'] = self.name() + d['display_name'] = self.display_name() + d['description'] = self.description() + return d + + # actions + # XXX: the necessary C bindings for start() and stop() are + # implemented but for now I prefer not to expose them. + # I may change my mind in the future. Reasons: + # - they require Administrator privileges + # - can't implement a timeout for stop() (unless by using a thread, + # which sucks) + # - would require adding ServiceAlreadyStarted and + # ServiceAlreadyStopped exceptions, adding two new APIs. + # - we might also want to have modify(), which would basically mean + # rewriting win32serviceutil.ChangeServiceConfig, which involves a + # lot of stuff (and API constants which would pollute the API), see: + # http://pyxr.sourceforge.net/PyXR/c/python24/lib/site-packages/ + # win32/lib/win32serviceutil.py.html#0175 + # - psutil is typically about "read only" monitoring stuff; + # win_service_* APIs should only be used to retrieve a service and + # check whether it's running + + # def start(self, timeout=None): + # with self._wrap_exceptions(): + # cext.winservice_start(self.name()) + # if timeout: + # giveup_at = time.time() + timeout + # while True: + # if self.status() == "running": + # return + # else: + # if time.time() > giveup_at: + # raise TimeoutExpired(timeout) + # else: + # time.sleep(.1) + + # def stop(self): + # # Note: timeout is not implemented because it's just not + # # possible, see: + # # http://stackoverflow.com/questions/11973228/ + # with self._wrap_exceptions(): + # return cext.winservice_stop(self.name()) + + +# ===================================================================== +# --- processes +# ===================================================================== + + +pids = cext.pids +pid_exists = cext.pid_exists +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 converts OSError into NoSuchProcess or AccessDenied.""" + + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + try: + return fun(self, *args, **kwargs) + except OSError as err: + raise convert_oserror(err, pid=self.pid, name=self._name) 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: + """Wrapper class around underlying C implementation.""" + + __slots__ = ["_cache", "_name", "_ppid", "pid"] + + def __init__(self, pid): + self.pid = pid + self._name = None + self._ppid = None + + # --- oneshot() stuff + + def oneshot_enter(self): + self._proc_info.cache_activate(self) + self.exe.cache_activate(self) + + def oneshot_exit(self): + self._proc_info.cache_deactivate(self) + self.exe.cache_deactivate(self) + + @memoize_when_activated + def _proc_info(self): + """Return multiple information about this process as a + raw tuple. + """ + ret = cext.proc_info(self.pid) + assert len(ret) == len(pinfo_map) + return ret + + def name(self): + """Return process name, which on Windows is always the final + part of the executable. + """ + # This is how PIDs 0 and 4 are always represented in taskmgr + # and process-hacker. + if self.pid == 0: + return "System Idle Process" + if self.pid == 4: + return "System" + return os.path.basename(self.exe()) + + @wrap_exceptions + @memoize_when_activated + def exe(self): + 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): + 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 cext.proc_cmdline(self.pid, use_peb=True) + + @wrap_exceptions + @retry_error_partial_copy + def environ(self): + 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) from None + + def _get_raw_meminfo(self): + try: + return cext.proc_memory_info(self.pid) + except OSError as err: + if is_permission_err(err): + # TODO: the C ext can probably be refactored in order + # to get this from cext.proc_info() + debug("attempting memory_info() fallback (slower)") + info = self._proc_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']], + ) + 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) + + @wrap_exceptions + def memory_full_info(self): + basic_mem = self.memory_info() + uss = cext.proc_memory_uss(self.pid) + uss *= getpagesize() + return pfullmem(*basic_mem + (uss,)) + + def memory_maps(self): + try: + raw = cext.proc_memory_maps(self.pid) + except OSError as err: + # XXX - can't use wrap_exceptions decorator as we're + # returning a generator; probably needs refactoring. + raise convert_oserror(err, self.pid, self._name) from err + else: + for addr, perm, path, rss in raw: + path = convert_dos_path(path) + addr = hex(addr) + yield (addr, perm, path, rss) + + @wrap_exceptions + def kill(self): + return cext.proc_kill(self.pid) + + @wrap_exceptions + def send_signal(self, 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): + if timeout is None: + cext_timeout = cext.INFINITE + else: + # WaitForSingleObject() expects time in milliseconds. + cext_timeout = int(timeout * 1000) + + timer = getattr(time, 'monotonic', time.time) + stop_at = timer() + timeout if timeout is not None else None + + try: + # Exit code is supposed to come from GetExitCodeProcess(). + # 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 as err: + # WaitForSingleObject() returned WAIT_TIMEOUT. Just raise. + 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 + exit_code = None + + # At this point WaitForSingleObject() returned WAIT_OBJECT_0, + # meaning the process is gone. Stupidly there are cases where + # its PID may still stick around so we do a further internal + # polling. + delay = 0.0001 + while True: + if not pid_exists(self.pid): + return exit_code + if stop_at and timer() >= stop_at: + raise TimeoutExpired(timeout, pid=self.pid, name=self._name) + time.sleep(delay) + delay = min(delay * 2, 0.04) # incremental delay + + @wrap_exceptions + def username(self): + if self.pid in {0, 4}: + return 'NT AUTHORITY\\SYSTEM' + domain, user = cext.proc_username(self.pid) + return f"{domain}\\{user}" + + @wrap_exceptions + 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: + _user, _system, created = cext.proc_times(self.pid) + return created + except OSError as err: + if is_permission_err(err): + if fast_only: + raise + debug("attempting create_time() fallback (slower)") + return self._proc_info()[pinfo_map['create_time']] + raise + + @wrap_exceptions + def num_threads(self): + return self._proc_info()[pinfo_map['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) + retlist.append(ntuple) + return retlist + + @wrap_exceptions + def cpu_times(self): + try: + user, system, _created = cext.proc_times(self.pid) + except OSError as err: + if not is_permission_err(err): + raise + debug("attempting cpu_times() fallback (slower)") + info = self._proc_info() + user = info[pinfo_map['user_time']] + system = info[pinfo_map['kernel_time']] + # Children user/system times are not retrievable (set to 0). + return _common.pcputimes(user, system, 0.0, 0.0) + + @wrap_exceptions + def suspend(self): + cext.proc_suspend_or_resume(self.pid, True) + + @wrap_exceptions + def resume(self): + cext.proc_suspend_or_resume(self.pid, False) + + @wrap_exceptions + @retry_error_partial_copy + def cwd(self): + if self.pid in {0, 4}: + raise AccessDenied(self.pid, self._name) + # return a normalized pathname since the native C function appends + # "\\" at the and of the path + path = cext.proc_cwd(self.pid) + return os.path.normpath(path) + + @wrap_exceptions + def open_files(self): + if self.pid in {0, 4}: + return [] + ret = set() + # Filenames come in in native format like: + # "\Device\HarddiskVolume1\Windows\systemew\file.txt" + # Convert the first part in the corresponding drive letter + # (e.g. "C:\") by using Windows's QueryDosDevice() + raw_file_names = cext.proc_open_files(self.pid) + for file in raw_file_names: + file = convert_dos_path(file) + if isfile_strict(file): + ntuple = _common.popenfile(file, -1) + ret.add(ntuple) + return list(ret) + + @wrap_exceptions + 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) + value = Priority(value) + return value + + @wrap_exceptions + def nice_set(self, value): + return cext.proc_priority_set(self.pid, value) + + @wrap_exceptions + def ionice_get(self): + ret = cext.proc_io_priority_get(self.pid) + ret = IOPriority(ret) + return ret + + @wrap_exceptions + def ionice_set(self, ioclass, value): + if value: + msg = "value argument not accepted on Windows" + raise TypeError(msg) + if ioclass not in { + IOPriority.IOPRIO_VERYLOW, + IOPriority.IOPRIO_LOW, + IOPriority.IOPRIO_NORMAL, + IOPriority.IOPRIO_HIGH, + }: + 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 not is_permission_err(err): + raise + debug("attempting io_counters() fallback (slower)") + info = self._proc_info() + ret = ( + info[pinfo_map['io_rcount']], + info[pinfo_map['io_wcount']], + info[pinfo_map['io_rbytes']], + info[pinfo_map['io_wbytes']], + info[pinfo_map['io_count_others']], + info[pinfo_map['io_bytes_others']], + ) + return pio(*ret) + + @wrap_exceptions + def status(self): + suspended = cext.proc_is_suspended(self.pid) + if suspended: + return _common.STATUS_STOPPED + else: + return _common.STATUS_RUNNING + + @wrap_exceptions + def cpu_affinity_get(self): + def from_bitmask(x): + return [i for i in range(64) if (1 << i) & x] + + bitmask = cext.proc_cpu_affinity_get(self.pid) + return from_bitmask(bitmask) + + @wrap_exceptions + def cpu_affinity_set(self, value): + def to_bitmask(ls): + if not ls: + msg = f"invalid argument {ls!r}" + raise ValueError(msg) + out = 0 + for b in ls: + out |= 2**b + return out + + # SetProcessAffinityMask() states that ERROR_INVALID_PARAMETER + # is returned for an invalid CPU but this seems not to be true, + # therefore we check CPUs validy beforehand. + allcpus = list(range(len(per_cpu_times()))) + for cpu in value: + if cpu not in allcpus: + 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) + + @wrap_exceptions + def num_handles(self): + try: + return cext.proc_num_handles(self.pid) + except OSError as err: + if is_permission_err(err): + debug("attempting num_handles() fallback (slower)") + return self._proc_info()[pinfo_map['num_handles']] + raise + + @wrap_exceptions + def num_ctx_switches(self): + ctx_switches = self._proc_info()[pinfo_map['ctx_switches']] + # only voluntary ctx switches are supported + return _common.pctxsw(ctx_switches, 0) diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__init__.py b/.venv/lib/python3.12/site-packages/psutil/tests/__init__.py new file mode 100644 index 0000000..5d4b3ab --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/__init__.py @@ -0,0 +1,2025 @@ +# Copyright (c) 2009, Giampaolo 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 gc +import importlib +import ipaddress +import os +import platform +import random +import re +import select +import shlex +import shutil +import signal +import socket +import stat +import subprocess +import sys +import tempfile +import textwrap +import threading +import time +import unittest +import warnings +from socket import AF_INET +from socket import AF_INET6 +from socket import SOCK_STREAM + + +try: + import pytest +except ImportError: + pytest = None + +import psutil +from psutil import AIX +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil._common import bytes2human +from psutil._common import debug +from psutil._common import memoize +from psutil._common import print_color +from psutil._common import supports_ipv6 + + +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', 'SCRIPTS_DIR', + 'TESTFN_PREFIX', 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', + 'CI_TESTING', 'VALID_PROC_STATUSES', 'TOLERANCE_DISK_USAGE', 'IS_64BIT', + "HAS_CPU_AFFINITY", "HAS_CPU_FREQ", "HAS_ENVIRON", "HAS_PROC_IO_COUNTERS", + "HAS_IONICE", "HAS_MEMORY_MAPS", "HAS_PROC_CPU_NUM", "HAS_RLIMIT", + "HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS", + "HAS_SENSORS_TEMPERATURES", "HAS_NET_CONNECTIONS_UNIX", "MACOS_11PLUS", + "MACOS_12PLUS", "COVERAGE", 'AARCH64', "PYTEST_PARALLEL", + # subprocesses + 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', + 'spawn_children_pair', + # threads + 'ThreadTask', + # test utils + 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', + 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', + 'process_namespace', 'system_namespace', 'print_sysinfo', + 'is_win_secure_system_proc', 'fake_pytest', + # 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 +AARCH64 = platform.machine() == "aarch64" + + +@memoize +def macos_version(): + version_str = platform.mac_ver()[0] + version = tuple(map(int, version_str.split(".")[:2])) + if version == (10, 16): + # When built against an older macOS SDK, Python will report + # macOS 10.16 instead of the real version. + version_str = subprocess.check_output( + [ + sys.executable, + "-sS", + "-c", + "import platform; print(platform.mac_ver()[0])", + ], + env={"SYSTEM_VERSION_COMPAT": "0"}, + universal_newlines=True, + ) + version = tuple(map(int, version_str.split(".")[:2])) + return version + + +if MACOS: + MACOS_11PLUS = macos_version() > (10, 15) + MACOS_12PLUS = macos_version() >= (12, 0) +else: + MACOS_11PLUS = False + MACOS_12PLUS = False + + +# --- configurable defaults + +# how many times retry_on_failure() decorator will retry +NO_RETRIES = 10 +# bytes tolerance for system-wide related tests +TOLERANCE_SYS_MEM = 5 * 1024 * 1024 # 5MB +TOLERANCE_DISK_USAGE = 10 * 1024 * 1024 # 10MB +# the timeout used in functions which have to wait +GLOBAL_TIMEOUT = 5 +# be more tolerant if we're on CI in order to avoid false positives +if CI_TESTING: + NO_RETRIES *= 3 + GLOBAL_TIMEOUT *= 3 + TOLERANCE_SYS_MEM *= 4 + TOLERANCE_DISK_USAGE *= 3 + +# --- file names + +# Disambiguate TESTFN for parallel testing. +if os.name == 'java': + # Jython disallows @ in module names + TESTFN_PREFIX = f"$psutil-{os.getpid()}-" +else: + 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.path.realpath( + os.path.join(os.path.dirname(__file__), '..', '..') +) +SCRIPTS_DIR = os.environ.get( + "PSUTIL_SCRIPTS_DIR", os.path.join(ROOT_DIR, 'scripts') +) +HERE = os.path.realpath(os.path.dirname(__file__)) + +# --- support + +HAS_CPU_AFFINITY = hasattr(psutil.Process, "cpu_affinity") +HAS_CPU_FREQ = hasattr(psutil, "cpu_freq") +HAS_ENVIRON = hasattr(psutil.Process, "environ") +HAS_GETLOADAVG = hasattr(psutil, "getloadavg") +HAS_IONICE = hasattr(psutil.Process, "ionice") +HAS_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") +HAS_NET_CONNECTIONS_UNIX = POSIX and not SUNOS +HAS_NET_IO_COUNTERS = hasattr(psutil, "net_io_counters") +HAS_PROC_CPU_NUM = hasattr(psutil.Process, "cpu_num") +HAS_PROC_IO_COUNTERS = hasattr(psutil.Process, "io_counters") +HAS_RLIMIT = hasattr(psutil.Process, "rlimit") +HAS_SENSORS_BATTERY = hasattr(psutil, "sensors_battery") +try: + HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) +except Exception: # noqa: BLE001 + HAS_BATTERY = False +HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") +HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") +HAS_THREADS = hasattr(psutil.Process, "threads") +SKIP_SYSCONS = (MACOS or AIX) and os.getuid() != 0 + +# --- misc + + +def _get_py_exe(): + def attempt(exe): + try: + subprocess.check_call( + [exe, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + except subprocess.CalledProcessError: + return None + else: + return exe + + env = os.environ.copy() + + # On Windows, starting with python 3.7, virtual environments use a + # venv launcher startup process. This does not play well when + # counting spawned processes, or when relying on the PID of the + # spawned process to do some checks, e.g. connections check per PID. + # Let's use the base python in this case. + base = getattr(sys, "_base_executable", None) + if WINDOWS and sys.version_info >= (3, 7) and base is not None: + # We need to set __PYVENV_LAUNCHER__ to sys.executable for the + # base python executable to know about the environment. + env["__PYVENV_LAUNCHER__"] = sys.executable + return base, env + elif GITHUB_ACTIONS: + return sys.executable, env + elif MACOS: + exe = ( + attempt(sys.executable) + or attempt(os.path.realpath(sys.executable)) + or attempt( + 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_testproc(cmd=None, **kwds): + """Create a python subprocess which does nothing for some secs and + return it as a subprocess.Popen instance. + If "cmd" is specified that is used instead of python. + By default stdin and stdout are redirected to /dev/null. + It also attempts to make sure the process is in a reasonably + initialized state. + The process is registered for cleanup on reap_children(). + """ + kwds.setdefault("stdin", DEVNULL) + kwds.setdefault("stdout", DEVNULL) + kwds.setdefault("cwd", os.getcwd()) + kwds.setdefault("env", PYTHON_EXE_ENV) + if WINDOWS: + # Prevents the subprocess to open error dialogs. This will also + # cause stderr to be suppressed, which is suboptimal in order + # to debug broken tests. + CREATE_NO_WINDOW = 0x8000000 + kwds.setdefault("creationflags", CREATE_NO_WINDOW) + if cmd is None: + testfn = get_testfn(dir=os.getcwd()) + try: + safe_rmpath(testfn) + pyline = ( + "import time;" + 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_testproc([PYTHON_EXE, f.name], **kwds) + wait_for_pid(subp.pid) + return (subp, srcfile) + except Exception: + safe_rmpath(srcfile) + raise + + +@_reap_children_on_err +def sh(cmd, **kwds): + """Run cmd in a subprocess and return its output. + raises RuntimeError on error. + """ + # Prevents subprocess to open error dialogs in case of error. + flags = 0x8000000 if WINDOWS else 0 + kwds.setdefault("stdout", subprocess.PIPE) + kwds.setdefault("stderr", subprocess.PIPE) + kwds.setdefault("universal_newlines", True) + kwds.setdefault("creationflags", flags) + if isinstance(cmd, str): + cmd = shlex.split(cmd) + p = subprocess.Popen(cmd, **kwds) + _subprocesses_started.add(p) + 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, + 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"): + raise 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. + """ + while True: + name = tempfile.mktemp(prefix=TESTFN_PREFIX, suffix=suffix, dir=dir) + if not os.path.exists(name): # also include dirs + path = os.path.realpath(name) # needed for OSX + atexit.register(safe_rmpath, path) + return path + + +# =================================================================== +# --- testing +# =================================================================== + + +class fake_pytest: + """A class that mimics some basic pytest APIs. This is meant for + when unit tests are run in production, where pytest may not be + installed. Still, the user can test psutil installation via: + + $ python3 -m psutil.tests + """ + + @staticmethod + def main(*args, **kw): # noqa: ARG004 + """Mimics pytest.main(). It has the same effect as running + `python3 -m unittest -v` from the project root directory. + """ + suite = unittest.TestLoader().discover(HERE) + unittest.TextTestRunner(verbosity=2).run(suite) + warnings.warn( + "Fake pytest module was used. Test results may be inaccurate.", + UserWarning, + stacklevel=1, + ) + return suite + + @staticmethod + def raises(exc, match=None): + """Mimics `pytest.raises`.""" + + class ExceptionInfo: + _exc = None + + @property + def value(self): + return self._exc + + @contextlib.contextmanager + def context(exc, match=None): + einfo = ExceptionInfo() + try: + yield einfo + except exc as err: + if match and not re.search(match, str(err)): + msg = f'"{match}" does not match "{err}"' + raise AssertionError(msg) + einfo._exc = err + else: + raise AssertionError(f"{exc!r} not raised") + + return context(exc, match=match) + + @staticmethod + def warns(warning, match=None): + """Mimics `pytest.warns`.""" + if match: + return unittest.TestCase().assertWarnsRegex(warning, match) + return unittest.TestCase().assertWarns(warning) + + @staticmethod + def skip(reason=""): + """Mimics `unittest.SkipTest`.""" + raise unittest.SkipTest(reason) + + class mark: + + @staticmethod + def skipif(condition, reason=""): + """Mimics `@pytest.mark.skipif` decorator.""" + return unittest.skipIf(condition, reason) + + class xdist_group: + """Mimics `@pytest.mark.xdist_group` decorator (no-op).""" + + def __init__(self, name=None): + pass + + def __call__(self, cls_or_meth): + return cls_or_meth + + +if pytest is None: + pytest = fake_pytest + + +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. + """ + + def get_testfn(self, suffix="", dir=None): + fname = get_testfn(suffix=suffix, dir=dir) + self.addCleanup(safe_rmpath, fname) + return fname + + def spawn_testproc(self, *args, **kwds): + sproc = spawn_testproc(*args, **kwds) + self.addCleanup(terminate, sproc) + return sproc + + def spawn_children_pair(self): + child1, child2 = spawn_children_pair() + self.addCleanup(terminate, child2) + self.addCleanup(terminate, child1) # executed first + return (child1, child2) + + def spawn_zombie(self): + parent, zombie = spawn_zombie() + self.addCleanup(terminate, zombie) + self.addCleanup(terminate, parent) # executed first + return (parent, zombie) + + def pyrun(self, *args, **kwds): + sproc, srcfile = pyrun(*args, **kwds) + self.addCleanup(safe_rmpath, srcfile) + self.addCleanup(terminate, sproc) # executed first + return sproc + + def _check_proc_exc(self, proc, exc): + 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 assertPidGone(self, pid): + with pytest.raises(psutil.NoSuchProcess) as cm: + try: + psutil.Process(pid) + except psutil.ZombieProcess: + raise AssertionError("wasn't supposed to raise ZombieProcess") + assert cm.value.pid == pid + assert cm.value.name is None + 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 assertProcessGone(self, proc): + self.assertPidGone(proc.pid) + ns = process_namespace(proc) + for fun, name in ns.iter(ns.all, clear_cache=True): + with self.subTest(proc=proc, name=name): + try: + ret = fun() + except psutil.ZombieProcess: + raise + except psutil.NoSuchProcess as exc: + self._check_proc_exc(proc, exc) + else: + msg = ( + f"Process.{name}() didn't raise NSP and returned" + f" {ret!r}" + ) + raise AssertionError(msg) + proc.wait(timeout=0) # assert not raise TimeoutExpired + + def assertProcessZombie(self, proc): + # A zombie process should always be instantiable. + clone = psutil.Process(proc.pid) + # Cloned zombie on Open/NetBSD has null creation time, see: + # https://github.com/giampaolo/psutil/issues/2287 + assert proc == clone + if not (OPENBSD or NETBSD): + 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 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()] + # Call all methods. + ns = process_namespace(proc) + for fun, name in ns.iter(ns.all, clear_cache=True): + with self.subTest(proc=proc, name=name): + try: + fun() + except (psutil.ZombieProcess, psutil.AccessDenied) as exc: + self._check_proc_exc(proc, exc) + if LINUX: + # https://github.com/giampaolo/psutil/pull/2288 + with 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 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()] + + # Its parent should 'see' it (edit: not true on BSD and MACOS). + # descendants = [x.pid for x in psutil.Process().children( + # recursive=True)] + # self.assertIn(proc.pid, descendants) + + # __eq__ can't be relied upon because creation time may not be + # querable. + # self.assertEqual(proc, psutil.Process(proc.pid)) + + # XXX should we also assume ppid() to be usable? Note: this + # would be an important use case as the only way to get + # rid of a zombie is to kill its parent. + # self.assertEqual(proc.ppid(), os.getpid()) + + +@pytest.mark.skipif(PYPY, reason="unreliable on PYPY") +class TestMemoryLeak(PsutilTestCase): + """Test framework class for detecting function memory leaks, + typically functions implemented in C which forgot to free() memory + from the heap. It does so by checking whether the process memory + usage increased before and after calling the function many times. + + Note that this is hard (probably impossible) to do reliably, due + to how the OS handles memory, the GC and so on (memory can even + decrease!). In order to avoid false positives, in case of failure + (mem > 0) we retry the test for up to 5 times, increasing call + repetitions each time. If the memory keeps increasing then it's a + failure. + + If available (Linux, OSX, Windows), USS memory is used for comparison, + since it's supposed to be more precise, see: + https://gmpy.dev/blog/2016/real-process-memory-and-environ-in-python + If not, RSS memory is used. mallinfo() on Linux and _heapwalk() on + Windows may give even more precision, but at the moment are not + implemented. + + PyPy appears to be completely unstable for this framework, probably + because of its JIT, so tests on PYPY are skipped. + + Usage: + + class TestLeaks(psutil.tests.TestMemoryLeak): + + def test_fun(self): + self.execute(some_function) + """ + + # Configurable class attrs. + times = 200 + warmup_times = 10 + tolerance = 0 # memory + retries = 10 if CI_TESTING else 5 + verbose = True + _thisproc = psutil.Process() + _psutil_debug_orig = bool(os.getenv('PSUTIL_DEBUG')) + + @classmethod + def setUpClass(cls): + psutil._set_debug(False) # avoid spamming to stderr + + @classmethod + def tearDownClass(cls): + psutil._set_debug(cls._psutil_debug_orig) + + def _get_mem(self): + # USS is the closest thing we have to "real" memory usage and it + # should be less likely to produce false positives. + mem = self._thisproc.memory_full_info() + return getattr(mem, "uss", mem.rss) + + def _get_num_fds(self): + if POSIX: + return self._thisproc.num_fds() + else: + return self._thisproc.num_handles() + + def _log(self, msg): + if self.verbose: + print_color(msg, color="yellow", file=sys.stderr) + + def _check_fds(self, fun): + """Makes sure num_fds() (POSIX) or num_handles() (Windows) does + not increase after calling a function. Used to discover forgotten + close(2) and CloseHandle syscalls. + """ + before = self._get_num_fds() + self.call(fun) + after = self._get_num_fds() + diff = after - before + if diff < 0: + msg = ( + f"negative diff {diff!r} (gc probably collected a" + " resource from a previous test)" + ) + raise self.fail(msg) + if diff > 0: + type_ = "fd" if POSIX else "handle" + if diff > 1: + type_ += "s" + msg = f"{diff} unclosed {type_} after calling {fun!r}" + raise self.fail(msg) + + def _call_ntimes(self, fun, times): + """Get 2 distinct memory samples, before and after having + called fun repeatedly, and return the memory difference. + """ + gc.collect(generation=1) + mem1 = self._get_mem() + for x in range(times): + ret = self.call(fun) + del x, ret + gc.collect(generation=1) + mem2 = self._get_mem() + assert gc.garbage == [] + diff = mem2 - mem1 # can also be negative + return diff + + def _check_mem(self, fun, times, retries, tolerance): + messages = [] + prev_mem = 0 + increase = times + for idx in range(1, retries + 1): + mem = self._call_ntimes(fun, times) + msg = "Run #{}: extra-mem={}, per-call={}, calls={}".format( + idx, + bytes2human(mem), + bytes2human(mem / times), + times, + ) + messages.append(msg) + success = mem <= tolerance or mem <= prev_mem + if success: + if idx > 1: + self._log(msg) + return + else: + if idx == 1: + print() # noqa: T201 + self._log(msg) + times += increase + prev_mem = mem + raise self.fail(". ".join(messages)) + + # --- + + def call(self, fun): + return fun() + + def execute( + self, fun, times=None, warmup_times=None, retries=None, tolerance=None + ): + """Test a callable.""" + times = times if times is not None else self.times + warmup_times = ( + warmup_times if warmup_times is not None else self.warmup_times + ) + retries = retries if retries is not None else self.retries + tolerance = tolerance if tolerance is not None else self.tolerance + try: + assert times >= 1, "times must be >= 1" + assert warmup_times >= 0, "warmup_times must be >= 0" + assert retries >= 0, "retries must be >= 0" + assert tolerance >= 0, "tolerance must be >= 0" + except AssertionError as err: + raise ValueError(str(err)) + + self._call_ntimes(fun, warmup_times) # warm up + self._check_fds(fun) + self._check_mem(fun, times=times, retries=retries, tolerance=tolerance) + + def execute_w_exc(self, exc, fun, **kwargs): + """Convenience method to test a callable while making sure it + raises an exception on every call. + """ + + def call(): + self.assertRaises(exc, fun) + + self.execute(call, **kwargs) + + +def print_sysinfo(): + import collections + import datetime + import getpass + import locale + import pprint + + try: + import pip + except ImportError: + pip = None + try: + import wheel + except ImportError: + wheel = None + + info = collections.OrderedDict() + + # OS + if psutil.LINUX and shutil.which("lsb_release"): + info['OS'] = sh('lsb_release -d -s') + elif psutil.OSX: + info['OS'] = f"Darwin {platform.mac_ver()[0]}" + elif psutil.WINDOWS: + info['OS'] = "Windows " + ' '.join(map(str, platform.win32_ver())) + if hasattr(platform, 'win32_edition'): + info['OS'] += ", " + platform.win32_edition() + else: + info['OS'] = f"{platform.system()} {platform.version()}" + info['arch'] = ', '.join( + list(platform.architecture()) + [platform.machine()] + ) + if psutil.POSIX: + info['kernel'] = platform.uname()[2] + + # python + info['python'] = ', '.join([ + platform.python_implementation(), + platform.python_version(), + platform.python_compiler(), + ]) + info['pip'] = getattr(pip, '__version__', 'not installed') + if wheel is not None: + info['pip'] += f" (wheel={wheel.__version__})" + + # UNIX + if psutil.POSIX: + if shutil.which("gcc"): + out = sh(['gcc', '--version']) + info['gcc'] = str(out).split('\n')[0] + else: + info['gcc'] = 'not installed' + s = platform.libc_ver()[1] + if s: + info['glibc'] = s + + # system + info['fs-encoding'] = sys.getfilesystemencoding() + lang = locale.getlocale() + info['lang'] = f"{lang[0]}, {lang[1]}" + info['boot-time'] = datetime.datetime.fromtimestamp( + psutil.boot_time() + ).strftime("%Y-%m-%d %H:%M:%S") + info['time'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + info['user'] = getpass.getuser() + info['home'] = os.path.expanduser("~") + info['cwd'] = os.getcwd() + info['pyexe'] = PYTHON_EXE + info['hostname'] = platform.node() + info['PID'] = os.getpid() + + # metrics + info['cpus'] = psutil.cpu_count() + info['loadavg'] = "{:.1f}%, {:.1f}%, {:.1f}%".format( + *tuple(x / psutil.cpu_count() * 100 for x in psutil.getloadavg()) + ) + mem = psutil.virtual_memory() + info['memory'] = "{}%%, used={}, total={}".format( + int(mem.percent), + bytes2human(mem.used), + bytes2human(mem.total), + ) + swap = psutil.swap_memory() + info['swap'] = "{}%%, used={}, total={}".format( + int(swap.percent), + bytes2human(swap.used), + bytes2human(swap.total), + ) + info['pids'] = len(psutil.pids()) + pinfo = psutil.Process().as_dict() + pinfo.pop('memory_maps', None) + info['proc'] = pprint.pformat(pinfo) + + print("=" * 70, file=sys.stderr) # noqa: T201 + for k, v in info.items(): + print("{:<17} {}".format(k + ":", v), file=sys.stderr) # noqa: T201 + print("=" * 70, file=sys.stderr) # noqa: T201 + sys.stdout.flush() + + # if WINDOWS: + # os.system("tasklist") + # elif shutil.which("ps"): + # os.system("ps aux") + # print("=" * 70, file=sys.stderr) + + sys.stdout.flush() + + +def is_win_secure_system_proc(pid): + # see: https://github.com/giampaolo/psutil/issues/2338 + @memoize + def get_procs(): + ret = {} + out = sh("tasklist.exe /NH /FO csv") + for line in out.splitlines()[1:]: + bits = [x.replace('"', "") for x in line.split(",")] + name, pid = bits[0], int(bits[1]) + ret[pid] = name + return ret + + try: + return get_procs()[pid] == "Secure System" + except KeyError: + return False + + +def _get_eligible_cpu(): + p = psutil.Process() + if hasattr(p, "cpu_num"): + return p.cpu_num() + elif hasattr(p, "cpu_affinity"): + return random.choice(p.cpu_affinity()) + return 0 + + +class process_namespace: + """A container that lists all Process class method names + some + reasonable parameters to be called with. Utility methods (parent(), + children(), ...) are excluded. + + >>> ns = process_namespace(psutil.Process()) + >>> for fun, name in ns.iter(ns.getters): + ... fun() + """ + + utils = [('cpu_percent', (), {}), ('memory_percent', (), {})] + + ignored = [ + ('as_dict', (), {}), + ('children', (), {'recursive': True}), + ('connections', (), {}), # deprecated + ('is_running', (), {}), + ('oneshot', (), {}), + ('parent', (), {}), + ('parents', (), {}), + ('pid', (), {}), + ('wait', (0,), {}), + ] + + getters = [ + ('cmdline', (), {}), + ('cpu_times', (), {}), + ('create_time', (), {}), + ('cwd', (), {}), + ('exe', (), {}), + ('memory_full_info', (), {}), + ('memory_info', (), {}), + ('name', (), {}), + ('net_connections', (), {'kind': 'all'}), + ('nice', (), {}), + ('num_ctx_switches', (), {}), + ('num_threads', (), {}), + ('open_files', (), {}), + ('ppid', (), {}), + ('status', (), {}), + ('threads', (), {}), + ('username', (), {}), + ] + if POSIX: + getters += [('uids', (), {})] + getters += [('gids', (), {})] + getters += [('terminal', (), {})] + getters += [('num_fds', (), {})] + if HAS_PROC_IO_COUNTERS: + getters += [('io_counters', (), {})] + if HAS_IONICE: + getters += [('ionice', (), {})] + if HAS_RLIMIT: + getters += [('rlimit', (psutil.RLIMIT_NOFILE,), {})] + if HAS_CPU_AFFINITY: + getters += [('cpu_affinity', (), {})] + if HAS_PROC_CPU_NUM: + getters += [('cpu_num', (), {})] + if HAS_ENVIRON: + getters += [('environ', (), {})] + if WINDOWS: + getters += [('num_handles', (), {})] + if HAS_MEMORY_MAPS: + getters += [('memory_maps', (), {'grouped': False})] + + setters = [] + if POSIX: + setters += [('nice', (0,), {})] + else: + setters += [('nice', (psutil.NORMAL_PRIORITY_CLASS,), {})] + if HAS_RLIMIT: + setters += [('rlimit', (psutil.RLIMIT_NOFILE, (1024, 4096)), {})] + if HAS_IONICE: + if LINUX: + setters += [('ionice', (psutil.IOPRIO_CLASS_NONE, 0), {})] + else: + setters += [('ionice', (psutil.IOPRIO_NORMAL,), {})] + if HAS_CPU_AFFINITY: + setters += [('cpu_affinity', ([_get_eligible_cpu()],), {})] + + killers = [ + ('send_signal', (signal.SIGTERM,), {}), + ('suspend', (), {}), + ('resume', (), {}), + ('terminate', (), {}), + ('kill', (), {}), + ] + if WINDOWS: + killers += [('send_signal', (signal.CTRL_C_EVENT,), {})] + killers += [('send_signal', (signal.CTRL_BREAK_EVENT,), {})] + + all = utils + getters + setters + killers + + def __init__(self, proc): + self._proc = proc + + def iter(self, ls, clear_cache=True): + """Given a list of tuples yields a set of (fun, fun_name) tuples + in random order. + """ + ls = list(ls) + random.shuffle(ls) + for fun_name, args, kwds in ls: + if clear_cache: + self.clear_cache() + fun = getattr(self._proc, fun_name) + fun = functools.partial(fun, *args, **kwds) + yield (fun, fun_name) + + def clear_cache(self): + """Clear the cache of a Process instance.""" + self._proc._init(self._proc.pid, _ignore_nsp=True) + + @classmethod + def test_class_coverage(cls, test_class, ls): + """Given a TestCase instance and a list of tuples checks that + the class defines the required test method names. + """ + for fun_name, _, _ in ls: + meth_name = 'test_' + fun_name + if not hasattr(test_class, meth_name): + msg = ( + 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': True}), + ('disk_partitions', (), {'all': True}), + ('disk_usage', (os.getcwd(),), {}), + ('net_connections', (), {'kind': 'all'}), + ('net_if_addrs', (), {}), + ('net_if_stats', (), {}), + ('net_io_counters', (), {'pernic': True}), + ('pid_exists', (os.getpid(),), {}), + ('pids', (), {}), + ('swap_memory', (), {}), + ('users', (), {}), + ('virtual_memory', (), {}), + ] + if HAS_CPU_FREQ: + if MACOS and platform.machine() == 'arm64': # skipped due to #1892 + pass + else: + getters += [('cpu_freq', (), {'percpu': True})] + if HAS_GETLOADAVG: + getters += [('getloadavg', (), {})] + if HAS_SENSORS_TEMPERATURES: + getters += [('sensors_temperatures', (), {})] + if HAS_SENSORS_FANS: + getters += [('sensors_fans', (), {})] + if HAS_SENSORS_BATTERY: + getters += [('sensors_battery', (), {})] + if WINDOWS: + getters += [('win_service_iter', (), {})] + getters += [('win_service_get', ('alg',), {})] + + ignored = [ + ('process_iter', (), {}), + ('wait_procs', ([psutil.Process()],), {}), + ('cpu_percent', (), {}), + ('cpu_times_percent', (), {}), + ] + + all = getters + + @staticmethod + def iter(ls): + """Given a list of tuples yields a set of (fun, fun_name) tuples + in random order. + """ + ls = list(ls) + random.shuffle(ls) + for fun_name, args, kwds in ls: + fun = getattr(psutil, fun_name) + fun = functools.partial(fun, *args, **kwds) + yield (fun, fun_name) + + test_class_coverage = process_namespace.test_class_coverage + + +def retry_on_failure(retries=NO_RETRIES): + """Decorator which runs a test function and retries N times before + actually failing. + """ + + def logfun(exc): + print(f"{exc!r}, retrying", file=sys.stderr) # noqa: T201 + + return retry( + exception=AssertionError, timeout=None, retries=retries, logfun=logfun + ) + + +def skip_on_access_denied(only_if=None): + """Decorator to Ignore AccessDenied exceptions.""" + + def decorator(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + try: + return fun(*args, **kwargs) + except psutil.AccessDenied: + if only_if is not None: + if not only_if: + raise + raise 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" + ) + raise 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 namedtuple.""" + + def check_ntuple(conn): + has_pid = len(conn) == 7 + assert len(conn) in {6, 7}, len(conn) + assert conn[0] == conn.fd, conn.fd + assert conn[1] == conn.family, conn.family + assert conn[2] == conn.type, conn.type + assert conn[3] == conn.laddr, conn.laddr + assert conn[4] == conn.raddr, conn.raddr + assert conn[5] == conn.status, conn.status + if has_pid: + assert conn[6] == conn.pid, conn.pid + + def check_family(conn): + assert conn.family in {AF_INET, AF_INET6, AF_UNIX}, conn.family + 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(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 + + +# =================================================================== +# --- import utils +# =================================================================== + + +def reload_module(module): + return importlib.reload(module) + + +def import_module_by_path(path): + name = os.path.splitext(os.path.basename(path))[0] + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +# =================================================================== +# --- others +# =================================================================== + + +def warn(msg): + """Raise a warning msg.""" + warnings.warn(msg, UserWarning, stacklevel=2) + + +def is_namedtuple(x): + """Check if object is an instance of namedtuple.""" + t = type(x) + b = t.__bases__ + if len(b) != 1 or b[0] is not tuple: + return False + f = getattr(t, '_fields', None) + if not isinstance(f, tuple): + return False + return all(isinstance(n, str) for n in f) + + +if POSIX: + + @contextlib.contextmanager + def copyload_shared_lib(suffix=""): + """Ctx manager which picks up a random shared CO lib used + by this process, copies it in another location and loads it + in memory via ctypes. Return the new absolutized path. + """ + exe = 'pypy' if PYPY else 'python' + ext = ".so" + dst = get_testfn(suffix=suffix + ext) + libs = [ + x.path + for x in psutil.Process().memory_maps() + if os.path.splitext(x.path)[1] == ext and exe in x.path.lower() + ] + src = random.choice(libs) + shutil.copyfile(src, dst) + try: + ctypes.CDLL(dst) + yield dst + finally: + safe_rmpath(dst) + +else: + + @contextlib.contextmanager + def copyload_shared_lib(suffix=""): + """Ctx manager which picks up a random shared DLL lib used + by this process, copies it in another location and loads it + in memory via ctypes. + Return the new absolutized, normcased path. + """ + from ctypes import WinError + from ctypes import wintypes + + ext = ".dll" + dst = get_testfn(suffix=suffix + ext) + libs = [ + x.path + for x in psutil.Process().memory_maps() + if x.path.lower().endswith(ext) + and 'python' in os.path.basename(x.path).lower() + and 'wow64' not in x.path.lower() + ] + if PYPY and not libs: + libs = [ + x.path + for x in psutil.Process().memory_maps() + if 'pypy' in os.path.basename(x.path).lower() + ] + src = random.choice(libs) + shutil.copyfile(src, dst) + cfile = None + try: + cfile = ctypes.WinDLL(dst) + yield dst + finally: + # Work around OverflowError: + # - https://ci.appveyor.com/project/giampaolo/psutil/build/1207/ + # job/o53330pbnri9bcw7 + # - http://bugs.python.org/issue30286 + # - http://stackoverflow.com/questions/23522055 + if cfile is not None: + FreeLibrary = ctypes.windll.kernel32.FreeLibrary + FreeLibrary.argtypes = [wintypes.HMODULE] + ret = FreeLibrary(cfile._handle) + if ret == 0: + 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/.venv/lib/python3.12/site-packages/psutil/tests/__main__.py b/.venv/lib/python3.12/site-packages/psutil/tests/__main__.py new file mode 100644 index 0000000..ce6fc24 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/psutil/tests/__main__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Run unit tests. This is invoked by: +$ python -m psutil.tests. +""" + +from psutil.tests import pytest + + +pytest.main(["-v", "-s", "--tb=short"]) diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/__init__.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13dde0532f70a619354f3457eb8a20aa72190eef GIT binary patch literal 91398 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 literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/__main__.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f46c83ad6828e93a0cdeea13c551c701a01f4b20 GIT binary patch literal 400 zcmYk2u};G<5Qc3hgeq;{!2<$iNGw1ssPqw#Kn$phl{8jjnmAGHM5Gf;JOVo#;!)TT zED;+MTT#WrgiWd7m+yQh{rBl~kHeu0w4Ps2(gO&eFMNsYry~j literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_aix.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_aix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed4c7a1f7c111509679df57c17a16abccd77fd88 GIT binary patch literal 5155 zcmc&&O>7fM7OwuYJN|2fgA)>dGW>+t!Eqq`gw?=qfWRz)z!Fx13_C5y-AUSM|4er~ zLp%ZK537T=qwVN zZXv-VGWRKa?0$9esAq%~C~Dpo`RZNMQa&75p$l{?kt*t$I8NrD&{pZ7Y&R!a3Agx=4RRo+{G(US_)*Ew9#HPwZXYMr76A!IjLyV>FEeUU9dxYhRKor0Tg zAlY1(aJZuTqa?%5`g&h%`jF%diui^LTjP0EiH}K{xH_HAW;$XQJMtMxJ3e%>(3BR_ zS!L$B8U_EQRjDAFjFic%qDhY{qG(b%DPy{)B}L08WV0zTosi_jm<+4Z>8v2WrMrHA z>C?CDiR&ZXk?U%=guopYeE13m0R zHlKqywIbIHRPFKh=1fM*YG9p8jHxCKdlPKsSZ~A!yB6hflgj2b)1!!dE}?0nk};Wa zNt6YXO((SEWOgiNl0zqzb`Y_Jw_^@#F#WbwUJ=#2teKmxTs%K`d0_a|AU|?#gugI& zLB(Iuv8uhj8lTLj#dty%Z^S1gEv`(8GB3jiWu(kRyk}a>OvhzuEDpFcna%X|95@(P zB~6Uw63MB=gs8@ClW_~cEXpUO8$G!hGlYluwclUZ*8o>~8Z5-Fp$-C&e1nz(!OEun zMj%$Ao^1J~lgw@c@CmBq(gcZKb)aNStb@x{`Isc+3q{*ntW+P@RtIq3FUjJ#@t2j#hp*^q2#6LltglJ!zdASb@_-jp6$@ znE4QVuMf?_YG@{lqz%ohfR1bk%aj8sPCrWt^ep|6Ft-}^nT7yI7a(RG-9@SncK{&F zaAA`LASy->S=S=81QK@isNNbiTKAXyZ(Ilh3*5L<4|OhV)?JExlLne3YdIX@5gY@0CX%vIJ4? z9e%sHRwkr#s+Q1jn7anfpU1^P*rd`>2~g=o%B0h|BqU;>ib)CLG-TlHxQ5xJN#-U^ z5@sn02s4(=X9P(1Uz&6-uTGk-k-l7BmQ7})Z$c4FdZgE)2P}HfqJ0)UWYNQzsxxX* z!8fYlCxrAHIKG0zQW?bL5GIF#RBMY>IAWnkQ!iP&Jy)(%MP5{TddUH+bFdkfVnpZ*Guio zF0>P}e7gloU3zPm(HbrJU--D&{>5Na4@Mu1JZ$}W$4@(cp^XEBM(m6pJY)FIJ_&sC znj-wWmMN;qvrGc_2IC+W>ePds4?+u9fBfR!7mv;uu~SCmv>rTd_y*Sz>e7Q<5Be5v z{CM-;%}1gUKW%gm>cK(7cV-=-m>!J%n|k=!&%-|r|8m4Qbk67<(t|^W@BA8}S9p8x z!zj)X7K}awX5)0O;n$nhQdshxwmPwYWC|zi@Yo0(1^NNwz#e`FzLS<+!%7_XQ1Hm$ zB*(jOUVRDvfv&)_)-7izByeZ&SG-l*&+8eso?NZ)NJ5lq5xVZ$rq>1l9;-IJ)%qC! zP@%lHeA#y7aLth|9emYnA-b+ZuUUHw6ski&su|XH?4|J5?|1zWxf>~8c-UvO9bIfY zuD2b3lrY-{)iFArLSt0spF!MLa;Hvv+E^aMg+(j=M#dU07_4ZGwPbV4LC~pOd>6+rmuRv)~|Q&x5c{O z)e3E)LiYzR`2EHIfNiR0fmCmJw-MM=qL#L9n;-e%+TCm4eQs=xlmbgFZQmbRZ12(A zdyMwp#rDH``(dN~sL|42@;%|Um$yBJ+MbQ+T+F)N;RtK8ytNyiH$6NLuhG0L0`KGb zFX63Mx9xvzUnD|05t^rNpDmxO5FvwzzaeJ`qG!Hm1>t^$LS*kNYsj+rUpBaOUH||9 literal 0 HcmV?d00001 diff --git a/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_bsd.cpython-312.pyc b/.venv/lib/python3.12/site-packages/psutil/tests/__pycache__/test_bsd.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..23bd6419ff12c13895b7946a9538215111d14ae0 GIT binary patch literal 35778 zcmeHw32+?Obzt}000!u}a8tzKCII5%eN!L+N&tzWMAEVdOPLJO17bj2?jC@|7_exs zwgN^<1e%Ent>o2crIf%(S-~4^h2BbSWM_BFd(>7pn%aTJ)J9IcNwig41rn;Y!%8Ll z-tXyS28|&o%ZZfQmiY0f|IxqSfB*e={n!6BKR=g(>-L=|hyE-;QGbCyl*^XNtXa$y zb%SE5eu|}8(+J&9lfR~Z6ZvcIHNaWEsk(Zc>hnle%0!naee*|Ov5e8>uZ0N8Z3Qr z8%15FIp`JYT87)SI!capy-myc{?aiEYw9mz-TkGk=Xt8XjP*iV9%hF3DYdn~!w0pz zk_+3N6zhAZ@NJWP$3#6wu|>~QY;m~YZI~0hl(F@1eTaS?&~HgbzopP`nL)o5zhuAV z(C-$5e*F*8Z!V1Yk&N+fg?=jx`mKD3esiE-e@4HR&~KGNzg5$whU!?&*)SjF{rp6D zATk&k@JGk}p^*{)u`^x%iIGrraGV?ETRy>w2{h12Gu($i_T#6!yXCAc(0w*_^VAdF z0XduZc*mLGv5vE6yHB5k>h80rPIjN}2y}J_&zw6G?Cb7>n*~qslhMcsKE5-=hvANG zVmb=HAm`%3QEoaoJ{BAdMMfq$EH&{%*ovPBT^b8wy$No70Pf^O!`x_OEEL5`c^AUb ziO_{GABlyNmeEN*e0(5I`*>x@ zhsOLN?!x3~cq|He);|&A_^>}Z6!vpresUz5>xTVCQhO9D+V+8H|jv972afa3;;rO)~c> z?6jNX#<>Pl(mpWCjzq@7Nt-+p9((iq??2r-G(H+`4UL2^w_b=uTe+d|NN^+^A_Ht~ znF^0hwT?v2w*nFkjgReYY2VSxN21~8iO|4B#E8}jf^V&eIy^~(=XtheVmfJ)$Ig|* zgSNvzJ_^Yd>Qe_*;F`}_cuaIvOZhcdj(=n?SaBCG*j~$fIWOKNl{74Nh$YQJNvq&) zyK4I=$Mw6%wen7>WZPo9Sh8IxX%XD5bGB8J*;;gm@yzde;TwXZ>=$?P3hz<0wP=MY z{LTDTGvw~`9(eE%-MJlGs2^_0>u5Cpuz`lu7=v#?-E<6AFk}|7HAPLKl6q;_+C@EE zNKvz<8IuuK�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