commit c3691b876b502f276790729e6558137f672ac2f5 Author: Zhengchen Tao Date: Tue May 5 17:59:54 2026 +0800 init: 三角洲行动开镜自动屏息脚本 按住鼠标右键时自动按下并保持配置的按键(默认 F12,对应游戏内屏息),松开右键时释放。 通过进程名识别游戏,仅在游戏运行时激活监听。 仓库内置 PortablePython 解释器与 pynput/psutil 依赖,下载即用。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3bbe7b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +*.pyo diff --git a/PortablePython/LICENSE.txt b/PortablePython/LICENSE.txt new file mode 100644 index 0000000..4059c16 --- /dev/null +++ b/PortablePython/LICENSE.txt @@ -0,0 +1,676 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001 Python Software Foundation; All Rights Reserved" +are retained in Python alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + + +Additional Conditions for this Windows binary build +--------------------------------------------------- + +This program is linked with and uses Microsoft Distributable Code, +copyrighted by Microsoft Corporation. The Microsoft Distributable Code +is embedded in each .exe, .dll and .pyd file as a result of running +the code through a linker. + +If you further distribute programs that include the Microsoft +Distributable Code, you must comply with the restrictions on +distribution specified by Microsoft. In particular, you must require +distributors and external end users to agree to terms that protect the +Microsoft Distributable Code at least as much as Microsoft's own +requirements for the Distributable Code. See Microsoft's documentation +(included in its developer tools and on its website at microsoft.com) +for specific details. + +Redistribution of the Windows binary build of the Python interpreter +complies with this agreement, provided that you do not: + +- alter any copyright, trademark or patent notice in Microsoft's +Distributable Code; + +- use Microsoft's trademarks in your programs' names or in a way that +suggests your programs come from or are endorsed by Microsoft; + +- distribute Microsoft's Distributable Code to run on a platform other +than Microsoft operating systems, run-time technologies or application +platforms; or + +- include Microsoft Distributable Code in malicious, deceptive or +unlawful programs. + +These restrictions apply only to the Microsoft Distributable Code as +defined above, not to Python itself or any programs running on the +Python interpreter. The redistribution of the Python interpreter and +libraries is governed by the Python Software License included with this +file, or by other licenses as marked. + + + +-------------------------------------------------------------------------- + +This program, "bzip2", the associated library "libbzip2", and all +documentation, are copyright (C) 1996-2019 Julian R Seward. All +rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +3. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + +4. The name of the author may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. + +Julian Seward, jseward@acm.org +bzip2/libbzip2 version 1.0.8 of 13 July 2019 + +-------------------------------------------------------------------------- + +libffi - Copyright (c) 1996-2022 Anthony Green, Red Hat, Inc and others. +See source files for details. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +``Software''), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +BSD License + +For Zstandard software + +Copyright (c) Meta Platforms, Inc. and affiliates. 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 Facebook, nor Meta, 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 HOLDER 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. + + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +This software is copyrighted by the Regents of the University of +California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState +Corporation and other parties. The following terms apply to all files +associated with the software unless explicitly disclaimed in +individual files. + +The authors hereby grant permission to use, copy, modify, distribute, +and license this software and its documentation for any purpose, provided +that existing copyright notices are retained in all copies and that this +notice is included verbatim in any distributions. No written agreement, +license, or royalty fee is required for any of the authorized uses. +Modifications to this software may be copyrighted by their authors +and need not follow the licensing terms described here, provided that +the new terms are clearly indicated on the first page of each file where +they apply. + +IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY +FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY +DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE +IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE +NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR +MODIFICATIONS. + +GOVERNMENT USE: If you are acquiring this software on behalf of the +U.S. government, the Government shall have only "Restricted Rights" +in the software and related documentation as defined in the Federal +Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you +are acquiring the software on behalf of the Department of Defense, the +software shall be classified as "Commercial Computer Software" and the +Government shall have only "Restricted Rights" as defined in Clause +252.227-7014 (b) (3) of DFARs. Notwithstanding the foregoing, the +authors grant the U.S. Government and others acting in its behalf +permission to use and distribute the software in accordance with the +terms specified in this license. + +This software is copyrighted by the Regents of the University of +California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState +Corporation, Apple Inc. and other parties. The following terms apply to +all files associated with the software unless explicitly disclaimed in +individual files. + +The authors hereby grant permission to use, copy, modify, distribute, +and license this software and its documentation for any purpose, provided +that existing copyright notices are retained in all copies and that this +notice is included verbatim in any distributions. No written agreement, +license, or royalty fee is required for any of the authorized uses. +Modifications to this software may be copyrighted by their authors +and need not follow the licensing terms described here, provided that +the new terms are clearly indicated on the first page of each file where +they apply. + +IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY +FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY +DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE +IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE +NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR +MODIFICATIONS. + +GOVERNMENT USE: If you are acquiring this software on behalf of the +U.S. government, the Government shall have only "Restricted Rights" +in the software and related documentation as defined in the Federal +Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you +are acquiring the software on behalf of the Department of Defense, the +software shall be classified as "Commercial Computer Software" and the +Government shall have only "Restricted Rights" as defined in Clause +252.227-7013 (b) (3) of DFARs. Notwithstanding the foregoing, the +authors grant the U.S. Government and others acting in its behalf +permission to use and distribute the software in accordance with the +terms specified in this license. + diff --git a/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/INSTALLER b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/LICENSE b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/LICENSE new file mode 100644 index 0000000..cff5eb7 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/METADATA b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/METADATA new file mode 100644 index 0000000..e689c06 --- /dev/null +++ b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/METADATA @@ -0,0 +1,551 @@ +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: pywin32 ; extra == 'dev' +Requires-Dist: wheel ; extra == 'dev' +Requires-Dist: wmi ; 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: pyreadline ; extra == 'dev' +Requires-Dist: pdbpp ; extra == 'dev' +Provides-Extra: test +Requires-Dist: pytest ; extra == 'test' +Requires-Dist: pytest-xdist ; extra == 'test' +Requires-Dist: setuptools ; extra == 'test' +Requires-Dist: pywin32 ; extra == 'test' +Requires-Dist: wheel ; extra == 'test' +Requires-Dist: wmi ; 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/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/RECORD b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/RECORD new file mode 100644 index 0000000..294bafc --- /dev/null +++ b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/RECORD @@ -0,0 +1,64 @@ +psutil-7.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +psutil-7.0.0.dist-info/LICENSE,sha256=x63E1dEzelSLlnQh8fviWLkwM6BBdwj9b044-Oy864A,1577 +psutil-7.0.0.dist-info/METADATA,sha256=jEGY38opff7gdO5GOUIH8xeWXCXcGvitOIYrlUeVp8E,23136 +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=-EX5DQzNGQEoyL99Q-0P0-D-CXbfqafenaAeiSQ_Ufk,100 +psutil-7.0.0.dist-info/top_level.txt,sha256=gCNhn57wzksDjSAISmgMJ0aiXzQulk0GJhb2-BAyYgw,7 +psutil/__init__.py,sha256=lvZWdYQ3W0flcZeW1vbN5QgoSHuqofXBnMxHZLzwgrU,89075 +psutil/__pycache__/__init__.cpython-313.pyc,, +psutil/__pycache__/_common.cpython-313.pyc,, +psutil/__pycache__/_psaix.cpython-313.pyc,, +psutil/__pycache__/_psbsd.cpython-313.pyc,, +psutil/__pycache__/_pslinux.cpython-313.pyc,, +psutil/__pycache__/_psosx.cpython-313.pyc,, +psutil/__pycache__/_psposix.cpython-313.pyc,, +psutil/__pycache__/_pssunos.cpython-313.pyc,, +psutil/__pycache__/_pswindows.cpython-313.pyc,, +psutil/_common.py,sha256=tPE7YVzC0ZIBhZzYdzqOFnh-geJVALbyBY3TSAwASXw,29592 +psutil/_psaix.py,sha256=CFBLwUi8DR5KsDC0yCs0jlLtLf2dhhyGArAhG_udqK8,18817 +psutil/_psbsd.py,sha256=UXd-QXUVk_H_wbFHWt2vshcChWxBrPwn38PX0HeYXfo,32727 +psutil/_pslinux.py,sha256=wKT1c3HX8XhnZ8sDNX1hiKRbVj7p53ASJ6VaniKaxs4,88323 +psutil/_psosx.py,sha256=LwFP6AtKp2hzNWRSaSLaWHB6nh1CiKSMu_KvP5009IE,16421 +psutil/_psposix.py,sha256=AJxyaRPf1h8dyT9rnsF8c-psHwXEbKqaNEt3OOm4Zuk,7349 +psutil/_pssunos.py,sha256=B58FY4JjbfndrdmbEV7QGX6lVi0v--V-g_Hxsg958MM,25654 +psutil/_psutil_windows.pyd,sha256=nH_IVdnRYU5wcFx9zG9Kw83Kta3-tqZ9OC9a3gnq3BU,67072 +psutil/_pswindows.py,sha256=is_Cq3yMuFnqGUfpOeiU8oWzZQNWmK_-Xt7asb8YCu4,37052 +psutil/tests/__init__.py,sha256=tFfa1RqnPJP9UuVe-JN7sAavHVoHlmqiN0pyk3I4KI0,66129 +psutil/tests/__main__.py,sha256=AQDwErrSFPsBGSY5wIKmh7LziqWTAARYKEqz_zrXMTc,321 +psutil/tests/__pycache__/__init__.cpython-313.pyc,, +psutil/tests/__pycache__/__main__.cpython-313.pyc,, +psutil/tests/__pycache__/test_aix.cpython-313.pyc,, +psutil/tests/__pycache__/test_bsd.cpython-313.pyc,, +psutil/tests/__pycache__/test_connections.cpython-313.pyc,, +psutil/tests/__pycache__/test_contracts.cpython-313.pyc,, +psutil/tests/__pycache__/test_linux.cpython-313.pyc,, +psutil/tests/__pycache__/test_memleaks.cpython-313.pyc,, +psutil/tests/__pycache__/test_misc.cpython-313.pyc,, +psutil/tests/__pycache__/test_osx.cpython-313.pyc,, +psutil/tests/__pycache__/test_posix.cpython-313.pyc,, +psutil/tests/__pycache__/test_process.cpython-313.pyc,, +psutil/tests/__pycache__/test_process_all.cpython-313.pyc,, +psutil/tests/__pycache__/test_scripts.cpython-313.pyc,, +psutil/tests/__pycache__/test_sunos.cpython-313.pyc,, +psutil/tests/__pycache__/test_system.cpython-313.pyc,, +psutil/tests/__pycache__/test_testutils.cpython-313.pyc,, +psutil/tests/__pycache__/test_unicode.cpython-313.pyc,, +psutil/tests/__pycache__/test_windows.cpython-313.pyc,, +psutil/tests/test_aix.py,sha256=M84ZfM1EeSDRyzrf404JGu5zy_ErRn5MK3t3yT11lz0,4550 +psutil/tests/test_bsd.py,sha256=GRbzguegV7K2m-O4dQJlUJGh7M7UH9fI8jvE2UjFOks,20784 +psutil/tests/test_connections.py,sha256=1D4HEQl_bZfZbk_4g-hc4rnrDqUD7q42rb9aIoJ1Amc,21723 +psutil/tests/test_contracts.py,sha256=8enorS1KmOftKyC4XTINXtnWNnmh18qFEYdLHKtrknY,12326 +psutil/tests/test_linux.py,sha256=Gap-GA6Bv9TfKbRupph49NwZC0vjv1V5isa5l-Eg3VY,91187 +psutil/tests/test_memleaks.py,sha256=yGEhTOOllW2NV-R2S5lybD2S8iO9CiS7--ND2E9rZTY,15608 +psutil/tests/test_misc.py,sha256=eQUmQqnh7nM0aIND_yOtgO4-Lwyspw3tgt3aWje0BPY,30545 +psutil/tests/test_osx.py,sha256=Y_NubjMylA88A7WAepiDkYTZl4YWvwgtCRFvu23Rm8A,6512 +psutil/tests/test_posix.py,sha256=yKL9N6ixiFUPNmv5v7cBS1OaoOZl-bm7hGdf7m2_M0E,17675 +psutil/tests/test_process.py,sha256=lmVD9FO6GJp4GZqSxR0j4Wsr7dcG8Py2qWsiv-furbY,61548 +psutil/tests/test_process_all.py,sha256=MfKc2BpL3pFBKqdmkq5gJ1GDD4CZzxUvesQ7sgnzzJg,18882 +psutil/tests/test_scripts.py,sha256=a3z4vFdBNlto_2pKe37kOPK6zVenfWzCsibduzyeO1c,7965 +psutil/tests/test_sunos.py,sha256=FxIAhIC3hycvJhgdVK8_98AmmV1pTZgXtovBgDmD9RA,1229 +psutil/tests/test_system.py,sha256=gi5Mci_pL5za6Q1Wp3nOv4J6Bj7L-IeM76rR5-1rFZk,37086 +psutil/tests/test_testutils.py,sha256=IUc3mMHGKGdnPpf_mhMWbDq0UDK9sXI4DZZ2dwujS7A,18915 +psutil/tests/test_unicode.py,sha256=kH08SGFmi0bxnL-LHEA7JX0fwauGPvY9W3zfkQumgTw,10705 +psutil/tests/test_windows.py,sha256=Zpg4Ek9KHkloYHpgHtHIrYBDXIgSf8HppSWcgWgogWE,34128 diff --git a/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/REQUESTED b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/WHEEL b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/WHEEL new file mode 100644 index 0000000..816dfea --- /dev/null +++ b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: false +Tag: cp37-abi3-win_amd64 + diff --git a/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/top_level.txt b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/top_level.txt new file mode 100644 index 0000000..a4d92cc --- /dev/null +++ b/PortablePython/Lib/site-packages/psutil-7.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +psutil diff --git a/PortablePython/Lib/site-packages/psutil/__init__.py b/PortablePython/Lib/site-packages/psutil/__init__.py new file mode 100644 index 0000000..cf4a580 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/_common.py b/PortablePython/Lib/site-packages/psutil/_common.py new file mode 100644 index 0000000..4096c0a --- /dev/null +++ b/PortablePython/Lib/site-packages/psutil/_common.py @@ -0,0 +1,950 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Common objects shared by __init__.py and _ps*.py modules. + +Note: this module is imported by setup.py, so it should not import +psutil or third-party modules. +""" + +import collections +import enum +import functools +import os +import socket +import stat +import sys +import threading +import warnings +from collections import namedtuple +from socket import AF_INET +from socket import SOCK_DGRAM +from socket import SOCK_STREAM + + +try: + from socket import AF_INET6 +except ImportError: + AF_INET6 = None +try: + from socket import AF_UNIX +except ImportError: + AF_UNIX = None + + +PSUTIL_DEBUG = bool(os.getenv('PSUTIL_DEBUG')) +_DEFAULT = object() + +# fmt: off +__all__ = [ + # OS constants + 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'MACOS', 'OSX', 'POSIX', + 'SUNOS', 'WINDOWS', + # connection constants + 'CONN_CLOSE', 'CONN_CLOSE_WAIT', 'CONN_CLOSING', 'CONN_ESTABLISHED', + 'CONN_FIN_WAIT1', 'CONN_FIN_WAIT2', 'CONN_LAST_ACK', 'CONN_LISTEN', + 'CONN_NONE', 'CONN_SYN_RECV', 'CONN_SYN_SENT', 'CONN_TIME_WAIT', + # net constants + 'NIC_DUPLEX_FULL', 'NIC_DUPLEX_HALF', 'NIC_DUPLEX_UNKNOWN', # noqa: F822 + # process status constants + 'STATUS_DEAD', 'STATUS_DISK_SLEEP', 'STATUS_IDLE', 'STATUS_LOCKED', + 'STATUS_RUNNING', 'STATUS_SLEEPING', 'STATUS_STOPPED', 'STATUS_SUSPENDED', + 'STATUS_TRACING_STOP', 'STATUS_WAITING', 'STATUS_WAKE_KILL', + 'STATUS_WAKING', 'STATUS_ZOMBIE', 'STATUS_PARKED', + # other constants + 'ENCODING', 'ENCODING_ERRS', 'AF_INET6', + # named tuples + 'pconn', 'pcputimes', 'pctxsw', 'pgids', 'pio', 'pionice', 'popenfile', + 'pthread', 'puids', 'sconn', 'scpustats', 'sdiskio', 'sdiskpart', + 'sdiskusage', 'snetio', 'snicaddr', 'snicstats', 'sswap', 'suser', + # utility functions + 'conn_tmap', 'deprecated_method', 'isfile_strict', 'memoize', + 'parse_environ_block', 'path_exists_strict', 'usage_percent', + 'supports_ipv6', 'sockfam_to_enum', 'socktype_to_enum', "wrap_numbers", + 'open_text', 'open_binary', 'cat', 'bcat', + 'bytes2human', 'conn_to_ntuple', 'debug', + # shell utils + 'hilite', 'term_supports_colors', 'print_color', +] +# fmt: on + + +# =================================================================== +# --- OS constants +# =================================================================== + + +POSIX = os.name == "posix" +WINDOWS = os.name == "nt" +LINUX = sys.platform.startswith("linux") +MACOS = sys.platform.startswith("darwin") +OSX = MACOS # deprecated alias +FREEBSD = sys.platform.startswith(("freebsd", "midnightbsd")) +OPENBSD = sys.platform.startswith("openbsd") +NETBSD = sys.platform.startswith("netbsd") +BSD = FREEBSD or OPENBSD or NETBSD +SUNOS = sys.platform.startswith(("sunos", "solaris")) +AIX = sys.platform.startswith("aix") + + +# =================================================================== +# --- API constants +# =================================================================== + + +# Process.status() +STATUS_RUNNING = "running" +STATUS_SLEEPING = "sleeping" +STATUS_DISK_SLEEP = "disk-sleep" +STATUS_STOPPED = "stopped" +STATUS_TRACING_STOP = "tracing-stop" +STATUS_ZOMBIE = "zombie" +STATUS_DEAD = "dead" +STATUS_WAKE_KILL = "wake-kill" +STATUS_WAKING = "waking" +STATUS_IDLE = "idle" # Linux, macOS, FreeBSD +STATUS_LOCKED = "locked" # FreeBSD +STATUS_WAITING = "waiting" # FreeBSD +STATUS_SUSPENDED = "suspended" # NetBSD +STATUS_PARKED = "parked" # Linux + +# Process.net_connections() and psutil.net_connections() +CONN_ESTABLISHED = "ESTABLISHED" +CONN_SYN_SENT = "SYN_SENT" +CONN_SYN_RECV = "SYN_RECV" +CONN_FIN_WAIT1 = "FIN_WAIT1" +CONN_FIN_WAIT2 = "FIN_WAIT2" +CONN_TIME_WAIT = "TIME_WAIT" +CONN_CLOSE = "CLOSE" +CONN_CLOSE_WAIT = "CLOSE_WAIT" +CONN_LAST_ACK = "LAST_ACK" +CONN_LISTEN = "LISTEN" +CONN_CLOSING = "CLOSING" +CONN_NONE = "NONE" + + +# net_if_stats() +class NicDuplex(enum.IntEnum): + NIC_DUPLEX_FULL = 2 + NIC_DUPLEX_HALF = 1 + NIC_DUPLEX_UNKNOWN = 0 + + +globals().update(NicDuplex.__members__) + + +# sensors_battery() +class BatteryTime(enum.IntEnum): + POWER_TIME_UNKNOWN = -1 + POWER_TIME_UNLIMITED = -2 + + +globals().update(BatteryTime.__members__) + +# --- others + +ENCODING = sys.getfilesystemencoding() +ENCODING_ERRS = sys.getfilesystemencodeerrors() + + +# =================================================================== +# --- namedtuples +# =================================================================== + +# --- for system functions + +# fmt: off +# psutil.swap_memory() +sswap = namedtuple('sswap', ['total', 'used', 'free', 'percent', 'sin', + 'sout']) +# psutil.disk_usage() +sdiskusage = namedtuple('sdiskusage', ['total', 'used', 'free', 'percent']) +# psutil.disk_io_counters() +sdiskio = namedtuple('sdiskio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes', + 'read_time', 'write_time']) +# psutil.disk_partitions() +sdiskpart = namedtuple('sdiskpart', ['device', 'mountpoint', 'fstype', 'opts']) +# psutil.net_io_counters() +snetio = namedtuple('snetio', ['bytes_sent', 'bytes_recv', + 'packets_sent', 'packets_recv', + 'errin', 'errout', + 'dropin', 'dropout']) +# psutil.users() +suser = namedtuple('suser', ['name', 'terminal', 'host', 'started', 'pid']) +# psutil.net_connections() +sconn = namedtuple('sconn', ['fd', 'family', 'type', 'laddr', 'raddr', + 'status', 'pid']) +# psutil.net_if_addrs() +snicaddr = namedtuple('snicaddr', + ['family', 'address', 'netmask', 'broadcast', 'ptp']) +# psutil.net_if_stats() +snicstats = namedtuple('snicstats', + ['isup', 'duplex', 'speed', 'mtu', 'flags']) +# psutil.cpu_stats() +scpustats = namedtuple( + 'scpustats', ['ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls']) +# psutil.cpu_freq() +scpufreq = namedtuple('scpufreq', ['current', 'min', 'max']) +# psutil.sensors_temperatures() +shwtemp = namedtuple( + 'shwtemp', ['label', 'current', 'high', 'critical']) +# psutil.sensors_battery() +sbattery = namedtuple('sbattery', ['percent', 'secsleft', 'power_plugged']) +# psutil.sensors_fans() +sfan = namedtuple('sfan', ['label', 'current']) +# fmt: on + +# --- for Process methods + +# psutil.Process.cpu_times() +pcputimes = namedtuple( + 'pcputimes', ['user', 'system', 'children_user', 'children_system'] +) +# psutil.Process.open_files() +popenfile = namedtuple('popenfile', ['path', 'fd']) +# psutil.Process.threads() +pthread = namedtuple('pthread', ['id', 'user_time', 'system_time']) +# psutil.Process.uids() +puids = namedtuple('puids', ['real', 'effective', 'saved']) +# psutil.Process.gids() +pgids = namedtuple('pgids', ['real', 'effective', 'saved']) +# psutil.Process.io_counters() +pio = namedtuple( + 'pio', ['read_count', 'write_count', 'read_bytes', 'write_bytes'] +) +# psutil.Process.ionice() +pionice = namedtuple('pionice', ['ioclass', 'value']) +# psutil.Process.ctx_switches() +pctxsw = namedtuple('pctxsw', ['voluntary', 'involuntary']) +# psutil.Process.net_connections() +pconn = namedtuple( + 'pconn', ['fd', 'family', 'type', 'laddr', 'raddr', 'status'] +) + +# psutil.net_connections() and psutil.Process.net_connections() +addr = namedtuple('addr', ['ip', 'port']) + + +# =================================================================== +# --- Process.net_connections() 'kind' parameter mapping +# =================================================================== + + +conn_tmap = { + "all": ([AF_INET, AF_INET6, AF_UNIX], [SOCK_STREAM, SOCK_DGRAM]), + "tcp": ([AF_INET, AF_INET6], [SOCK_STREAM]), + "tcp4": ([AF_INET], [SOCK_STREAM]), + "udp": ([AF_INET, AF_INET6], [SOCK_DGRAM]), + "udp4": ([AF_INET], [SOCK_DGRAM]), + "inet": ([AF_INET, AF_INET6], [SOCK_STREAM, SOCK_DGRAM]), + "inet4": ([AF_INET], [SOCK_STREAM, SOCK_DGRAM]), + "inet6": ([AF_INET6], [SOCK_STREAM, SOCK_DGRAM]), +} + +if AF_INET6 is not None: + conn_tmap.update({ + "tcp6": ([AF_INET6], [SOCK_STREAM]), + "udp6": ([AF_INET6], [SOCK_DGRAM]), + }) + +if AF_UNIX is not None and not SUNOS: + conn_tmap.update({"unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM])}) + + +# ===================================================================== +# --- Exceptions +# ===================================================================== + + +class Error(Exception): + """Base exception class. All other psutil exceptions inherit + from this one. + """ + + __module__ = 'psutil' + + def _infodict(self, attrs): + info = collections.OrderedDict() + for name in attrs: + value = getattr(self, name, None) + if value or (name == "pid" and value == 0): + info[name] = value + return info + + def __str__(self): + # invoked on `raise Error` + info = self._infodict(("pid", "ppid", "name")) + if info: + details = "({})".format( + ", ".join([f"{k}={v!r}" for k, v in info.items()]) + ) + else: + details = None + return " ".join([x for x in (getattr(self, "msg", ""), details) if x]) + + def __repr__(self): + # invoked on `repr(Error)` + info = self._infodict(("pid", "ppid", "name", "seconds", "msg")) + details = ", ".join([f"{k}={v!r}" for k, v in info.items()]) + return f"psutil.{self.__class__.__name__}({details})" + + +class NoSuchProcess(Error): + """Exception raised when a process with a certain PID doesn't + or no longer exists. + """ + + __module__ = 'psutil' + + def __init__(self, pid, name=None, msg=None): + Error.__init__(self) + self.pid = pid + self.name = name + self.msg = msg or "process no longer exists" + + def __reduce__(self): + return (self.__class__, (self.pid, self.name, self.msg)) + + +class ZombieProcess(NoSuchProcess): + """Exception raised when querying a zombie process. This is + raised on macOS, BSD and Solaris only, and not always: depending + on the query the OS may be able to succeed anyway. + On Linux all zombie processes are querable (hence this is never + raised). Windows doesn't have zombie processes. + """ + + __module__ = 'psutil' + + def __init__(self, pid, name=None, ppid=None, msg=None): + NoSuchProcess.__init__(self, pid, name, msg) + self.ppid = ppid + self.msg = msg or "PID still exists but it's a zombie" + + def __reduce__(self): + return (self.__class__, (self.pid, self.name, self.ppid, self.msg)) + + +class AccessDenied(Error): + """Exception raised when permission to perform an action is denied.""" + + __module__ = 'psutil' + + def __init__(self, pid=None, name=None, msg=None): + Error.__init__(self) + self.pid = pid + self.name = name + self.msg = msg or "" + + def __reduce__(self): + return (self.__class__, (self.pid, self.name, self.msg)) + + +class TimeoutExpired(Error): + """Raised on Process.wait(timeout) if timeout expires and process + is still alive. + """ + + __module__ = 'psutil' + + def __init__(self, seconds, pid=None, name=None): + Error.__init__(self) + self.seconds = seconds + self.pid = pid + self.name = name + self.msg = f"timeout after {seconds} seconds" + + def __reduce__(self): + return (self.__class__, (self.seconds, self.pid, self.name)) + + +# =================================================================== +# --- utils +# =================================================================== + + +def usage_percent(used, total, round_=None): + """Calculate percentage usage of 'used' against 'total'.""" + try: + ret = (float(used) / total) * 100 + except ZeroDivisionError: + return 0.0 + else: + if round_ is not None: + ret = round(ret, round_) + return ret + + +def memoize(fun): + """A simple memoize decorator for functions supporting (hashable) + positional arguments. + It also provides a cache_clear() function for clearing the cache: + + >>> @memoize + ... def foo() + ... return 1 + ... + >>> foo() + 1 + >>> foo.cache_clear() + >>> + + 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/PortablePython/Lib/site-packages/psutil/_psaix.py b/PortablePython/Lib/site-packages/psutil/_psaix.py new file mode 100644 index 0000000..ba2725f --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/_psbsd.py b/PortablePython/Lib/site-packages/psutil/_psbsd.py new file mode 100644 index 0000000..13bd926 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/_pslinux.py b/PortablePython/Lib/site-packages/psutil/_pslinux.py new file mode 100644 index 0000000..8cc64e9 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/_psosx.py b/PortablePython/Lib/site-packages/psutil/_psosx.py new file mode 100644 index 0000000..620497b --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/_psposix.py b/PortablePython/Lib/site-packages/psutil/_psposix.py new file mode 100644 index 0000000..88703fd --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/_pssunos.py b/PortablePython/Lib/site-packages/psutil/_pssunos.py new file mode 100644 index 0000000..78d941c --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/_psutil_windows.pyd b/PortablePython/Lib/site-packages/psutil/_psutil_windows.pyd new file mode 100644 index 0000000..1e7f44e Binary files /dev/null and b/PortablePython/Lib/site-packages/psutil/_psutil_windows.pyd differ diff --git a/PortablePython/Lib/site-packages/psutil/_pswindows.py b/PortablePython/Lib/site-packages/psutil/_pswindows.py new file mode 100644 index 0000000..e5af3c9 --- /dev/null +++ b/PortablePython/Lib/site-packages/psutil/_pswindows.py @@ -0,0 +1,1103 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Windows platform implementation.""" + +import contextlib +import enum +import functools +import os +import signal +import sys +import time +from collections import namedtuple + +from . import _common +from ._common import ENCODING +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import TimeoutExpired +from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import debug +from ._common import isfile_strict +from ._common import memoize +from ._common import memoize_when_activated +from ._common import parse_environ_block +from ._common import usage_percent +from ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS +from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS +from ._psutil_windows import HIGH_PRIORITY_CLASS +from ._psutil_windows import IDLE_PRIORITY_CLASS +from ._psutil_windows import NORMAL_PRIORITY_CLASS +from ._psutil_windows import REALTIME_PRIORITY_CLASS + + +try: + from . import _psutil_windows as cext +except ImportError as err: + if ( + str(err).lower().startswith("dll load failed") + and sys.getwindowsversion()[0] < 6 + ): + # We may get here if: + # 1) we are on an old Windows version + # 2) psutil was installed via pip + wheel + # See: https://github.com/giampaolo/psutil/issues/811 + msg = "this Windows version is too old (< Windows Vista); " + msg += "psutil 3.4.2 is the latest version which supports Windows " + msg += "2000, XP and 2003 server" + raise RuntimeError(msg) from err + else: + raise + + +# process priority constants, import from __init__.py: +# http://msdn.microsoft.com/en-us/library/ms686219(v=vs.85).aspx +# fmt: off +__extra__all__ = [ + "win_service_iter", "win_service_get", + # Process priority + "ABOVE_NORMAL_PRIORITY_CLASS", "BELOW_NORMAL_PRIORITY_CLASS", + "HIGH_PRIORITY_CLASS", "IDLE_PRIORITY_CLASS", "NORMAL_PRIORITY_CLASS", + "REALTIME_PRIORITY_CLASS", + # IO priority + "IOPRIO_VERYLOW", "IOPRIO_LOW", "IOPRIO_NORMAL", "IOPRIO_HIGH", + # others + "CONN_DELETE_TCB", "AF_LINK", +] +# fmt: on + + +# ===================================================================== +# --- globals +# ===================================================================== + +CONN_DELETE_TCB = "DELETE_TCB" +ERROR_PARTIAL_COPY = 299 +PYPY = '__pypy__' in sys.builtin_module_names + +AddressFamily = enum.IntEnum('AddressFamily', {'AF_LINK': -1}) +AF_LINK = AddressFamily.AF_LINK + +TCP_STATUSES = { + cext.MIB_TCP_STATE_ESTAB: _common.CONN_ESTABLISHED, + cext.MIB_TCP_STATE_SYN_SENT: _common.CONN_SYN_SENT, + cext.MIB_TCP_STATE_SYN_RCVD: _common.CONN_SYN_RECV, + cext.MIB_TCP_STATE_FIN_WAIT1: _common.CONN_FIN_WAIT1, + cext.MIB_TCP_STATE_FIN_WAIT2: _common.CONN_FIN_WAIT2, + cext.MIB_TCP_STATE_TIME_WAIT: _common.CONN_TIME_WAIT, + cext.MIB_TCP_STATE_CLOSED: _common.CONN_CLOSE, + cext.MIB_TCP_STATE_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, + cext.MIB_TCP_STATE_LAST_ACK: _common.CONN_LAST_ACK, + cext.MIB_TCP_STATE_LISTEN: _common.CONN_LISTEN, + cext.MIB_TCP_STATE_CLOSING: _common.CONN_CLOSING, + cext.MIB_TCP_STATE_DELETE_TCB: CONN_DELETE_TCB, + cext.PSUTIL_CONN_NONE: _common.CONN_NONE, +} + + +class Priority(enum.IntEnum): + ABOVE_NORMAL_PRIORITY_CLASS = ABOVE_NORMAL_PRIORITY_CLASS + BELOW_NORMAL_PRIORITY_CLASS = BELOW_NORMAL_PRIORITY_CLASS + HIGH_PRIORITY_CLASS = HIGH_PRIORITY_CLASS + IDLE_PRIORITY_CLASS = IDLE_PRIORITY_CLASS + NORMAL_PRIORITY_CLASS = NORMAL_PRIORITY_CLASS + REALTIME_PRIORITY_CLASS = REALTIME_PRIORITY_CLASS + + +globals().update(Priority.__members__) + + +class IOPriority(enum.IntEnum): + IOPRIO_VERYLOW = 0 + IOPRIO_LOW = 1 + IOPRIO_NORMAL = 2 + IOPRIO_HIGH = 3 + + +globals().update(IOPriority.__members__) + +pinfo_map = dict( + num_handles=0, + ctx_switches=1, + user_time=2, + kernel_time=3, + create_time=4, + num_threads=5, + io_rcount=6, + io_wcount=7, + io_rbytes=8, + io_wbytes=9, + io_count_others=10, + io_bytes_others=11, + num_page_faults=12, + peak_wset=13, + wset=14, + peak_paged_pool=15, + paged_pool=16, + peak_non_paged_pool=17, + non_paged_pool=18, + pagefile=19, + peak_pagefile=20, + mem_private=21, +) + + +# ===================================================================== +# --- named tuples +# ===================================================================== + + +# fmt: off +# psutil.cpu_times() +scputimes = namedtuple('scputimes', + ['user', 'system', 'idle', 'interrupt', 'dpc']) +# psutil.virtual_memory() +svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free']) +# psutil.Process.memory_info() +pmem = namedtuple( + 'pmem', ['rss', 'vms', + 'num_page_faults', 'peak_wset', 'wset', 'peak_paged_pool', + 'paged_pool', 'peak_nonpaged_pool', 'nonpaged_pool', + 'pagefile', 'peak_pagefile', 'private']) +# psutil.Process.memory_full_info() +pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', )) +# psutil.Process.memory_maps(grouped=True) +pmmap_grouped = namedtuple('pmmap_grouped', ['path', 'rss']) +# psutil.Process.memory_maps(grouped=False) +pmmap_ext = namedtuple( + 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) +# psutil.Process.io_counters() +pio = namedtuple('pio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes', + 'other_count', 'other_bytes']) +# fmt: on + + +# ===================================================================== +# --- utils +# ===================================================================== + + +@functools.lru_cache(maxsize=512) +def convert_dos_path(s): + r"""Convert paths using native DOS format like: + "\Device\HarddiskVolume1\Windows\systemew\file.txt" + into: + "C:\Windows\systemew\file.txt". + """ + rawdrive = '\\'.join(s.split('\\')[:3]) + driveletter = cext.QueryDosDevice(rawdrive) + remainder = s[len(rawdrive) :] + return os.path.join(driveletter, remainder) + + +@memoize +def getpagesize(): + return cext.getpagesize() + + +# ===================================================================== +# --- memory +# ===================================================================== + + +def virtual_memory(): + """System virtual memory as a namedtuple.""" + mem = cext.virtual_mem() + totphys, availphys, _totsys, _availsys = mem + total = totphys + avail = availphys + free = availphys + used = total - avail + percent = usage_percent((total - avail), total, round_=1) + return svmem(total, avail, percent, used, free) + + +def swap_memory(): + """Swap system memory as a (total, used, free, sin, sout) tuple.""" + mem = cext.virtual_mem() + + total_phys = mem[0] + total_system = mem[2] + + # system memory (commit total/limit) is the sum of physical and swap + # thus physical memory values need to be subtracted to get swap values + total = total_system - total_phys + # commit total is incremented immediately (decrementing free_system) + # while the corresponding free physical value is not decremented until + # pages are accessed, so we can't use free system memory for swap. + # instead, we calculate page file usage based on performance counter + if total > 0: + percentswap = cext.swap_percent() + used = int(0.01 * percentswap * total) + else: + percentswap = 0.0 + used = 0 + + free = total - used + percent = round(percentswap, 1) + return _common.sswap(total, used, free, percent, 0, 0) + + +# ===================================================================== +# --- 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/PortablePython/Lib/site-packages/psutil/tests/__init__.py b/PortablePython/Lib/site-packages/psutil/tests/__init__.py new file mode 100644 index 0000000..5d4b3ab --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/__main__.py b/PortablePython/Lib/site-packages/psutil/tests/__main__.py new file mode 100644 index 0000000..ce6fc24 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_aix.py b/PortablePython/Lib/site-packages/psutil/tests/test_aix.py new file mode 100644 index 0000000..10934c1 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_bsd.py b/PortablePython/Lib/site-packages/psutil/tests/test_bsd.py new file mode 100644 index 0000000..2786c34 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_connections.py b/PortablePython/Lib/site-packages/psutil/tests/test_connections.py new file mode 100644 index 0000000..5ddeb85 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_contracts.py b/PortablePython/Lib/site-packages/psutil/tests/test_contracts.py new file mode 100644 index 0000000..55f3a5d --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_linux.py b/PortablePython/Lib/site-packages/psutil/tests/test_linux.py new file mode 100644 index 0000000..f4342d7 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_memleaks.py b/PortablePython/Lib/site-packages/psutil/tests/test_memleaks.py new file mode 100644 index 0000000..7f78fae --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_misc.py b/PortablePython/Lib/site-packages/psutil/tests/test_misc.py new file mode 100644 index 0000000..c484264 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_osx.py b/PortablePython/Lib/site-packages/psutil/tests/test_osx.py new file mode 100644 index 0000000..050418c --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_posix.py b/PortablePython/Lib/site-packages/psutil/tests/test_posix.py new file mode 100644 index 0000000..a784492 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_process.py b/PortablePython/Lib/site-packages/psutil/tests/test_process.py new file mode 100644 index 0000000..9ba1ba0 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_process_all.py b/PortablePython/Lib/site-packages/psutil/tests/test_process_all.py new file mode 100644 index 0000000..aaa3fa0 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_scripts.py b/PortablePython/Lib/site-packages/psutil/tests/test_scripts.py new file mode 100644 index 0000000..de0ad2a --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_sunos.py b/PortablePython/Lib/site-packages/psutil/tests/test_sunos.py new file mode 100644 index 0000000..b5d9d35 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_system.py b/PortablePython/Lib/site-packages/psutil/tests/test_system.py new file mode 100644 index 0000000..b961e1f --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_testutils.py b/PortablePython/Lib/site-packages/psutil/tests/test_testutils.py new file mode 100644 index 0000000..6db66e5 --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_unicode.py b/PortablePython/Lib/site-packages/psutil/tests/test_unicode.py new file mode 100644 index 0000000..d8a8c4b --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/psutil/tests/test_windows.py b/PortablePython/Lib/site-packages/psutil/tests/test_windows.py new file mode 100644 index 0000000..c5c536b --- /dev/null +++ b/PortablePython/Lib/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/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/COPYING.LGPL b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/COPYING.LGPL new file mode 100644 index 0000000..0df067b --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/COPYING.LGPL @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007-2024 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/INSTALLER b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/METADATA b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/METADATA new file mode 100644 index 0000000..fecb69e --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/METADATA @@ -0,0 +1,914 @@ +Metadata-Version: 2.1 +Name: pynput +Version: 1.8.1 +Summary: Monitor and control user input devices +Home-page: https://github.com/moses-palmer/pynput +Author: Moses Palmér +Author-email: moses.palmer@gmail.com +License: LGPLv3 +Keywords: control mouse,mouse input,control keyboard,keyboard input +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3) +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows :: Windows NT/2000 +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Monitoring +License-File: COPYING.LGPL +Requires-Dist: six +Requires-Dist: evdev>=1.3; "linux" in sys_platform +Requires-Dist: python-xlib>=0.17; "linux" in sys_platform +Requires-Dist: enum34; python_version == "2.7" +Requires-Dist: pyobjc-framework-ApplicationServices>=8.0; sys_platform == "darwin" +Requires-Dist: pyobjc-framework-Quartz>=8.0; sys_platform == "darwin" + +pynput +====== + +This library allows you to control and monitor input devices. + +Currently, mouse and keyboard input and monitoring are supported. + +See `here `_ for the full +documentation. + + +Controlling the mouse +--------------------- + +Use ``pynput.mouse.Controller`` like this:: + + from pynput.mouse import Button, Controller + + mouse = Controller() + + # Read pointer position + print('The current pointer position is {}'.format( + mouse.position)) + + # Set pointer position + mouse.position = (10, 20) + print('Now we have moved it to {}'.format( + mouse.position)) + + # Move pointer relative to current position + mouse.move(5, -5) + + # Press and release + mouse.press(Button.left) + mouse.release(Button.left) + + # Double click; this is different from pressing and releasing + # twice on macOS + mouse.click(Button.left, 2) + + # Scroll two steps down + mouse.scroll(0, 2) + + +Monitoring the mouse +-------------------- + +Use ``pynput.mouse.Listener`` like this:: + + from pynput import mouse + + def on_move(x, y, injected): + print('Pointer moved to {}; it was {}'.format( + (x, y, 'faked' if injected else 'not faked'))) + + def on_click(x, y, button, pressed, injected): + print('{} at {}; it was {}'.format( + 'Pressed' if pressed else 'Released', + (x, y, 'faked' if injected else 'not faked'))) + if not pressed: + # Stop listener + return False + + def on_scroll(x, y, dx, dy, injected): + print('Scrolled {} at {}; it was {}'.format( + 'down' if dy < 0 else 'up', + (x, y, 'faked' if injected else 'not faked'))) + + # Collect events until released + with mouse.Listener( + on_move=on_move, + on_click=on_click, + on_scroll=on_scroll) as listener: + listener.join() + + # ...or, in a non-blocking fashion: + listener = mouse.Listener( + on_move=on_move, + on_click=on_click, + on_scroll=on_scroll) + listener.start() + +A mouse listener is a ``threading.Thread``, and all callbacks will be invoked +from the thread. + +Call ``pynput.mouse.Listener.stop`` from anywhere, raise ``StopException`` or +return ``False`` from a callback to stop the listener. + +When using the non-blocking version above, the current thread will continue +executing. This might be necessary when integrating with other GUI frameworks +that incorporate a main-loop, but when run from a script, this will cause the +program to terminate immediately. + + +The mouse listener thread +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The listener callbacks are invoked directly from an operating thread on some +platforms, notably *Windows*. + +This means that long running procedures and blocking operations should not be +invoked from the callback, as this risks freezing input for all processes. + +A possible workaround is to just dispatch incoming messages to a queue, and let +a separate thread handle them. + + +Handling mouse listener errors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a callback handler raises an exception, the listener will be stopped. Since +callbacks run in a dedicated thread, the exceptions will not automatically be +reraised. + +To be notified about callback errors, call ``Thread.join`` on the listener +instance:: + + from pynput import mouse + + class MyException(Exception): pass + + def on_click(x, y, button, pressed): + if button == mouse.Button.left: + raise MyException(button) + + # Collect events until released + with mouse.Listener( + on_click=on_click) as listener: + try: + listener.join() + except MyException as e: + print('{} was clicked'.format(e.args[0])) + + +Toggling event listening for the mouse listener +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once ``pynput.mouse.Listener.stop`` has been called, the listener cannot be +restarted, since listeners are instances of ``threading.Thread``. + +If your application requires toggling listening events, you must either add an +internal flag to ignore events when not required, or create a new listener when +resuming listening. + + +Synchronous event listening for the mouse listener +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To simplify scripting, synchronous event listening is supported through the +utility class ``pynput.mouse.Events``. This class supports reading single +events in a non-blocking fashion, as well as iterating over all events. + +To read a single event, use the following code:: + + from pynput import mouse + + # The event listener will be running in this block + with mouse.Events() as events: + # Block at most one second + event = events.get(1.0) + if event is None: + print('You did not interact with the mouse within one second') + else: + print('Received event {}'.format(event)) + +To iterate over mouse events, use the following code:: + + from pynput import mouse + + # The event listener will be running in this block + with mouse.Events() as events: + for event in events: + if event.button == mouse.Button.right: + break + else: + print('Received event {}'.format(event)) + +Please note that the iterator method does not support non-blocking operation, +so it will wait for at least one mouse event. + +The events will be instances of the inner classes found in +``pynput.mouse.Events``. + + +Ensuring consistent coordinates between listener and controller on Windows +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Recent versions of _Windows_ support running legacy applications scaled when +the system scaling has been increased beyond 100%. This allows old applications +to scale, albeit with a blurry look, and avoids tiny, unusable user interfaces. + +This scaling is unfortunately inconsistently applied to a mouse listener and a +controller: the listener will receive physical coordinates, but the controller +has to work with scaled coordinates. + +This can be worked around by telling Windows that your application is DPI +aware. This is a process global setting, so _pynput_ cannot do it +automatically. Do enable DPI awareness, run the following code:: + + import ctypes + + + PROCESS_PER_MONITOR_DPI_AWARE = 2 + + ctypes.windll.shcore.SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE) + + +Controlling the keyboard +------------------------ + +Use ``pynput.keyboard.Controller`` like this:: + + from pynput.keyboard import Key, Controller + + keyboard = Controller() + + # Press and release space + keyboard.press(Key.space) + keyboard.release(Key.space) + + # Type a lower case A; this will work even if no key on the + # physical keyboard is labelled 'A' + keyboard.press('a') + keyboard.release('a') + + # Type two upper case As + keyboard.press('A') + keyboard.release('A') + with keyboard.pressed(Key.shift): + keyboard.press('a') + keyboard.release('a') + + # Type 'Hello World' using the shortcut type method + keyboard.type('Hello World') + + +Monitoring the keyboard +----------------------- + +Use ``pynput.keyboard.Listener`` like this:: + + from pynput import keyboard + + def on_press(key, injected): + try: + print('alphanumeric key {} pressed; it was {}'.format( + key.char, 'faked' if injected else 'not faked')) + except AttributeError: + print('special key {} pressed'.format( + key)) + + def on_release(key, injected): + print('{} released; it was {}'.format( + key, 'faked' if injected else 'not faked')) + if key == keyboard.Key.esc: + # Stop listener + return False + + # Collect events until released + with keyboard.Listener( + on_press=on_press, + on_release=on_release) as listener: + listener.join() + + # ...or, in a non-blocking fashion: + listener = keyboard.Listener( + on_press=on_press, + on_release=on_release) + listener.start() + +A keyboard listener is a ``threading.Thread``, and all callbacks will be +invoked from the thread. + +Call ``pynput.keyboard.Listener.stop`` from anywhere, raise ``StopException`` +or return ``False`` from a callback to stop the listener. + +The ``key`` parameter passed to callbacks is a ``pynput.keyboard.Key``, for +special keys, a ``pynput.keyboard.KeyCode`` for normal alphanumeric keys, or +just ``None`` for unknown keys. + +When using the non-blocking version above, the current thread will continue +executing. This might be necessary when integrating with other GUI frameworks +that incorporate a main-loop, but when run from a script, this will cause the +program to terminate immediately. + + +The keyboard listener thread +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The listener callbacks are invoked directly from an operating thread on some +platforms, notably *Windows*. + +This means that long running procedures and blocking operations should not be +invoked from the callback, as this risks freezing input for all processes. + +A possible workaround is to just dispatch incoming messages to a queue, and let +a separate thread handle them. + + +Handling keyboard listener errors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a callback handler raises an exception, the listener will be stopped. Since +callbacks run in a dedicated thread, the exceptions will not automatically be +reraised. + +To be notified about callback errors, call ``Thread.join`` on the listener +instance:: + + from pynput import keyboard + + class MyException(Exception): pass + + def on_press(key): + if key == keyboard.Key.esc: + raise MyException(key) + + # Collect events until released + with keyboard.Listener( + on_press=on_press) as listener: + try: + listener.join() + except MyException as e: + print('{} was pressed'.format(e.args[0])) + + +Toggling event listening for the keyboard listener +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once ``pynput.keyboard.Listener.stop`` has been called, the listener cannot be +restarted, since listeners are instances of ``threading.Thread``. + +If your application requires toggling listening events, you must either add an +internal flag to ignore events when not required, or create a new listener when +resuming listening. + + +Synchronous event listening for the keyboard listener +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To simplify scripting, synchronous event listening is supported through the +utility class ``pynput.keyboard.Events``. This class supports reading single +events in a non-blocking fashion, as well as iterating over all events. + +To read a single event, use the following code:: + + from pynput import keyboard + + # The event listener will be running in this block + with keyboard.Events() as events: + # Block at most one second + event = events.get(1.0) + if event is None: + print('You did not press a key within one second') + else: + print('Received event {}'.format(event)) + +To iterate over keyboard events, use the following code:: + + from pynput import keyboard + + # The event listener will be running in this block + with keyboard.Events() as events: + for event in events: + if event.key == keyboard.Key.esc: + break + else: + print('Received event {}'.format(event)) + +Please note that the iterator method does not support non-blocking operation, +so it will wait for at least one keyboard event. + +The events will be instances of the inner classes found in +``pynput.keyboard.Events``. + + +Global hotkeys +~~~~~~~~~~~~~~ + +A common use case for keyboard monitors is reacting to global hotkeys. Since a +listener does not maintain any state, hotkeys involving multiple keys must +store this state somewhere. + +*pynput* provides the class ``pynput.keyboard.HotKey`` for this purpose. It +contains two methods to update the state, designed to be easily interoperable +with a keyboard listener: ``pynput.keyboard.HotKey.press`` and +``pynput.keyboard.HotKey.release`` which can be directly passed as listener +callbacks. + +The intended usage is as follows:: + + from pynput import keyboard + + def on_activate(): + print('Global hotkey activated!') + + def for_canonical(f): + return lambda k: f(l.canonical(k)) + + hotkey = keyboard.HotKey( + keyboard.HotKey.parse('++h'), + on_activate) + with keyboard.Listener( + on_press=for_canonical(hotkey.press), + on_release=for_canonical(hotkey.release)) as l: + l.join() + +This will create a hotkey, and then use a listener to update its state. Once +all the specified keys are pressed simultaneously, ``on_activate`` will be +invoked. + +Note that keys are passed through ``pynput.keyboard.Listener.canonical`` before +being passed to the ``HotKey`` instance. This is to remove any modifier state +from the key events, and to normalise modifiers with more than one physical +button. + +The method ``pynput.keyboard.HotKey.parse`` is a convenience function to +transform shortcut strings to key collections. Please see its documentation for +more information. + +To register a number of global hotkeys, use the convenience class +``pynput.keyboard.GlobalHotKeys``:: + + from pynput import keyboard + + def on_activate_h(): + print('++h pressed') + + def on_activate_i(): + print('++i pressed') + + with keyboard.GlobalHotKeys({ + '++h': on_activate_h, + '++i': on_activate_i}) as h: + h.join() + + +Release Notes +============= + +v1.8.1 (2025-03-17) - Various fixes +----------------------------------- +* Remove incorrectly merged line for the *Xorg* backend. Thanks to *sphh*! +* Let events know about the new ``injected`` parameter. Thanks to + *phpjunkie420*! + + +v1.8.0 (2025-03-03) - Allow detecting injected events +----------------------------------------------------- +* Add a flag to callbacks to allow detecting injected input events. +* Add ``media_stop`` key for *macOS*. Thanks to *laura-3*! +* Add ``eject`` key for *macOS*. Thanks to *DiMNDev*! + + +v1.7.8 (2025-02-28) - Fixes for Python 3.12 +------------------------------------------- +* Rename method for listeners to not conflict with new ``threading.Thread`` + field. + + +v1.7.7 (2024-05-10) - Various fixes +----------------------------------- +* Small corrections to the documentation. +* Handle explicit timeout when calling ``join`` on listeners. +* Correct regression in hot key handling for special keys. +* Reverted changes to lazy loading of ``CoreFoundation`` and ``Quartz``, since + this still does not appear to work. Thanks to *Zach Zaiman*! +* Let the type of values in ``Key`` be ``KeyCode`` so that type checkers are + not confused. Thanks to *Amund Eggen Svandal*! +* Do not crash in ``__del__`` on *Xorg* if display creation fails. Thanks to + *Gabriele Pongelli*! +* Correct support for emojis on *Windows*. Thanks to *Yunus Emre*! + + +v1.7.6 (2022-01-01) - Various fixes +----------------------------------- +* Allow passing virtual key codes to the parser for global hot keys. +* Stop the recording context asynchronously on *Xorg*. +* Do not pass ``None`` to ``objc.objc_object``. Thanks to *yejunxi*! +* Do not crash when pressing the *alt* key on *uinput*. Thanks to *Caldas + Lopes*! +* Use the correct option prefix for listeners derived from the backend + implementations. Thanks to *Yu Wang*! + + +v1.7.5 (2021-11-19) - Various fixes +----------------------------------- +* Corrected crashes on *Xorg* when a listener was configured to suppress + system events. Thanks to *jpramosi*! +* Improved handling of keyboard controller on *Windows*. The controller now + has a greater change of working with applications using lower level events. + Thanks to *bhudax*! +* Updated *macOS* implementation to use new version of *pyobjc*. + + +v1.7.4 (2021-10-10) - Various fixes +----------------------------------- +* Detect whether permissions are lacking on *macOS*. Thanks to *Dane Finlay*! +* Eagerly import symbols from ``CoreFoundation`` and ``Quartz``. Thanks to + *Ronald Oussoren*! +* Improved handling of ``dumpkeys`` utility. Thanks to *Markus Niedermann*! +* Removed ambiguous license file. + + +v1.7.3 (2021-02-10) - Various fixes +----------------------------------- +* Corrected *keysym* handling on *Xorg*; not all groups were loaded, and the + fallback to our internal tables was never triggered. Thanks to *Philipp + Klaus*! +* Updated the version of *Quartz* used for the *macOS* backend to allow + *pynput* to be installed on *Big Sur*. Thanks to *Michael Madden*! +* Added missing function keys on *Windows*. Thanks to *Dave Atkinson*! +* Corrected scroll speed for mouse controller on *macOS*. Thanks to *Albert + Zeyer*! +* Corrected media keys for *Xorg*. Thanks to *Gabriele N. Tornetta*! +* Corrected parameter name in documentation. Thanks to *Jinesi Yelizati*! + + +v1.7.2 (2020-12-21) - Corrected uinput key mapping +-------------------------------------------------- +* Corrected mapping of virtual key codes to characters for the *uinput* + backend. +* Corrected spelling errors. Thanks to *Martin Michlmayr*! +* Corrected and improved documentation. + + +v1.7.1 (2020-08-30) - Corrected release notes +--------------------------------------------- +* Corrected thanks for arbitrary unicode character support for *Xorg*. + + +v1.7.0 (2020-08-30) - A new backend and many new features and bug fixes +----------------------------------------------------------------------- +* Added a new *uinput* based keyboard backend for *Linux*, when no *X* server + is available. +* Allow typing arbitrary unicode characters on *Xorg* backend. Thanks to + *gdiShun*! +* Allow overriding the automatically selected backend with an environment + variable, and added a dummy backend. +* Added support for mouse side button on *Windows*. Thanks to *danielkovarik*! +* Added convenience method to tap keys. +* Allow specifying raw virtual key codes in hotkeys. +* Improved error messages when a backend cannot be loaded. +* Include more information in stringification of events. +* Corrected return value of ``Events.get`` to that specified by the + documentation. +* Corrected keyboard listener not to type random characters on certain + keyboard layouts. +* Corrected errors when pressing certain keys on *Windows*, where the + operating system reports that they are dead but no combining version exists. +* Improved documentation. + + +v1.6.8 (2020-02-28) - Various fixes +----------------------------------- +* Updated documentation. +* Corrected lint warnings and tests. +* Do not use internal types in ``argtypes`` for ``win32`` functions; this + renders them uncallable for other code running in the same runtime. +* Include scan codes in events on *Windows*. Thanks to *bhudax*! +* Correctly apply transformation to scroll event values on *Windows*. Thanks + to *DOCCA0*! + + +v1.6.7 (2020-02-17) - Various fixes +----------------------------------- +* Corrected infinite scrolling on *macOS* when providing non-integer deltas. + Thanks to *Iván Munsuri Ibáñez*! +* Corrected controller and listener handling of media keys on *macOS*. Thanks + to *Iván Munsuri Ibáñez*! + + +v1.6.6 (2020-01-23) - Corrected hot key documentation +----------------------------------------------------- +* The code examples for the simple ``pynput.keyboard.HotKey`` now work. Thanks + to *jfongattw*! + + +v1.6.5 (2020-01-08) - Corrected media key mappings +-------------------------------------------------- +* Corrected media key mappings on *macOS*. Thanks to *Luis Nachtigall*! + + +v1.6.4 (2020-01-03) - Corrected imports yet again +------------------------------------------------- +* Corrected imports for keyboard Controller. Thanks to *rhystedstone*! + + +v1.6.3 (2019-12-28) - Corrected imports again +--------------------------------------------- +* Corrected imports for keyboard Controller. Thanks to *Matt Iversen*! + + +v1.6.2 (2019-12-28) - Corrected imports +--------------------------------------- +* Corrected imports for keyboard Controller. Thanks to *Matt Iversen*! + + +v1.6.1 (2019-12-27) - Corrections for *Windows* +----------------------------------------------- +* Corrected global hotkeys on *Windows*. +* Corrected pressed / released state for keyboard listener on *Windows*. + Thanks to *segalion*! + +v1.6.0 (2019-12-11) - Global Hotkeys +------------------------------------ +* Added support for global hotkeys. +* Added support for streaming listener events synchronously. + + +v1.5.2 (2019-12-06) - Corrected media key names for *Xorg* +---------------------------------------------------------- +* Removed media flag from *Xorg* keys. + + +v1.5.1 (2019-12-06) - Corrected media key names for *macOS* +----------------------------------------------------------- +* Corrected attribute names for media keys on *macOS*. Thanks to *ah3243*! + + +v1.5.0 (2019-12-04) - Various improvements +------------------------------------------ +* Corrected keyboard listener on *Windows*. Thanks to *akiratakasaki*, + *segalion*, *SpecialCharacter*! +* Corrected handling of some special keys, including arrow keys, when combined + with modifiers on *Windows*. Thanks to *tuessetr*! +* Updated documentation to include information about DPI scaling on *Windows*. + Thanks to *david-szarka*! +* Added experimental support for media keys. Thanks to *ShivamJoker*, + *StormTersteeg*! + + +v1.4.5 (2019-11-05) - Corrected errors on *Python 3.8* +------------------------------------------------------ +* Corrected errors about using `in` operator for enums on *Python 3.8* on + *macOS*. + + +v1.4.4 (2019-09-24) - Actually corrected keyboard listener on macOS +------------------------------------------------------------------- +* Included commit to correctly fall back on + ``CGEventKeyboardGetUnicodeString``. +* Corrected deprecation warnings about ``Enum`` usage on *Python 3.8*. + + +v1.4.3 (2019-09-24) - Corrected keyboard listener on macOS again +---------------------------------------------------------------- +* Correctly fall back on ``CGEventKeyboardGetUnicodeString``. +* Updated documentation. + + +v1.4.2 (2019-03-22) - Corrected keyboard listener on macOS +---------------------------------------------------------- +* Use ``CGEventKeyboardGetUnicodeString`` in *macOS* keyboard listener to send + correct characters. +* Include keysym instead of key code in *Xorg* keyboard listener. +* Corrected logging to not include expected ``StopException``. +* Updated and corrected documentation. + + +v1.4.1 (2018-09-07) - Logging +----------------------------- +* Log unhandled exceptions raised by listener callbacks. + + +v1.4 (2018-07-03) - Event suppression +------------------------------------- +* Added possibility to fully suppress events when listening. +* Added support for typing some control characters. +* Added support for mouse drag events on *OSX*. Thanks to *jungledrum*! +* Include the key code in keyboard listener events. +* Correctly handle the numeric key pad on *Xorg* with *num lock* active. + Thanks to *TheoRet*! +* Corrected handling of current thread keyboard layout on *Windows*. Thanks to + *Schmettaling*! +* Corrected stopping of listeners on *Xorg*. +* Corrected import of ``Xlib.keysymdef.xkb`` on *Xorg*. Thanks to *Glandos*! + + +v1.3.10 (2018-02-05) - Do not crash under *Xephyr* +-------------------------------------------------- +* Do not crash when ``Xlib.display.Display.get_input_focus`` returns an + integer, as it may when running under *Xephyr*. Thanks to *Eli Skeggs*! + + +v1.3.9 (2018-01-12) - Correctly handle the letter *A* on *OSX* +-------------------------------------------------------------- +* Corrected check for virtual key code when generating keyboard events on + *OSX*. This fixes an issue where pressing *A* with *shift* explicitly pressed + would still type a minuscule letter. + + +v1.3.8 (2017-12-08) - Do not crash on some keyboard layouts on *OSX* +-------------------------------------------------------------------- +* Fall back on a different method to retrieve the keyboard layout on *OSX*. + This helps for some keyboard layouts, such as *Chinese*. Thanks to + *haoflynet*! + + +v1.3.7 (2017-08-23) - *Xorg* corrections +---------------------------------------- +* Include mouse buttons up to *30* for *Xorg*. + + +v1.3.6 (2017-08-13) - *win32* corrections +----------------------------------------- +* Corrected double delivery of fake keyboard events on *Windows*. +* Corrected handling of synthetic unicode keys on *Windows*. + + +v1.3.5 (2017-06-07) - Corrected dependencies again +-------------------------------------------------- +* Reverted changes in *1.3.3*. +* Corrected platform specifier for *Python 2* on *Linux*. + + +v1.3.4 (2017-06-05) - *Xorg* corrections +---------------------------------------- +* Corrected bounds check for values on *Xorg*. + + +v1.3.3 (2017-06-05) - Make dependencies non-optional +---------------------------------------------------- +* Made platform dependencies non-optional. + + +v1.3.2 (2017-05-15) - Fix for button click on Mac +------------------------------------------------- +* Corrected regression from previous release where button clicks would + crash the *Mac* mouse listener. + + +v1.3.1 (2017-05-12) - Fixes for unknown buttons on Linux +-------------------------------------------------------- +* Fall back on `Button.unknown` for unknown mouse buttons in *Xorg* mouse + listener. + + +v1.3 (2017-04-10) - Platform specific features +---------------------------------------------- +* Added ability to stop event propagation on *Windows*. This will prevent + events from reaching other applications. +* Added ability to ignore events on *Windows*. This is a workaround for systems + where the keyboard monitor interferes with normal keyboard events. +* Added ability to modify events on *OSX*. This allows intercepting and + altering input events before they reach other applications. +* Corrected crash on *OSX* when some types of third party input sources are + installed. + + +v1.2 (2017-01-06) - Improved error handling +------------------------------------------- +* Allow catching exceptions thrown from listener callbacks. This changes the + API, as joining a listener now potentially raises unhandled exceptions, + and unhandled exceptions will stop listeners. +* Added support for the numeric keypad on *Linux*. +* Improved documentation. +* Thanks to *jollysean* and *gilleswijnker* for their input! + + +v1.1.7 (2017-01-02) - Handle middle button on Windows +----------------------------------------------------- +* Listen for and dispatch middle button mouse clicks on *Windows*. + + +v1.1.6 (2016-11-24) - Corrected context manager for pressing keys +----------------------------------------------------------------- +* Corrected bug in ``pynput.keyboard.Controller.pressed`` which caused it to + never release the key. Many thanks to Toby Southwell! + + +v1.1.5 (2016-11-17) - Corrected modifier key combinations on Linux +------------------------------------------------------------------ +* Corrected handling of modifier keys to allow them to be composable on + *Linux*. + + +v1.1.4 (2016-10-30) - Small bugfixes +------------------------------------ +* Corrected error generation when ``GetKeyboardState`` fails. +* Make sure to apply shift state to borrowed keys on *X*. +* Use *pylint*. + + +v1.1.3 (2016-09-27) - Changed Xlib backend library +-------------------------------------------------- +* Changed *Xlib* library. + + +v1.1.2 (2016-09-26) - Added missing type for Python 2 +----------------------------------------------------- +* Added missing ``LPDWORD`` for *Python 2* on *Windows*. + + +v1.1.1 (2016-09-26) - Fixes for listeners and controllers on Windows +-------------------------------------------------------------------- +* Corrected keyboard listener on *Windows*. Modifier keys and other keys + changing the state of the keyboard are now handled correctly. +* Corrected mouse click and release on *Windows*. +* Corrected code samples. + + +v1.1 (2016-06-22) - Simplified usage on Linux +--------------------------------------------- +* Propagate import errors raised on Linux to help troubleshoot missing + ``Xlib`` module. +* Declare ``python3-xlib`` as dependency on *Linux* for *Python 3*. + + +v1.0.6 (2016-04-19) - Universal wheel +------------------------------------- +* Make sure to build a universal wheel for all python versions. + + +v1.0.5 (2016-04-11) - Fixes for dragging on OSX +----------------------------------------------- +* Corrected dragging on *OSX*. +* Added scroll speed constant for *OSX* to correct slow scroll speed. + + +v1.0.4 (2016-04-11) - Fixes for clicking and scrolling on Windows +----------------------------------------------------------------- +* Corrected name of mouse input field when sending click and scroll events. + + +v1.0.3 (2016-04-05) - Fixes for Python 3 on Windows +--------------------------------------------------- +* Corrected use of ``ctypes`` on Windows. + + +v1.0.2 (2016-04-03) - Fixes for thread identifiers +-------------------------------------------------- +* Use thread identifiers to identify threads, not Thread instances. + + +v1.0.1 (2016-04-03) - Fixes for Python 3 +---------------------------------------- +* Corrected bugs which prevented the library from being used on *Python 3*. + + +v1.0 (2016-02-28) - Stable Release +---------------------------------- +* Changed license to *LGPL*. +* Corrected minor bugs and inconsistencies. +* Corrected and extended documentation. + + +v0.6 (2016-02-08) - Keyboard Monitor +------------------------------------ +* Added support for monitoring the keyboard. +* Corrected wheel packaging. +* Corrected deadlock when stopping a listener in some cases on *X*. +* Corrected key code constants on *Mac OSX*. +* Do not intercept events on *Mac OSX*. + + +v0.5.1 (2016-01-26) - Do not die on dead keys +--------------------------------------------- +* Corrected handling of dead keys. +* Corrected documentation. + + +v0.5 (2016-01-18) - Keyboard Modifiers +-------------------------------------- +* Added support for modifiers. + + +v0.4 (2015-12-22) - Keyboard Controller +--------------------------------------- +* Added keyboard controller. + + +v0.3 (2015-12-22) - Cleanup +--------------------------- +* Moved ``pynput.mouse.Controller.Button`` to top-level. + + +v0.2 (2015-10-28) - Initial Release +----------------------------------- +* Support for controlling the mouse on *Linux*, *Mac OSX* and *Windows*. +* Support for monitoring the mouse on *Linux*, *Mac OSX* and *Windows*. diff --git a/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/RECORD b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/RECORD new file mode 100644 index 0000000..f1c0319 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/RECORD @@ -0,0 +1,55 @@ +pynput-1.8.1.dist-info/COPYING.LGPL,sha256=eInlwsfJhthC1m5_bBVCQ1Mmf5nTUtL8MpjKfZxi0LU,7656 +pynput-1.8.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pynput-1.8.1.dist-info/METADATA,sha256=YNPNdd5SDrz20GVbmn7XZ-viRHHlyyZu1rUhCoYTo0k,32018 +pynput-1.8.1.dist-info/RECORD,, +pynput-1.8.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pynput-1.8.1.dist-info/WHEEL,sha256=qUzzGenXXuJTzyjFah76kDVqDvnk-YDzY00svnrl84w,109 +pynput-1.8.1.dist-info/pbr.json,sha256=CAnWerrCQ6A-ekJTVVKkD9J-ia4q-xoZzQWKOlPcseQ,47 +pynput-1.8.1.dist-info/top_level.txt,sha256=DpJjYf-VkYaa_COk_yUczD0pHqsLndB9SjmwcQGkXJQ,7 +pynput-1.8.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 +pynput/__init__.py,sha256=dJ7uVZs3BRQILOxuM9BqDNDS36Qav123Hpx-3HUklpo,1334 +pynput/__pycache__/__init__.cpython-313.pyc,, +pynput/__pycache__/_info.cpython-313.pyc,, +pynput/_info.py,sha256=2aajH_7soMpmZO_oMDHp7R2aXpMher6jdVa5Mnhjj6g,775 +pynput/_util/__init__.py,sha256=ZB2LiSbGsbeQlnlVUQP9TNze15Fv6cb9m1zXYuJmy5g,15482 +pynput/_util/__pycache__/__init__.cpython-313.pyc,, +pynput/_util/__pycache__/darwin.cpython-313.pyc,, +pynput/_util/__pycache__/darwin_vks.cpython-313.pyc,, +pynput/_util/__pycache__/uinput.cpython-313.pyc,, +pynput/_util/__pycache__/win32.cpython-313.pyc,, +pynput/_util/__pycache__/win32_vks.cpython-313.pyc,, +pynput/_util/__pycache__/xorg.cpython-313.pyc,, +pynput/_util/__pycache__/xorg_keysyms.cpython-313.pyc,, +pynput/_util/darwin.py,sha256=AxtbSopLkKzWI1WLdCIZWMQtUtTuCxbRvWB6-WKcsCM,9200 +pynput/_util/darwin_vks.py,sha256=x-SbG4VgJ6Qc5-ZyF8cniqN3qIqahuQtXzG8ei94uWI,1512 +pynput/_util/uinput.py,sha256=ZFYiTsjQ7lr4h43X2Axrj-_FvWxiVT6dik7Z7Ra_sQE,2837 +pynput/_util/win32.py,sha256=YzEct1u1pzQ8v69kRlF-PnMB9BxwqkxCroNFVy4Ychw,18135 +pynput/_util/win32_vks.py,sha256=lC4y8E85nCGHmr4avVs44SvuBvUHBxmDeikNZcPGt6o,2894 +pynput/_util/xorg.py,sha256=ETHqMURfpvnA4q1UFIFQpWCj0c_9YTgasH3e0M4PRrM,15251 +pynput/_util/xorg_keysyms.py,sha256=YGRUXj1P0HT0gEkci2HZHyI_wcOwt0imt6R-7wPQVJo,69338 +pynput/keyboard/__init__.py,sha256=-5KBcWTSFjv2z1tu6sMV40UwhcqcX5O-yIex9fAmC_o,7903 +pynput/keyboard/__pycache__/__init__.cpython-313.pyc,, +pynput/keyboard/__pycache__/_base.cpython-313.pyc,, +pynput/keyboard/__pycache__/_darwin.cpython-313.pyc,, +pynput/keyboard/__pycache__/_dummy.cpython-313.pyc,, +pynput/keyboard/__pycache__/_uinput.cpython-313.pyc,, +pynput/keyboard/__pycache__/_win32.cpython-313.pyc,, +pynput/keyboard/__pycache__/_xorg.cpython-313.pyc,, +pynput/keyboard/_base.py,sha256=CPkWx8uE-oEhYMiRUY3uxqZuAMgT7Phrv3k8WoYp-D0,24082 +pynput/keyboard/_darwin.py,sha256=apF_nOR5EECuo1q-tZ5bi66JM3uFa83ZJy7Cfpc6xSQ,11672 +pynput/keyboard/_dummy.py,sha256=hkBFRY7HWyl86B_HdFiTeLthJjg926kN2peF-HUUXB0,895 +pynput/keyboard/_uinput.py,sha256=EH9FJX45IlbmjGvE1x4O2Ixvgkynp7XOka-vbtjVEAI,14479 +pynput/keyboard/_win32.py,sha256=i3S975s2SlEL9wva9ZMTi0icrNMWHyoj2zPN6gLeqsQ,12645 +pynput/keyboard/_xorg.py,sha256=QEGbet8NreQElzaSGfyARLaVoZxEvcIOTtjU6EHPnRQ,22677 +pynput/mouse/__init__.py,sha256=RiwZMZiWJEikRr8amiMoqJDVTn9i0irEfFcy3IA-s4w,2864 +pynput/mouse/__pycache__/__init__.cpython-313.pyc,, +pynput/mouse/__pycache__/_base.cpython-313.pyc,, +pynput/mouse/__pycache__/_darwin.cpython-313.pyc,, +pynput/mouse/__pycache__/_dummy.cpython-313.pyc,, +pynput/mouse/__pycache__/_win32.cpython-313.pyc,, +pynput/mouse/__pycache__/_xorg.cpython-313.pyc,, +pynput/mouse/_base.py,sha256=FfxHWA126ts1vz05YdcOHJOWb3XaF6WTctM-_ix6FNU,9338 +pynput/mouse/_darwin.py,sha256=a6M_Od7yCzEdxxrYGWLlHuInOWk6Nibd6dKnm5HC79s,6793 +pynput/mouse/_dummy.py,sha256=p16GreQcJbLMhE51uf8H3F8iarHf7Tu2s0JGt3J2xeQ,874 +pynput/mouse/_win32.py,sha256=e1NYKhsKe_Z2W4SZOJK3xWqDjtAa7Dy-zmvT_WzKx7M,7117 +pynput/mouse/_xorg.py,sha256=0P-N-jvRw0VTXH5P1O4LVnTds8jeHlqBiiDRpoUxVzQ,5559 diff --git a/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/REQUESTED b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/WHEEL b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/WHEEL new file mode 100644 index 0000000..de294b9 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: setuptools (74.1.2) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/pbr.json b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/pbr.json new file mode 100644 index 0000000..ab50f33 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/pbr.json @@ -0,0 +1 @@ +{"is_release": false, "git_version": "2d6ab69"} \ No newline at end of file diff --git a/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/top_level.txt b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/top_level.txt new file mode 100644 index 0000000..91a67a9 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/top_level.txt @@ -0,0 +1 @@ +pynput diff --git a/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/zip-safe b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput-1.8.1.dist-info/zip-safe @@ -0,0 +1 @@ + diff --git a/PortablePython/Lib/site-packages/pynput/__init__.py b/PortablePython/Lib/site-packages/pynput/__init__.py new file mode 100644 index 0000000..b6ab843 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/__init__.py @@ -0,0 +1,41 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The main *pynput* module. + +This module imports ``keyboard`` and ``mouse``. +""" + +def _logger(cls): + """Creates a logger with a name suitable for a specific class. + + This function takes into account that implementations for classes reside in + platform dependent modules, and thus removes the final part of the module + name. + + :param type cls: The class for which to create a logger. + + :return: a logger + """ + import logging + return logging.getLogger('{}.{}'.format( + '.'.join(cls.__module__.split('.', 2)[:2]), + cls.__name__)) + + +from . import keyboard +from . import mouse diff --git a/PortablePython/Lib/site-packages/pynput/_info.py b/PortablePython/Lib/site-packages/pynput/_info.py new file mode 100644 index 0000000..91bf033 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/_info.py @@ -0,0 +1,19 @@ +# coding=utf-8 +# pystray +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +__author__ = u'Moses Palmér' +__version__ = (1, 8, 1) diff --git a/PortablePython/Lib/site-packages/pynput/_util/__init__.py b/PortablePython/Lib/site-packages/pynput/_util/__init__.py new file mode 100644 index 0000000..c7d98b4 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/_util/__init__.py @@ -0,0 +1,489 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +General utility functions and classes. +""" + +# pylint: disable=R0903 +# We implement minimal mixins + +# pylint: disable=W0212 +# We implement an internal API + +import contextlib +import functools +import importlib +import inspect +import os +import sys +import threading +import time + +import six + +from six.moves import queue + + +#: Possible resolutions for import related errors. +RESOLUTIONS = { + 'darwin': 'Please make sure that you have Python bindings for the ' + 'system frameworks installed', + 'uinput': 'Please make sure that you are running as root, and that ' + 'the utility dumpkeys is installed', + 'xorg': 'Please make sure that you have an X server running, and that ' + 'the DISPLAY environment variable is set correctly'} + + +def backend(package): + """Returns the backend module for a package. + + :param str package: The package for which to load a backend. + """ + backend_name = os.environ.get( + 'PYNPUT_BACKEND_{}'.format(package.rsplit('.')[-1].upper()), + os.environ.get('PYNPUT_BACKEND', None)) + if backend_name: + modules = [backend_name] + elif sys.platform == 'darwin': + modules = ['darwin'] + elif sys.platform == 'win32': + modules = ['win32'] + else: + modules = ['xorg'] + + errors = [] + resolutions = [] + for module in modules: + try: + return importlib.import_module('._' + module, package) + except ImportError as e: + errors.append(e) + if module in RESOLUTIONS: + resolutions.append(RESOLUTIONS[module]) + + raise ImportError('this platform is not supported: {}'.format( + '; '.join(str(e) for e in errors)) + ('\n\n' + 'Try one of the following resolutions:\n\n' + + '\n\n'.join( + ' * {}'.format(s) + for s in resolutions)) + if resolutions else '') + + +def prefix(base, cls): + """Calculates the prefix to use for platform specific options for a + specific class. + + The prefix if the name of the module containing the class that is an + immediate subclass of ``base`` among the super classes of ``cls``. + """ + for super_cls in filter( + lambda cls: issubclass(cls, base), + cls.__mro__[1:]): + if super_cls is base: + return cls.__module__.rsplit('.', 1)[-1][1:] + '_' + else: + result = prefix(base, super_cls) + if result is not None: + return result + + +class AbstractListener(threading.Thread): + """A class implementing the basic behaviour for event listeners. + + Instances of this class can be used as context managers. This is equivalent + to the following code:: + + listener.start() + listener.wait() + try: + with_statements() + finally: + listener.stop() + + Actual implementations of this class must set the attribute ``_log``, which + must be an instance of :class:`logging.Logger`. + + :param bool suppress: Whether to suppress events. Setting this to ``True`` + will prevent the input events from being passed to the rest of the + system. + + :param kwargs: A mapping from callback attribute to callback handler. All + handlers will be wrapped in a function reading the return value of the + callback, and if it ``is False``, raising :class:`StopException`. + + Any callback that is falsy will be ignored. + """ + class StopException(Exception): + """If an event listener callback raises this exception, the current + listener is stopped. + """ + pass + + #: Exceptions that are handled outside of the emitter and should thus not + #: be passed through the queue + _HANDLED_EXCEPTIONS = tuple() + + def __init__(self, suppress=False, **kwargs): + super(AbstractListener, self).__init__() + + def wrapper(f): + def inner(*args): + if f(*args) is False: + raise self.StopException() + return inner + + self._suppress = suppress + self._running = False + self._thread = threading.current_thread() + self._condition = threading.Condition() + self._ready = False + + # Allow multiple calls to stop + self._queue = queue.Queue(10) + + self.daemon = True + + for name, callback in kwargs.items(): + setattr(self, name, wrapper(callback)) + + @property + def suppress(self): + """Whether to suppress events. + """ + return self._suppress + + @property + def running(self): + """Whether the listener is currently running. + """ + return self._running + + def stop(self): + """Stops listening for events. + + When this method returns, no more events will be delivered. Once this + method has been called, the listener instance cannot be used any more, + since a listener is a :class:`threading.Thread`, and once stopped it + cannot be restarted. + + To resume listening for event, a new listener must be created. + """ + if self._running: + self._running = False + self._queue.put(None) + self._stop_platform() + + def __enter__(self): + self.start() + self.wait() + return self + + def __exit__(self, exc_type, value, traceback): + self.stop() + + def wait(self): + """Waits for this listener to become ready. + """ + self._condition.acquire() + while not self._ready: + self._condition.wait() + self._condition.release() + + def run(self): + """The thread runner method. + """ + self._running = True + self._thread = threading.current_thread() + self._run() + + # Make sure that the queue contains something + self._queue.put(None) + + @classmethod + def _emitter(cls, f): + """A decorator to mark a method as the one emitting the callbacks. + + This decorator will wrap the method and catch exception. If a + :class:`StopException` is caught, the listener will be stopped + gracefully. If any other exception is caught, it will be propagated to + the thread calling :meth:`join` and reraised there. + """ + @functools.wraps(f) + def inner(self, *args, **kwargs): + # pylint: disable=W0702; we want to catch all exception + try: + return f(self, *args, **kwargs) + except Exception as e: + if not isinstance(e, self._HANDLED_EXCEPTIONS): + if not isinstance(e, AbstractListener.StopException): + self._log.exception( + 'Unhandled exception in listener callback') + self._queue.put( + None if isinstance(e, cls.StopException) + else sys.exc_info()) + self.stop() + raise + # pylint: enable=W0702 + + return inner + + def _mark_ready(self): + """Marks this listener as ready to receive events. + + This method must be called from :meth:`_run`. :meth:`wait` will block + until this method is called. + """ + self._condition.acquire() + self._ready = True + self._condition.notify() + self._condition.release() + + def _run(self): + """The implementation of the :meth:`run` method. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def _stop_platform(self): + """The implementation of the :meth:`stop` method. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def _wrap(self, f, args): + """Wraps a callable to make it accept ``args`` number of arguments. + + :param f: The callable to wrap. If this is ``None`` a no-op wrapper is + returned. + + :param int args: The number of arguments to accept. + + :raises ValueError: if f requires more than ``args`` arguments + """ + if f is None: + return lambda *a: None + else: + argspec = inspect.getfullargspec(f) + actual = len(inspect.signature(f).parameters) + defaults = len(argspec.defaults) if argspec.defaults else 0 + if actual - defaults > args: + raise ValueError(f) + elif actual >= args or argspec.varargs is not None: + return f + else: + return lambda *a: f(*a[:actual]) + + def join(self, timeout=None, *args): + start = time.time() + super(AbstractListener, self).join(timeout, *args) + timeout = max(0.0, timeout - (time.time() - start)) \ + if timeout is not None \ + else None + + # Reraise any exceptions; make sure not to block if a timeout was + # provided + try: + exc_type, exc_value, exc_traceback = self._queue.get( + timeout=timeout) + six.reraise(exc_type, exc_value, exc_traceback) + except queue.Empty: + pass + except TypeError: + return + + +class Events(object): + """A base class to enable iterating over events. + """ + #: The listener class providing events. + _Listener = None + + class Event(object): + def __str__(self): + return '{}({})'.format( + self.__class__.__name__, + ', '.join( + '{}={}'.format(k, v) + for (k, v) in vars(self).items())) + + def __eq__(self, other): + return self.__class__ == other.__class__ \ + and dir(self) == dir(other) \ + and all( + getattr(self, k) == getattr(other, k) + for k in dir(self)) + + def __init__(self, *args, **kwargs): + super(Events, self).__init__() + self._event_queue = queue.Queue() + self._sentinel = object() + self._listener = self._Listener(*args, **{ + key: self._event_mapper(value) + for (key, value) in kwargs.items()}) + self.start = self._listener.start + + def __enter__(self): + self._listener.__enter__() + return self + + def __exit__(self, *args): + self._listener.__exit__(*args) + + # Drain the queue to ensure that the put does not block + while True: + try: + self._event_queue.get_nowait() + except queue.Empty: + break + + self._event_queue.put(self._sentinel) + + def __iter__(self): + return self + + def __next__(self): + event = self.get() + if event is not None: + return event + else: + raise StopIteration() + + def get(self, timeout=None): + """Attempts to read the next event. + + :param int timeout: An optional timeout. If this is not provided, this + method may block infinitely. + + :return: the next event, or ``None`` if the source has been stopped or + no events were received + """ + try: + event = self._event_queue.get(timeout=timeout) + return event if event is not self._sentinel else None + except queue.Empty: + return None + + def _event_mapper(self, event): + """Generates an event callback to transforms the callback arguments to + an event and then publishes it. + + :param callback event: A function generating an event object. + + :return: a callback + """ + @functools.wraps(event) + def inner(*args): + try: + self._event_queue.put(event(*args), block=False) + except queue.Full: + pass + + return inner + + +class NotifierMixin(object): + """A mixin for notifiers of fake events. + + This mixin can be used for controllers on platforms where sending fake + events does not cause a listener to receive a notification. + """ + def _emit(self, action, *args): + """Sends a notification to all registered listeners. + + This method will ensure that listeners that raise + :class:`StopException` are stopped. + + :param str action: The name of the notification. + + :param args: The arguments to pass. + """ + stopped = [] + for listener in self._listeners(): + try: + getattr(listener, action)(*args) + except listener.StopException: + stopped.append(listener) + for listener in stopped: + listener.stop() + + @classmethod + def _receiver(cls, listener_class): + """A decorator to make a class able to receive fake events from a + controller. + + This decorator will add the method ``_receive`` to the decorated class. + + This method is a context manager which ensures that all calls to + :meth:`_emit` will invoke the named method in the listener instance + while the block is active. + """ + @contextlib.contextmanager + def receive(self): + """Executes a code block with this listener instance registered as + a receiver of fake input events. + """ + self._controller_class._add_listener(self) + try: + yield + finally: + self._controller_class._remove_listener(self) + + listener_class._receive = receive + listener_class._controller_class = cls + + # Make sure this class has the necessary attributes + if not hasattr(cls, '_listener_cache'): + cls._listener_cache = set() + cls._listener_lock = threading.Lock() + + return listener_class + + @classmethod + def _listeners(cls): + """Iterates over the set of running listeners. + + This method will quit without acquiring the lock if the set is empty, + so there is potential for race conditions. This is an optimisation, + since :class:`Controller` will need to call this method for every + control event. + """ + if not cls._listener_cache: + return + with cls._listener_lock: + for listener in cls._listener_cache: + yield listener + + @classmethod + def _add_listener(cls, listener): + """Adds a listener to the set of running listeners. + + :param listener: The listener for fake events. + """ + with cls._listener_lock: + cls._listener_cache.add(listener) + + @classmethod + def _remove_listener(cls, listener): + """Removes this listener from the set of running listeners. + + :param listener: The listener for fake events. + """ + with cls._listener_lock: + cls._listener_cache.remove(listener) diff --git a/PortablePython/Lib/site-packages/pynput/_util/darwin.py b/PortablePython/Lib/site-packages/pynput/_util/darwin.py new file mode 100644 index 0000000..fdc284b --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/_util/darwin.py @@ -0,0 +1,302 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +Utility functions and classes for the *Darwin* backend. +""" + +# pylint: disable=C0103 +# pylint: disable=R0903 +# This module contains wrapper classes + +import contextlib +import ctypes +import ctypes.util +import six + +import objc +import HIServices + +from CoreFoundation import ( + CFRelease +) + +from Quartz import ( + CFMachPortCreateRunLoopSource, + CFRunLoopAddSource, + CFRunLoopGetCurrent, + CFRunLoopRunInMode, + CFRunLoopStop, + CGEventGetIntegerValueField, + CGEventTapCreate, + CGEventTapEnable, + kCFRunLoopDefaultMode, + kCFRunLoopRunTimedOut, + kCGEventSourceUnixProcessID, + kCGEventTapOptionDefault, + kCGEventTapOptionListenOnly, + kCGHeadInsertEventTap, + kCGSessionEventTap) + + +from . import AbstractListener + + +def _wrap_value(value): + """Converts a pointer to a *Python objc* value. + + :param value: The pointer to convert. + + :return: a wrapped value + """ + return objc.objc_object(c_void_p=value) if value is not None else None + + +@contextlib.contextmanager +def _wrapped(value): + """A context manager that converts a raw pointer to a *Python objc* value. + + When the block is exited, the value is released. + + :param value: The raw value to wrap. + """ + wrapped_value = _wrap_value(value) + + try: + yield value + finally: + CFRelease(wrapped_value) + + +class CarbonExtra(object): + """A class exposing some missing functionality from *Carbon* as class + attributes. + """ + _Carbon = ctypes.cdll.LoadLibrary(ctypes.util.find_library('Carbon')) + + _Carbon.TISCopyCurrentKeyboardInputSource.argtypes = [] + _Carbon.TISCopyCurrentKeyboardInputSource.restype = ctypes.c_void_p + + _Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource.argtypes = [] + _Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource.restype = \ + ctypes.c_void_p + + _Carbon.TISGetInputSourceProperty.argtypes = [ + ctypes.c_void_p, ctypes.c_void_p] + _Carbon.TISGetInputSourceProperty.restype = ctypes.c_void_p + + _Carbon.LMGetKbdType.argtypes = [] + _Carbon.LMGetKbdType.restype = ctypes.c_uint32 + + _Carbon.UCKeyTranslate.argtypes = [ + ctypes.c_void_p, + ctypes.c_uint16, + ctypes.c_uint16, + ctypes.c_uint32, + ctypes.c_uint32, + ctypes.c_uint32, + ctypes.POINTER(ctypes.c_uint32), + ctypes.c_uint8, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint16 * 4] + _Carbon.UCKeyTranslate.restype = ctypes.c_uint32 + + TISCopyCurrentKeyboardInputSource = \ + _Carbon.TISCopyCurrentKeyboardInputSource + + TISCopyCurrentASCIICapableKeyboardLayoutInputSource = \ + _Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource + + kTISPropertyUnicodeKeyLayoutData = ctypes.c_void_p.in_dll( + _Carbon, 'kTISPropertyUnicodeKeyLayoutData') + + TISGetInputSourceProperty = \ + _Carbon.TISGetInputSourceProperty + + LMGetKbdType = \ + _Carbon.LMGetKbdType + + kUCKeyActionDisplay = 3 + kUCKeyTranslateNoDeadKeysBit = 0 + + UCKeyTranslate = \ + _Carbon.UCKeyTranslate + + +@contextlib.contextmanager +def keycode_context(): + """Returns an opaque value representing a context for translating keycodes + to strings. + """ + keyboard_type, layout_data = None, None + for source in [ + CarbonExtra.TISCopyCurrentKeyboardInputSource, + CarbonExtra.TISCopyCurrentASCIICapableKeyboardLayoutInputSource]: + with _wrapped(source()) as keyboard: + keyboard_type = CarbonExtra.LMGetKbdType() + layout = _wrap_value(CarbonExtra.TISGetInputSourceProperty( + keyboard, + CarbonExtra.kTISPropertyUnicodeKeyLayoutData)) + layout_data = layout.bytes().tobytes() if layout else None + if keyboard is not None and layout_data is not None: + break + yield (keyboard_type, layout_data) + + +def keycode_to_string(context, keycode, modifier_state=0): + """Converts a keycode to a string. + """ + LENGTH = 4 + + keyboard_type, layout_data = context + + dead_key_state = ctypes.c_uint32() + length = ctypes.c_uint8() + unicode_string = (ctypes.c_uint16 * LENGTH)() + CarbonExtra.UCKeyTranslate( + layout_data, + keycode, + CarbonExtra.kUCKeyActionDisplay, + modifier_state, + keyboard_type, + CarbonExtra.kUCKeyTranslateNoDeadKeysBit, + ctypes.byref(dead_key_state), + LENGTH, + ctypes.byref(length), + unicode_string) + return u''.join( + six.unichr(unicode_string[i]) + for i in range(length.value)) + + +def get_unicode_to_keycode_map(): + """Returns a mapping from unicode strings to virtual key codes. + + :return: a dict mapping key codes to strings + """ + with keycode_context() as context: + return { + keycode_to_string(context, keycode): keycode + for keycode in range(128)} + + +class ListenerMixin(object): + """A mixin for *Quartz* event listeners. + + Subclasses should set a value for :attr:`_EVENTS` and implement + :meth:`_handle_message`. + """ + #: The events that we listen to + _EVENTS = tuple() + + #: Whether this process is trusted to monitor input events. + IS_TRUSTED = False + + def _run(self): + self.IS_TRUSTED = HIServices.AXIsProcessTrusted() + if not self.IS_TRUSTED: + self._log.warning( + 'This process is not trusted! Input event monitoring will not ' + 'be possible until it is added to accessibility clients.') + + self._loop = None + try: + tap = self._create_event_tap() + if tap is None: + self._mark_ready() + return + + loop_source = CFMachPortCreateRunLoopSource( + None, tap, 0) + self._loop = CFRunLoopGetCurrent() + + CFRunLoopAddSource( + self._loop, loop_source, kCFRunLoopDefaultMode) + CGEventTapEnable(tap, True) + + self._mark_ready() + + # pylint: disable=W0702; we want to silence errors + try: + while self.running: + result = CFRunLoopRunInMode( + kCFRunLoopDefaultMode, 1, False) + try: + if result != kCFRunLoopRunTimedOut: + break + except AttributeError: + # This happens during teardown of the virtual machine + break + + except: + # This exception will have been passed to the main thread + pass + # pylint: enable=W0702 + + finally: + self._loop = None + + def _stop_platform(self): + # The base class sets the running flag to False; this will cause the + # loop around run loop invocations to terminate and set this event + try: + if self._loop is not None: + CFRunLoopStop(self._loop) + except AttributeError: + # The loop may not have been created + pass + + def _create_event_tap(self): + """Creates the event tap used by the listener. + + :return: an event tap + """ + return CGEventTapCreate( + kCGSessionEventTap, + kCGHeadInsertEventTap, + kCGEventTapOptionListenOnly if ( + True + and not self.suppress + and self._intercept is None) + else kCGEventTapOptionDefault, + self._EVENTS, + self._handler, + None) + + @AbstractListener._emitter + def _handler(self, proxy, event_type, event, refcon): + """The callback registered with *macOS* for mouse events. + + This method will call the callbacks registered on initialisation. + """ + # An injected event will have a Unix process ID attached + is_injected = (CGEventGetIntegerValueField( + event, + kCGEventSourceUnixProcessID)) != 0 + + self._handle_message(proxy, event_type, event, refcon, is_injected) + if self._intercept is not None: + return self._intercept(event_type, event) + elif self.suppress: + return None + + def _handle_message(self, proxy, event_type, event, refcon): + """The device specific callback handler. + + This method calls the appropriate callback registered when this + listener was created based on the event. + """ + raise NotImplementedError() diff --git a/PortablePython/Lib/site-packages/pynput/_util/darwin_vks.py b/PortablePython/Lib/site-packages/pynput/_util/darwin_vks.py new file mode 100644 index 0000000..6499fb1 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/_util/darwin_vks.py @@ -0,0 +1,79 @@ +# coding: utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +# pylint: disable=C0111,C0302 + +SYMBOLS = { + 0: 'a', + 1: 's', + 2: 'd', + 3: 'f', + 4: 'h', + 5: 'g', + 6: 'z', + 7: 'x', + 8: 'c', + 9: 'v', + 11: 'b', + 12: 'q', + 13: 'w', + 14: 'e', + 15: 'r', + 16: 'y', + 17: 't', + 18: '1', + 19: '2', + 20: '3', + 21: '4', + 22: '6', + 23: '5', + 24: '=', + 25: '9', + 26: '7', + 27: '-', + 28: '8', + 29: '0', + 30: ']', + 31: 'o', + 32: 'u', + 33: '[', + 34: 'i', + 35: 'p', + 37: 'l', + 38: 'j', + 39: '\'', + 40: 'k', + 41: ';', + 42: '\\', + 43: ',', + 44: '/', + 45: 'n', + 46: 'm', + 47: '.', + 49: ' ', + 50: '`', + 82: '0', + 83: '1', + 84: '2', + 85: '3', + 86: '4', + 87: '5', + 88: '6', + 89: '7', + 91: '8', + 92: '9', +} diff --git a/PortablePython/Lib/site-packages/pynput/_util/uinput.py b/PortablePython/Lib/site-packages/pynput/_util/uinput.py new file mode 100644 index 0000000..b0a6a78 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/_util/uinput.py @@ -0,0 +1,99 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +Utility functions and classes for the *uinput* backend. +""" + +# pylint: disable=R0903 +# We implement stubs + +import evdev + + +# Check that we have permissions to continue +def _check(): + # TODO: Implement! + pass +_check() +del _check + + +class ListenerMixin(object): + """A mixin for *uinput* event listeners. + + Subclasses should set a value for :attr:`_EVENTS` and implement + :meth:`_handle_message`. + """ + #: The events for which to listen + _EVENTS = tuple() + + def __init__(self, *args, **kwargs): + super(ListenerMixin, self).__init__(*args, **kwargs) + self._dev = self._device(self._options.get( + 'device_paths', + evdev.list_devices())) + if self.suppress: + self._dev.grab() + + def _run(self): + for event in self._dev.read_loop(): + if event.type in self._EVENTS: + self._handle_message(event) + + def _stop_platform(self): + self._dev.close() + + def _device(self, paths): + """Attempts to load a readable keyboard device. + + :param paths: A list of paths. + + :return: a compatible device + """ + dev, count = None, 0 + for path in paths: + # Open the device + try: + next_dev = evdev.InputDevice(path) + except OSError: + continue + + # Does this device provide more handled event codes? + capabilities = next_dev.capabilities() + next_count = sum( + len(codes) + for event, codes in capabilities.items() + if event in self._EVENTS) + if next_count > count: + dev = next_dev + count = next_count + else: + next_dev.close() + + if dev is None: + raise OSError('no keyboard device available') + else: + return dev + + def _handle_message(self, event): + """Handles a single event. + + This method should call one of the registered event callbacks. + + :param event: The event. + """ + raise NotImplementedError() diff --git a/PortablePython/Lib/site-packages/pynput/_util/win32.py b/PortablePython/Lib/site-packages/pynput/_util/win32.py new file mode 100644 index 0000000..b945a59 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/_util/win32.py @@ -0,0 +1,598 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +Utility functions and classes for the *win32* backend. +""" + +# pylint: disable=C0103 +# We want to make it obvious how structs are related + +# pylint: disable=R0903 +# This module contains a number of structs + +import contextlib +import ctypes +import itertools +import threading + +from ctypes import ( + windll, + wintypes) + +from . import AbstractListener, win32_vks as VK + + +# LPDWORD is not in ctypes.wintypes on Python 2 +if not hasattr(wintypes, 'LPDWORD'): + wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD) + + +class MOUSEINPUT(ctypes.Structure): + """Contains information about a simulated mouse event. + """ + MOVE = 0x0001 + LEFTDOWN = 0x0002 + LEFTUP = 0x0004 + RIGHTDOWN = 0x0008 + RIGHTUP = 0x0010 + MIDDLEDOWN = 0x0020 + MIDDLEUP = 0x0040 + XDOWN = 0x0080 + XUP = 0x0100 + WHEEL = 0x0800 + HWHEEL = 0x1000 + ABSOLUTE = 0x8000 + + XBUTTON1 = 0x0001 + XBUTTON2 = 0x0002 + + _fields_ = [ + ('dx', wintypes.LONG), + ('dy', wintypes.LONG), + ('mouseData', wintypes.DWORD), + ('dwFlags', wintypes.DWORD), + ('time', wintypes.DWORD), + ('dwExtraInfo', ctypes.c_void_p)] + + +class KEYBDINPUT(ctypes.Structure): + """Contains information about a simulated keyboard event. + """ + EXTENDEDKEY = 0x0001 + KEYUP = 0x0002 + SCANCODE = 0x0008 + UNICODE = 0x0004 + + _fields_ = [ + ('wVk', wintypes.WORD), + ('wScan', wintypes.WORD), + ('dwFlags', wintypes.DWORD), + ('time', wintypes.DWORD), + ('dwExtraInfo', ctypes.c_void_p)] + + +class HARDWAREINPUT(ctypes.Structure): + """Contains information about a simulated message generated by an input + device other than a keyboard or mouse. + """ + _fields_ = [ + ('uMsg', wintypes.DWORD), + ('wParamL', wintypes.WORD), + ('wParamH', wintypes.WORD)] + + +class INPUT_union(ctypes.Union): + """Represents the union of input types in :class:`INPUT`. + """ + _fields_ = [ + ('mi', MOUSEINPUT), + ('ki', KEYBDINPUT), + ('hi', HARDWAREINPUT)] + + +class INPUT(ctypes.Structure): + """Used by :attr:`SendInput` to store information for synthesizing input + events such as keystrokes, mouse movement, and mouse clicks. + """ + MOUSE = 0 + KEYBOARD = 1 + HARDWARE = 2 + + _fields_ = [ + ('type', wintypes.DWORD), + ('value', INPUT_union)] + + +LPINPUT = ctypes.POINTER(INPUT) + +VkKeyScan = windll.user32.VkKeyScanW +VkKeyScan.argtypes = ( + wintypes.WCHAR,) + +MapVirtualKey = windll.user32.MapVirtualKeyW +MapVirtualKey.argtypes = ( + wintypes.UINT, + wintypes.UINT) +MapVirtualKey.MAPVK_VK_TO_VSC = 0 + +SendInput = windll.user32.SendInput +SendInput.argtypes = ( + wintypes.UINT, + ctypes.c_voidp, # Really LPINPUT + ctypes.c_int) + +GetCurrentThreadId = windll.kernel32.GetCurrentThreadId +GetCurrentThreadId.restype = wintypes.DWORD + + +class MessageLoop(object): + """A class representing a message loop. + """ + #: The message that signals this loop to terminate + WM_STOP = 0x0401 + + _LPMSG = ctypes.POINTER(wintypes.MSG) + + _GetMessage = windll.user32.GetMessageW + _GetMessage.argtypes = ( + ctypes.c_voidp, # Really _LPMSG + wintypes.HWND, + wintypes.UINT, + wintypes.UINT) + _PeekMessage = windll.user32.PeekMessageW + _PeekMessage.argtypes = ( + ctypes.c_voidp, # Really _LPMSG + wintypes.HWND, + wintypes.UINT, + wintypes.UINT, + wintypes.UINT) + _PostThreadMessage = windll.user32.PostThreadMessageW + _PostThreadMessage.argtypes = ( + wintypes.DWORD, + wintypes.UINT, + wintypes.WPARAM, + wintypes.LPARAM) + + PM_NOREMOVE = 0 + + def __init__(self): + self._threadid = None + self._event = threading.Event() + self.thread = None + + def __iter__(self): + """Initialises the message loop and yields all messages until + :meth:`stop` is called. + + :raises AssertionError: if :meth:`start` has not been called + """ + assert self._threadid is not None + + try: + # Pump messages until WM_STOP + while True: + msg = wintypes.MSG() + lpmsg = ctypes.byref(msg) + r = self._GetMessage(lpmsg, None, 0, 0) + if r <= 0 or msg.message == self.WM_STOP: + break + else: + yield msg + + finally: + self._threadid = None + self.thread = None + + def start(self): + """Starts the message loop. + + This method must be called before iterating over messages, and it must + be called from the same thread. + """ + self._threadid = GetCurrentThreadId() + self.thread = threading.current_thread() + + # Create the message loop + msg = wintypes.MSG() + lpmsg = ctypes.byref(msg) + self._PeekMessage(lpmsg, None, 0x0400, 0x0400, self.PM_NOREMOVE) + + # Set the event to signal to other threads that the loop is created + self._event.set() + + def stop(self): + """Stops the message loop. + """ + self._event.wait() + if self._threadid: + self.post(self.WM_STOP, 0, 0) + + def post(self, msg, wparam, lparam): + """Posts a message to this message loop. + + :param ctypes.wintypes.UINT msg: The message. + + :param ctypes.wintypes.WPARAM wparam: The value of ``wParam``. + + :param ctypes.wintypes.LPARAM lparam: The value of ``lParam``. + """ + self._PostThreadMessage(self._threadid, msg, wparam, lparam) + + +class SystemHook(object): + """A class to handle Windows hooks. + """ + #: The hook action value for actions we should check + HC_ACTION = 0 + + _HOOKPROC = ctypes.WINFUNCTYPE( + wintypes.LPARAM, + ctypes.c_int32, wintypes.WPARAM, wintypes.LPARAM) + + _SetWindowsHookEx = windll.user32.SetWindowsHookExW + _SetWindowsHookEx.argtypes = ( + ctypes.c_int, + _HOOKPROC, + wintypes.HINSTANCE, + wintypes.DWORD) + _UnhookWindowsHookEx = windll.user32.UnhookWindowsHookEx + _UnhookWindowsHookEx.argtypes = ( + wintypes.HHOOK,) + _CallNextHookEx = windll.user32.CallNextHookEx + _CallNextHookEx.argtypes = ( + wintypes.HHOOK, + ctypes.c_int, + wintypes.WPARAM, + wintypes.LPARAM) + + #: The registered hook procedures + _HOOKS = {} + + class SuppressException(Exception): + """An exception raised by a hook callback to suppress further + propagation of events. + """ + pass + + def __init__(self, hook_id, on_hook=lambda code, msg, lpdata: None): + self.hook_id = hook_id + self.on_hook = on_hook + self._hook = None + + def __enter__(self): + key = threading.current_thread().ident + assert key not in self._HOOKS + + # Add ourself to lookup table and install the hook + self._HOOKS[key] = self + self._hook = self._SetWindowsHookEx( + self.hook_id, + self._handler, + None, + 0) + + return self + + def __exit__(self, exc_type, value, traceback): + key = threading.current_thread().ident + assert key in self._HOOKS + + if self._hook is not None: + # Uninstall the hook and remove ourself from lookup table + self._UnhookWindowsHookEx(self._hook) + del self._HOOKS[key] + + @staticmethod + @_HOOKPROC + def _handler(code, msg, lpdata): + key = threading.current_thread().ident + self = SystemHook._HOOKS.get(key, None) + if self: + # pylint: disable=W0702; we want to silence errors + try: + self.on_hook(code, msg, lpdata) + except self.SuppressException: + # Return non-zero to stop event propagation + return 1 + except: + # Ignore any errors + pass + # pylint: enable=W0702 + return SystemHook._CallNextHookEx(0, code, msg, lpdata) + + +class ListenerMixin(object): + """A mixin for *win32* event listeners. + + Subclasses should set a value for :attr:`_EVENTS` and implement + :meth:`_handle_message`. + + Subclasses must also be decorated with a decorator compatible with + :meth:`pynput._util.NotifierMixin._receiver` or implement the method + ``_receive()``. + """ + #: The Windows hook ID for the events to capture. + _EVENTS = None + + #: The window message used to signal that an even should be handled. + _WM_PROCESS = 0x410 + + #: Additional window messages to propagate to the subclass handler. + _WM_NOTIFICATIONS = [] + + def suppress_event(self): + """Causes the currently filtered event to be suppressed. + + This has a system wide effect and will generally result in no + applications receiving the event. + + This method will raise an undefined exception. + """ + raise SystemHook.SuppressException() + + def _run(self): + self._message_loop = MessageLoop() + with self._receive(): + self._mark_ready() + self._message_loop.start() + + # pylint: disable=W0702; we want to silence errors + try: + with SystemHook(self._EVENTS, self._handler): + # Just pump messages + for msg in self._message_loop: + if not self.running: + break + if msg.message == self._WM_PROCESS: + self._process(msg.wParam, msg.lParam) + elif msg.message in self._WM_NOTIFICATIONS: + self._on_notification( + msg.message, msg.wParam, msg.lParam) + except: + # This exception will have been passed to the main thread + pass + # pylint: enable=W0702 + + def _stop_platform(self): + try: + self._message_loop.stop() + except AttributeError: + # The loop may not have been created + pass + + @AbstractListener._emitter + def _handler(self, code, msg, lpdata): + """The callback registered with *Windows* for events. + + This method will post the message :attr:`_WM_PROCESS` to the message + loop started with this listener using :meth:`MessageLoop.post`. The + parameters are retrieved with a call to :meth:`_handle`. + """ + try: + converted = self._convert(code, msg, lpdata) + if converted is not None: + self._message_loop.post(self._WM_PROCESS, *converted) + except NotImplementedError: + self._handle_message(code, msg, lpdata) + + if self.suppress: + self.suppress_event() + + def _convert(self, code, msg, lpdata): + """The device specific callback handler. + + This method converts a low-level message and data to a + ``WPARAM`` / ``LPARAM`` pair. + """ + raise NotImplementedError() + + def _process(self, wparam, lparam): + """The device specific callback handler. + + This method performs the actual dispatching of events. + """ + raise NotImplementedError() + + def _handle_message(self, code, msg, lpdata): + """The device specific callback handler. + + This method calls the appropriate callback registered when this + listener was created based on the event. + + This method is only called if :meth:`_convert` is not implemented. + """ + raise NotImplementedError() + + def _on_notification(self, code, wparam, lparam): + """An additional notification handler. + + This method will be called for every message in + :attr:`_WM_NOTIFICATIONS`. + """ + raise NotImplementedError() + + +class KeyTranslator(object): + """A class to translate virtual key codes to characters. + """ + _GetAsyncKeyState = ctypes.windll.user32.GetAsyncKeyState + _GetAsyncKeyState.argtypes = ( + ctypes.c_int,) + _GetKeyboardLayout = ctypes.windll.user32.GetKeyboardLayout + _GetKeyboardLayout.argtypes = ( + wintypes.DWORD,) + _GetKeyboardState = ctypes.windll.user32.GetKeyboardState + _GetKeyboardState.argtypes = ( + ctypes.c_voidp,) + _GetKeyState = ctypes.windll.user32.GetAsyncKeyState + _GetKeyState.argtypes = ( + ctypes.c_int,) + _MapVirtualKeyEx = ctypes.windll.user32.MapVirtualKeyExW + _MapVirtualKeyEx.argtypes = ( + wintypes.UINT, + wintypes.UINT, + wintypes.HKL) + _ToUnicodeEx = ctypes.windll.user32.ToUnicodeEx + _ToUnicodeEx.argtypes = ( + wintypes.UINT, + wintypes.UINT, + ctypes.c_voidp, + ctypes.c_voidp, + ctypes.c_int, + wintypes.UINT, + wintypes.HKL) + + _MAPVK_VK_TO_VSC = 0 + _MAPVK_VSC_TO_VK = 1 + _MAPVK_VK_TO_CHAR = 2 + + def __init__(self): + self.update_layout() + + def __call__(self, vk, is_press): + """Converts a virtual key code to a string. + + :param int vk: The virtual key code. + + :param bool is_press: Whether this is a press. + + :return: parameters suitable for the :class:`pynput.keyboard.KeyCode` + constructor + + :raises OSError: if a call to any *win32* function fails + """ + # Get a string representation of the key + layout_data = self._layout_data[self._modifier_state()] + scan = self._to_scan(vk, self._layout) + character, is_dead = layout_data[scan] + + return { + 'char': character, + 'is_dead': is_dead, + 'vk': vk, + '_scan': scan} + + def update_layout(self): + """Updates the cached layout data. + """ + self._layout, self._layout_data = self._generate_layout() + + def char_from_scan(self, scan): + """Translates a scan code to a character, if possible. + + :param int scan: The scan code to translate. + + :return: maybe a character + :rtype: str or None + """ + return self._layout_data[(False, False, False)][scan][0] + + def _generate_layout(self): + """Generates the keyboard layout. + + This method will call ``ToUnicodeEx``, which modifies kernel buffers, + so it must *not* be called from the keyboard hook. + + The return value is the tuple ``(layout_handle, layout_data)``, where + ``layout_data`` is a mapping from the tuple ``(shift, ctrl, alt)`` to + an array indexed by scan code containing the data + ``(character, is_dead)``, and ``layout_handle`` is the handle of the + layout. + + :return: a composite layout + """ + layout_data = {} + + state = (ctypes.c_ubyte * 255)() + with self._thread_input() as active_thread: + layout = self._GetKeyboardLayout(active_thread) + vks = [ + self._to_vk(scan, layout) + for scan in range(len(state))] + + for shift, ctrl, alt in itertools.product( + (False, True), (False, True), (False, True)): + current = [(None, False)] * len(state) + layout_data[(shift, ctrl, alt)] = current + + # Update the keyboard state based on the modifier state + state[VK.SHIFT] = 0x80 if shift else 0x00 + state[VK.CONTROL] = 0x80 if ctrl else 0x00 + state[VK.MENU] = 0x80 if alt else 0x00 + + # For each virtual key code... + out = (ctypes.wintypes.WCHAR * 5)() + for (scan, vk) in enumerate(vks): + # ...translate it to a unicode character + count = self._ToUnicodeEx( + vk, scan, ctypes.byref(state), ctypes.byref(out), + len(out), 0, layout) + + # Cache the result if a key is mapped + if count != 0: + character = out[0] + is_dead = count < 0 + current[scan] = (character, is_dead) + + # If the key is dead, flush the keyboard state + if is_dead: + self._ToUnicodeEx( + vk, scan, ctypes.byref(state), + ctypes.byref(out), len(out), 0, layout) + + return (layout, layout_data) + + def _to_scan(self, vk, layout): + """Retrieves the scan code for a virtual key code. + + :param int vk: The virtual key code. + + :param layout: The keyboard layout. + + :return: the scan code + """ + return self._MapVirtualKeyEx( + vk, self._MAPVK_VK_TO_VSC, layout) + + def _to_vk(self, scan, layout): + """Retrieves the virtual key code for a scan code. + + :param int vscan: The scan code. + + :param layout: The keyboard layout. + + :return: the virtual key code + """ + return self._MapVirtualKeyEx( + scan, self._MAPVK_VSC_TO_VK, layout) + + def _modifier_state(self): + """Returns a key into :attr:`_layout_data` for the current modifier + state. + + :return: the current modifier state + """ + shift = bool(self._GetAsyncKeyState(VK.SHIFT) & 0x8000) + ctrl = bool(self._GetAsyncKeyState(VK.CONTROL) & 0x8000) + alt = bool(self._GetAsyncKeyState(VK.MENU) & 0x8000) + return (shift, ctrl, alt) + + @contextlib.contextmanager + def _thread_input(self): + """Yields the current thread ID. + """ + yield GetCurrentThreadId() diff --git a/PortablePython/Lib/site-packages/pynput/_util/win32_vks.py b/PortablePython/Lib/site-packages/pynput/_util/win32_vks.py new file mode 100644 index 0000000..7b30238 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/_util/win32_vks.py @@ -0,0 +1,179 @@ +# coding: utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +# pylint: disable=C0111,C0302 + +LBUTTON = 1 +RBUTTON = 2 +CANCEL = 3 +MBUTTON = 4 +XBUTTON1 = 5 +XBUTTON2 = 6 +BACK = 8 +TAB = 9 +CLEAR = 12 +RETURN = 13 +SHIFT = 16 +CONTROL = 17 +MENU = 18 +PAUSE = 19 +CAPITAL = 20 +KANA = 21 +HANGEUL = 21 +HANGUL = 21 +JUNJA = 23 +FINAL = 24 +HANJA = 25 +KANJI = 25 +ESCAPE = 27 +CONVERT = 28 +NONCONVERT = 29 +ACCEPT = 30 +MODECHANGE = 31 +SPACE = 32 +PRIOR = 33 +NEXT = 34 +END = 35 +HOME = 36 +LEFT = 37 +UP = 38 +RIGHT = 39 +DOWN = 40 +SELECT = 41 +PRINT = 42 +EXECUTE = 43 +SNAPSHOT = 44 +INSERT = 45 +DELETE = 46 +HELP = 47 +LWIN = 91 +RWIN = 92 +APPS = 93 +SLEEP = 95 +NUMPAD0 = 96 +NUMPAD1 = 97 +NUMPAD2 = 98 +NUMPAD3 = 99 +NUMPAD4 = 100 +NUMPAD5 = 101 +NUMPAD6 = 102 +NUMPAD7 = 103 +NUMPAD8 = 104 +NUMPAD9 = 105 +MULTIPLY = 106 +ADD = 107 +SEPARATOR = 108 +SUBTRACT = 109 +DECIMAL = 110 +DIVIDE = 111 +F1 = 112 +F2 = 113 +F3 = 114 +F4 = 115 +F5 = 116 +F6 = 117 +F7 = 118 +F8 = 119 +F9 = 120 +F10 = 121 +F11 = 122 +F12 = 123 +F13 = 124 +F14 = 125 +F15 = 126 +F16 = 127 +F17 = 128 +F18 = 129 +F19 = 130 +F20 = 131 +F21 = 132 +F22 = 133 +F23 = 134 +F24 = 135 +NUMLOCK = 144 +SCROLL = 145 +OEM_NEC_EQUAL = 146 +OEM_FJ_JISHO = 146 +OEM_FJ_MASSHOU = 147 +OEM_FJ_TOUROKU = 148 +OEM_FJ_LOYA = 149 +OEM_FJ_ROYA = 150 +LSHIFT = 160 +RSHIFT = 161 +LCONTROL = 162 +RCONTROL = 163 +LMENU = 164 +RMENU = 165 +BROWSER_BACK = 166 +BROWSER_FORWARD = 167 +BROWSER_REFRESH = 168 +BROWSER_STOP = 169 +BROWSER_SEARCH = 170 +BROWSER_FAVORITES = 171 +BROWSER_HOME = 172 +VOLUME_MUTE = 173 +VOLUME_DOWN = 174 +VOLUME_UP = 175 +MEDIA_NEXT_TRACK = 176 +MEDIA_PREV_TRACK = 177 +MEDIA_STOP = 178 +MEDIA_PLAY_PAUSE = 179 +LAUNCH_MAIL = 180 +LAUNCH_MEDIA_SELECT = 181 +LAUNCH_APP1 = 182 +LAUNCH_APP2 = 183 +OEM_1 = 186 +OEM_PLUS = 187 +OEM_COMMA = 188 +OEM_MINUS = 189 +OEM_PERIOD = 190 +OEM_2 = 191 +OEM_3 = 192 +OEM_4 = 219 +OEM_5 = 220 +OEM_6 = 221 +OEM_7 = 222 +OEM_8 = 223 +OEM_AX = 225 +OEM_102 = 226 +ICO_HELP = 227 +ICO_00 = 228 +PROCESSKEY = 229 +ICO_CLEAR = 230 +PACKET = 231 +OEM_RESET = 233 +OEM_JUMP = 234 +OEM_PA1 = 235 +OEM_PA2 = 236 +OEM_PA3 = 237 +OEM_WSCTRL = 238 +OEM_CUSEL = 239 +OEM_ATTN = 240 +OEM_FINISH = 241 +OEM_COPY = 242 +OEM_AUTO = 243 +OEM_ENLW = 244 +OEM_BACKTAB = 245 +ATTN = 246 +CRSEL = 247 +EXSEL = 248 +EREOF = 249 +PLAY = 250 +ZOOM = 251 +NONAME = 252 +PA1 = 253 +OEM_CLEAR = 254 diff --git a/PortablePython/Lib/site-packages/pynput/_util/xorg.py b/PortablePython/Lib/site-packages/pynput/_util/xorg.py new file mode 100644 index 0000000..52995f3 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/_util/xorg.py @@ -0,0 +1,496 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +Utility functions and classes for the *Xorg* backend. +""" + +# pylint: disable=R0903 +# We implement stubs + +import contextlib +import functools +import itertools +import operator +import Xlib.display +import Xlib.keysymdef +import Xlib.threaded +import Xlib.XK + +from . import AbstractListener +from .xorg_keysyms import SYMBOLS + + +# Create a display to verify that we have an X connection +def _check_and_initialize(): + display = Xlib.display.Display() + display.close() + + for group in Xlib.keysymdef.__all__: + Xlib.XK.load_keysym_group(group) +_check_and_initialize() +del _check_and_initialize + + +class X11Error(Exception): + """An error that is thrown at the end of a code block managed by a + :func:`display_manager` if an *X11* error occurred. + """ + pass + + +@contextlib.contextmanager +def display_manager(display): + """Traps *X* errors and raises an :class:``X11Error`` at the end if any + error occurred. + + This handler also ensures that the :class:`Xlib.display.Display` being + managed is sync'd. + + :param Xlib.display.Display display: The *X* display. + + :return: the display + :rtype: Xlib.display.Display + """ + errors = [] + + def handler(*args): + """The *Xlib* error handler. + """ + errors.append(args) + + old_handler = display.set_error_handler(handler) + try: + yield display + display.sync() + finally: + display.set_error_handler(old_handler) + if errors: + raise X11Error(errors) + + +def _find_mask(display, symbol): + """Returns the mode flags to use for a modifier symbol. + + :param Xlib.display.Display display: The *X* display. + + :param str symbol: The name of the symbol. + + :return: the modifier mask + """ + # Get the key code for the symbol + modifier_keycode = display.keysym_to_keycode( + Xlib.XK.string_to_keysym(symbol)) + + for index, keycodes in enumerate(display.get_modifier_mapping()): + for keycode in keycodes: + if keycode == modifier_keycode: + return 1 << index + + return 0 + + +def alt_mask(display): + """Returns the *alt* mask flags. + + The first time this function is called for a display, the value is cached. + Subsequent calls will return the cached value. + + :param Xlib.display.Display display: The *X* display. + + :return: the modifier mask + """ + if not hasattr(display, '__alt_mask'): + display.__alt_mask = _find_mask(display, 'Alt_L') + return display.__alt_mask + + +def alt_gr_mask(display): + """Returns the *alt* mask flags. + + The first time this function is called for a display, the value is cached. + Subsequent calls will return the cached value. + + :param Xlib.display.Display display: The *X* display. + + :return: the modifier mask + """ + if not hasattr(display, '__altgr_mask'): + display.__altgr_mask = _find_mask(display, 'Mode_switch') + return display.__altgr_mask + + +def numlock_mask(display): + """Returns the *numlock* mask flags. + + The first time this function is called for a display, the value is cached. + Subsequent calls will return the cached value. + + :param Xlib.display.Display display: The *X* display. + + :return: the modifier mask + """ + if not hasattr(display, '__numlock_mask'): + display.__numlock_mask = _find_mask(display, 'Num_Lock') + return display.__numlock_mask + + +def keysym_is_latin_upper(keysym): + """Determines whether a *keysym* is an upper case *latin* character. + + This is true only if ``XK_A`` <= ``keysym`` <= ` XK_Z``. + + :param in keysym: The *keysym* to check. + """ + return Xlib.XK.XK_A <= keysym <= Xlib.XK.XK_Z + + +def keysym_is_latin_lower(keysym): + """Determines whether a *keysym* is a lower case *latin* character. + + This is true only if ``XK_a`` <= ``keysym`` <= ` XK_z``. + + :param in keysym: The *keysym* to check. + """ + return Xlib.XK.XK_a <= keysym <= Xlib.XK.XK_z + + +def keysym_group(ks1, ks2): + """Generates a group from two *keysyms*. + + The implementation of this function comes from: + + Within each group, if the second element of the group is ``NoSymbol``, + then the group should be treated as if the second element were the same + as the first element, except when the first element is an alphabetic + *KeySym* ``K`` for which both lowercase and uppercase forms are + defined. + + In that case, the group should be treated as if the first element were + the lowercase form of ``K`` and the second element were the uppercase + form of ``K``. + + This function assumes that *alphabetic* means *latin*; this assumption + appears to be consistent with observations of the return values from + ``XGetKeyboardMapping``. + + :param ks1: The first *keysym*. + + :param ks2: The second *keysym*. + + :return: a tuple conforming to the description above + """ + if ks2 == Xlib.XK.NoSymbol: + if keysym_is_latin_upper(ks1): + return (Xlib.XK.XK_a + ks1 - Xlib.XK.XK_A, ks1) + elif keysym_is_latin_lower(ks1): + return (ks1, Xlib.XK.XK_A + ks1 - Xlib.XK.XK_a) + else: + return (ks1, ks1) + else: + return (ks1, ks2) + + +def keysym_normalize(keysym): + """Normalises a list of *keysyms*. + + The implementation of this function comes from: + + If the list (ignoring trailing ``NoSymbol`` entries) is a single + *KeySym* ``K``, then the list is treated as if it were the list + ``K NoSymbol K NoSymbol``. + + If the list (ignoring trailing ``NoSymbol`` entries) is a pair of + *KeySyms* ``K1 K2``, then the list is treated as if it were the list + ``K1 K2 K1 K2``. + + If the list (ignoring trailing ``NoSymbol`` entries) is a triple of + *KeySyms* ``K1 K2 K3``, then the list is treated as if it were the list + ``K1 K2 K3 NoSymbol``. + + This function will also group the *keysyms* using :func:`keysym_group`. + + :param keysyms: A list of keysyms. + + :return: the tuple ``(group_1, group_2)`` or ``None`` + """ + # Remove trailing NoSymbol + stripped = list(reversed(list( + itertools.dropwhile( + lambda n: n == Xlib.XK.NoSymbol, + reversed(keysym))))) + + if not stripped: + return + + elif len(stripped) == 1: + return ( + keysym_group(stripped[0], Xlib.XK.NoSymbol), + keysym_group(stripped[0], Xlib.XK.NoSymbol)) + + elif len(stripped) == 2: + return ( + keysym_group(stripped[0], stripped[1]), + keysym_group(stripped[0], stripped[1])) + + elif len(stripped) == 3: + return ( + keysym_group(stripped[0], stripped[1]), + keysym_group(stripped[2], Xlib.XK.NoSymbol)) + + elif len(stripped) >= 6: + # TODO: Find out why this is necessary; using only the documented + # behaviour may lead to only a US layout being used? + return ( + keysym_group(stripped[0], stripped[1]), + keysym_group(stripped[4], stripped[5])) + + else: + return ( + keysym_group(stripped[0], stripped[1]), + keysym_group(stripped[2], stripped[3])) + + +def index_to_shift(display, index): + """Converts an index in a *key code* list to the corresponding shift state. + + :param Xlib.display.Display display: The display for which to retrieve the + shift mask. + + :param int index: The keyboard mapping *key code* index. + + :return: a shift mask + """ + return ( + (1 << 0 if index & 1 else 0) | + (alt_gr_mask(display) if index & 2 else 0)) + + +def shift_to_index(display, shift): + """Converts an index in a *key code* list to the corresponding shift state. + + :param Xlib.display.Display display: The display for which to retrieve the + shift mask. + + :param int index: The keyboard mapping *key code* index. + + :return: a shift mask + """ + return ( + (1 if shift & 1 else 0) + + (2 if shift & alt_gr_mask(display) else 0)) + + +def keyboard_mapping(display): + """Generates a mapping from *keysyms* to *key codes* and required + modifier shift states. + + :param Xlib.display.Display display: The display for which to retrieve the + keyboard mapping. + + :return: the keyboard mapping + """ + mapping = {} + + shift_mask = 1 << 0 + group_mask = alt_gr_mask(display) + + # Iterate over all keysym lists in the keyboard mapping + min_keycode = display.display.info.min_keycode + keycode_count = display.display.info.max_keycode - min_keycode + 1 + for index, keysyms in enumerate(display.get_keyboard_mapping( + min_keycode, keycode_count)): + key_code = index + min_keycode + + # Normalise the keysym list to yield a tuple containing the two groups + normalized = keysym_normalize(keysyms) + if not normalized: + continue + + # Iterate over the groups to extract the shift and modifier state + for groups, group in zip(normalized, (False, True)): + for keysym, shift in zip(groups, (False, True)): + if not keysym: + continue + shift_state = 0 \ + | (shift_mask if shift else 0) \ + | (group_mask if group else 0) + + # Prefer already known lesser shift states + if keysym in mapping and mapping[keysym][1] < shift_state: + continue + mapping[keysym] = (key_code, shift_state) + + return mapping + + +def char_to_keysym(char): + """Converts a unicode character to a *keysym*. + + :param str char: The unicode character. + + :return: the corresponding *keysym*, or ``0`` if it cannot be found + """ + ordinal = ord(char) + if ordinal < 0x100: + return ordinal + else: + return ordinal | 0x01000000 + + +def symbol_to_keysym(symbol): + """Converts a symbol name to a *keysym*. + + :param str symbol: The name of the symbol. + + :return: the corresponding *keysym*, or ``0`` if it cannot be found + """ + # First try simple translation, the try a module attribute of + # Xlib.keysymdef.xkb and fall back on our pre-generated table + return (0 + or Xlib.XK.string_to_keysym(symbol) + or getattr(Xlib.keysymdef.xkb, "XK_" + symbol, 0) + or SYMBOLS.get(symbol, (0,))[0]) + + +class ListenerMixin(object): + """A mixin for *X* event listeners. + + Subclasses should set a value for :attr:`_EVENTS` and implement + :meth:`_handle_message`. + """ + #: The events for which to listen + _EVENTS = tuple() + + #: We use this instance for parsing the binary data + _EVENT_PARSER = Xlib.protocol.rq.EventField(None) + + def _run(self): + self._display_stop = Xlib.display.Display() + self._display_record = Xlib.display.Display() + self._stopped = False + with display_manager(self._display_record) as dm: + self._context = dm.record_create_context( + 0, + [Xlib.ext.record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (0, 0), + 'device_events': self._EVENTS, + 'errors': (0, 0), + 'client_started': False, + 'client_died': False}]) + + # pylint: disable=W0702; we want to silence errors + try: + self._initialize(self._display_stop) + self._mark_ready() + if self.suppress: + with display_manager(self._display_stop) as dm: + self._suppress_start(dm) + self._display_record.record_enable_context( + self._context, self._handler) + except: + # This exception will have been passed to the main thread + pass + finally: + if self.suppress: + with display_manager(self._display_stop) as dm: + self._suppress_stop(dm) + self._display_stop.record_disable_context(self._context) + self._display_stop.flush() + self._display_record.record_free_context(self._context) + self._display_stop.close() + self._display_record.close() + # pylint: enable=W0702 + + def _stop_platform(self): + if not hasattr(self, '_context'): + self.wait() + + # Do this asynchronously to avoid deadlocks + self._display_record.record_disable_context(self._context) + + def _suppress_start(self, display): + """Starts suppressing events. + + :param Xlib.display.Display display: The display for which to suppress + events. + """ + raise NotImplementedError() + + def _suppress_stop(self, display): + """Starts suppressing events. + + :param Xlib.display.Display display: The display for which to suppress + events. + """ + raise NotImplementedError() + + @property + def _event_mask(self): + """The event mask. + """ + return functools.reduce(operator.__or__, self._EVENTS, 0) + + @AbstractListener._emitter + def _handler(self, events): + """The callback registered with *X* for mouse events. + + This method will parse the response and call the callbacks registered + on initialisation. + + :param events: The events passed by *X*. This is a binary block + parsable by :attr:`_EVENT_PARSER`. + """ + if not self.running: + raise self.StopException() + + data = events.data + + while data and len(data): + event, data = self._EVENT_PARSER.parse_binary_value( + data, self._display_record.display, None, None) + + injected = event.send_event + self._handle_message(self._display_stop, event, injected) + + def _initialize(self, display): + """Initialises this listener. + + This method is called immediately before the event loop, from the + handler thread. + + :param display: The display being used. + """ + pass + + def _handle_message(self, display, event, injected): + """The device specific callback handler. + + This method calls the appropriate callback registered when this + listener was created based on the event. + + :param display: The display being used. + + :param event: The event. + + :param bool injected: Whether the event was injected. + """ + pass diff --git a/PortablePython/Lib/site-packages/pynput/_util/xorg_keysyms.py b/PortablePython/Lib/site-packages/pynput/_util/xorg_keysyms.py new file mode 100644 index 0000000..7505e89 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/_util/xorg_keysyms.py @@ -0,0 +1,1715 @@ +# coding: utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +# pylint: disable=C0111,C0302 + +SYMBOLS = { + '0': (0x0030, u'\u0030'), + '1': (0x0031, u'\u0031'), + '2': (0x0032, u'\u0032'), + '3': (0x0033, u'\u0033'), + '4': (0x0034, u'\u0034'), + '5': (0x0035, u'\u0035'), + '6': (0x0036, u'\u0036'), + '7': (0x0037, u'\u0037'), + '8': (0x0038, u'\u0038'), + '9': (0x0039, u'\u0039'), + 'A': (0x0041, u'\u0041'), + 'AE': (0x00c6, u'\u00C6'), + 'Aacute': (0x00c1, u'\u00C1'), + 'Abelowdot': (0x1001ea0, u'\u1EA0'), + 'Abreve': (0x01c3, u'\u0102'), + 'Abreveacute': (0x1001eae, u'\u1EAE'), + 'Abrevebelowdot': (0x1001eb6, u'\u1EB6'), + 'Abrevegrave': (0x1001eb0, u'\u1EB0'), + 'Abrevehook': (0x1001eb2, u'\u1EB2'), + 'Abrevetilde': (0x1001eb4, u'\u1EB4'), + 'Acircumflex': (0x00c2, u'\u00C2'), + 'Acircumflexacute': (0x1001ea4, u'\u1EA4'), + 'Acircumflexbelowdot': (0x1001eac, u'\u1EAC'), + 'Acircumflexgrave': (0x1001ea6, u'\u1EA6'), + 'Acircumflexhook': (0x1001ea8, u'\u1EA8'), + 'Acircumflextilde': (0x1001eaa, u'\u1EAA'), + 'Adiaeresis': (0x00c4, u'\u00C4'), + 'Agrave': (0x00c0, u'\u00C0'), + 'Ahook': (0x1001ea2, u'\u1EA2'), + 'Amacron': (0x03c0, u'\u0100'), + 'Aogonek': (0x01a1, u'\u0104'), + 'Arabic_0': (0x1000660, u'\u0660'), + 'Arabic_1': (0x1000661, u'\u0661'), + 'Arabic_2': (0x1000662, u'\u0662'), + 'Arabic_3': (0x1000663, u'\u0663'), + 'Arabic_4': (0x1000664, u'\u0664'), + 'Arabic_5': (0x1000665, u'\u0665'), + 'Arabic_6': (0x1000666, u'\u0666'), + 'Arabic_7': (0x1000667, u'\u0667'), + 'Arabic_8': (0x1000668, u'\u0668'), + 'Arabic_9': (0x1000669, u'\u0669'), + 'Arabic_ain': (0x05d9, u'\u0639'), + 'Arabic_alef': (0x05c7, u'\u0627'), + 'Arabic_alefmaksura': (0x05e9, u'\u0649'), + 'Arabic_beh': (0x05c8, u'\u0628'), + 'Arabic_comma': (0x05ac, u'\u060C'), + 'Arabic_dad': (0x05d6, u'\u0636'), + 'Arabic_dal': (0x05cf, u'\u062F'), + 'Arabic_damma': (0x05ef, u'\u064F'), + 'Arabic_dammatan': (0x05ec, u'\u064C'), + 'Arabic_ddal': (0x1000688, u'\u0688'), + 'Arabic_farsi_yeh': (0x10006cc, u'\u06CC'), + 'Arabic_fatha': (0x05ee, u'\u064E'), + 'Arabic_fathatan': (0x05eb, u'\u064B'), + 'Arabic_feh': (0x05e1, u'\u0641'), + 'Arabic_fullstop': (0x10006d4, u'\u06D4'), + 'Arabic_gaf': (0x10006af, u'\u06AF'), + 'Arabic_ghain': (0x05da, u'\u063A'), + 'Arabic_ha': (0x05e7, u'\u0647'), + 'Arabic_hah': (0x05cd, u'\u062D'), + 'Arabic_hamza': (0x05c1, u'\u0621'), + 'Arabic_hamza_above': (0x1000654, u'\u0654'), + 'Arabic_hamza_below': (0x1000655, u'\u0655'), + 'Arabic_hamzaonalef': (0x05c3, u'\u0623'), + 'Arabic_hamzaonwaw': (0x05c4, u'\u0624'), + 'Arabic_hamzaonyeh': (0x05c6, u'\u0626'), + 'Arabic_hamzaunderalef': (0x05c5, u'\u0625'), + 'Arabic_heh_doachashmee': (0x10006be, u'\u06BE'), + 'Arabic_heh_goal': (0x10006c1, u'\u06C1'), + 'Arabic_jeem': (0x05cc, u'\u062C'), + 'Arabic_jeh': (0x1000698, u'\u0698'), + 'Arabic_kaf': (0x05e3, u'\u0643'), + 'Arabic_kasra': (0x05f0, u'\u0650'), + 'Arabic_kasratan': (0x05ed, u'\u064D'), + 'Arabic_keheh': (0x10006a9, u'\u06A9'), + 'Arabic_khah': (0x05ce, u'\u062E'), + 'Arabic_lam': (0x05e4, u'\u0644'), + 'Arabic_madda_above': (0x1000653, u'\u0653'), + 'Arabic_maddaonalef': (0x05c2, u'\u0622'), + 'Arabic_meem': (0x05e5, u'\u0645'), + 'Arabic_noon': (0x05e6, u'\u0646'), + 'Arabic_noon_ghunna': (0x10006ba, u'\u06BA'), + 'Arabic_peh': (0x100067e, u'\u067E'), + 'Arabic_percent': (0x100066a, u'\u066A'), + 'Arabic_qaf': (0x05e2, u'\u0642'), + 'Arabic_question_mark': (0x05bf, u'\u061F'), + 'Arabic_ra': (0x05d1, u'\u0631'), + 'Arabic_rreh': (0x1000691, u'\u0691'), + 'Arabic_sad': (0x05d5, u'\u0635'), + 'Arabic_seen': (0x05d3, u'\u0633'), + 'Arabic_semicolon': (0x05bb, u'\u061B'), + 'Arabic_shadda': (0x05f1, u'\u0651'), + 'Arabic_sheen': (0x05d4, u'\u0634'), + 'Arabic_sukun': (0x05f2, u'\u0652'), + 'Arabic_superscript_alef': (0x1000670, u'\u0670'), + 'Arabic_tah': (0x05d7, u'\u0637'), + 'Arabic_tatweel': (0x05e0, u'\u0640'), + 'Arabic_tcheh': (0x1000686, u'\u0686'), + 'Arabic_teh': (0x05ca, u'\u062A'), + 'Arabic_tehmarbuta': (0x05c9, u'\u0629'), + 'Arabic_thal': (0x05d0, u'\u0630'), + 'Arabic_theh': (0x05cb, u'\u062B'), + 'Arabic_tteh': (0x1000679, u'\u0679'), + 'Arabic_veh': (0x10006a4, u'\u06A4'), + 'Arabic_waw': (0x05e8, u'\u0648'), + 'Arabic_yeh': (0x05ea, u'\u064A'), + 'Arabic_yeh_baree': (0x10006d2, u'\u06D2'), + 'Arabic_zah': (0x05d8, u'\u0638'), + 'Arabic_zain': (0x05d2, u'\u0632'), + 'Aring': (0x00c5, u'\u00C5'), + 'Armenian_AT': (0x1000538, u'\u0538'), + 'Armenian_AYB': (0x1000531, u'\u0531'), + 'Armenian_BEN': (0x1000532, u'\u0532'), + 'Armenian_CHA': (0x1000549, u'\u0549'), + 'Armenian_DA': (0x1000534, u'\u0534'), + 'Armenian_DZA': (0x1000541, u'\u0541'), + 'Armenian_E': (0x1000537, u'\u0537'), + 'Armenian_FE': (0x1000556, u'\u0556'), + 'Armenian_GHAT': (0x1000542, u'\u0542'), + 'Armenian_GIM': (0x1000533, u'\u0533'), + 'Armenian_HI': (0x1000545, u'\u0545'), + 'Armenian_HO': (0x1000540, u'\u0540'), + 'Armenian_INI': (0x100053b, u'\u053B'), + 'Armenian_JE': (0x100054b, u'\u054B'), + 'Armenian_KE': (0x1000554, u'\u0554'), + 'Armenian_KEN': (0x100053f, u'\u053F'), + 'Armenian_KHE': (0x100053d, u'\u053D'), + 'Armenian_LYUN': (0x100053c, u'\u053C'), + 'Armenian_MEN': (0x1000544, u'\u0544'), + 'Armenian_NU': (0x1000546, u'\u0546'), + 'Armenian_O': (0x1000555, u'\u0555'), + 'Armenian_PE': (0x100054a, u'\u054A'), + 'Armenian_PYUR': (0x1000553, u'\u0553'), + 'Armenian_RA': (0x100054c, u'\u054C'), + 'Armenian_RE': (0x1000550, u'\u0550'), + 'Armenian_SE': (0x100054d, u'\u054D'), + 'Armenian_SHA': (0x1000547, u'\u0547'), + 'Armenian_TCHE': (0x1000543, u'\u0543'), + 'Armenian_TO': (0x1000539, u'\u0539'), + 'Armenian_TSA': (0x100053e, u'\u053E'), + 'Armenian_TSO': (0x1000551, u'\u0551'), + 'Armenian_TYUN': (0x100054f, u'\u054F'), + 'Armenian_VEV': (0x100054e, u'\u054E'), + 'Armenian_VO': (0x1000548, u'\u0548'), + 'Armenian_VYUN': (0x1000552, u'\u0552'), + 'Armenian_YECH': (0x1000535, u'\u0535'), + 'Armenian_ZA': (0x1000536, u'\u0536'), + 'Armenian_ZHE': (0x100053a, u'\u053A'), + 'Armenian_accent': (0x100055b, u'\u055B'), + 'Armenian_amanak': (0x100055c, u'\u055C'), + 'Armenian_apostrophe': (0x100055a, u'\u055A'), + 'Armenian_at': (0x1000568, u'\u0568'), + 'Armenian_ayb': (0x1000561, u'\u0561'), + 'Armenian_ben': (0x1000562, u'\u0562'), + 'Armenian_but': (0x100055d, u'\u055D'), + 'Armenian_cha': (0x1000579, u'\u0579'), + 'Armenian_da': (0x1000564, u'\u0564'), + 'Armenian_dza': (0x1000571, u'\u0571'), + 'Armenian_e': (0x1000567, u'\u0567'), + 'Armenian_exclam': (0x100055c, u'\u055C'), + 'Armenian_fe': (0x1000586, u'\u0586'), + 'Armenian_full_stop': (0x1000589, u'\u0589'), + 'Armenian_ghat': (0x1000572, u'\u0572'), + 'Armenian_gim': (0x1000563, u'\u0563'), + 'Armenian_hi': (0x1000575, u'\u0575'), + 'Armenian_ho': (0x1000570, u'\u0570'), + 'Armenian_hyphen': (0x100058a, u'\u058A'), + 'Armenian_ini': (0x100056b, u'\u056B'), + 'Armenian_je': (0x100057b, u'\u057B'), + 'Armenian_ke': (0x1000584, u'\u0584'), + 'Armenian_ken': (0x100056f, u'\u056F'), + 'Armenian_khe': (0x100056d, u'\u056D'), + 'Armenian_ligature_ew': (0x1000587, u'\u0587'), + 'Armenian_lyun': (0x100056c, u'\u056C'), + 'Armenian_men': (0x1000574, u'\u0574'), + 'Armenian_nu': (0x1000576, u'\u0576'), + 'Armenian_o': (0x1000585, u'\u0585'), + 'Armenian_paruyk': (0x100055e, u'\u055E'), + 'Armenian_pe': (0x100057a, u'\u057A'), + 'Armenian_pyur': (0x1000583, u'\u0583'), + 'Armenian_question': (0x100055e, u'\u055E'), + 'Armenian_ra': (0x100057c, u'\u057C'), + 'Armenian_re': (0x1000580, u'\u0580'), + 'Armenian_se': (0x100057d, u'\u057D'), + 'Armenian_separation_mark': (0x100055d, u'\u055D'), + 'Armenian_sha': (0x1000577, u'\u0577'), + 'Armenian_shesht': (0x100055b, u'\u055B'), + 'Armenian_tche': (0x1000573, u'\u0573'), + 'Armenian_to': (0x1000569, u'\u0569'), + 'Armenian_tsa': (0x100056e, u'\u056E'), + 'Armenian_tso': (0x1000581, u'\u0581'), + 'Armenian_tyun': (0x100057f, u'\u057F'), + 'Armenian_verjaket': (0x1000589, u'\u0589'), + 'Armenian_vev': (0x100057e, u'\u057E'), + 'Armenian_vo': (0x1000578, u'\u0578'), + 'Armenian_vyun': (0x1000582, u'\u0582'), + 'Armenian_yech': (0x1000565, u'\u0565'), + 'Armenian_yentamna': (0x100058a, u'\u058A'), + 'Armenian_za': (0x1000566, u'\u0566'), + 'Armenian_zhe': (0x100056a, u'\u056A'), + 'Atilde': (0x00c3, u'\u00C3'), + 'B': (0x0042, u'\u0042'), + 'Babovedot': (0x1001e02, u'\u1E02'), + 'Byelorussian_SHORTU': (0x06be, u'\u040E'), + 'Byelorussian_shortu': (0x06ae, u'\u045E'), + 'C': (0x0043, u'\u0043'), + 'Cabovedot': (0x02c5, u'\u010A'), + 'Cacute': (0x01c6, u'\u0106'), + 'Ccaron': (0x01c8, u'\u010C'), + 'Ccedilla': (0x00c7, u'\u00C7'), + 'Ccircumflex': (0x02c6, u'\u0108'), + 'ColonSign': (0x10020a1, u'\u20A1'), + 'CruzeiroSign': (0x10020a2, u'\u20A2'), + 'Cyrillic_A': (0x06e1, u'\u0410'), + 'Cyrillic_BE': (0x06e2, u'\u0411'), + 'Cyrillic_CHE': (0x06fe, u'\u0427'), + 'Cyrillic_CHE_descender': (0x10004b6, u'\u04B6'), + 'Cyrillic_CHE_vertstroke': (0x10004b8, u'\u04B8'), + 'Cyrillic_DE': (0x06e4, u'\u0414'), + 'Cyrillic_DZHE': (0x06bf, u'\u040F'), + 'Cyrillic_E': (0x06fc, u'\u042D'), + 'Cyrillic_EF': (0x06e6, u'\u0424'), + 'Cyrillic_EL': (0x06ec, u'\u041B'), + 'Cyrillic_EM': (0x06ed, u'\u041C'), + 'Cyrillic_EN': (0x06ee, u'\u041D'), + 'Cyrillic_EN_descender': (0x10004a2, u'\u04A2'), + 'Cyrillic_ER': (0x06f2, u'\u0420'), + 'Cyrillic_ES': (0x06f3, u'\u0421'), + 'Cyrillic_GHE': (0x06e7, u'\u0413'), + 'Cyrillic_GHE_bar': (0x1000492, u'\u0492'), + 'Cyrillic_HA': (0x06e8, u'\u0425'), + 'Cyrillic_HARDSIGN': (0x06ff, u'\u042A'), + 'Cyrillic_HA_descender': (0x10004b2, u'\u04B2'), + 'Cyrillic_I': (0x06e9, u'\u0418'), + 'Cyrillic_IE': (0x06e5, u'\u0415'), + 'Cyrillic_IO': (0x06b3, u'\u0401'), + 'Cyrillic_I_macron': (0x10004e2, u'\u04E2'), + 'Cyrillic_JE': (0x06b8, u'\u0408'), + 'Cyrillic_KA': (0x06eb, u'\u041A'), + 'Cyrillic_KA_descender': (0x100049a, u'\u049A'), + 'Cyrillic_KA_vertstroke': (0x100049c, u'\u049C'), + 'Cyrillic_LJE': (0x06b9, u'\u0409'), + 'Cyrillic_NJE': (0x06ba, u'\u040A'), + 'Cyrillic_O': (0x06ef, u'\u041E'), + 'Cyrillic_O_bar': (0x10004e8, u'\u04E8'), + 'Cyrillic_PE': (0x06f0, u'\u041F'), + 'Cyrillic_SCHWA': (0x10004d8, u'\u04D8'), + 'Cyrillic_SHA': (0x06fb, u'\u0428'), + 'Cyrillic_SHCHA': (0x06fd, u'\u0429'), + 'Cyrillic_SHHA': (0x10004ba, u'\u04BA'), + 'Cyrillic_SHORTI': (0x06ea, u'\u0419'), + 'Cyrillic_SOFTSIGN': (0x06f8, u'\u042C'), + 'Cyrillic_TE': (0x06f4, u'\u0422'), + 'Cyrillic_TSE': (0x06e3, u'\u0426'), + 'Cyrillic_U': (0x06f5, u'\u0423'), + 'Cyrillic_U_macron': (0x10004ee, u'\u04EE'), + 'Cyrillic_U_straight': (0x10004ae, u'\u04AE'), + 'Cyrillic_U_straight_bar': (0x10004b0, u'\u04B0'), + 'Cyrillic_VE': (0x06f7, u'\u0412'), + 'Cyrillic_YA': (0x06f1, u'\u042F'), + 'Cyrillic_YERU': (0x06f9, u'\u042B'), + 'Cyrillic_YU': (0x06e0, u'\u042E'), + 'Cyrillic_ZE': (0x06fa, u'\u0417'), + 'Cyrillic_ZHE': (0x06f6, u'\u0416'), + 'Cyrillic_ZHE_descender': (0x1000496, u'\u0496'), + 'Cyrillic_a': (0x06c1, u'\u0430'), + 'Cyrillic_be': (0x06c2, u'\u0431'), + 'Cyrillic_che': (0x06de, u'\u0447'), + 'Cyrillic_che_descender': (0x10004b7, u'\u04B7'), + 'Cyrillic_che_vertstroke': (0x10004b9, u'\u04B9'), + 'Cyrillic_de': (0x06c4, u'\u0434'), + 'Cyrillic_dzhe': (0x06af, u'\u045F'), + 'Cyrillic_e': (0x06dc, u'\u044D'), + 'Cyrillic_ef': (0x06c6, u'\u0444'), + 'Cyrillic_el': (0x06cc, u'\u043B'), + 'Cyrillic_em': (0x06cd, u'\u043C'), + 'Cyrillic_en': (0x06ce, u'\u043D'), + 'Cyrillic_en_descender': (0x10004a3, u'\u04A3'), + 'Cyrillic_er': (0x06d2, u'\u0440'), + 'Cyrillic_es': (0x06d3, u'\u0441'), + 'Cyrillic_ghe': (0x06c7, u'\u0433'), + 'Cyrillic_ghe_bar': (0x1000493, u'\u0493'), + 'Cyrillic_ha': (0x06c8, u'\u0445'), + 'Cyrillic_ha_descender': (0x10004b3, u'\u04B3'), + 'Cyrillic_hardsign': (0x06df, u'\u044A'), + 'Cyrillic_i': (0x06c9, u'\u0438'), + 'Cyrillic_i_macron': (0x10004e3, u'\u04E3'), + 'Cyrillic_ie': (0x06c5, u'\u0435'), + 'Cyrillic_io': (0x06a3, u'\u0451'), + 'Cyrillic_je': (0x06a8, u'\u0458'), + 'Cyrillic_ka': (0x06cb, u'\u043A'), + 'Cyrillic_ka_descender': (0x100049b, u'\u049B'), + 'Cyrillic_ka_vertstroke': (0x100049d, u'\u049D'), + 'Cyrillic_lje': (0x06a9, u'\u0459'), + 'Cyrillic_nje': (0x06aa, u'\u045A'), + 'Cyrillic_o': (0x06cf, u'\u043E'), + 'Cyrillic_o_bar': (0x10004e9, u'\u04E9'), + 'Cyrillic_pe': (0x06d0, u'\u043F'), + 'Cyrillic_schwa': (0x10004d9, u'\u04D9'), + 'Cyrillic_sha': (0x06db, u'\u0448'), + 'Cyrillic_shcha': (0x06dd, u'\u0449'), + 'Cyrillic_shha': (0x10004bb, u'\u04BB'), + 'Cyrillic_shorti': (0x06ca, u'\u0439'), + 'Cyrillic_softsign': (0x06d8, u'\u044C'), + 'Cyrillic_te': (0x06d4, u'\u0442'), + 'Cyrillic_tse': (0x06c3, u'\u0446'), + 'Cyrillic_u': (0x06d5, u'\u0443'), + 'Cyrillic_u_macron': (0x10004ef, u'\u04EF'), + 'Cyrillic_u_straight': (0x10004af, u'\u04AF'), + 'Cyrillic_u_straight_bar': (0x10004b1, u'\u04B1'), + 'Cyrillic_ve': (0x06d7, u'\u0432'), + 'Cyrillic_ya': (0x06d1, u'\u044F'), + 'Cyrillic_yeru': (0x06d9, u'\u044B'), + 'Cyrillic_yu': (0x06c0, u'\u044E'), + 'Cyrillic_ze': (0x06da, u'\u0437'), + 'Cyrillic_zhe': (0x06d6, u'\u0436'), + 'Cyrillic_zhe_descender': (0x1000497, u'\u0497'), + 'D': (0x0044, u'\u0044'), + 'Dabovedot': (0x1001e0a, u'\u1E0A'), + 'Dcaron': (0x01cf, u'\u010E'), + 'DongSign': (0x10020ab, u'\u20AB'), + 'Dstroke': (0x01d0, u'\u0110'), + 'E': (0x0045, u'\u0045'), + 'ENG': (0x03bd, u'\u014A'), + 'ETH': (0x00d0, u'\u00D0'), + 'EZH': (0x10001b7, u'\u01B7'), + 'Eabovedot': (0x03cc, u'\u0116'), + 'Eacute': (0x00c9, u'\u00C9'), + 'Ebelowdot': (0x1001eb8, u'\u1EB8'), + 'Ecaron': (0x01cc, u'\u011A'), + 'Ecircumflex': (0x00ca, u'\u00CA'), + 'Ecircumflexacute': (0x1001ebe, u'\u1EBE'), + 'Ecircumflexbelowdot': (0x1001ec6, u'\u1EC6'), + 'Ecircumflexgrave': (0x1001ec0, u'\u1EC0'), + 'Ecircumflexhook': (0x1001ec2, u'\u1EC2'), + 'Ecircumflextilde': (0x1001ec4, u'\u1EC4'), + 'EcuSign': (0x10020a0, u'\u20A0'), + 'Ediaeresis': (0x00cb, u'\u00CB'), + 'Egrave': (0x00c8, u'\u00C8'), + 'Ehook': (0x1001eba, u'\u1EBA'), + 'Emacron': (0x03aa, u'\u0112'), + 'Eogonek': (0x01ca, u'\u0118'), + 'Etilde': (0x1001ebc, u'\u1EBC'), + 'EuroSign': (0x20ac, u'\u20AC'), + 'F': (0x0046, u'\u0046'), + 'FFrancSign': (0x10020a3, u'\u20A3'), + 'Fabovedot': (0x1001e1e, u'\u1E1E'), + 'Farsi_0': (0x10006f0, u'\u06F0'), + 'Farsi_1': (0x10006f1, u'\u06F1'), + 'Farsi_2': (0x10006f2, u'\u06F2'), + 'Farsi_3': (0x10006f3, u'\u06F3'), + 'Farsi_4': (0x10006f4, u'\u06F4'), + 'Farsi_5': (0x10006f5, u'\u06F5'), + 'Farsi_6': (0x10006f6, u'\u06F6'), + 'Farsi_7': (0x10006f7, u'\u06F7'), + 'Farsi_8': (0x10006f8, u'\u06F8'), + 'Farsi_9': (0x10006f9, u'\u06F9'), + 'Farsi_yeh': (0x10006cc, u'\u06CC'), + 'G': (0x0047, u'\u0047'), + 'Gabovedot': (0x02d5, u'\u0120'), + 'Gbreve': (0x02ab, u'\u011E'), + 'Gcaron': (0x10001e6, u'\u01E6'), + 'Gcedilla': (0x03ab, u'\u0122'), + 'Gcircumflex': (0x02d8, u'\u011C'), + 'Georgian_an': (0x10010d0, u'\u10D0'), + 'Georgian_ban': (0x10010d1, u'\u10D1'), + 'Georgian_can': (0x10010ea, u'\u10EA'), + 'Georgian_char': (0x10010ed, u'\u10ED'), + 'Georgian_chin': (0x10010e9, u'\u10E9'), + 'Georgian_cil': (0x10010ec, u'\u10EC'), + 'Georgian_don': (0x10010d3, u'\u10D3'), + 'Georgian_en': (0x10010d4, u'\u10D4'), + 'Georgian_fi': (0x10010f6, u'\u10F6'), + 'Georgian_gan': (0x10010d2, u'\u10D2'), + 'Georgian_ghan': (0x10010e6, u'\u10E6'), + 'Georgian_hae': (0x10010f0, u'\u10F0'), + 'Georgian_har': (0x10010f4, u'\u10F4'), + 'Georgian_he': (0x10010f1, u'\u10F1'), + 'Georgian_hie': (0x10010f2, u'\u10F2'), + 'Georgian_hoe': (0x10010f5, u'\u10F5'), + 'Georgian_in': (0x10010d8, u'\u10D8'), + 'Georgian_jhan': (0x10010ef, u'\u10EF'), + 'Georgian_jil': (0x10010eb, u'\u10EB'), + 'Georgian_kan': (0x10010d9, u'\u10D9'), + 'Georgian_khar': (0x10010e5, u'\u10E5'), + 'Georgian_las': (0x10010da, u'\u10DA'), + 'Georgian_man': (0x10010db, u'\u10DB'), + 'Georgian_nar': (0x10010dc, u'\u10DC'), + 'Georgian_on': (0x10010dd, u'\u10DD'), + 'Georgian_par': (0x10010de, u'\u10DE'), + 'Georgian_phar': (0x10010e4, u'\u10E4'), + 'Georgian_qar': (0x10010e7, u'\u10E7'), + 'Georgian_rae': (0x10010e0, u'\u10E0'), + 'Georgian_san': (0x10010e1, u'\u10E1'), + 'Georgian_shin': (0x10010e8, u'\u10E8'), + 'Georgian_tan': (0x10010d7, u'\u10D7'), + 'Georgian_tar': (0x10010e2, u'\u10E2'), + 'Georgian_un': (0x10010e3, u'\u10E3'), + 'Georgian_vin': (0x10010d5, u'\u10D5'), + 'Georgian_we': (0x10010f3, u'\u10F3'), + 'Georgian_xan': (0x10010ee, u'\u10EE'), + 'Georgian_zen': (0x10010d6, u'\u10D6'), + 'Georgian_zhar': (0x10010df, u'\u10DF'), + 'Greek_ALPHA': (0x07c1, u'\u0391'), + 'Greek_ALPHAaccent': (0x07a1, u'\u0386'), + 'Greek_BETA': (0x07c2, u'\u0392'), + 'Greek_CHI': (0x07d7, u'\u03A7'), + 'Greek_DELTA': (0x07c4, u'\u0394'), + 'Greek_EPSILON': (0x07c5, u'\u0395'), + 'Greek_EPSILONaccent': (0x07a2, u'\u0388'), + 'Greek_ETA': (0x07c7, u'\u0397'), + 'Greek_ETAaccent': (0x07a3, u'\u0389'), + 'Greek_GAMMA': (0x07c3, u'\u0393'), + 'Greek_IOTA': (0x07c9, u'\u0399'), + 'Greek_IOTAaccent': (0x07a4, u'\u038A'), + 'Greek_IOTAdieresis': (0x07a5, u'\u03AA'), + 'Greek_KAPPA': (0x07ca, u'\u039A'), + 'Greek_LAMBDA': (0x07cb, u'\u039B'), + 'Greek_LAMDA': (0x07cb, u'\u039B'), + 'Greek_MU': (0x07cc, u'\u039C'), + 'Greek_NU': (0x07cd, u'\u039D'), + 'Greek_OMEGA': (0x07d9, u'\u03A9'), + 'Greek_OMEGAaccent': (0x07ab, u'\u038F'), + 'Greek_OMICRON': (0x07cf, u'\u039F'), + 'Greek_OMICRONaccent': (0x07a7, u'\u038C'), + 'Greek_PHI': (0x07d6, u'\u03A6'), + 'Greek_PI': (0x07d0, u'\u03A0'), + 'Greek_PSI': (0x07d8, u'\u03A8'), + 'Greek_RHO': (0x07d1, u'\u03A1'), + 'Greek_SIGMA': (0x07d2, u'\u03A3'), + 'Greek_TAU': (0x07d4, u'\u03A4'), + 'Greek_THETA': (0x07c8, u'\u0398'), + 'Greek_UPSILON': (0x07d5, u'\u03A5'), + 'Greek_UPSILONaccent': (0x07a8, u'\u038E'), + 'Greek_UPSILONdieresis': (0x07a9, u'\u03AB'), + 'Greek_XI': (0x07ce, u'\u039E'), + 'Greek_ZETA': (0x07c6, u'\u0396'), + 'Greek_accentdieresis': (0x07ae, u'\u0385'), + 'Greek_alpha': (0x07e1, u'\u03B1'), + 'Greek_alphaaccent': (0x07b1, u'\u03AC'), + 'Greek_beta': (0x07e2, u'\u03B2'), + 'Greek_chi': (0x07f7, u'\u03C7'), + 'Greek_delta': (0x07e4, u'\u03B4'), + 'Greek_epsilon': (0x07e5, u'\u03B5'), + 'Greek_epsilonaccent': (0x07b2, u'\u03AD'), + 'Greek_eta': (0x07e7, u'\u03B7'), + 'Greek_etaaccent': (0x07b3, u'\u03AE'), + 'Greek_finalsmallsigma': (0x07f3, u'\u03C2'), + 'Greek_gamma': (0x07e3, u'\u03B3'), + 'Greek_horizbar': (0x07af, u'\u2015'), + 'Greek_iota': (0x07e9, u'\u03B9'), + 'Greek_iotaaccent': (0x07b4, u'\u03AF'), + 'Greek_iotaaccentdieresis': (0x07b6, u'\u0390'), + 'Greek_iotadieresis': (0x07b5, u'\u03CA'), + 'Greek_kappa': (0x07ea, u'\u03BA'), + 'Greek_lambda': (0x07eb, u'\u03BB'), + 'Greek_lamda': (0x07eb, u'\u03BB'), + 'Greek_mu': (0x07ec, u'\u03BC'), + 'Greek_nu': (0x07ed, u'\u03BD'), + 'Greek_omega': (0x07f9, u'\u03C9'), + 'Greek_omegaaccent': (0x07bb, u'\u03CE'), + 'Greek_omicron': (0x07ef, u'\u03BF'), + 'Greek_omicronaccent': (0x07b7, u'\u03CC'), + 'Greek_phi': (0x07f6, u'\u03C6'), + 'Greek_pi': (0x07f0, u'\u03C0'), + 'Greek_psi': (0x07f8, u'\u03C8'), + 'Greek_rho': (0x07f1, u'\u03C1'), + 'Greek_sigma': (0x07f2, u'\u03C3'), + 'Greek_tau': (0x07f4, u'\u03C4'), + 'Greek_theta': (0x07e8, u'\u03B8'), + 'Greek_upsilon': (0x07f5, u'\u03C5'), + 'Greek_upsilonaccent': (0x07b8, u'\u03CD'), + 'Greek_upsilonaccentdieresis': (0x07ba, u'\u03B0'), + 'Greek_upsilondieresis': (0x07b9, u'\u03CB'), + 'Greek_xi': (0x07ee, u'\u03BE'), + 'Greek_zeta': (0x07e6, u'\u03B6'), + 'H': (0x0048, u'\u0048'), + 'Hcircumflex': (0x02a6, u'\u0124'), + 'Hstroke': (0x02a1, u'\u0126'), + 'I': (0x0049, u'\u0049'), + 'Iabovedot': (0x02a9, u'\u0130'), + 'Iacute': (0x00cd, u'\u00CD'), + 'Ibelowdot': (0x1001eca, u'\u1ECA'), + 'Ibreve': (0x100012c, u'\u012C'), + 'Icircumflex': (0x00ce, u'\u00CE'), + 'Idiaeresis': (0x00cf, u'\u00CF'), + 'Igrave': (0x00cc, u'\u00CC'), + 'Ihook': (0x1001ec8, u'\u1EC8'), + 'Imacron': (0x03cf, u'\u012A'), + 'Iogonek': (0x03c7, u'\u012E'), + 'Itilde': (0x03a5, u'\u0128'), + 'J': (0x004a, u'\u004A'), + 'Jcircumflex': (0x02ac, u'\u0134'), + 'K': (0x004b, u'\u004B'), + 'KP_0': (0xffb0, None), + 'KP_1': (0xffb1, None), + 'KP_2': (0xffb2, None), + 'KP_3': (0xffb3, None), + 'KP_4': (0xffb4, None), + 'KP_5': (0xffb5, None), + 'KP_6': (0xffb6, None), + 'KP_7': (0xffb7, None), + 'KP_8': (0xffb8, None), + 'KP_9': (0xffb9, None), + 'KP_Add': (0xffab, None), + 'KP_Begin': (0xff9d, None), + 'KP_Decimal': (0xffae, None), + 'KP_Delete': (0xff9f, None), + 'KP_Divide': (0xffaf, None), + 'KP_Down': (0xff99, None), + 'KP_End': (0xff9c, None), + 'KP_Enter': (0xff8d, None), + 'KP_Equal': (0xffbd, None), + 'KP_F1': (0xff91, None), + 'KP_F2': (0xff92, None), + 'KP_F3': (0xff93, None), + 'KP_F4': (0xff94, None), + 'KP_Home': (0xff95, None), + 'KP_Insert': (0xff9e, None), + 'KP_Left': (0xff96, None), + 'KP_Multiply': (0xffaa, None), + 'KP_Next': (0xff9b, None), + 'KP_Page_Down': (0xff9b, None), + 'KP_Page_Up': (0xff9a, None), + 'KP_Prior': (0xff9a, None), + 'KP_Right': (0xff98, None), + 'KP_Separator': (0xffac, None), + 'KP_Space': (0xff80, None), + 'KP_Subtract': (0xffad, None), + 'KP_Tab': (0xff89, None), + 'KP_Up': (0xff97, None), + 'Kcedilla': (0x03d3, u'\u0136'), + 'L': (0x004c, u'\u004C'), + 'Lacute': (0x01c5, u'\u0139'), + 'Lbelowdot': (0x1001e36, u'\u1E36'), + 'Lcaron': (0x01a5, u'\u013D'), + 'Lcedilla': (0x03a6, u'\u013B'), + 'LiraSign': (0x10020a4, u'\u20A4'), + 'Lstroke': (0x01a3, u'\u0141'), + 'M': (0x004d, u'\u004D'), + 'Mabovedot': (0x1001e40, u'\u1E40'), + 'Macedonia_DSE': (0x06b5, u'\u0405'), + 'Macedonia_GJE': (0x06b2, u'\u0403'), + 'Macedonia_KJE': (0x06bc, u'\u040C'), + 'Macedonia_dse': (0x06a5, u'\u0455'), + 'Macedonia_gje': (0x06a2, u'\u0453'), + 'Macedonia_kje': (0x06ac, u'\u045C'), + 'MillSign': (0x10020a5, u'\u20A5'), + 'N': (0x004e, u'\u004E'), + 'Nacute': (0x01d1, u'\u0143'), + 'NairaSign': (0x10020a6, u'\u20A6'), + 'Ncaron': (0x01d2, u'\u0147'), + 'Ncedilla': (0x03d1, u'\u0145'), + 'NewSheqelSign': (0x10020aa, u'\u20AA'), + 'Ntilde': (0x00d1, u'\u00D1'), + 'O': (0x004f, u'\u004F'), + 'OE': (0x13bc, u'\u0152'), + 'Oacute': (0x00d3, u'\u00D3'), + 'Obarred': (0x100019f, u'\u019F'), + 'Obelowdot': (0x1001ecc, u'\u1ECC'), + 'Ocaron': (0x10001d1, u'\u01D2'), + 'Ocircumflex': (0x00d4, u'\u00D4'), + 'Ocircumflexacute': (0x1001ed0, u'\u1ED0'), + 'Ocircumflexbelowdot': (0x1001ed8, u'\u1ED8'), + 'Ocircumflexgrave': (0x1001ed2, u'\u1ED2'), + 'Ocircumflexhook': (0x1001ed4, u'\u1ED4'), + 'Ocircumflextilde': (0x1001ed6, u'\u1ED6'), + 'Odiaeresis': (0x00d6, u'\u00D6'), + 'Odoubleacute': (0x01d5, u'\u0150'), + 'Ograve': (0x00d2, u'\u00D2'), + 'Ohook': (0x1001ece, u'\u1ECE'), + 'Ohorn': (0x10001a0, u'\u01A0'), + 'Ohornacute': (0x1001eda, u'\u1EDA'), + 'Ohornbelowdot': (0x1001ee2, u'\u1EE2'), + 'Ohorngrave': (0x1001edc, u'\u1EDC'), + 'Ohornhook': (0x1001ede, u'\u1EDE'), + 'Ohorntilde': (0x1001ee0, u'\u1EE0'), + 'Omacron': (0x03d2, u'\u014C'), + 'Ooblique': (0x00d8, u'\u00D8'), + 'Oslash': (0x00d8, u'\u00D8'), + 'Otilde': (0x00d5, u'\u00D5'), + 'P': (0x0050, u'\u0050'), + 'Pabovedot': (0x1001e56, u'\u1E56'), + 'PesetaSign': (0x10020a7, u'\u20A7'), + 'Q': (0x0051, u'\u0051'), + 'R': (0x0052, u'\u0052'), + 'Racute': (0x01c0, u'\u0154'), + 'Rcaron': (0x01d8, u'\u0158'), + 'Rcedilla': (0x03a3, u'\u0156'), + 'RupeeSign': (0x10020a8, u'\u20A8'), + 'S': (0x0053, u'\u0053'), + 'SCHWA': (0x100018f, u'\u018F'), + 'Sabovedot': (0x1001e60, u'\u1E60'), + 'Sacute': (0x01a6, u'\u015A'), + 'Scaron': (0x01a9, u'\u0160'), + 'Scedilla': (0x01aa, u'\u015E'), + 'Scircumflex': (0x02de, u'\u015C'), + 'Serbian_DJE': (0x06b1, u'\u0402'), + 'Serbian_TSHE': (0x06bb, u'\u040B'), + 'Serbian_dje': (0x06a1, u'\u0452'), + 'Serbian_tshe': (0x06ab, u'\u045B'), + 'Sinh_a': (0x1000d85, u'\u0D85'), + 'Sinh_aa': (0x1000d86, u'\u0D86'), + 'Sinh_aa2': (0x1000dcf, u'\u0DCF'), + 'Sinh_ae': (0x1000d87, u'\u0D87'), + 'Sinh_ae2': (0x1000dd0, u'\u0DD0'), + 'Sinh_aee': (0x1000d88, u'\u0D88'), + 'Sinh_aee2': (0x1000dd1, u'\u0DD1'), + 'Sinh_ai': (0x1000d93, u'\u0D93'), + 'Sinh_ai2': (0x1000ddb, u'\u0DDB'), + 'Sinh_al': (0x1000dca, u'\u0DCA'), + 'Sinh_au': (0x1000d96, u'\u0D96'), + 'Sinh_au2': (0x1000dde, u'\u0DDE'), + 'Sinh_ba': (0x1000db6, u'\u0DB6'), + 'Sinh_bha': (0x1000db7, u'\u0DB7'), + 'Sinh_ca': (0x1000da0, u'\u0DA0'), + 'Sinh_cha': (0x1000da1, u'\u0DA1'), + 'Sinh_dda': (0x1000da9, u'\u0DA9'), + 'Sinh_ddha': (0x1000daa, u'\u0DAA'), + 'Sinh_dha': (0x1000daf, u'\u0DAF'), + 'Sinh_dhha': (0x1000db0, u'\u0DB0'), + 'Sinh_e': (0x1000d91, u'\u0D91'), + 'Sinh_e2': (0x1000dd9, u'\u0DD9'), + 'Sinh_ee': (0x1000d92, u'\u0D92'), + 'Sinh_ee2': (0x1000dda, u'\u0DDA'), + 'Sinh_fa': (0x1000dc6, u'\u0DC6'), + 'Sinh_ga': (0x1000d9c, u'\u0D9C'), + 'Sinh_gha': (0x1000d9d, u'\u0D9D'), + 'Sinh_h2': (0x1000d83, u'\u0D83'), + 'Sinh_ha': (0x1000dc4, u'\u0DC4'), + 'Sinh_i': (0x1000d89, u'\u0D89'), + 'Sinh_i2': (0x1000dd2, u'\u0DD2'), + 'Sinh_ii': (0x1000d8a, u'\u0D8A'), + 'Sinh_ii2': (0x1000dd3, u'\u0DD3'), + 'Sinh_ja': (0x1000da2, u'\u0DA2'), + 'Sinh_jha': (0x1000da3, u'\u0DA3'), + 'Sinh_jnya': (0x1000da5, u'\u0DA5'), + 'Sinh_ka': (0x1000d9a, u'\u0D9A'), + 'Sinh_kha': (0x1000d9b, u'\u0D9B'), + 'Sinh_kunddaliya': (0x1000df4, u'\u0DF4'), + 'Sinh_la': (0x1000dbd, u'\u0DBD'), + 'Sinh_lla': (0x1000dc5, u'\u0DC5'), + 'Sinh_lu': (0x1000d8f, u'\u0D8F'), + 'Sinh_lu2': (0x1000ddf, u'\u0DDF'), + 'Sinh_luu': (0x1000d90, u'\u0D90'), + 'Sinh_luu2': (0x1000df3, u'\u0DF3'), + 'Sinh_ma': (0x1000db8, u'\u0DB8'), + 'Sinh_mba': (0x1000db9, u'\u0DB9'), + 'Sinh_na': (0x1000db1, u'\u0DB1'), + 'Sinh_ndda': (0x1000dac, u'\u0DAC'), + 'Sinh_ndha': (0x1000db3, u'\u0DB3'), + 'Sinh_ng': (0x1000d82, u'\u0D82'), + 'Sinh_ng2': (0x1000d9e, u'\u0D9E'), + 'Sinh_nga': (0x1000d9f, u'\u0D9F'), + 'Sinh_nja': (0x1000da6, u'\u0DA6'), + 'Sinh_nna': (0x1000dab, u'\u0DAB'), + 'Sinh_nya': (0x1000da4, u'\u0DA4'), + 'Sinh_o': (0x1000d94, u'\u0D94'), + 'Sinh_o2': (0x1000ddc, u'\u0DDC'), + 'Sinh_oo': (0x1000d95, u'\u0D95'), + 'Sinh_oo2': (0x1000ddd, u'\u0DDD'), + 'Sinh_pa': (0x1000db4, u'\u0DB4'), + 'Sinh_pha': (0x1000db5, u'\u0DB5'), + 'Sinh_ra': (0x1000dbb, u'\u0DBB'), + 'Sinh_ri': (0x1000d8d, u'\u0D8D'), + 'Sinh_rii': (0x1000d8e, u'\u0D8E'), + 'Sinh_ru2': (0x1000dd8, u'\u0DD8'), + 'Sinh_ruu2': (0x1000df2, u'\u0DF2'), + 'Sinh_sa': (0x1000dc3, u'\u0DC3'), + 'Sinh_sha': (0x1000dc1, u'\u0DC1'), + 'Sinh_ssha': (0x1000dc2, u'\u0DC2'), + 'Sinh_tha': (0x1000dad, u'\u0DAD'), + 'Sinh_thha': (0x1000dae, u'\u0DAE'), + 'Sinh_tta': (0x1000da7, u'\u0DA7'), + 'Sinh_ttha': (0x1000da8, u'\u0DA8'), + 'Sinh_u': (0x1000d8b, u'\u0D8B'), + 'Sinh_u2': (0x1000dd4, u'\u0DD4'), + 'Sinh_uu': (0x1000d8c, u'\u0D8C'), + 'Sinh_uu2': (0x1000dd6, u'\u0DD6'), + 'Sinh_va': (0x1000dc0, u'\u0DC0'), + 'Sinh_ya': (0x1000dba, u'\u0DBA'), + 'T': (0x0054, u'\u0054'), + 'THORN': (0x00de, u'\u00DE'), + 'Tabovedot': (0x1001e6a, u'\u1E6A'), + 'Tcaron': (0x01ab, u'\u0164'), + 'Tcedilla': (0x01de, u'\u0162'), + 'Thai_baht': (0x0ddf, u'\u0E3F'), + 'Thai_bobaimai': (0x0dba, u'\u0E1A'), + 'Thai_chochan': (0x0da8, u'\u0E08'), + 'Thai_chochang': (0x0daa, u'\u0E0A'), + 'Thai_choching': (0x0da9, u'\u0E09'), + 'Thai_chochoe': (0x0dac, u'\u0E0C'), + 'Thai_dochada': (0x0dae, u'\u0E0E'), + 'Thai_dodek': (0x0db4, u'\u0E14'), + 'Thai_fofa': (0x0dbd, u'\u0E1D'), + 'Thai_fofan': (0x0dbf, u'\u0E1F'), + 'Thai_hohip': (0x0dcb, u'\u0E2B'), + 'Thai_honokhuk': (0x0dce, u'\u0E2E'), + 'Thai_khokhai': (0x0da2, u'\u0E02'), + 'Thai_khokhon': (0x0da5, u'\u0E05'), + 'Thai_khokhuat': (0x0da3, u'\u0E03'), + 'Thai_khokhwai': (0x0da4, u'\u0E04'), + 'Thai_khorakhang': (0x0da6, u'\u0E06'), + 'Thai_kokai': (0x0da1, u'\u0E01'), + 'Thai_lakkhangyao': (0x0de5, u'\u0E45'), + 'Thai_lekchet': (0x0df7, u'\u0E57'), + 'Thai_lekha': (0x0df5, u'\u0E55'), + 'Thai_lekhok': (0x0df6, u'\u0E56'), + 'Thai_lekkao': (0x0df9, u'\u0E59'), + 'Thai_leknung': (0x0df1, u'\u0E51'), + 'Thai_lekpaet': (0x0df8, u'\u0E58'), + 'Thai_leksam': (0x0df3, u'\u0E53'), + 'Thai_leksi': (0x0df4, u'\u0E54'), + 'Thai_leksong': (0x0df2, u'\u0E52'), + 'Thai_leksun': (0x0df0, u'\u0E50'), + 'Thai_lochula': (0x0dcc, u'\u0E2C'), + 'Thai_loling': (0x0dc5, u'\u0E25'), + 'Thai_lu': (0x0dc6, u'\u0E26'), + 'Thai_maichattawa': (0x0deb, u'\u0E4B'), + 'Thai_maiek': (0x0de8, u'\u0E48'), + 'Thai_maihanakat': (0x0dd1, u'\u0E31'), + 'Thai_maitaikhu': (0x0de7, u'\u0E47'), + 'Thai_maitho': (0x0de9, u'\u0E49'), + 'Thai_maitri': (0x0dea, u'\u0E4A'), + 'Thai_maiyamok': (0x0de6, u'\u0E46'), + 'Thai_moma': (0x0dc1, u'\u0E21'), + 'Thai_ngongu': (0x0da7, u'\u0E07'), + 'Thai_nikhahit': (0x0ded, u'\u0E4D'), + 'Thai_nonen': (0x0db3, u'\u0E13'), + 'Thai_nonu': (0x0db9, u'\u0E19'), + 'Thai_oang': (0x0dcd, u'\u0E2D'), + 'Thai_paiyannoi': (0x0dcf, u'\u0E2F'), + 'Thai_phinthu': (0x0dda, u'\u0E3A'), + 'Thai_phophan': (0x0dbe, u'\u0E1E'), + 'Thai_phophung': (0x0dbc, u'\u0E1C'), + 'Thai_phosamphao': (0x0dc0, u'\u0E20'), + 'Thai_popla': (0x0dbb, u'\u0E1B'), + 'Thai_rorua': (0x0dc3, u'\u0E23'), + 'Thai_ru': (0x0dc4, u'\u0E24'), + 'Thai_saraa': (0x0dd0, u'\u0E30'), + 'Thai_saraaa': (0x0dd2, u'\u0E32'), + 'Thai_saraae': (0x0de1, u'\u0E41'), + 'Thai_saraaimaimalai': (0x0de4, u'\u0E44'), + 'Thai_saraaimaimuan': (0x0de3, u'\u0E43'), + 'Thai_saraam': (0x0dd3, u'\u0E33'), + 'Thai_sarae': (0x0de0, u'\u0E40'), + 'Thai_sarai': (0x0dd4, u'\u0E34'), + 'Thai_saraii': (0x0dd5, u'\u0E35'), + 'Thai_sarao': (0x0de2, u'\u0E42'), + 'Thai_sarau': (0x0dd8, u'\u0E38'), + 'Thai_saraue': (0x0dd6, u'\u0E36'), + 'Thai_sarauee': (0x0dd7, u'\u0E37'), + 'Thai_sarauu': (0x0dd9, u'\u0E39'), + 'Thai_sorusi': (0x0dc9, u'\u0E29'), + 'Thai_sosala': (0x0dc8, u'\u0E28'), + 'Thai_soso': (0x0dab, u'\u0E0B'), + 'Thai_sosua': (0x0dca, u'\u0E2A'), + 'Thai_thanthakhat': (0x0dec, u'\u0E4C'), + 'Thai_thonangmontho': (0x0db1, u'\u0E11'), + 'Thai_thophuthao': (0x0db2, u'\u0E12'), + 'Thai_thothahan': (0x0db7, u'\u0E17'), + 'Thai_thothan': (0x0db0, u'\u0E10'), + 'Thai_thothong': (0x0db8, u'\u0E18'), + 'Thai_thothung': (0x0db6, u'\u0E16'), + 'Thai_topatak': (0x0daf, u'\u0E0F'), + 'Thai_totao': (0x0db5, u'\u0E15'), + 'Thai_wowaen': (0x0dc7, u'\u0E27'), + 'Thai_yoyak': (0x0dc2, u'\u0E22'), + 'Thai_yoying': (0x0dad, u'\u0E0D'), + 'Tslash': (0x03ac, u'\u0166'), + 'U': (0x0055, u'\u0055'), + 'Uacute': (0x00da, u'\u00DA'), + 'Ubelowdot': (0x1001ee4, u'\u1EE4'), + 'Ubreve': (0x02dd, u'\u016C'), + 'Ucircumflex': (0x00db, u'\u00DB'), + 'Udiaeresis': (0x00dc, u'\u00DC'), + 'Udoubleacute': (0x01db, u'\u0170'), + 'Ugrave': (0x00d9, u'\u00D9'), + 'Uhook': (0x1001ee6, u'\u1EE6'), + 'Uhorn': (0x10001af, u'\u01AF'), + 'Uhornacute': (0x1001ee8, u'\u1EE8'), + 'Uhornbelowdot': (0x1001ef0, u'\u1EF0'), + 'Uhorngrave': (0x1001eea, u'\u1EEA'), + 'Uhornhook': (0x1001eec, u'\u1EEC'), + 'Uhorntilde': (0x1001eee, u'\u1EEE'), + 'Ukrainian_GHE_WITH_UPTURN': (0x06bd, u'\u0490'), + 'Ukrainian_I': (0x06b6, u'\u0406'), + 'Ukrainian_IE': (0x06b4, u'\u0404'), + 'Ukrainian_YI': (0x06b7, u'\u0407'), + 'Ukrainian_ghe_with_upturn': (0x06ad, u'\u0491'), + 'Ukrainian_i': (0x06a6, u'\u0456'), + 'Ukrainian_ie': (0x06a4, u'\u0454'), + 'Ukrainian_yi': (0x06a7, u'\u0457'), + 'Umacron': (0x03de, u'\u016A'), + 'Uogonek': (0x03d9, u'\u0172'), + 'Uring': (0x01d9, u'\u016E'), + 'Utilde': (0x03dd, u'\u0168'), + 'V': (0x0056, u'\u0056'), + 'W': (0x0057, u'\u0057'), + 'Wacute': (0x1001e82, u'\u1E82'), + 'Wcircumflex': (0x1000174, u'\u0174'), + 'Wdiaeresis': (0x1001e84, u'\u1E84'), + 'Wgrave': (0x1001e80, u'\u1E80'), + 'WonSign': (0x10020a9, u'\u20A9'), + 'X': (0x0058, u'\u0058'), + 'Xabovedot': (0x1001e8a, u'\u1E8A'), + 'Y': (0x0059, u'\u0059'), + 'Yacute': (0x00dd, u'\u00DD'), + 'Ybelowdot': (0x1001ef4, u'\u1EF4'), + 'Ycircumflex': (0x1000176, u'\u0176'), + 'Ydiaeresis': (0x13be, u'\u0178'), + 'Ygrave': (0x1001ef2, u'\u1EF2'), + 'Yhook': (0x1001ef6, u'\u1EF6'), + 'Ytilde': (0x1001ef8, u'\u1EF8'), + 'Z': (0x005a, u'\u005A'), + 'Zabovedot': (0x01af, u'\u017B'), + 'Zacute': (0x01ac, u'\u0179'), + 'Zcaron': (0x01ae, u'\u017D'), + 'Zstroke': (0x10001b5, u'\u01B5'), + 'a': (0x0061, u'\u0061'), + 'aacute': (0x00e1, u'\u00E1'), + 'abelowdot': (0x1001ea1, u'\u1EA1'), + 'abovedot': (0x01ff, u'\u02D9'), + 'abreve': (0x01e3, u'\u0103'), + 'abreveacute': (0x1001eaf, u'\u1EAF'), + 'abrevebelowdot': (0x1001eb7, u'\u1EB7'), + 'abrevegrave': (0x1001eb1, u'\u1EB1'), + 'abrevehook': (0x1001eb3, u'\u1EB3'), + 'abrevetilde': (0x1001eb5, u'\u1EB5'), + 'acircumflex': (0x00e2, u'\u00E2'), + 'acircumflexacute': (0x1001ea5, u'\u1EA5'), + 'acircumflexbelowdot': (0x1001ead, u'\u1EAD'), + 'acircumflexgrave': (0x1001ea7, u'\u1EA7'), + 'acircumflexhook': (0x1001ea9, u'\u1EA9'), + 'acircumflextilde': (0x1001eab, u'\u1EAB'), + 'acute': (0x00b4, u'\u00B4'), + 'adiaeresis': (0x00e4, u'\u00E4'), + 'ae': (0x00e6, u'\u00E6'), + 'agrave': (0x00e0, u'\u00E0'), + 'ahook': (0x1001ea3, u'\u1EA3'), + 'amacron': (0x03e0, u'\u0101'), + 'ampersand': (0x0026, u'\u0026'), + 'aogonek': (0x01b1, u'\u0105'), + 'apostrophe': (0x0027, u'\u0027'), + 'approxeq': (0x1002248, u'\u2245'), + 'approximate': (0x08c8, u'\u223C'), + 'aring': (0x00e5, u'\u00E5'), + 'asciicircum': (0x005e, u'\u005E'), + 'asciitilde': (0x007e, u'\u007E'), + 'asterisk': (0x002a, u'\u002A'), + 'at': (0x0040, u'\u0040'), + 'atilde': (0x00e3, u'\u00E3'), + 'b': (0x0062, u'\u0062'), + 'babovedot': (0x1001e03, u'\u1E03'), + 'backslash': (0x005c, u'\u005C'), + 'ballotcross': (0x0af4, u'\u2717'), + 'bar': (0x007c, u'\u007C'), + 'because': (0x1002235, u'\u2235'), + 'botintegral': (0x08a5, u'\u2321'), + 'botleftparens': (0x08ac, u'\u239D'), + 'botleftsqbracket': (0x08a8, u'\u23A3'), + 'botrightparens': (0x08ae, u'\u23A0'), + 'botrightsqbracket': (0x08aa, u'\u23A6'), + 'bott': (0x09f6, u'\u2534'), + 'braceleft': (0x007b, u'\u007B'), + 'braceright': (0x007d, u'\u007D'), + 'bracketleft': (0x005b, u'\u005B'), + 'bracketright': (0x005d, u'\u005D'), + 'braille_blank': (0x1002800, u'\u2800'), + 'braille_dots_1': (0x1002801, u'\u2801'), + 'braille_dots_12': (0x1002803, u'\u2803'), + 'braille_dots_123': (0x1002807, u'\u2807'), + 'braille_dots_1234': (0x100280f, u'\u280f'), + 'braille_dots_12345': (0x100281f, u'\u281f'), + 'braille_dots_123456': (0x100283f, u'\u283f'), + 'braille_dots_1234567': (0x100287f, u'\u287f'), + 'braille_dots_12345678': (0x10028ff, u'\u28ff'), + 'braille_dots_1234568': (0x10028bf, u'\u28bf'), + 'braille_dots_123457': (0x100285f, u'\u285f'), + 'braille_dots_1234578': (0x10028df, u'\u28df'), + 'braille_dots_123458': (0x100289f, u'\u289f'), + 'braille_dots_12346': (0x100282f, u'\u282f'), + 'braille_dots_123467': (0x100286f, u'\u286f'), + 'braille_dots_1234678': (0x10028ef, u'\u28ef'), + 'braille_dots_123468': (0x10028af, u'\u28af'), + 'braille_dots_12347': (0x100284f, u'\u284f'), + 'braille_dots_123478': (0x10028cf, u'\u28cf'), + 'braille_dots_12348': (0x100288f, u'\u288f'), + 'braille_dots_1235': (0x1002817, u'\u2817'), + 'braille_dots_12356': (0x1002837, u'\u2837'), + 'braille_dots_123567': (0x1002877, u'\u2877'), + 'braille_dots_1235678': (0x10028f7, u'\u28f7'), + 'braille_dots_123568': (0x10028b7, u'\u28b7'), + 'braille_dots_12357': (0x1002857, u'\u2857'), + 'braille_dots_123578': (0x10028d7, u'\u28d7'), + 'braille_dots_12358': (0x1002897, u'\u2897'), + 'braille_dots_1236': (0x1002827, u'\u2827'), + 'braille_dots_12367': (0x1002867, u'\u2867'), + 'braille_dots_123678': (0x10028e7, u'\u28e7'), + 'braille_dots_12368': (0x10028a7, u'\u28a7'), + 'braille_dots_1237': (0x1002847, u'\u2847'), + 'braille_dots_12378': (0x10028c7, u'\u28c7'), + 'braille_dots_1238': (0x1002887, u'\u2887'), + 'braille_dots_124': (0x100280b, u'\u280b'), + 'braille_dots_1245': (0x100281b, u'\u281b'), + 'braille_dots_12456': (0x100283b, u'\u283b'), + 'braille_dots_124567': (0x100287b, u'\u287b'), + 'braille_dots_1245678': (0x10028fb, u'\u28fb'), + 'braille_dots_124568': (0x10028bb, u'\u28bb'), + 'braille_dots_12457': (0x100285b, u'\u285b'), + 'braille_dots_124578': (0x10028db, u'\u28db'), + 'braille_dots_12458': (0x100289b, u'\u289b'), + 'braille_dots_1246': (0x100282b, u'\u282b'), + 'braille_dots_12467': (0x100286b, u'\u286b'), + 'braille_dots_124678': (0x10028eb, u'\u28eb'), + 'braille_dots_12468': (0x10028ab, u'\u28ab'), + 'braille_dots_1247': (0x100284b, u'\u284b'), + 'braille_dots_12478': (0x10028cb, u'\u28cb'), + 'braille_dots_1248': (0x100288b, u'\u288b'), + 'braille_dots_125': (0x1002813, u'\u2813'), + 'braille_dots_1256': (0x1002833, u'\u2833'), + 'braille_dots_12567': (0x1002873, u'\u2873'), + 'braille_dots_125678': (0x10028f3, u'\u28f3'), + 'braille_dots_12568': (0x10028b3, u'\u28b3'), + 'braille_dots_1257': (0x1002853, u'\u2853'), + 'braille_dots_12578': (0x10028d3, u'\u28d3'), + 'braille_dots_1258': (0x1002893, u'\u2893'), + 'braille_dots_126': (0x1002823, u'\u2823'), + 'braille_dots_1267': (0x1002863, u'\u2863'), + 'braille_dots_12678': (0x10028e3, u'\u28e3'), + 'braille_dots_1268': (0x10028a3, u'\u28a3'), + 'braille_dots_127': (0x1002843, u'\u2843'), + 'braille_dots_1278': (0x10028c3, u'\u28c3'), + 'braille_dots_128': (0x1002883, u'\u2883'), + 'braille_dots_13': (0x1002805, u'\u2805'), + 'braille_dots_134': (0x100280d, u'\u280d'), + 'braille_dots_1345': (0x100281d, u'\u281d'), + 'braille_dots_13456': (0x100283d, u'\u283d'), + 'braille_dots_134567': (0x100287d, u'\u287d'), + 'braille_dots_1345678': (0x10028fd, u'\u28fd'), + 'braille_dots_134568': (0x10028bd, u'\u28bd'), + 'braille_dots_13457': (0x100285d, u'\u285d'), + 'braille_dots_134578': (0x10028dd, u'\u28dd'), + 'braille_dots_13458': (0x100289d, u'\u289d'), + 'braille_dots_1346': (0x100282d, u'\u282d'), + 'braille_dots_13467': (0x100286d, u'\u286d'), + 'braille_dots_134678': (0x10028ed, u'\u28ed'), + 'braille_dots_13468': (0x10028ad, u'\u28ad'), + 'braille_dots_1347': (0x100284d, u'\u284d'), + 'braille_dots_13478': (0x10028cd, u'\u28cd'), + 'braille_dots_1348': (0x100288d, u'\u288d'), + 'braille_dots_135': (0x1002815, u'\u2815'), + 'braille_dots_1356': (0x1002835, u'\u2835'), + 'braille_dots_13567': (0x1002875, u'\u2875'), + 'braille_dots_135678': (0x10028f5, u'\u28f5'), + 'braille_dots_13568': (0x10028b5, u'\u28b5'), + 'braille_dots_1357': (0x1002855, u'\u2855'), + 'braille_dots_13578': (0x10028d5, u'\u28d5'), + 'braille_dots_1358': (0x1002895, u'\u2895'), + 'braille_dots_136': (0x1002825, u'\u2825'), + 'braille_dots_1367': (0x1002865, u'\u2865'), + 'braille_dots_13678': (0x10028e5, u'\u28e5'), + 'braille_dots_1368': (0x10028a5, u'\u28a5'), + 'braille_dots_137': (0x1002845, u'\u2845'), + 'braille_dots_1378': (0x10028c5, u'\u28c5'), + 'braille_dots_138': (0x1002885, u'\u2885'), + 'braille_dots_14': (0x1002809, u'\u2809'), + 'braille_dots_145': (0x1002819, u'\u2819'), + 'braille_dots_1456': (0x1002839, u'\u2839'), + 'braille_dots_14567': (0x1002879, u'\u2879'), + 'braille_dots_145678': (0x10028f9, u'\u28f9'), + 'braille_dots_14568': (0x10028b9, u'\u28b9'), + 'braille_dots_1457': (0x1002859, u'\u2859'), + 'braille_dots_14578': (0x10028d9, u'\u28d9'), + 'braille_dots_1458': (0x1002899, u'\u2899'), + 'braille_dots_146': (0x1002829, u'\u2829'), + 'braille_dots_1467': (0x1002869, u'\u2869'), + 'braille_dots_14678': (0x10028e9, u'\u28e9'), + 'braille_dots_1468': (0x10028a9, u'\u28a9'), + 'braille_dots_147': (0x1002849, u'\u2849'), + 'braille_dots_1478': (0x10028c9, u'\u28c9'), + 'braille_dots_148': (0x1002889, u'\u2889'), + 'braille_dots_15': (0x1002811, u'\u2811'), + 'braille_dots_156': (0x1002831, u'\u2831'), + 'braille_dots_1567': (0x1002871, u'\u2871'), + 'braille_dots_15678': (0x10028f1, u'\u28f1'), + 'braille_dots_1568': (0x10028b1, u'\u28b1'), + 'braille_dots_157': (0x1002851, u'\u2851'), + 'braille_dots_1578': (0x10028d1, u'\u28d1'), + 'braille_dots_158': (0x1002891, u'\u2891'), + 'braille_dots_16': (0x1002821, u'\u2821'), + 'braille_dots_167': (0x1002861, u'\u2861'), + 'braille_dots_1678': (0x10028e1, u'\u28e1'), + 'braille_dots_168': (0x10028a1, u'\u28a1'), + 'braille_dots_17': (0x1002841, u'\u2841'), + 'braille_dots_178': (0x10028c1, u'\u28c1'), + 'braille_dots_18': (0x1002881, u'\u2881'), + 'braille_dots_2': (0x1002802, u'\u2802'), + 'braille_dots_23': (0x1002806, u'\u2806'), + 'braille_dots_234': (0x100280e, u'\u280e'), + 'braille_dots_2345': (0x100281e, u'\u281e'), + 'braille_dots_23456': (0x100283e, u'\u283e'), + 'braille_dots_234567': (0x100287e, u'\u287e'), + 'braille_dots_2345678': (0x10028fe, u'\u28fe'), + 'braille_dots_234568': (0x10028be, u'\u28be'), + 'braille_dots_23457': (0x100285e, u'\u285e'), + 'braille_dots_234578': (0x10028de, u'\u28de'), + 'braille_dots_23458': (0x100289e, u'\u289e'), + 'braille_dots_2346': (0x100282e, u'\u282e'), + 'braille_dots_23467': (0x100286e, u'\u286e'), + 'braille_dots_234678': (0x10028ee, u'\u28ee'), + 'braille_dots_23468': (0x10028ae, u'\u28ae'), + 'braille_dots_2347': (0x100284e, u'\u284e'), + 'braille_dots_23478': (0x10028ce, u'\u28ce'), + 'braille_dots_2348': (0x100288e, u'\u288e'), + 'braille_dots_235': (0x1002816, u'\u2816'), + 'braille_dots_2356': (0x1002836, u'\u2836'), + 'braille_dots_23567': (0x1002876, u'\u2876'), + 'braille_dots_235678': (0x10028f6, u'\u28f6'), + 'braille_dots_23568': (0x10028b6, u'\u28b6'), + 'braille_dots_2357': (0x1002856, u'\u2856'), + 'braille_dots_23578': (0x10028d6, u'\u28d6'), + 'braille_dots_2358': (0x1002896, u'\u2896'), + 'braille_dots_236': (0x1002826, u'\u2826'), + 'braille_dots_2367': (0x1002866, u'\u2866'), + 'braille_dots_23678': (0x10028e6, u'\u28e6'), + 'braille_dots_2368': (0x10028a6, u'\u28a6'), + 'braille_dots_237': (0x1002846, u'\u2846'), + 'braille_dots_2378': (0x10028c6, u'\u28c6'), + 'braille_dots_238': (0x1002886, u'\u2886'), + 'braille_dots_24': (0x100280a, u'\u280a'), + 'braille_dots_245': (0x100281a, u'\u281a'), + 'braille_dots_2456': (0x100283a, u'\u283a'), + 'braille_dots_24567': (0x100287a, u'\u287a'), + 'braille_dots_245678': (0x10028fa, u'\u28fa'), + 'braille_dots_24568': (0x10028ba, u'\u28ba'), + 'braille_dots_2457': (0x100285a, u'\u285a'), + 'braille_dots_24578': (0x10028da, u'\u28da'), + 'braille_dots_2458': (0x100289a, u'\u289a'), + 'braille_dots_246': (0x100282a, u'\u282a'), + 'braille_dots_2467': (0x100286a, u'\u286a'), + 'braille_dots_24678': (0x10028ea, u'\u28ea'), + 'braille_dots_2468': (0x10028aa, u'\u28aa'), + 'braille_dots_247': (0x100284a, u'\u284a'), + 'braille_dots_2478': (0x10028ca, u'\u28ca'), + 'braille_dots_248': (0x100288a, u'\u288a'), + 'braille_dots_25': (0x1002812, u'\u2812'), + 'braille_dots_256': (0x1002832, u'\u2832'), + 'braille_dots_2567': (0x1002872, u'\u2872'), + 'braille_dots_25678': (0x10028f2, u'\u28f2'), + 'braille_dots_2568': (0x10028b2, u'\u28b2'), + 'braille_dots_257': (0x1002852, u'\u2852'), + 'braille_dots_2578': (0x10028d2, u'\u28d2'), + 'braille_dots_258': (0x1002892, u'\u2892'), + 'braille_dots_26': (0x1002822, u'\u2822'), + 'braille_dots_267': (0x1002862, u'\u2862'), + 'braille_dots_2678': (0x10028e2, u'\u28e2'), + 'braille_dots_268': (0x10028a2, u'\u28a2'), + 'braille_dots_27': (0x1002842, u'\u2842'), + 'braille_dots_278': (0x10028c2, u'\u28c2'), + 'braille_dots_28': (0x1002882, u'\u2882'), + 'braille_dots_3': (0x1002804, u'\u2804'), + 'braille_dots_34': (0x100280c, u'\u280c'), + 'braille_dots_345': (0x100281c, u'\u281c'), + 'braille_dots_3456': (0x100283c, u'\u283c'), + 'braille_dots_34567': (0x100287c, u'\u287c'), + 'braille_dots_345678': (0x10028fc, u'\u28fc'), + 'braille_dots_34568': (0x10028bc, u'\u28bc'), + 'braille_dots_3457': (0x100285c, u'\u285c'), + 'braille_dots_34578': (0x10028dc, u'\u28dc'), + 'braille_dots_3458': (0x100289c, u'\u289c'), + 'braille_dots_346': (0x100282c, u'\u282c'), + 'braille_dots_3467': (0x100286c, u'\u286c'), + 'braille_dots_34678': (0x10028ec, u'\u28ec'), + 'braille_dots_3468': (0x10028ac, u'\u28ac'), + 'braille_dots_347': (0x100284c, u'\u284c'), + 'braille_dots_3478': (0x10028cc, u'\u28cc'), + 'braille_dots_348': (0x100288c, u'\u288c'), + 'braille_dots_35': (0x1002814, u'\u2814'), + 'braille_dots_356': (0x1002834, u'\u2834'), + 'braille_dots_3567': (0x1002874, u'\u2874'), + 'braille_dots_35678': (0x10028f4, u'\u28f4'), + 'braille_dots_3568': (0x10028b4, u'\u28b4'), + 'braille_dots_357': (0x1002854, u'\u2854'), + 'braille_dots_3578': (0x10028d4, u'\u28d4'), + 'braille_dots_358': (0x1002894, u'\u2894'), + 'braille_dots_36': (0x1002824, u'\u2824'), + 'braille_dots_367': (0x1002864, u'\u2864'), + 'braille_dots_3678': (0x10028e4, u'\u28e4'), + 'braille_dots_368': (0x10028a4, u'\u28a4'), + 'braille_dots_37': (0x1002844, u'\u2844'), + 'braille_dots_378': (0x10028c4, u'\u28c4'), + 'braille_dots_38': (0x1002884, u'\u2884'), + 'braille_dots_4': (0x1002808, u'\u2808'), + 'braille_dots_45': (0x1002818, u'\u2818'), + 'braille_dots_456': (0x1002838, u'\u2838'), + 'braille_dots_4567': (0x1002878, u'\u2878'), + 'braille_dots_45678': (0x10028f8, u'\u28f8'), + 'braille_dots_4568': (0x10028b8, u'\u28b8'), + 'braille_dots_457': (0x1002858, u'\u2858'), + 'braille_dots_4578': (0x10028d8, u'\u28d8'), + 'braille_dots_458': (0x1002898, u'\u2898'), + 'braille_dots_46': (0x1002828, u'\u2828'), + 'braille_dots_467': (0x1002868, u'\u2868'), + 'braille_dots_4678': (0x10028e8, u'\u28e8'), + 'braille_dots_468': (0x10028a8, u'\u28a8'), + 'braille_dots_47': (0x1002848, u'\u2848'), + 'braille_dots_478': (0x10028c8, u'\u28c8'), + 'braille_dots_48': (0x1002888, u'\u2888'), + 'braille_dots_5': (0x1002810, u'\u2810'), + 'braille_dots_56': (0x1002830, u'\u2830'), + 'braille_dots_567': (0x1002870, u'\u2870'), + 'braille_dots_5678': (0x10028f0, u'\u28f0'), + 'braille_dots_568': (0x10028b0, u'\u28b0'), + 'braille_dots_57': (0x1002850, u'\u2850'), + 'braille_dots_578': (0x10028d0, u'\u28d0'), + 'braille_dots_58': (0x1002890, u'\u2890'), + 'braille_dots_6': (0x1002820, u'\u2820'), + 'braille_dots_67': (0x1002860, u'\u2860'), + 'braille_dots_678': (0x10028e0, u'\u28e0'), + 'braille_dots_68': (0x10028a0, u'\u28a0'), + 'braille_dots_7': (0x1002840, u'\u2840'), + 'braille_dots_78': (0x10028c0, u'\u28c0'), + 'braille_dots_8': (0x1002880, u'\u2880'), + 'breve': (0x01a2, u'\u02D8'), + 'brokenbar': (0x00a6, u'\u00A6'), + 'c': (0x0063, u'\u0063'), + 'cabovedot': (0x02e5, u'\u010B'), + 'cacute': (0x01e6, u'\u0107'), + 'careof': (0x0ab8, u'\u2105'), + 'caret': (0x0afc, u'\u2038'), + 'caron': (0x01b7, u'\u02C7'), + 'ccaron': (0x01e8, u'\u010D'), + 'ccedilla': (0x00e7, u'\u00E7'), + 'ccircumflex': (0x02e6, u'\u0109'), + 'cedilla': (0x00b8, u'\u00B8'), + 'cent': (0x00a2, u'\u00A2'), + 'checkerboard': (0x09e1, u'\u2592'), + 'checkmark': (0x0af3, u'\u2713'), + 'circle': (0x0bcf, u'\u25CB'), + 'club': (0x0aec, u'\u2663'), + 'colon': (0x003a, u'\u003A'), + 'comma': (0x002c, u'\u002C'), + 'containsas': (0x100220B, u'\u220B'), + 'copyright': (0x00a9, u'\u00A9'), + 'cr': (0x09e4, u'\u240D'), + 'crossinglines': (0x09ee, u'\u253C'), + 'cuberoot': (0x100221B, u'\u221B'), + 'currency': (0x00a4, u'\u00A4'), + 'd': (0x0064, u'\u0064'), + 'dabovedot': (0x1001e0b, u'\u1E0B'), + 'dagger': (0x0af1, u'\u2020'), + 'dcaron': (0x01ef, u'\u010F'), + 'dead_A': (0xfe81, None), + 'dead_E': (0xfe83, None), + 'dead_I': (0xfe85, None), + 'dead_O': (0xfe87, None), + 'dead_U': (0xfe89, None), + 'dead_a': (0xfe80, None), + 'dead_abovecomma': (0xfe64, u'\u0315'), + 'dead_abovedot': (0xfe56, u'\u0307'), + 'dead_abovereversedcomma': (0xfe65, u'\u0312'), + 'dead_abovering': (0xfe58, u'\u030A'), + 'dead_aboveverticalline': (0xfe91, u'\u030D'), + 'dead_acute': (0xfe51, u'\u0301'), + 'dead_belowbreve': (0xfe6b, u'\u032E'), + 'dead_belowcircumflex': (0xfe69, u'\u032D'), + 'dead_belowcomma': (0xfe6e, u'\u0326'), + 'dead_belowdiaeresis': (0xfe6c, u'\u0324'), + 'dead_belowdot': (0xfe60, u'\u0323'), + 'dead_belowmacron': (0xfe68, u'\u0331'), + 'dead_belowring': (0xfe67, u'\u0325'), + 'dead_belowtilde': (0xfe6a, u'\u0330'), + 'dead_belowverticalline': (0xfe92, u'\u0329'), + 'dead_breve': (0xfe55, u'\u0306'), + 'dead_capital_schwa': (0xfe8b, None), + 'dead_caron': (0xfe5a, u'\u030C'), + 'dead_cedilla': (0xfe5b, u'\u0327'), + 'dead_circumflex': (0xfe52, u'\u0302'), + 'dead_currency': (0xfe6f, None), + 'dead_diaeresis': (0xfe57, u'\u0308'), + 'dead_doubleacute': (0xfe59, u'\u030B'), + 'dead_doublegrave': (0xfe66, u'\u030F'), + 'dead_e': (0xfe82, None), + 'dead_grave': (0xfe50, u'\u0300'), + 'dead_greek': (0xfe8c, None), + 'dead_hook': (0xfe61, u'\u0309'), + 'dead_horn': (0xfe62, u'\u031B'), + 'dead_i': (0xfe84, None), + 'dead_invertedbreve': (0xfe6d, u'\u032F'), + 'dead_iota': (0xfe5d, u'\u0345'), + 'dead_longsolidusoverlay': (0xfe93, u'\u0338'), + 'dead_lowline': (0xfe90, u'\u0332'), + 'dead_macron': (0xfe54, u'\u0304'), + 'dead_o': (0xfe86, None), + 'dead_ogonek': (0xfe5c, u'\u0328'), + 'dead_semivoiced_sound': (0xfe5f, None), + 'dead_small_schwa': (0xfe8a, None), + 'dead_stroke': (0xfe63, u'\u0335'), + 'dead_tilde': (0xfe53, u'\u0303'), + 'dead_u': (0xfe88, None), + 'dead_voiced_sound': (0xfe5e, None), + 'degree': (0x00b0, u'\u00B0'), + 'diaeresis': (0x00a8, u'\u00A8'), + 'diamond': (0x0aed, u'\u2666'), + 'digitspace': (0x0aa5, u'\u2007'), + 'dintegral': (0x100222C, u'\u222C'), + 'division': (0x00f7, u'\u00F7'), + 'dollar': (0x0024, u'\u0024'), + 'doubbaselinedot': (0x0aaf, u'\u2025'), + 'doubleacute': (0x01bd, u'\u02DD'), + 'doubledagger': (0x0af2, u'\u2021'), + 'doublelowquotemark': (0x0afe, u'\u201E'), + 'downarrow': (0x08fe, u'\u2193'), + 'downstile': (0x0bc4, u'\u230A'), + 'downtack': (0x0bc2, u'\u22A4'), + 'dstroke': (0x01f0, u'\u0111'), + 'e': (0x0065, u'\u0065'), + 'eabovedot': (0x03ec, u'\u0117'), + 'eacute': (0x00e9, u'\u00E9'), + 'ebelowdot': (0x1001eb9, u'\u1EB9'), + 'ecaron': (0x01ec, u'\u011B'), + 'ecircumflex': (0x00ea, u'\u00EA'), + 'ecircumflexacute': (0x1001ebf, u'\u1EBF'), + 'ecircumflexbelowdot': (0x1001ec7, u'\u1EC7'), + 'ecircumflexgrave': (0x1001ec1, u'\u1EC1'), + 'ecircumflexhook': (0x1001ec3, u'\u1EC3'), + 'ecircumflextilde': (0x1001ec5, u'\u1EC5'), + 'ediaeresis': (0x00eb, u'\u00EB'), + 'egrave': (0x00e8, u'\u00E8'), + 'ehook': (0x1001ebb, u'\u1EBB'), + 'eightsubscript': (0x1002088, u'\u2088'), + 'eightsuperior': (0x1002078, u'\u2078'), + 'elementof': (0x1002208, u'\u2208'), + 'ellipsis': (0x0aae, u'\u2026'), + 'em3space': (0x0aa3, u'\u2004'), + 'em4space': (0x0aa4, u'\u2005'), + 'emacron': (0x03ba, u'\u0113'), + 'emdash': (0x0aa9, u'\u2014'), + 'emptyset': (0x1002205, u'\u2205'), + 'emspace': (0x0aa1, u'\u2003'), + 'endash': (0x0aaa, u'\u2013'), + 'eng': (0x03bf, u'\u014B'), + 'enspace': (0x0aa2, u'\u2002'), + 'eogonek': (0x01ea, u'\u0119'), + 'equal': (0x003d, u'\u003D'), + 'eth': (0x00f0, u'\u00F0'), + 'etilde': (0x1001ebd, u'\u1EBD'), + 'exclam': (0x0021, u'\u0021'), + 'exclamdown': (0x00a1, u'\u00A1'), + 'ezh': (0x1000292, u'\u0292'), + 'f': (0x0066, u'\u0066'), + 'fabovedot': (0x1001e1f, u'\u1E1F'), + 'femalesymbol': (0x0af8, u'\u2640'), + 'ff': (0x09e3, u'\u240C'), + 'figdash': (0x0abb, u'\u2012'), + 'fiveeighths': (0x0ac5, u'\u215D'), + 'fivesixths': (0x0ab7, u'\u215A'), + 'fivesubscript': (0x1002085, u'\u2085'), + 'fivesuperior': (0x1002075, u'\u2075'), + 'fourfifths': (0x0ab5, u'\u2158'), + 'foursubscript': (0x1002084, u'\u2084'), + 'foursuperior': (0x1002074, u'\u2074'), + 'fourthroot': (0x100221C, u'\u221C'), + 'function': (0x08f6, u'\u0192'), + 'g': (0x0067, u'\u0067'), + 'gabovedot': (0x02f5, u'\u0121'), + 'gbreve': (0x02bb, u'\u011F'), + 'gcaron': (0x10001e7, u'\u01E7'), + 'gcedilla': (0x03bb, u'\u0123'), + 'gcircumflex': (0x02f8, u'\u011D'), + 'grave': (0x0060, u'\u0060'), + 'greater': (0x003e, u'\u003E'), + 'greaterthanequal': (0x08be, u'\u2265'), + 'guillemotleft': (0x00ab, u'\u00AB'), + 'guillemotright': (0x00bb, u'\u00BB'), + 'h': (0x0068, u'\u0068'), + 'hairspace': (0x0aa8, u'\u200A'), + 'hcircumflex': (0x02b6, u'\u0125'), + 'heart': (0x0aee, u'\u2665'), + 'hebrew_aleph': (0x0ce0, u'\u05D0'), + 'hebrew_ayin': (0x0cf2, u'\u05E2'), + 'hebrew_bet': (0x0ce1, u'\u05D1'), + 'hebrew_chet': (0x0ce7, u'\u05D7'), + 'hebrew_dalet': (0x0ce3, u'\u05D3'), + 'hebrew_doublelowline': (0x0cdf, u'\u2017'), + 'hebrew_finalkaph': (0x0cea, u'\u05DA'), + 'hebrew_finalmem': (0x0ced, u'\u05DD'), + 'hebrew_finalnun': (0x0cef, u'\u05DF'), + 'hebrew_finalpe': (0x0cf3, u'\u05E3'), + 'hebrew_finalzade': (0x0cf5, u'\u05E5'), + 'hebrew_gimel': (0x0ce2, u'\u05D2'), + 'hebrew_he': (0x0ce4, u'\u05D4'), + 'hebrew_kaph': (0x0ceb, u'\u05DB'), + 'hebrew_lamed': (0x0cec, u'\u05DC'), + 'hebrew_mem': (0x0cee, u'\u05DE'), + 'hebrew_nun': (0x0cf0, u'\u05E0'), + 'hebrew_pe': (0x0cf4, u'\u05E4'), + 'hebrew_qoph': (0x0cf7, u'\u05E7'), + 'hebrew_resh': (0x0cf8, u'\u05E8'), + 'hebrew_samech': (0x0cf1, u'\u05E1'), + 'hebrew_shin': (0x0cf9, u'\u05E9'), + 'hebrew_taw': (0x0cfa, u'\u05EA'), + 'hebrew_tet': (0x0ce8, u'\u05D8'), + 'hebrew_waw': (0x0ce5, u'\u05D5'), + 'hebrew_yod': (0x0ce9, u'\u05D9'), + 'hebrew_zade': (0x0cf6, u'\u05E6'), + 'hebrew_zain': (0x0ce6, u'\u05D6'), + 'horizlinescan1': (0x09ef, u'\u23BA'), + 'horizlinescan3': (0x09f0, u'\u23BB'), + 'horizlinescan5': (0x09f1, u'\u2500'), + 'horizlinescan7': (0x09f2, u'\u23BC'), + 'horizlinescan9': (0x09f3, u'\u23BD'), + 'hstroke': (0x02b1, u'\u0127'), + 'ht': (0x09e2, u'\u2409'), + 'hyphen': (0x00ad, u'\u00AD'), + 'i': (0x0069, u'\u0069'), + 'iacute': (0x00ed, u'\u00ED'), + 'ibelowdot': (0x1001ecb, u'\u1ECB'), + 'ibreve': (0x100012d, u'\u012D'), + 'icircumflex': (0x00ee, u'\u00EE'), + 'identical': (0x08cf, u'\u2261'), + 'idiaeresis': (0x00ef, u'\u00EF'), + 'idotless': (0x02b9, u'\u0131'), + 'ifonlyif': (0x08cd, u'\u21D4'), + 'igrave': (0x00ec, u'\u00EC'), + 'ihook': (0x1001ec9, u'\u1EC9'), + 'imacron': (0x03ef, u'\u012B'), + 'implies': (0x08ce, u'\u21D2'), + 'includedin': (0x08da, u'\u2282'), + 'includes': (0x08db, u'\u2283'), + 'infinity': (0x08c2, u'\u221E'), + 'integral': (0x08bf, u'\u222B'), + 'intersection': (0x08dc, u'\u2229'), + 'iogonek': (0x03e7, u'\u012F'), + 'itilde': (0x03b5, u'\u0129'), + 'j': (0x006a, u'\u006A'), + 'jcircumflex': (0x02bc, u'\u0135'), + 'jot': (0x0bca, u'\u2218'), + 'k': (0x006b, u'\u006B'), + 'kana_A': (0x04b1, u'\u30A2'), + 'kana_CHI': (0x04c1, u'\u30C1'), + 'kana_E': (0x04b4, u'\u30A8'), + 'kana_FU': (0x04cc, u'\u30D5'), + 'kana_HA': (0x04ca, u'\u30CF'), + 'kana_HE': (0x04cd, u'\u30D8'), + 'kana_HI': (0x04cb, u'\u30D2'), + 'kana_HO': (0x04ce, u'\u30DB'), + 'kana_I': (0x04b2, u'\u30A4'), + 'kana_KA': (0x04b6, u'\u30AB'), + 'kana_KE': (0x04b9, u'\u30B1'), + 'kana_KI': (0x04b7, u'\u30AD'), + 'kana_KO': (0x04ba, u'\u30B3'), + 'kana_KU': (0x04b8, u'\u30AF'), + 'kana_MA': (0x04cf, u'\u30DE'), + 'kana_ME': (0x04d2, u'\u30E1'), + 'kana_MI': (0x04d0, u'\u30DF'), + 'kana_MO': (0x04d3, u'\u30E2'), + 'kana_MU': (0x04d1, u'\u30E0'), + 'kana_N': (0x04dd, u'\u30F3'), + 'kana_NA': (0x04c5, u'\u30CA'), + 'kana_NE': (0x04c8, u'\u30CD'), + 'kana_NI': (0x04c6, u'\u30CB'), + 'kana_NO': (0x04c9, u'\u30CE'), + 'kana_NU': (0x04c7, u'\u30CC'), + 'kana_O': (0x04b5, u'\u30AA'), + 'kana_RA': (0x04d7, u'\u30E9'), + 'kana_RE': (0x04da, u'\u30EC'), + 'kana_RI': (0x04d8, u'\u30EA'), + 'kana_RO': (0x04db, u'\u30ED'), + 'kana_RU': (0x04d9, u'\u30EB'), + 'kana_SA': (0x04bb, u'\u30B5'), + 'kana_SE': (0x04be, u'\u30BB'), + 'kana_SHI': (0x04bc, u'\u30B7'), + 'kana_SO': (0x04bf, u'\u30BD'), + 'kana_SU': (0x04bd, u'\u30B9'), + 'kana_TA': (0x04c0, u'\u30BF'), + 'kana_TE': (0x04c3, u'\u30C6'), + 'kana_TO': (0x04c4, u'\u30C8'), + 'kana_TSU': (0x04c2, u'\u30C4'), + 'kana_U': (0x04b3, u'\u30A6'), + 'kana_WA': (0x04dc, u'\u30EF'), + 'kana_WO': (0x04a6, u'\u30F2'), + 'kana_YA': (0x04d4, u'\u30E4'), + 'kana_YO': (0x04d6, u'\u30E8'), + 'kana_YU': (0x04d5, u'\u30E6'), + 'kana_a': (0x04a7, u'\u30A1'), + 'kana_closingbracket': (0x04a3, u'\u300D'), + 'kana_comma': (0x04a4, u'\u3001'), + 'kana_conjunctive': (0x04a5, u'\u30FB'), + 'kana_e': (0x04aa, u'\u30A7'), + 'kana_fullstop': (0x04a1, u'\u3002'), + 'kana_i': (0x04a8, u'\u30A3'), + 'kana_o': (0x04ab, u'\u30A9'), + 'kana_openingbracket': (0x04a2, u'\u300C'), + 'kana_tsu': (0x04af, u'\u30C3'), + 'kana_u': (0x04a9, u'\u30A5'), + 'kana_ya': (0x04ac, u'\u30E3'), + 'kana_yo': (0x04ae, u'\u30E7'), + 'kana_yu': (0x04ad, u'\u30E5'), + 'kcedilla': (0x03f3, u'\u0137'), + 'kra': (0x03a2, u'\u0138'), + 'l': (0x006c, u'\u006C'), + 'lacute': (0x01e5, u'\u013A'), + 'latincross': (0x0ad9, u'\u271D'), + 'lbelowdot': (0x1001e37, u'\u1E37'), + 'lcaron': (0x01b5, u'\u013E'), + 'lcedilla': (0x03b6, u'\u013C'), + 'leftarrow': (0x08fb, u'\u2190'), + 'leftdoublequotemark': (0x0ad2, u'\u201C'), + 'leftmiddlecurlybrace': (0x08af, u'\u23A8'), + 'leftradical': (0x08a1, u'\u23B7'), + 'leftsinglequotemark': (0x0ad0, u'\u2018'), + 'leftt': (0x09f4, u'\u251C'), + 'lefttack': (0x0bdc, u'\u22A3'), + 'less': (0x003c, u'\u003C'), + 'lessthanequal': (0x08bc, u'\u2264'), + 'lf': (0x09e5, u'\u240A'), + 'logicaland': (0x08de, u'\u2227'), + 'logicalor': (0x08df, u'\u2228'), + 'lowleftcorner': (0x09ed, u'\u2514'), + 'lowrightcorner': (0x09ea, u'\u2518'), + 'lstroke': (0x01b3, u'\u0142'), + 'm': (0x006d, u'\u006D'), + 'mabovedot': (0x1001e41, u'\u1E41'), + 'macron': (0x00af, u'\u00AF'), + 'malesymbol': (0x0af7, u'\u2642'), + 'maltesecross': (0x0af0, u'\u2720'), + 'masculine': (0x00ba, u'\u00BA'), + 'minus': (0x002d, u'\u002D'), + 'minutes': (0x0ad6, u'\u2032'), + 'mu': (0x00b5, u'\u00B5'), + 'multiply': (0x00d7, u'\u00D7'), + 'musicalflat': (0x0af6, u'\u266D'), + 'musicalsharp': (0x0af5, u'\u266F'), + 'n': (0x006e, u'\u006E'), + 'nabla': (0x08c5, u'\u2207'), + 'nacute': (0x01f1, u'\u0144'), + 'ncaron': (0x01f2, u'\u0148'), + 'ncedilla': (0x03f1, u'\u0146'), + 'ninesubscript': (0x1002089, u'\u2089'), + 'ninesuperior': (0x1002079, u'\u2079'), + 'nl': (0x09e8, u'\u2424'), + 'nobreakspace': (0x00a0, u'\u00A0'), + 'notapproxeq': (0x1002247, u'\u2247'), + 'notelementof': (0x1002209, u'\u2209'), + 'notequal': (0x08bd, u'\u2260'), + 'notidentical': (0x1002262, u'\u2262'), + 'notsign': (0x00ac, u'\u00AC'), + 'ntilde': (0x00f1, u'\u00F1'), + 'numbersign': (0x0023, u'\u0023'), + 'numerosign': (0x06b0, u'\u2116'), + 'o': (0x006f, u'\u006F'), + 'oacute': (0x00f3, u'\u00F3'), + 'obarred': (0x1000275, u'\u0275'), + 'obelowdot': (0x1001ecd, u'\u1ECD'), + 'ocaron': (0x10001d2, u'\u01D2'), + 'ocircumflex': (0x00f4, u'\u00F4'), + 'ocircumflexacute': (0x1001ed1, u'\u1ED1'), + 'ocircumflexbelowdot': (0x1001ed9, u'\u1ED9'), + 'ocircumflexgrave': (0x1001ed3, u'\u1ED3'), + 'ocircumflexhook': (0x1001ed5, u'\u1ED5'), + 'ocircumflextilde': (0x1001ed7, u'\u1ED7'), + 'odiaeresis': (0x00f6, u'\u00F6'), + 'odoubleacute': (0x01f5, u'\u0151'), + 'oe': (0x13bd, u'\u0153'), + 'ogonek': (0x01b2, u'\u02DB'), + 'ograve': (0x00f2, u'\u00F2'), + 'ohook': (0x1001ecf, u'\u1ECF'), + 'ohorn': (0x10001a1, u'\u01A1'), + 'ohornacute': (0x1001edb, u'\u1EDB'), + 'ohornbelowdot': (0x1001ee3, u'\u1EE3'), + 'ohorngrave': (0x1001edd, u'\u1EDD'), + 'ohornhook': (0x1001edf, u'\u1EDF'), + 'ohorntilde': (0x1001ee1, u'\u1EE1'), + 'omacron': (0x03f2, u'\u014D'), + 'oneeighth': (0x0ac3, u'\u215B'), + 'onefifth': (0x0ab2, u'\u2155'), + 'onehalf': (0x00bd, u'\u00BD'), + 'onequarter': (0x00bc, u'\u00BC'), + 'onesixth': (0x0ab6, u'\u2159'), + 'onesubscript': (0x1002081, u'\u2081'), + 'onesuperior': (0x00b9, u'\u00B9'), + 'onethird': (0x0ab0, u'\u2153'), + 'ooblique': (0x00f8, u'\u00F8'), + 'ordfeminine': (0x00aa, u'\u00AA'), + 'oslash': (0x00f8, u'\u00F8'), + 'otilde': (0x00f5, u'\u00F5'), + 'overline': (0x047e, u'\u203E'), + 'p': (0x0070, u'\u0070'), + 'pabovedot': (0x1001e57, u'\u1E57'), + 'paragraph': (0x00b6, u'\u00B6'), + 'parenleft': (0x0028, u'\u0028'), + 'parenright': (0x0029, u'\u0029'), + 'partdifferential': (0x1002202, u'\u2202'), + 'partialderivative': (0x08ef, u'\u2202'), + 'percent': (0x0025, u'\u0025'), + 'period': (0x002e, u'\u002E'), + 'periodcentered': (0x00b7, u'\u00B7'), + 'permille': (0x0ad5, u'\u2030'), + 'phonographcopyright': (0x0afb, u'\u2117'), + 'plus': (0x002b, u'\u002B'), + 'plusminus': (0x00b1, u'\u00B1'), + 'prescription': (0x0ad4, u'\u211E'), + 'prolongedsound': (0x04b0, u'\u30FC'), + 'punctspace': (0x0aa6, u'\u2008'), + 'q': (0x0071, u'\u0071'), + 'quad': (0x0bcc, u'\u2395'), + 'question': (0x003f, u'\u003F'), + 'questiondown': (0x00bf, u'\u00BF'), + 'quotedbl': (0x0022, u'\u0022'), + 'r': (0x0072, u'\u0072'), + 'racute': (0x01e0, u'\u0155'), + 'radical': (0x08d6, u'\u221A'), + 'rcaron': (0x01f8, u'\u0159'), + 'rcedilla': (0x03b3, u'\u0157'), + 'registered': (0x00ae, u'\u00AE'), + 'rightarrow': (0x08fd, u'\u2192'), + 'rightdoublequotemark': (0x0ad3, u'\u201D'), + 'rightmiddlecurlybrace': (0x08b0, u'\u23AC'), + 'rightsinglequotemark': (0x0ad1, u'\u2019'), + 'rightt': (0x09f5, u'\u2524'), + 'righttack': (0x0bfc, u'\u22A2'), + 's': (0x0073, u'\u0073'), + 'sabovedot': (0x1001e61, u'\u1E61'), + 'sacute': (0x01b6, u'\u015B'), + 'scaron': (0x01b9, u'\u0161'), + 'scedilla': (0x01ba, u'\u015F'), + 'schwa': (0x1000259, u'\u0259'), + 'scircumflex': (0x02fe, u'\u015D'), + 'seconds': (0x0ad7, u'\u2033'), + 'section': (0x00a7, u'\u00A7'), + 'semicolon': (0x003b, u'\u003B'), + 'semivoicedsound': (0x04df, u'\u309C'), + 'seveneighths': (0x0ac6, u'\u215E'), + 'sevensubscript': (0x1002087, u'\u2087'), + 'sevensuperior': (0x1002077, u'\u2077'), + 'similarequal': (0x08c9, u'\u2243'), + 'singlelowquotemark': (0x0afd, u'\u201A'), + 'sixsubscript': (0x1002086, u'\u2086'), + 'sixsuperior': (0x1002076, u'\u2076'), + 'slash': (0x002f, u'\u002F'), + 'soliddiamond': (0x09e0, u'\u25C6'), + 'space': (0x0020, u'\u0020'), + 'squareroot': (0x100221A, u'\u221A'), + 'ssharp': (0x00df, u'\u00DF'), + 'sterling': (0x00a3, u'\u00A3'), + 'stricteq': (0x1002263, u'\u2263'), + 't': (0x0074, u'\u0074'), + 'tabovedot': (0x1001e6b, u'\u1E6B'), + 'tcaron': (0x01bb, u'\u0165'), + 'tcedilla': (0x01fe, u'\u0163'), + 'telephone': (0x0af9, u'\u260E'), + 'telephonerecorder': (0x0afa, u'\u2315'), + 'therefore': (0x08c0, u'\u2234'), + 'thinspace': (0x0aa7, u'\u2009'), + 'thorn': (0x00fe, u'\u00FE'), + 'threeeighths': (0x0ac4, u'\u215C'), + 'threefifths': (0x0ab4, u'\u2157'), + 'threequarters': (0x00be, u'\u00BE'), + 'threesubscript': (0x1002083, u'\u2083'), + 'threesuperior': (0x00b3, u'\u00B3'), + 'tintegral': (0x100222D, u'\u222D'), + 'topintegral': (0x08a4, u'\u2320'), + 'topleftparens': (0x08ab, u'\u239B'), + 'topleftsqbracket': (0x08a7, u'\u23A1'), + 'toprightparens': (0x08ad, u'\u239E'), + 'toprightsqbracket': (0x08a9, u'\u23A4'), + 'topt': (0x09f7, u'\u252C'), + 'trademark': (0x0ac9, u'\u2122'), + 'tslash': (0x03bc, u'\u0167'), + 'twofifths': (0x0ab3, u'\u2156'), + 'twosubscript': (0x1002082, u'\u2082'), + 'twosuperior': (0x00b2, u'\u00B2'), + 'twothirds': (0x0ab1, u'\u2154'), + 'u': (0x0075, u'\u0075'), + 'uacute': (0x00fa, u'\u00FA'), + 'ubelowdot': (0x1001ee5, u'\u1EE5'), + 'ubreve': (0x02fd, u'\u016D'), + 'ucircumflex': (0x00fb, u'\u00FB'), + 'udiaeresis': (0x00fc, u'\u00FC'), + 'udoubleacute': (0x01fb, u'\u0171'), + 'ugrave': (0x00f9, u'\u00F9'), + 'uhook': (0x1001ee7, u'\u1EE7'), + 'uhorn': (0x10001b0, u'\u01B0'), + 'uhornacute': (0x1001ee9, u'\u1EE9'), + 'uhornbelowdot': (0x1001ef1, u'\u1EF1'), + 'uhorngrave': (0x1001eeb, u'\u1EEB'), + 'uhornhook': (0x1001eed, u'\u1EED'), + 'uhorntilde': (0x1001eef, u'\u1EEF'), + 'umacron': (0x03fe, u'\u016B'), + 'underscore': (0x005f, u'\u005F'), + 'union': (0x08dd, u'\u222A'), + 'uogonek': (0x03f9, u'\u0173'), + 'uparrow': (0x08fc, u'\u2191'), + 'upleftcorner': (0x09ec, u'\u250C'), + 'uprightcorner': (0x09eb, u'\u2510'), + 'upstile': (0x0bd3, u'\u2308'), + 'uptack': (0x0bce, u'\u22A5'), + 'uring': (0x01f9, u'\u016F'), + 'utilde': (0x03fd, u'\u0169'), + 'v': (0x0076, u'\u0076'), + 'variation': (0x08c1, u'\u221D'), + 'vertbar': (0x09f8, u'\u2502'), + 'voicedsound': (0x04de, u'\u309B'), + 'vt': (0x09e9, u'\u240B'), + 'w': (0x0077, u'\u0077'), + 'wacute': (0x1001e83, u'\u1E83'), + 'wcircumflex': (0x1000175, u'\u0175'), + 'wdiaeresis': (0x1001e85, u'\u1E85'), + 'wgrave': (0x1001e81, u'\u1E81'), + 'x': (0x0078, u'\u0078'), + 'xabovedot': (0x1001e8b, u'\u1E8B'), + 'y': (0x0079, u'\u0079'), + 'yacute': (0x00fd, u'\u00FD'), + 'ybelowdot': (0x1001ef5, u'\u1EF5'), + 'ycircumflex': (0x1000177, u'\u0177'), + 'ydiaeresis': (0x00ff, u'\u00FF'), + 'yen': (0x00a5, u'\u00A5'), + 'ygrave': (0x1001ef3, u'\u1EF3'), + 'yhook': (0x1001ef7, u'\u1EF7'), + 'ytilde': (0x1001ef9, u'\u1EF9'), + 'z': (0x007a, u'\u007A'), + 'zabovedot': (0x01bf, u'\u017C'), + 'zacute': (0x01bc, u'\u017A'), + 'zcaron': (0x01be, u'\u017E'), + 'zerosubscript': (0x1002080, u'\u2080'), + 'zerosuperior': (0x1002070, u'\u2070'), + 'zstroke': (0x10001b6, u'\u01B6')} + +DEAD_KEYS = { + u'\u0307': u'\u02D9', + u'\u030A': u'\u02DA', + u'\u0301': u'\u00B4', + u'\u0306': u'\u02D8', + u'\u030C': u'\u02C7', + u'\u0327': u'\u00B8', + u'\u0302': u'\u005E', + u'\u0308': u'\u00A8', + u'\u030B': u'\u02DD', + u'\u0300': u'\u0060', + u'\u0345': u'\u037A', + u'\u0332': u'\u005F', + u'\u0304': u'\u00AF', + u'\u0328': u'\u02DB', + u'\u0303': u'\u007E'} + +KEYPAD_KEYS = { + 'KP_0': 0xffb0, + 'KP_1': 0xffb1, + 'KP_2': 0xffb2, + 'KP_3': 0xffb3, + 'KP_4': 0xffb4, + 'KP_5': 0xffb5, + 'KP_6': 0xffb6, + 'KP_7': 0xffb7, + 'KP_8': 0xffb8, + 'KP_9': 0xffb9, + 'KP_Add': 0xffab, + 'KP_Begin': 0xff9d, + 'KP_Decimal': 0xffae, + 'KP_Delete': 0xff9f, + 'KP_Divide': 0xffaf, + 'KP_Down': 0xff99, + 'KP_End': 0xff9c, + 'KP_Enter': 0xff8d, + 'KP_Equal': 0xffbd, + 'KP_F1': 0xff91, + 'KP_F2': 0xff92, + 'KP_F3': 0xff93, + 'KP_F4': 0xff94, + 'KP_Home': 0xff95, + 'KP_Insert': 0xff9e, + 'KP_Left': 0xff96, + 'KP_Multiply': 0xffaa, + 'KP_Next': 0xff9b, + 'KP_Page_Down': 0xff9b, + 'KP_Page_Up': 0xff9a, + 'KP_Prior': 0xff9a, + 'KP_Right': 0xff98, + 'KP_Separator': 0xffac, + 'KP_Space': 0xff80, + 'KP_Subtract': 0xffad, + 'KP_Tab': 0xff89, + 'KP_Up': 0xff97} + +CHARS = { + codepoint: name + for name, (keysym, codepoint) in SYMBOLS.items() + if codepoint} + +KEYSYMS = { + keysym: name + for name, (keysym, codepoint) in SYMBOLS.items() + if codepoint} diff --git a/PortablePython/Lib/site-packages/pynput/keyboard/__init__.py b/PortablePython/Lib/site-packages/pynput/keyboard/__init__.py new file mode 100644 index 0000000..34a1921 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/keyboard/__init__.py @@ -0,0 +1,249 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The module containing keyboard classes. + +See the documentation for more information. +""" + +# pylint: disable=C0103 +# KeyCode, Key, Controller and Listener are not constants + +import itertools + +from pynput._util import backend, Events + + +backend = backend(__name__) +KeyCode = backend.KeyCode +Key = backend.Key +Controller = backend.Controller +Listener = backend.Listener +del backend + + +# pylint: disable=C0326; it is easier to read column aligned keys +#: The keys used as modifiers; the first value in each tuple is the +#: base modifier to use for subsequent modifiers. +_MODIFIER_KEYS = ( + (Key.alt_gr, (Key.alt_gr.value,)), + (Key.alt, (Key.alt.value, Key.alt_l.value, Key.alt_r.value)), + (Key.cmd, (Key.cmd.value, Key.cmd_l.value, Key.cmd_r.value)), + (Key.ctrl, (Key.ctrl.value, Key.ctrl_l.value, Key.ctrl_r.value)), + (Key.shift, (Key.shift.value, Key.shift_l.value, Key.shift_r.value))) + +#: Normalised modifiers as a mapping from virtual key code to basic modifier. +_NORMAL_MODIFIERS = { + value: key + for combination in _MODIFIER_KEYS + for key, value in zip( + itertools.cycle((combination[0],)), + combination[1])} + +#: Control codes to transform into key codes when typing +_CONTROL_CODES = { + '\n': Key.enter, + '\r': Key.enter, + '\t': Key.tab} +# pylint: enable=C0326 + + +class Events(Events): + """A keyboard event listener supporting synchronous iteration over the + events. + + Possible events are: + + :class:`Events.Press` + A key was pressed. + + :class:`Events.Release` + A key was released. + """ + _Listener = Listener + + class Press(Events.Event): + """A key press event. + """ + def __init__(self, key, injected): + #: The key. + self.key = key + + #: Whether this event is synthetic. + self.injected = injected + + class Release(Events.Event): + """A key release event. + """ + def __init__(self, key, injected): + #: The key. + self.key = key + + #: Whether this event is synthetic. + self.injected = injected + + def __init__(self): + super(Events, self).__init__( + on_press=self.Press, + on_release=self.Release) + + +class HotKey(object): + """A combination of keys acting as a hotkey. + + This class acts as a container of hotkey state for a keyboard listener. + + :param set keys: The collection of keys that must be pressed for this + hotkey to activate. Please note that a common limitation of the + hardware is that at most three simultaneously pressed keys are + supported, so using more keys may not work. + + :param callable on_activate: The activation callback. + """ + def __init__(self, keys, on_activate): + self._state = set() + self._keys = set(keys) + self._on_activate = on_activate + + @staticmethod + def parse(keys): + """Parses a key combination string. + + Key combination strings are sequences of key identifiers separated by + ``'+'``. Key identifiers are either single characters representing a + keyboard key, such as ``'a'``, or special key names identified by names + enclosed by brackets, such as ``''``. + + Keyboard keys are case-insensitive. + + :raises ValueError: if a part of the keys string is invalid, or if it + contains multiple equal parts + """ + def parts(): + start = 0 + for i, c in enumerate(keys): + if c == '+' and i != start: + yield keys[start:i] + start = i + 1 + if start == len(keys): + raise ValueError(keys) + else: + yield keys[start:] + + def parse(s): + if len(s) == 1: + return KeyCode.from_char(s.lower()) + elif len(s) > 2 and (s[0], s[-1]) == ('<', '>'): + p = s[1:-1] + try: + # We want to represent modifiers as Key instances, and all + # other keys as KeyCodes + key = Key[p.lower()] + if key in _NORMAL_MODIFIERS.values(): + return key + else: + return KeyCode.from_vk(key.value.vk) + except KeyError: + try: + return KeyCode.from_vk(int(p)) + except ValueError: + raise ValueError(s) + else: + raise ValueError(s) + + # Split the string and parse the individual parts + raw_parts = list(parts()) + parsed_parts = [ + parse(s) + for s in raw_parts] + + # Ensure no duplicate parts + if len(parsed_parts) != len(set(parsed_parts)): + raise ValueError(keys) + else: + return parsed_parts + + def press(self, key): + """Updates the hotkey state for a pressed key. + + If the key is not currently pressed, but is the last key for the full + combination, the activation callback will be invoked. + + Please note that the callback will only be invoked once. + + :param key: The key being pressed. + :type key: Key or KeyCode + """ + if key in self._keys and key not in self._state: + self._state.add(key) + if self._state == self._keys: + self._on_activate() + + def release(self, key): + """Updates the hotkey state for a released key. + + :param key: The key being released. + :type key: Key or KeyCode + """ + if key in self._state: + self._state.remove(key) + + +class GlobalHotKeys(Listener): + """A keyboard listener supporting a number of global hotkeys. + + This is a convenience wrapper to simplify registering a number of global + hotkeys. + + :param dict hotkeys: A mapping from hotkey description to hotkey action. + Keys are strings passed to :meth:`HotKey.parse`. + + :raises ValueError: if any hotkey description is invalid + """ + def __init__(self, hotkeys, *args, **kwargs): + self._hotkeys = [ + HotKey(HotKey.parse(key), value) + for key, value in hotkeys.items()] + super(GlobalHotKeys, self).__init__( + on_press=self._on_press, + on_release=self._on_release, + *args, + **kwargs) + + def _on_press(self, key, injected): + """The press callback. + + This is automatically registered upon creation. + + :param key: The key provided by the base class. + :param injected: Whether the event was injected. + """ + if not injected: + for hotkey in self._hotkeys: + hotkey.press(self.canonical(key)) + + def _on_release(self, key, injected): + """The release callback. + + This is automatically registered upon creation. + + :param key: The key provided by the base class. + :param injected: Whether the event was injected. + """ + if not injected: + for hotkey in self._hotkeys: + hotkey.release(self.canonical(key)) diff --git a/PortablePython/Lib/site-packages/pynput/keyboard/_base.py b/PortablePython/Lib/site-packages/pynput/keyboard/_base.py new file mode 100644 index 0000000..6035743 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/keyboard/_base.py @@ -0,0 +1,754 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +This module contains the base implementation. + +The actual interface to keyboard classes is defined here, but the +implementation is located in a platform dependent module. +""" + +# pylint: disable=R0903 +# We implement stubs + +import contextlib +import enum +import threading +import unicodedata + +import six + +from pynput._util import AbstractListener, prefix +from pynput import _logger + + +class KeyCode(object): + """ + A :class:`KeyCode` represents the description of a key code used by the + operating system. + """ + #: The names of attributes used as platform extensions. + _PLATFORM_EXTENSIONS = [] + + def __init__(self, vk=None, char=None, is_dead=False, **kwargs): + self.vk = vk + self.char = six.text_type(char) if char is not None else None + self.is_dead = is_dead + + if self.is_dead: + try: + self.combining = unicodedata.lookup( + 'COMBINING ' + unicodedata.name(self.char)) + except KeyError: + self.is_dead = False + self.combining = None + if self.is_dead and not self.combining: + raise KeyError(char) + else: + self.combining = None + + for key in self._PLATFORM_EXTENSIONS: + setattr(self, key, kwargs.pop(key, None)) + if kwargs: + raise ValueError(kwargs) + + + def __repr__(self): + if self.is_dead: + return '[%s]' % repr(self.char) + if self.char is not None: + return repr(self.char) + else: + return '<%d>' % self.vk + + def __str__(self): + return repr(self) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + if self.char is not None and other.char is not None: + return self.char == other.char and self.is_dead == other.is_dead + else: + return self.vk == other.vk and all( + getattr(self, f) == getattr(other, f) + for f in self._PLATFORM_EXTENSIONS) + + def __hash__(self): + return hash(repr(self)) + + def join(self, key): + """Applies this dead key to another key and returns the result. + + Joining a dead key with space (``' '``) or itself yields the non-dead + version of this key, if one exists; for example, + ``KeyCode.from_dead('~').join(KeyCode.from_char(' '))`` equals + ``KeyCode.from_char('~')`` and + ``KeyCode.from_dead('~').join(KeyCode.from_dead('~'))``. + + :param KeyCode key: The key to join with this key. + + :return: a key code + + :raises ValueError: if the keys cannot be joined + """ + # A non-dead key cannot be joined + if not self.is_dead: + raise ValueError(self) + + # Joining two of the same keycodes, or joining with space, yields the + # non-dead version of the key + if key.char == ' ' or self == key: + return self.from_char(self.char) + + # Otherwise we combine the characters + if key.char is not None: + combined = unicodedata.normalize( + 'NFC', + key.char + self.combining) + if combined: + return self.from_char(combined) + + raise ValueError(key) + + @classmethod + def from_vk(cls, vk, **kwargs): + """Creates a key from a virtual key code. + + :param vk: The virtual key code. + + :param kwargs: Any other parameters to pass. + + :return: a key code + """ + return cls(vk=vk, **kwargs) + + @classmethod + def from_char(cls, char, **kwargs): + """Creates a key from a character. + + :param str char: The character. + + :return: a key code + """ + return cls(char=char, **kwargs) + + @classmethod + def from_dead(cls, char, **kwargs): + """Creates a dead key. + + :param char: The dead key. This should be the unicode character + representing the stand alone character, such as ``'~'`` for + *COMBINING TILDE*. + + :return: a key code + """ + return cls(char=char, is_dead=True, **kwargs) + + +class Key(enum.Enum): + """A class representing various buttons that may not correspond to + letters. This includes modifier keys and function keys. + + The actual values for these items differ between platforms. Some platforms + may have additional buttons, but these are guaranteed to be present + everywhere. + """ + #: A generic Alt key. This is a modifier. + alt = KeyCode.from_vk(0) + + #: The left Alt key. This is a modifier. + alt_l = KeyCode.from_vk(0) + + #: The right Alt key. This is a modifier. + alt_r = KeyCode.from_vk(0) + + #: The AltGr key. This is a modifier. + alt_gr = KeyCode.from_vk(0) + + #: The Backspace key. + backspace = KeyCode.from_vk(0) + + #: The CapsLock key. + caps_lock = KeyCode.from_vk(0) + + #: A generic command button. On *PC* platforms, this corresponds to the + #: Super key or Windows key, and on *Mac* it corresponds to the Command + #: key. This may be a modifier. + cmd = KeyCode.from_vk(0) + + #: The left command button. On *PC* platforms, this corresponds to the + #: Super key or Windows key, and on *Mac* it corresponds to the Command + #: key. This may be a modifier. + cmd_l = KeyCode.from_vk(0) + + #: The right command button. On *PC* platforms, this corresponds to the + #: Super key or Windows key, and on *Mac* it corresponds to the Command + #: key. This may be a modifier. + cmd_r = KeyCode.from_vk(0) + + #: A generic Ctrl key. This is a modifier. + ctrl = KeyCode.from_vk(0) + + #: The left Ctrl key. This is a modifier. + ctrl_l = KeyCode.from_vk(0) + + #: The right Ctrl key. This is a modifier. + ctrl_r = KeyCode.from_vk(0) + + #: The Delete key. + delete = KeyCode.from_vk(0) + + #: A down arrow key. + down = KeyCode.from_vk(0) + + #: The End key. + end = KeyCode.from_vk(0) + + #: The Enter or Return key. + enter = KeyCode.from_vk(0) + + #: The Esc key. + esc = KeyCode.from_vk(0) + + #: The function keys. F1 to F20 are defined. + f1 = KeyCode.from_vk(0) + f2 = KeyCode.from_vk(0) + f3 = KeyCode.from_vk(0) + f4 = KeyCode.from_vk(0) + f5 = KeyCode.from_vk(0) + f6 = KeyCode.from_vk(0) + f7 = KeyCode.from_vk(0) + f8 = KeyCode.from_vk(0) + f9 = KeyCode.from_vk(0) + f10 = KeyCode.from_vk(0) + f11 = KeyCode.from_vk(0) + f12 = KeyCode.from_vk(0) + f13 = KeyCode.from_vk(0) + f14 = KeyCode.from_vk(0) + f15 = KeyCode.from_vk(0) + f16 = KeyCode.from_vk(0) + f17 = KeyCode.from_vk(0) + f18 = KeyCode.from_vk(0) + f19 = KeyCode.from_vk(0) + f20 = KeyCode.from_vk(0) + + #: The Home key. + home = KeyCode.from_vk(0) + + #: A left arrow key. + left = KeyCode.from_vk(0) + + #: The PageDown key. + page_down = KeyCode.from_vk(0) + + #: The PageUp key. + page_up = KeyCode.from_vk(0) + + #: A right arrow key. + right = KeyCode.from_vk(0) + + #: A generic Shift key. This is a modifier. + shift = KeyCode.from_vk(0) + + #: The left Shift key. This is a modifier. + shift_l = KeyCode.from_vk(0) + + #: The right Shift key. This is a modifier. + shift_r = KeyCode.from_vk(0) + + #: The Space key. + space = KeyCode.from_vk(0) + + #: The Tab key. + tab = KeyCode.from_vk(0) + + #: An up arrow key. + up = KeyCode.from_vk(0) + + #: The play/pause toggle. + media_play_pause = KeyCode.from_vk(0) + + #: The volume mute button. + media_volume_mute = KeyCode.from_vk(0) + + #: The volume down button. + media_volume_down = KeyCode.from_vk(0) + + #: The volume up button. + media_volume_up = KeyCode.from_vk(0) + + #: The previous track button. + media_previous = KeyCode.from_vk(0) + + #: The next track button. + media_next = KeyCode.from_vk(0) + + #: The Insert key. This may be undefined for some platforms. + insert = KeyCode.from_vk(0) + + #: The Menu key. This may be undefined for some platforms. + menu = KeyCode.from_vk(0) + + #: The NumLock key. This may be undefined for some platforms. + num_lock = KeyCode.from_vk(0) + + #: The Pause/Break key. This may be undefined for some platforms. + pause = KeyCode.from_vk(0) + + #: The PrintScreen key. This may be undefined for some platforms. + print_screen = KeyCode.from_vk(0) + + #: The ScrollLock key. This may be undefined for some platforms. + scroll_lock = KeyCode.from_vk(0) + + +class Controller(object): + """A controller for sending virtual keyboard events to the system. + """ + #: The virtual key codes + _KeyCode = KeyCode + + #: The various keys. + _Key = Key + + class InvalidKeyException(Exception): + """The exception raised when an invalid ``key`` parameter is passed to + either :meth:`Controller.press` or :meth:`Controller.release`. + + Its first argument is the ``key`` parameter. + """ + pass + + class InvalidCharacterException(Exception): + """The exception raised when an invalid character is encountered in + the string passed to :meth:`Controller.type`. + + Its first argument is the index of the character in the string, and the + second the character. + """ + pass + + def __init__(self): + self._log = _logger(self.__class__) + self._modifiers_lock = threading.RLock() + self._modifiers = set() + self._caps_lock = False + self._dead_key = None + + def press(self, key): + """Presses a key. + + A key may be either a string of length 1, one of the :class:`Key` + members or a :class:`KeyCode`. + + Strings will be transformed to :class:`KeyCode` using + :meth:`KeyCode.char`. Members of :class:`Key` will be translated to + their :meth:`~Key.value`. + + :param key: The key to press. + + :raises InvalidKeyException: if the key is invalid + + :raises ValueError: if ``key`` is a string, but its length is not ``1`` + """ + resolved = self._resolve(key) + if resolved is None: + raise self.InvalidKeyException(key) + self._update_modifiers(resolved, True) + + # Update caps lock state + if resolved == self._Key.caps_lock.value: + self._caps_lock = not self._caps_lock + + # If we currently have a dead key pressed, join it with this key + original = resolved + if self._dead_key: + try: + resolved = self._dead_key.join(resolved) + except ValueError: + self._handle(self._dead_key, True) + self._handle(self._dead_key, False) + + # If the key is a dead key, keep it for later + if resolved.is_dead: + self._dead_key = resolved + return + + try: + self._handle(resolved, True) + except self.InvalidKeyException: + if resolved != original: + self._handle(self._dead_key, True) + self._handle(self._dead_key, False) + self._handle(original, True) + + self._dead_key = None + + def release(self, key): + """Releases a key. + + A key may be either a string of length 1, one of the :class:`Key` + members or a :class:`KeyCode`. + + Strings will be transformed to :class:`KeyCode` using + :meth:`KeyCode.char`. Members of :class:`Key` will be translated to + their :meth:`~Key.value`. + + :param key: The key to release. If this is a string, it is passed to + :meth:`touches` and the returned releases are used. + + :raises InvalidKeyException: if the key is invalid + + :raises ValueError: if ``key`` is a string, but its length is not ``1`` + """ + resolved = self._resolve(key) + if resolved is None: + raise self.InvalidKeyException(key) + self._update_modifiers(resolved, False) + + # Ignore released dead keys + if resolved.is_dead: + return + + self._handle(resolved, False) + + def tap(self, key): + """Presses and releases a key. + + This is equivalent to the following code:: + + controller.press(key) + controller.release(key) + + :param key: The key to press. + + :raises InvalidKeyException: if the key is invalid + + :raises ValueError: if ``key`` is a string, but its length is not ``1`` + """ + self.press(key) + self.release(key) + + def touch(self, key, is_press): + """Calls either :meth:`press` or :meth:`release` depending on the value + of ``is_press``. + + :param key: The key to press or release. + + :param bool is_press: Whether to press the key. + + :raises InvalidKeyException: if the key is invalid + """ + if is_press: + self.press(key) + else: + self.release(key) + + @contextlib.contextmanager + def pressed(self, *args): + """Executes a block with some keys pressed. + + :param keys: The keys to keep pressed. + """ + for key in args: + self.press(key) + + try: + yield + finally: + for key in reversed(args): + self.release(key) + + def type(self, string): + """Types a string. + + This method will send all key presses and releases necessary to type + all characters in the string. + + :param str string: The string to type. + + :raises InvalidCharacterException: if an untypable character is + encountered + """ + from . import _CONTROL_CODES + for i, character in enumerate(string): + key = _CONTROL_CODES.get(character, character) + try: + self.press(key) + self.release(key) + + except (ValueError, self.InvalidKeyException): + raise self.InvalidCharacterException(i, character) + + @property + @contextlib.contextmanager + def modifiers(self): + """The currently pressed modifier keys. + + Please note that this reflects only the internal state of this + controller, and not the state of the operating system keyboard buffer. + This property cannot be used to determine whether a key is physically + pressed. + + Only the generic modifiers will be set; when pressing either + :attr:`Key.shift_l`, :attr:`Key.shift_r` or :attr:`Key.shift`, only + :attr:`Key.shift` will be present. + + Use this property within a context block thus:: + + with controller.modifiers as modifiers: + with_block() + + This ensures that the modifiers cannot be modified by another thread. + """ + with self._modifiers_lock: + yield set( + self._as_modifier(modifier) + for modifier in self._modifiers) + + @property + def alt_pressed(self): + """Whether any *alt* key is pressed. + + Please note that this reflects only the internal state of this + controller. See :attr:`modifiers` for more information. + """ + with self.modifiers as modifiers: + return self._Key.alt in modifiers + + @property + def alt_gr_pressed(self): + """Whether *altgr* is pressed. + + Please note that this reflects only the internal state of this + controller. See :attr:`modifiers` for more information. + """ + with self.modifiers as modifiers: + return self._Key.alt_gr in modifiers + + @property + def ctrl_pressed(self): + """Whether any *ctrl* key is pressed. + + Please note that this reflects only the internal state of this + controller. See :attr:`modifiers` for more information. + """ + with self.modifiers as modifiers: + return self._Key.ctrl in modifiers + + @property + def shift_pressed(self): + """Whether any *shift* key is pressed, or *caps lock* is toggled. + + Please note that this reflects only the internal state of this + controller. See :attr:`modifiers` for more information. + """ + if self._caps_lock: + return True + + with self.modifiers as modifiers: + return self._Key.shift in modifiers + + def _resolve(self, key): + """Resolves a key to a :class:`KeyCode` instance. + + This method will convert any key representing a character to uppercase + if a shift modifier is active. + + :param key: The key to resolve. + + :return: a key code, or ``None`` if it cannot be resolved + """ + # Use the value for the key constants + if key in (k for k in self._Key): + return key.value + + # Convert strings to key codes + if isinstance(key, six.string_types): + if len(key) != 1: + raise ValueError(key) + return self._KeyCode.from_char(key) + + # Assume this is a proper key + if isinstance(key, self._KeyCode): + if key.char is not None and self.shift_pressed: + return self._KeyCode(vk=key.vk, char=key.char.upper()) + else: + return key + + def _update_modifiers(self, key, is_press): + """Updates the current modifier list. + + If ``key`` is not a modifier, no action is taken. + + :param key: The key being pressed or released. + """ + # Check whether the key is a modifier + if self._as_modifier(key): + with self._modifiers_lock: + if is_press: + self._modifiers.add(key) + else: + try: + self._modifiers.remove(key) + except KeyError: + pass + + def _as_modifier(self, key): + """Returns a key as the modifier used internally if defined. + + This method will convert values like :attr:`Key.alt_r.value` and + :attr:`Key.shift_l.value` to :attr:`Key.alt` and :attr:`Key.shift`. + + :param key: The possible modifier key. + + :return: the base modifier key, or ``None`` if ``key`` is not a + modifier + """ + from . import _NORMAL_MODIFIERS + return _NORMAL_MODIFIERS.get(key, None) + + def _handle(self, key, is_press): + """The platform implementation of the actual emitting of keyboard + events. + + This is a platform dependent implementation. + + :param Key key: The key to handle. + + :param bool is_press: Whether this is a key press event. + """ + raise NotImplementedError() + + +# pylint: disable=W0223; This is also an abstract class +class Listener(AbstractListener): + """A listener for keyboard events. + + Instances of this class can be used as context managers. This is equivalent + to the following code:: + + listener.start() + try: + listener.wait() + with_statements() + finally: + listener.stop() + + This class inherits from :class:`threading.Thread` and supports all its + methods. It will set :attr:`daemon` to ``True`` when created. + + All callback arguments are optional; a callback may take less arguments + than actually passed, but not more, unless they are optional. + + :param callable on_press: The callback to call when a button is pressed. + + It will be called with the arguments ``(key, injected)``, where ``key`` + is a :class:`KeyCode`, a :class:`Key` or ``None`` if the key is + unknown, and ``injected`` whether the event was injected and thus not + generated by an actual input device. + + Please note that not all backends support ``injected`` and will always + set it to ``False``. + + :param callable on_release: The callback to call when a button is released. + + It will be called with the arguments ``(key, injected)``, where ``key`` + is a :class:`KeyCode`, a :class:`Key` or ``None`` if the key is + unknown, and ``injected`` whether the event was injected and thus not + generated by an actual input device. + + Please note that not all backends support ``injected`` and will always + set it to ``False``. + + :param bool suppress: Whether to suppress events. Setting this to ``True`` + will prevent the input events from being passed to the rest of the + system. + + :param kwargs: Any non-standard platform dependent options. These should be + prefixed with the platform name thus: ``darwin_``, ``uinput_``, + ``xorg_`` or ``win32_``. + + Supported values are: + + ``darwin_intercept`` + A callable taking the arguments ``(event_type, event)``, where + ``event_type`` is ``Quartz.kCGEventKeyDown`` or + ``Quartz.kCGEventKeyUp``, and ``event`` is a ``CGEventRef``. + + This callable can freely modify the event using functions like + ``Quartz.CGEventSetIntegerValueField``. If this callable does not + return the event, the event is suppressed system wide. + + ``uinput_device_paths`` + A list of device paths. + + If this is specified, *pynput* will limit the number of devices + checked for the capabilities needed to those passed, otherwise all + system devices will be used. Passing this might be required if an + incorrect device is chosen. + + ``win32_event_filter`` + A callable taking the arguments ``(msg, data)``, where ``msg`` is + the current message, and ``data`` associated data as a + `KBDLLHOOKSTRUCT `_. + + If this callback returns ``False``, the event will not be + propagated to the listener callback. + + If ``self.suppress_event()`` is called, the event is suppressed + system wide. + """ + def __init__(self, on_press=None, on_release=None, suppress=False, + **kwargs): + self._log = _logger(self.__class__) + option_prefix = prefix(Listener, self.__class__) + self._options = { + key[len(option_prefix):]: value + for key, value in kwargs.items() + if key.startswith(option_prefix)} + super(Listener, self).__init__( + on_press=self._wrap(on_press, 2), + on_release=self._wrap(on_release, 2), + suppress=suppress) +# pylint: enable=W0223 + + def canonical(self, key): + """Performs normalisation of a key. + + This method attempts to convert key events to their canonical form, so + that events will equal regardless of modifier state. + + This method will convert upper case keys to lower case keys, convert + any modifiers with a right and left version to the same value, and may + slow perform additional platform dependent normalisation. + + :param key: The key to normalise. + :type key: Key or KeyCode + + :return: a key + :rtype: Key or KeyCode + """ + from pynput.keyboard import Key, KeyCode, _NORMAL_MODIFIERS + if isinstance(key, KeyCode) and key.char is not None: + return KeyCode.from_char(key.char.lower()) + elif isinstance(key, Key) and key.value in _NORMAL_MODIFIERS: + return _NORMAL_MODIFIERS[key.value] + elif isinstance(key, Key) and key.value.vk is not None: + return KeyCode.from_vk(key.value.vk) + else: + return key diff --git a/PortablePython/Lib/site-packages/pynput/keyboard/_darwin.py b/PortablePython/Lib/site-packages/pynput/keyboard/_darwin.py new file mode 100644 index 0000000..4065164 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/keyboard/_darwin.py @@ -0,0 +1,367 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The keyboard implementation for *macOS*. +""" + +# pylint: disable=C0111 +# The documentation is extracted from the base classes + +# pylint: disable=R0903 +# We implement stubs + +import enum + +from Quartz import ( + CGEventCreateKeyboardEvent, + CGEventGetFlags, + CGEventGetIntegerValueField, + CGEventGetType, + CGEventKeyboardGetUnicodeString, + CGEventKeyboardSetUnicodeString, + CGEventMaskBit, + CGEventPost, + CGEventSetFlags, + kCGEventFlagMaskAlternate, + kCGEventFlagMaskCommand, + kCGEventFlagMaskControl, + kCGEventFlagMaskShift, + kCGEventFlagsChanged, + kCGEventKeyDown, + kCGEventKeyUp, + kCGHIDEventTap, + kCGKeyboardEventKeycode, + NSEvent, + NSSystemDefined) + +from pynput._util.darwin import ( + get_unicode_to_keycode_map, + keycode_context, + ListenerMixin) +from pynput._util.darwin_vks import SYMBOLS +from . import _base + + +# From hidsystem/ev_keymap.h +NX_KEYTYPE_PLAY = 16 +NX_KEYTYPE_MUTE = 7 +NX_KEYTYPE_SOUND_DOWN = 1 +NX_KEYTYPE_SOUND_UP = 0 +NX_KEYTYPE_NEXT = 17 +NX_KEYTYPE_PREVIOUS = 18 +NX_KEYTYPE_EJECT = 14 + +# pylint: disable=C0103; We want to use the names from the C API +# This is undocumented, but still widely known +kSystemDefinedEventMediaKeysSubtype = 8 + +# We extract this here since the name is very long +otherEventWithType = getattr( + NSEvent, + 'otherEventWithType_' + 'location_' + 'modifierFlags_' + 'timestamp_' + 'windowNumber_' + 'context_' + 'subtype_' + 'data1_' + 'data2_') +# pylint: enable=C0103 + + +class KeyCode(_base.KeyCode): + _PLATFORM_EXTENSIONS = ( + # Whether this is a media key + '_is_media', + ) + + # Be explicit about fields + _is_media = None + + @classmethod + def _from_media(cls, vk, **kwargs): + """Creates a media key from a key code. + + :param int vk: The key code. + + :return: a key code + """ + return cls.from_vk(vk, _is_media=True, **kwargs) + + def _event(self, modifiers, mapping, is_pressed): + """This key as a *Quartz* event. + + :param set modifiers: The currently active modifiers. + + :param mapping: The current keyboard mapping. + + :param bool is_press: Whether to generate a press event. + + :return: a *Quartz* event + """ + vk = self.vk or mapping.get(self.char) + if self._is_media: + result = otherEventWithType( + NSSystemDefined, + (0, 0), + 0xa00 if is_pressed else 0xb00, + 0, + 0, + 0, + 8, + (self.vk << 16) | ((0xa if is_pressed else 0xb) << 8), + -1).CGEvent() + else: + result = CGEventCreateKeyboardEvent( + None, 0 if vk is None else vk, is_pressed) + + CGEventSetFlags( + result, + 0 + | (kCGEventFlagMaskAlternate + if Key.alt in modifiers else 0) + + | (kCGEventFlagMaskCommand + if Key.cmd in modifiers else 0) + + | (kCGEventFlagMaskControl + if Key.ctrl in modifiers else 0) + + | (kCGEventFlagMaskShift + if Key.shift in modifiers else 0)) + + if vk is None and self.char is not None: + CGEventKeyboardSetUnicodeString( + result, len(self.char), self.char) + + return result + + +# pylint: disable=W0212 +class Key(enum.Enum): + # Default keys + alt = KeyCode.from_vk(0x3A) + alt_l = KeyCode.from_vk(0x3A) + alt_r = KeyCode.from_vk(0x3D) + alt_gr = KeyCode.from_vk(0x3D) + backspace = KeyCode.from_vk(0x33) + caps_lock = KeyCode.from_vk(0x39) + cmd = KeyCode.from_vk(0x37) + cmd_l = KeyCode.from_vk(0x37) + cmd_r = KeyCode.from_vk(0x36) + ctrl = KeyCode.from_vk(0x3B) + ctrl_l = KeyCode.from_vk(0x3B) + ctrl_r = KeyCode.from_vk(0x3E) + delete = KeyCode.from_vk(0x75) + down = KeyCode.from_vk(0x7D) + end = KeyCode.from_vk(0x77) + enter = KeyCode.from_vk(0x24) + esc = KeyCode.from_vk(0x35) + f1 = KeyCode.from_vk(0x7A) + f2 = KeyCode.from_vk(0x78) + f3 = KeyCode.from_vk(0x63) + f4 = KeyCode.from_vk(0x76) + f5 = KeyCode.from_vk(0x60) + f6 = KeyCode.from_vk(0x61) + f7 = KeyCode.from_vk(0x62) + f8 = KeyCode.from_vk(0x64) + f9 = KeyCode.from_vk(0x65) + f10 = KeyCode.from_vk(0x6D) + f11 = KeyCode.from_vk(0x67) + f12 = KeyCode.from_vk(0x6F) + f13 = KeyCode.from_vk(0x69) + f14 = KeyCode.from_vk(0x6B) + f15 = KeyCode.from_vk(0x71) + f16 = KeyCode.from_vk(0x6A) + f17 = KeyCode.from_vk(0x40) + f18 = KeyCode.from_vk(0x4F) + f19 = KeyCode.from_vk(0x50) + f20 = KeyCode.from_vk(0x5A) + home = KeyCode.from_vk(0x73) + left = KeyCode.from_vk(0x7B) + page_down = KeyCode.from_vk(0x79) + page_up = KeyCode.from_vk(0x74) + right = KeyCode.from_vk(0x7C) + shift = KeyCode.from_vk(0x38) + shift_l = KeyCode.from_vk(0x38) + shift_r = KeyCode.from_vk(0x3C) + space = KeyCode.from_vk(0x31, char=' ') + tab = KeyCode.from_vk(0x30) + up = KeyCode.from_vk(0x7E) + + media_play_pause = KeyCode._from_media(NX_KEYTYPE_PLAY) + media_volume_mute = KeyCode._from_media(NX_KEYTYPE_MUTE) + media_volume_down = KeyCode._from_media(NX_KEYTYPE_SOUND_DOWN) + media_volume_up = KeyCode._from_media(NX_KEYTYPE_SOUND_UP) + media_previous = KeyCode._from_media(NX_KEYTYPE_PREVIOUS) + media_next = KeyCode._from_media(NX_KEYTYPE_NEXT) + media_eject = KeyCode._from_media(NX_KEYTYPE_EJECT) +# pylint: enable=W0212 + + +class Controller(_base.Controller): + _KeyCode = KeyCode + _Key = Key + + def __init__(self): + super(Controller, self).__init__() + self._mapping = get_unicode_to_keycode_map() + + def _handle(self, key, is_press): + with self.modifiers as modifiers: + CGEventPost( + kCGHIDEventTap, + (key if key not in (k for k in Key) else key.value)._event( + modifiers, self._mapping, is_press)) + + +class Listener(ListenerMixin, _base.Listener): + #: The events that we listen to + _EVENTS = ( + CGEventMaskBit(kCGEventKeyDown) | + CGEventMaskBit(kCGEventKeyUp) | + CGEventMaskBit(kCGEventFlagsChanged) | + CGEventMaskBit(NSSystemDefined) + ) + + # pylint: disable=W0212 + #: A mapping from keysym to special key + _SPECIAL_KEYS = { + (key.value.vk, key.value._is_media): key + for key in Key} + # pylint: enable=W0212 + + #: The event flags set for the various modifier keys + _MODIFIER_FLAGS = { + Key.alt: kCGEventFlagMaskAlternate, + Key.alt_l: kCGEventFlagMaskAlternate, + Key.alt_r: kCGEventFlagMaskAlternate, + Key.cmd: kCGEventFlagMaskCommand, + Key.cmd_l: kCGEventFlagMaskCommand, + Key.cmd_r: kCGEventFlagMaskCommand, + Key.ctrl: kCGEventFlagMaskControl, + Key.ctrl_l: kCGEventFlagMaskControl, + Key.ctrl_r: kCGEventFlagMaskControl, + Key.shift: kCGEventFlagMaskShift, + Key.shift_l: kCGEventFlagMaskShift, + Key.shift_r: kCGEventFlagMaskShift} + + def __init__(self, *args, **kwargs): + super(Listener, self).__init__(*args, **kwargs) + self._flags = 0 + self._context = None + self._intercept = self._options.get( + 'intercept', + None) + + def _run(self): + with keycode_context() as context: + self._context = context + try: + super(Listener, self)._run() + finally: + self._context = None + + def _handle_message(self, _proxy, event_type, event, _refcon, injected): + # Convert the event to a KeyCode; this may fail, and in that case we + # pass None + try: + key = self._event_to_key(event) + except IndexError: + key = None + + try: + if event_type == kCGEventKeyDown: + # This is a normal key press + self.on_press(key, injected) + + elif event_type == kCGEventKeyUp: + # This is a normal key release + self.on_release(key, injected) + + elif key == Key.caps_lock: + # We only get an event when caps lock is toggled, so we fake + # press and release + self.on_press(key, injected) + self.on_release(key, injected) + + elif event_type == NSSystemDefined: + sys_event = NSEvent.eventWithCGEvent_(event) + if sys_event.subtype() == kSystemDefinedEventMediaKeysSubtype: + # The key in the special key dict; True since it is a media + # key + key = ((sys_event.data1() & 0xffff0000) >> 16, True) + if key in self._SPECIAL_KEYS: + flags = sys_event.data1() & 0x0000ffff + is_press = ((flags & 0xff00) >> 8) == 0x0a + if is_press: + self.on_press(self._SPECIAL_KEYS[key]) + else: + self.on_release(self._SPECIAL_KEYS[key]) + + else: + # This is a modifier event---excluding caps lock---for which we + # must check the current modifier state to determine whether + # the key was pressed or released + flags = CGEventGetFlags(event) + is_press = flags & self._MODIFIER_FLAGS.get(key, 0) + if is_press: + self.on_press(key, injected) + else: + self.on_release(key, injected) + + finally: + # Store the current flag mask to be able to detect modifier state + # changes + self._flags = CGEventGetFlags(event) + + def _event_to_key(self, event): + """Converts a *Quartz* event to a :class:`KeyCode`. + + :param event: The event to convert. + + :return: a :class:`pynput.keyboard.KeyCode` + + :raises IndexError: if the key code is invalid + """ + vk = CGEventGetIntegerValueField( + event, kCGKeyboardEventKeycode) + event_type = CGEventGetType(event) + is_media = True if event_type == NSSystemDefined else None + + # First try special keys... + key = (vk, is_media) + if key in self._SPECIAL_KEYS: + return self._SPECIAL_KEYS[key] + + # ...then try characters... + length, chars = CGEventKeyboardGetUnicodeString( + event, 100, None, None) + try: + printable = chars.isprintable() + except AttributeError: + printable = chars.isalnum() + if not printable and vk in SYMBOLS \ + and CGEventGetFlags(event) \ + & kCGEventFlagMaskControl: + return KeyCode.from_char(SYMBOLS[vk], vk=vk) + elif length > 0: + return KeyCode.from_char(chars, vk=vk) + + # ...and fall back on a virtual key code + return KeyCode.from_vk(vk) diff --git a/PortablePython/Lib/site-packages/pynput/keyboard/_dummy.py b/PortablePython/Lib/site-packages/pynput/keyboard/_dummy.py new file mode 100644 index 0000000..10727f6 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/keyboard/_dummy.py @@ -0,0 +1,23 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +This module contains a dummy implementation. + +It cannot be used, but importing it will not raise any exceptions. +""" + +from ._base import Controller, Key, KeyCode, Listener diff --git a/PortablePython/Lib/site-packages/pynput/keyboard/_uinput.py b/PortablePython/Lib/site-packages/pynput/keyboard/_uinput.py new file mode 100644 index 0000000..2f63dd3 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/keyboard/_uinput.py @@ -0,0 +1,446 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The keyboard implementation for *uinput*. +""" + +# pylint: disable=C0111 +# The documentation is extracted from the base classes + +# pylint: disable=R0903 +# We implement stubs + +import enum +import errno +import functools +import os +import re +import subprocess + +import evdev + +from evdev.events import KeyEvent + +from pynput._util import xorg_keysyms +from pynput._util.uinput import ListenerMixin +from . import _base + + +class KeyCode(_base.KeyCode): + _PLATFORM_EXTENSIONS = ( + # The name for this key + '_x_name', + '_kernel_name', + ) + + # Be explicit about fields + _x_name = None + _kernel_name = None +# pylint: enable=W0212 + + @classmethod + def _from_name(cls, x_name, kernel_name, **kwargs): + """Creates a key from a name. + + :param str x_name: The X name. + + :param str kernel_name: The kernel name. + + :return: a key code + """ + try: + vk = getattr(evdev.ecodes, kernel_name) + except AttributeError: + vk = None + return cls.from_vk( + vk, _x_name=x_name, _kernel_name=kernel_name, **kwargs) + + +# pylint: disable=W0212 +class Key(enum.Enum): + alt = KeyCode._from_name('Alt_L', 'KEY_LEFTALT') + alt_l = KeyCode._from_name('Alt_L', 'KEY_LEFTALT') + alt_r = KeyCode._from_name('Alt_R', 'KEY_RIGHTALT') + alt_gr = KeyCode._from_name('Mode_switch', 'KEY_RIGHTALT') + backspace = KeyCode._from_name('BackSpace', 'KEY_BACKSPACE') + caps_lock = KeyCode._from_name('Caps_Lock', 'KEY_CAPSLOCK') + cmd = KeyCode._from_name('Super_L', 'KEY_LEFTMETA') + cmd_l = KeyCode._from_name('Super_L', 'KEY_LEFTMETA') + cmd_r = KeyCode._from_name('Super_R', 'KEY_RIGHTMETA') + ctrl = KeyCode._from_name('Control_L', 'KEY_LEFTCTRL') + ctrl_l = KeyCode._from_name('Control_L', 'KEY_LEFTCTRL') + ctrl_r = KeyCode._from_name('Control_R', 'KEY_RIGHTCTRL') + delete = KeyCode._from_name('Delete', 'KEY_DELETE') + down = KeyCode._from_name('Down', 'KEY_DOWN') + end = KeyCode._from_name('End', 'KEY_END') + enter = KeyCode._from_name('Return', 'KEY_ENTER') + esc = KeyCode._from_name('Escape', 'KEY_ESC') + f1 = KeyCode._from_name('F1', 'KEY_F1') + f2 = KeyCode._from_name('F2', 'KEY_F2') + f3 = KeyCode._from_name('F3', 'KEY_F3') + f4 = KeyCode._from_name('F4', 'KEY_F4') + f5 = KeyCode._from_name('F5', 'KEY_F5') + f6 = KeyCode._from_name('F6', 'KEY_F6') + f7 = KeyCode._from_name('F7', 'KEY_F7') + f8 = KeyCode._from_name('F8', 'KEY_F8') + f9 = KeyCode._from_name('F9', 'KEY_F9') + f10 = KeyCode._from_name('F10', 'KEY_F10') + f11 = KeyCode._from_name('F11', 'KEY_F11') + f12 = KeyCode._from_name('F12', 'KEY_F12') + f13 = KeyCode._from_name('F13', 'KEY_F13') + f14 = KeyCode._from_name('F14', 'KEY_F14') + f15 = KeyCode._from_name('F15', 'KEY_F15') + f16 = KeyCode._from_name('F16', 'KEY_F16') + f17 = KeyCode._from_name('F17', 'KEY_F17') + f18 = KeyCode._from_name('F18', 'KEY_F18') + f19 = KeyCode._from_name('F19', 'KEY_F19') + f20 = KeyCode._from_name('F20', 'KEY_F20') + home = KeyCode._from_name('Home', 'KEY_HOME') + left = KeyCode._from_name('Left', 'KEY_LEFT') + page_down = KeyCode._from_name('Page_Down', 'KEY_PAGEDOWN') + page_up = KeyCode._from_name('Page_Up', 'KEY_PAGEUP') + right = KeyCode._from_name('Right', 'KEY_RIGHT') + shift = KeyCode._from_name('Shift_L', 'KEY_LEFTSHIFT') + shift_l = KeyCode._from_name('Shift_L', 'KEY_LEFTSHIFT') + shift_r = KeyCode._from_name('Shift_R', 'KEY_RIGHTSHIFT') + space = KeyCode._from_name('space', 'KEY_SPACE', char=' ') + tab = KeyCode._from_name('Tab', 'KEY_TAB', char='\t') + up = KeyCode._from_name('Up', 'KEY_UP') + + media_play_pause = KeyCode._from_name('Play', 'KEY_PLAYPAUSE') + media_volume_mute = KeyCode._from_name('Mute', 'KEY_MUTE') + media_volume_down = KeyCode._from_name('LowerVolume', 'KEY_VOLUMEDOWN') + media_volume_up = KeyCode._from_name('RaiseVolume', 'KEY_VOLUMEUP') + media_previous = KeyCode._from_name('Prev', 'KEY_PREVIOUSSONG') + media_next = KeyCode._from_name('Next', 'KEY_NEXTSONG') + + insert = KeyCode._from_name('Insert', 'KEY_INSERT') + menu = KeyCode._from_name('Menu', 'KEY_MENU') + num_lock = KeyCode._from_name('Num_Lock', 'KEY_NUMLOCK') + pause = KeyCode._from_name('Pause', 'KEY_PAUSE') + print_screen = KeyCode._from_name('Print', 'KEY_SYSRQ') + scroll_lock = KeyCode._from_name('Scroll_Lock', 'KEY_SCROLLLOCK') +# pylint: enable=W0212 + + +class Layout(object): + """A description of the keyboard layout. + """ + #: A regular expression to parse keycodes in the dumpkeys output + #: + #: The groups are: keycode number, key names. + KEYCODE_RE = re.compile( + r'keycode\s+(\d+)\s+=(.*)') + + class Key(object): + """A key in a keyboard layout. + """ + def __init__(self, normal, shifted, alt, alt_shifted): + self._values = ( + normal, + shifted, + alt, + alt_shifted) + + def __str__(self): + return ('<' + 'normal: {}, ' + 'shifted: {}, ' + 'alternative: {}, ' + 'shifted alternative: {}>').format( + self.normal, self.shifted, self.alt, self.alt_shifted) + + __repr__ = __str__ + + def __iter__(self): + return iter(self._values) + + def __getitem__(self, i): + return self._values[i] + + @property + def normal(self): + """The normal key. + """ + return self._values[0] + + @property + def shifted(self): + """The shifted key. + """ + return self._values[1] + + @property + def alt(self): + """The alternative key. + """ + return self._values[2] + + @property + def alt_shifted(self): + """The shifted alternative key. + """ + return self._values[3] + + def __init__(self): + def as_char(k): + return k.value.char if isinstance(k, Key) else k.char + self._vk_table = self._load() + self._char_table = { + as_char(key): ( + vk, + set() + | {Key.shift} if i & 1 else set() + | {Key.alt_gr} if i & 2 else set()) + for vk, keys in self._vk_table.items() + for i, key in enumerate(keys) + if key is not None and as_char(key) is not None} + + def for_vk(self, vk, modifiers): + """Reads a key for a virtual key code and modifier state. + + :param int vk: The virtual key code. + + :param set modifiers: A set of modifiers. + + :return: a mapped key + + :raises KeyError: if ``vk`` is an unknown key + """ + return self._vk_table[vk][ + 0 + | (1 if Key.shift in modifiers else 0) + | (2 if Key.alt_gr in modifiers else 0)] + + def for_char(self, char): + """Reads a virtual key code and modifier state for a character. + + :param str char: The character. + + :return: the tuple ``(vk, modifiers)`` + + :raises KeyError: if ``vk`` is an unknown key + """ + return self._char_table[char] + + @functools.lru_cache() + def _load(self): + """Loads the keyboard layout. + + For simplicity, we call out to the ``dumpkeys`` binary. In the future, + we may want to implement this ourselves. + """ + result = {} + for keycode, names in self.KEYCODE_RE.findall( + subprocess.check_output( + ['dumpkeys', '--full-table', '--keys-only']).decode('utf-8')): + vk = int(keycode) + keys = tuple( + self._parse(vk, name) + for name in names.split()[:4]) + if any(key is not None for key in keys): + result[vk] = self.Key(*keys) + return result + + def _parse(self, vk, name): + """Parses a single key from the ``dumpkeys`` output. + + :param int vk: The key code. + + :param str name: The key name. + + :return: a key representation + """ + try: + # First try special keys... + return next( + key + for key in Key + if key.value._x_name == name) + except StopIteration: + # ...then characters... + try: + _, char = xorg_keysyms.SYMBOLS[name.lstrip('+')] + if char: + return KeyCode.from_char(char, vk=vk) + except KeyError: + pass + + # ...and finally special dumpkeys names + try: + return KeyCode.from_char({ + 'one': '1', + 'two': '2', + 'three': '3', + 'four': '4', + 'five': '5', + 'six': '6', + 'seven': '7', + 'eight': '8', + 'nine': '9', + 'zero': '0'}[name]) + except KeyError: + pass + + +class Controller(_base.Controller): + _KeyCode = KeyCode + _Key = Key + + def __init__(self, *args, **kwargs): + super(Controller, self).__init__(*args, **kwargs) + self._layout = LAYOUT + self._dev = evdev.UInput() + + def __del__(self): + if hasattr(self, '_dev'): + self._dev.close() + + def _handle(self, key, is_press): + # Resolve the key to a virtual key code and a possible set of required + # modifiers + try: + vk, required_modifiers = self._to_vk_and_modifiers(key) + except ValueError: + raise self.InvalidKeyException(key) + + # Determine how we need to modify the modifier state + if is_press and required_modifiers is not None: + with self.modifiers as modifiers: + vk, required_modifiers = self._layout.for_char(key.char) + to_press = { + getattr(evdev.ecodes, key.value._kernel_name) + for key in (required_modifiers - modifiers)} + to_release = { + getattr(evdev.ecodes, key.value._kernel_name) + for key in (modifiers - required_modifiers)} + else: + to_release = set() + to_press = set() + + # Update the modifier state, send the key, and finally release any + # modifiers + cleanup = [] + try: + for k in to_release: + self._send(k, False) + cleanup.append((k, True)) + for k in to_press: + self._send(k, True) + cleanup.append((k, False)) + + self._send(vk, is_press) + + finally: + for e in reversed(cleanup): + # pylint: disable E722; we want to suppress exceptions + try: + self._send(*e) + except: + pass + # pylint: enable E722 + + self._dev.syn() + + def _to_vk_and_modifiers(self, key): + """Resolves a key to a virtual key code and a modifier set. + + :param key: The key to resolve. + :type key: Key or KeyCode + + :return: a virtual key code and possible required modifiers + """ + if hasattr(key, 'vk') and key.vk is not None: + return (key.vk, None) + elif hasattr(key, 'char') and key.char is not None: + return self._layout.for_char(key.char) + else: + raise ValueError(key) + + def _send(self, vk, is_press): + """Sends a virtual key event. + + This method does not perform ``SYN``. + + :param int vk: The virtual key. + + :param bool is_press: Whether this is a press event. + """ + self._dev.write(evdev.ecodes.EV_KEY, vk, int(is_press)) + + +class Listener(ListenerMixin, _base.Listener): + _EVENTS = ( + evdev.ecodes.EV_KEY,) + + #: A + _MODIFIERS = { + Key.alt.value.vk: Key.alt, + Key.alt_l.value.vk: Key.alt, + Key.alt_r.value.vk: Key.alt, + Key.alt_gr.value.vk: Key.alt_gr, + Key.shift.value.vk: Key.shift, + Key.shift_l.value.vk: Key.shift, + Key.shift_r.value.vk: Key.shift} + + def __init__(self, *args, **kwargs): + super(Listener, self).__init__(*args, **kwargs) + self._layout = LAYOUT + self._modifiers = set() + + def _handle_message(self, event): + is_press = event.value in (KeyEvent.key_down, KeyEvent.key_hold) + vk = event.code + + # Update the modifier state + if vk in self._MODIFIERS: + modifier = self._MODIFIERS[vk] + if is_press: + self._modifiers.add(modifier) + elif modifier in self._modifiers: + self._modifiers.remove(modifier) + + # Attempt to map the virtual key code to a key + try: + key = self._layout.for_vk(vk, self._modifiers) + except KeyError: + try: + key = next( + key + for key in Key + if key.value.vk == vk) + except StopIteration: + key = KeyCode.from_vk(vk) + + # We do not know whether these events are injected + if is_press: + self.on_press(key, False) + else: + self.on_release(key, False) + + +try: + #: The keyboard layout. + LAYOUT = Layout() +except subprocess.CalledProcessError as e: + raise ImportError('failed to load keyboard layout: "' + str(e) + ( + '"; please make sure you are root' if os.getuid() != 1 else '"')) +except OSError as e: + raise ImportError({ + errno.ENOENT: 'the binary dumpkeys is not installed'}.get( + e.args[0], + str(e))) diff --git a/PortablePython/Lib/site-packages/pynput/keyboard/_win32.py b/PortablePython/Lib/site-packages/pynput/keyboard/_win32.py new file mode 100644 index 0000000..fbbcc98 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/keyboard/_win32.py @@ -0,0 +1,389 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The keyboard implementation for *Windows*. +""" + +# pylint: disable=C0111 +# The documentation is extracted from the base classes + +# pylint: disable=R0903 +# We implement stubs + +import contextlib +import ctypes +import enum +import six + +from ctypes import wintypes + +import pynput._util.win32_vks as VK + +from pynput._util import AbstractListener +from pynput._util.win32 import ( + INPUT, + INPUT_union, + KEYBDINPUT, + KeyTranslator, + ListenerMixin, + MapVirtualKey, + SendInput, + SystemHook, + VkKeyScan) +from . import _base + + +class KeyCode(_base.KeyCode): + _PLATFORM_EXTENSIONS = ( + # Any extra flags. + '_flags', + + #: The scan code. + '_scan', + ) + + # Be explicit about fields + _flags = None + _scan = None + + def _parameters(self, is_press): + """The parameters to pass to ``SendInput`` to generate this key. + + :param bool is_press: Whether to generate a press event. + + :return: all arguments to pass to ``SendInput`` for this key + + :rtype: dict + + :raise ValueError: if this key is a unicode character that cannot be + represented by a single UTF-16 value + """ + if self.vk: + vk = self.vk + scan = self._scan \ + or MapVirtualKey(vk, MapVirtualKey.MAPVK_VK_TO_VSC) + flags = 0 + elif ord(self.char) > 0xFFFF: + raise ValueError + else: + res = VkKeyScan(self.char) + if (res >> 8) & 0xFF == 0: + vk = res & 0xFF + scan = self._scan \ + or MapVirtualKey(vk, MapVirtualKey.MAPVK_VK_TO_VSC) + flags = 0 + else: + vk = 0 + scan = ord(self.char) + flags = KEYBDINPUT.UNICODE + state_flags = (KEYBDINPUT.KEYUP if not is_press else 0) + return dict( + dwFlags=(self._flags or 0) | flags | state_flags, + wVk=vk, + wScan=scan) + + @classmethod + def _from_ext(cls, vk, **kwargs): + """Creates an extended key code. + + :param vk: The virtual key code. + + :param kwargs: Any other parameters to pass. + + :return: a key code + """ + return cls.from_vk(vk, _flags=KEYBDINPUT.EXTENDEDKEY, **kwargs) + + +# pylint: disable=W0212 +class Key(enum.Enum): + alt = KeyCode.from_vk(VK.MENU) + alt_l = KeyCode.from_vk(VK.LMENU) + alt_r = KeyCode._from_ext(VK.RMENU) + alt_gr = KeyCode.from_vk(VK.RMENU) + backspace = KeyCode.from_vk(VK.BACK) + caps_lock = KeyCode.from_vk(VK.CAPITAL) + cmd = KeyCode.from_vk(VK.LWIN) + cmd_l = KeyCode.from_vk(VK.LWIN) + cmd_r = KeyCode.from_vk(VK.RWIN) + ctrl = KeyCode.from_vk(VK.CONTROL) + ctrl_l = KeyCode.from_vk(VK.LCONTROL) + ctrl_r = KeyCode._from_ext(VK.RCONTROL) + delete = KeyCode._from_ext(VK.DELETE) + down = KeyCode._from_ext(VK.DOWN) + end = KeyCode._from_ext(VK.END) + enter = KeyCode.from_vk(VK.RETURN) + esc = KeyCode.from_vk(VK.ESCAPE) + f1 = KeyCode.from_vk(VK.F1) + f2 = KeyCode.from_vk(VK.F2) + f3 = KeyCode.from_vk(VK.F3) + f4 = KeyCode.from_vk(VK.F4) + f5 = KeyCode.from_vk(VK.F5) + f6 = KeyCode.from_vk(VK.F6) + f7 = KeyCode.from_vk(VK.F7) + f8 = KeyCode.from_vk(VK.F8) + f9 = KeyCode.from_vk(VK.F9) + f10 = KeyCode.from_vk(VK.F10) + f11 = KeyCode.from_vk(VK.F11) + f12 = KeyCode.from_vk(VK.F12) + f13 = KeyCode.from_vk(VK.F13) + f14 = KeyCode.from_vk(VK.F14) + f15 = KeyCode.from_vk(VK.F15) + f16 = KeyCode.from_vk(VK.F16) + f17 = KeyCode.from_vk(VK.F17) + f18 = KeyCode.from_vk(VK.F18) + f19 = KeyCode.from_vk(VK.F19) + f20 = KeyCode.from_vk(VK.F20) + f21 = KeyCode.from_vk(VK.F21) + f22 = KeyCode.from_vk(VK.F22) + f23 = KeyCode.from_vk(VK.F23) + f24 = KeyCode.from_vk(VK.F24) + home = KeyCode._from_ext(VK.HOME) + left = KeyCode._from_ext(VK.LEFT) + page_down = KeyCode._from_ext(VK.NEXT) + page_up = KeyCode._from_ext(VK.PRIOR) + right = KeyCode._from_ext(VK.RIGHT) + shift = KeyCode.from_vk(VK.LSHIFT) + shift_l = KeyCode.from_vk(VK.LSHIFT) + shift_r = KeyCode.from_vk(VK.RSHIFT) + space = KeyCode.from_vk(VK.SPACE, char=' ') + tab = KeyCode.from_vk(VK.TAB) + up = KeyCode._from_ext(VK.UP) + + media_play_pause = KeyCode._from_ext(VK.MEDIA_PLAY_PAUSE) + media_stop = KeyCode._from_ext(VK.MEDIA_STOP) + media_volume_mute = KeyCode._from_ext(VK.VOLUME_MUTE) + media_volume_down = KeyCode._from_ext(VK.VOLUME_DOWN) + media_volume_up = KeyCode._from_ext(VK.VOLUME_UP) + media_previous = KeyCode._from_ext(VK.MEDIA_PREV_TRACK) + media_next = KeyCode._from_ext(VK.MEDIA_NEXT_TRACK) + + insert = KeyCode._from_ext(VK.INSERT) + menu = KeyCode.from_vk(VK.APPS) + num_lock = KeyCode._from_ext(VK.NUMLOCK) + pause = KeyCode.from_vk(VK.PAUSE) + print_screen = KeyCode._from_ext(VK.SNAPSHOT) + scroll_lock = KeyCode.from_vk(VK.SCROLL) +# pylint: enable=W0212 + + +class Controller(_base.Controller): + _KeyCode = KeyCode + _Key = Key + + def __init__(self, *args, **kwargs): + super(Controller, self).__init__(*args, **kwargs) + + def _handle(self, key, is_press): + try: + SendInput( + 1, + ctypes.byref(INPUT( + type=INPUT.KEYBOARD, + value=INPUT_union( + ki=KEYBDINPUT(**key._parameters(is_press))))), + ctypes.sizeof(INPUT)) + except ValueError: + # If key._parameters raises ValueError, the key is a unicode + # characters outsice of the range of a single UTF-16 value, and we + # must break it up into its surrogates + byte_data = bytearray(key.char.encode('utf-16le')) + surrogates = [ + byte_data[i] | (byte_data[i + 1] << 8) + for i in range(0, len(byte_data), 2)] + + state_flags = KEYBDINPUT.UNICODE \ + | (KEYBDINPUT.KEYUP if not is_press else 0) + + SendInput( + len(surrogates), + (INPUT * len(surrogates))(*( + INPUT( + INPUT.KEYBOARD, + INPUT_union( + ki=KEYBDINPUT( + dwFlags=state_flags, + wScan=scan))) + for scan in surrogates)), + ctypes.sizeof(INPUT)) + + +class Listener(ListenerMixin, _base.Listener): + #: The Windows hook ID for low level keyboard events, ``WH_KEYBOARD_LL`` + _EVENTS = 13 + + _WM_INPUTLANGCHANGE = 0x0051 + _WM_KEYDOWN = 0x0100 + _WM_KEYUP = 0x0101 + _WM_SYSKEYDOWN = 0x0104 + _WM_SYSKEYUP = 0x0105 + + # A bit flag attached to messages indicating that the payload is an actual + # UTF-16 character code + _UTF16_FLAG = 0x1000 + + # A bit flag attached to messages indicating that the event was injected + _INJECTED_FLAG = 0x2000 + + # A special virtual key code designating unicode characters + _VK_PACKET = 0xE7 + + #: The messages that correspond to a key press + _PRESS_MESSAGES = (_WM_KEYDOWN, _WM_SYSKEYDOWN) + + #: The messages that correspond to a key release + _RELEASE_MESSAGES = (_WM_KEYUP, _WM_SYSKEYUP) + + #: Additional window messages to propagate to the subclass handler. + _WM_NOTIFICATIONS = ( + _WM_INPUTLANGCHANGE, + ) + + #: A mapping from keysym to special key + _SPECIAL_KEYS = { + key.value.vk: key + for key in Key} + + _HANDLED_EXCEPTIONS = ( + SystemHook.SuppressException,) + + class _KBDLLHOOKSTRUCT(ctypes.Structure): + """Contains information about a mouse event passed to a + ``WH_KEYBOARD_LL`` hook procedure, ``LowLevelKeyboardProc``. + """ + LLKHF_INJECTED = 0x00000010 + LLKHF_LOWER_IL_INJECTED = 0x00000002 + _fields_ = [ + ('vkCode', wintypes.DWORD), + ('scanCode', wintypes.DWORD), + ('flags', wintypes.DWORD), + ('time', wintypes.DWORD), + ('dwExtraInfo', ctypes.c_void_p)] + + #: A pointer to a :class:`KBDLLHOOKSTRUCT` + _LPKBDLLHOOKSTRUCT = ctypes.POINTER(_KBDLLHOOKSTRUCT) + + def __init__(self, *args, **kwargs): + super(Listener, self).__init__(*args, **kwargs) + self._translator = KeyTranslator() + self._event_filter = self._options.get( + 'event_filter', + lambda msg, data: True) + + def _convert(self, code, msg, lpdata): + if code != SystemHook.HC_ACTION: + return + + data = ctypes.cast(lpdata, self._LPKBDLLHOOKSTRUCT).contents + is_packet = data.vkCode == self._VK_PACKET + injected = (data.flags & (0 + | self._KBDLLHOOKSTRUCT.LLKHF_INJECTED + | self._KBDLLHOOKSTRUCT.LLKHF_LOWER_IL_INJECTED)) != 0 + message = (msg + | (self._UTF16_FLAG if is_packet else 0) + | (self._INJECTED_FLAG if injected else 0)) + + # Suppress further propagation of the event if it is filtered + if self._event_filter(msg, data) is False: + return None + elif is_packet: + return (message, data.scanCode) + else: + return (message, data.vkCode) + + @AbstractListener._emitter + def _process(self, wparam, lparam): + msg = wparam + vk = lparam + + # If the key has the UTF-16 flag, we treat it as a unicode character, + # otherwise convert the event to a KeyCode; this may fail, and in that + # case we pass None + is_utf16 = msg & self._UTF16_FLAG + injected = bool(msg & self._INJECTED_FLAG) + message = msg & ~(self._UTF16_FLAG | self._INJECTED_FLAG) + if is_utf16: + scan = vk + key = KeyCode.from_char(six.unichr(scan)) + else: + try: + key = self._event_to_key(msg, vk) + except OSError: + key = None + + if message in self._PRESS_MESSAGES: + self.on_press(key, injected) + + elif message in self._RELEASE_MESSAGES: + self.on_release(key, injected) + + # pylint: disable=R0201 + @contextlib.contextmanager + def _receive(self): + """An empty context manager; we do not need to fake keyboard events. + """ + yield + # pylint: enable=R0201 + + def _on_notification(self, code, wparam, lparam): + """Receives ``WM_INPUTLANGCHANGE`` and updates the cached layout. + """ + if code == self._WM_INPUTLANGCHANGE: + self._translator.update_layout() + + def _event_to_key(self, msg, vk): + """Converts an :class:`_KBDLLHOOKSTRUCT` to a :class:`KeyCode`. + + :param msg: The message received. + + :param vk: The virtual key code to convert. + + :return: a :class:`pynput.keyboard.KeyCode` + + :raises OSError: if the message and data could not be converted + """ + # If the virtual key code corresponds to a Key value, we prefer that + if vk in self._SPECIAL_KEYS: + return self._SPECIAL_KEYS[vk] + else: + return KeyCode(**self._translate( + vk, + msg in self._PRESS_MESSAGES)) + + def _translate(self, vk, is_press): + """Translates a virtual key code to a parameter list passable to + :class:`pynput.keyboard.KeyCode`. + + :param int vk: The virtual key code. + + :param bool is_press: Whether this is a press event. + + :return: a parameter list to the :class:`pynput.keyboard.KeyCode` + constructor + """ + return self._translator(vk, is_press) + + def canonical(self, key): + # If the key has a scan code, and we can find the character for it, + # return that, otherwise call the super class + scan = getattr(key, '_scan', None) + if scan is not None: + char = self._translator.char_from_scan(scan) + if char is not None: + return KeyCode.from_char(char) + + return super(Listener, self).canonical(key) diff --git a/PortablePython/Lib/site-packages/pynput/keyboard/_xorg.py b/PortablePython/Lib/site-packages/pynput/keyboard/_xorg.py new file mode 100644 index 0000000..7011a5a --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/keyboard/_xorg.py @@ -0,0 +1,667 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The keyboard implementation for *Xorg*. +""" + +# pylint: disable=C0111 +# The documentation is extracted from the base classes + +# pylint: disable=R0903 +# We implement stubs + +# pylint: disable=W0611 +try: + import pynput._util.xorg +except Exception as e: + raise ImportError('failed to acquire X connection: {}'.format(str(e)), e) +# pylint: enable=W0611 + +import enum +import threading + +import Xlib.display +import Xlib.ext +import Xlib.ext.xtest +import Xlib.X +import Xlib.XK +import Xlib.protocol +import Xlib.keysymdef.xkb + +from pynput._util import NotifierMixin +from pynput._util.xorg import ( + alt_mask, + alt_gr_mask, + char_to_keysym, + display_manager, + index_to_shift, + keyboard_mapping, + ListenerMixin, + numlock_mask, + shift_to_index, + symbol_to_keysym) +from pynput._util.xorg_keysyms import ( + CHARS, + DEAD_KEYS, + KEYPAD_KEYS, + KEYSYMS, + SYMBOLS) +from . import _base + + +class KeyCode(_base.KeyCode): + _PLATFORM_EXTENSIONS = ( + # The symbol name for this key + '_symbol', + ) + + # Be explicit about fields + _symbol = None + + @classmethod + def _from_symbol(cls, symbol, **kwargs): + """Creates a key from a symbol. + + :param str symbol: The symbol name. + + :return: a key code + """ + # First try simple translation + keysym = Xlib.XK.string_to_keysym(symbol) + if keysym: + return cls.from_vk(keysym, _symbol=symbol, **kwargs) + + # If that fails, try checking a module attribute of Xlib.keysymdef.xkb + if not keysym: + # pylint: disable=W0702; we want to ignore errors + try: + symbol = 'XK_' + symbol + return cls.from_vk( + getattr(Xlib.keysymdef.xkb, symbol, 0), + _symbol=symbol, + **kwargs) + except: + return cls.from_vk( + SYMBOLS.get(symbol, (0,))[0], + _symbol=symbol, + **kwargs) + # pylint: enable=W0702 + + @classmethod + def _from_media(cls, name, **kwargs): + """Creates a media key from a partial name. + + :param str name: The name. The actual symbol name will be this string + with ``'XF86_Audio'`` prepended. + + :return: a key code + """ + return cls._from_symbol('XF86_Audio' + name, **kwargs) + + +# pylint: disable=W0212 +class Key(enum.Enum): + # Default keys + alt = KeyCode._from_symbol('Alt_L') + alt_l = KeyCode._from_symbol('Alt_L') + alt_r = KeyCode._from_symbol('Alt_R') + alt_gr = KeyCode._from_symbol('Mode_switch') + backspace = KeyCode._from_symbol('BackSpace') + caps_lock = KeyCode._from_symbol('Caps_Lock') + cmd = KeyCode._from_symbol('Super_L') + cmd_l = KeyCode._from_symbol('Super_L') + cmd_r = KeyCode._from_symbol('Super_R') + ctrl = KeyCode._from_symbol('Control_L') + ctrl_l = KeyCode._from_symbol('Control_L') + ctrl_r = KeyCode._from_symbol('Control_R') + delete = KeyCode._from_symbol('Delete') + down = KeyCode._from_symbol('Down') + end = KeyCode._from_symbol('End') + enter = KeyCode._from_symbol('Return') + esc = KeyCode._from_symbol('Escape') + f1 = KeyCode._from_symbol('F1') + f2 = KeyCode._from_symbol('F2') + f3 = KeyCode._from_symbol('F3') + f4 = KeyCode._from_symbol('F4') + f5 = KeyCode._from_symbol('F5') + f6 = KeyCode._from_symbol('F6') + f7 = KeyCode._from_symbol('F7') + f8 = KeyCode._from_symbol('F8') + f9 = KeyCode._from_symbol('F9') + f10 = KeyCode._from_symbol('F10') + f11 = KeyCode._from_symbol('F11') + f12 = KeyCode._from_symbol('F12') + f13 = KeyCode._from_symbol('F13') + f14 = KeyCode._from_symbol('F14') + f15 = KeyCode._from_symbol('F15') + f16 = KeyCode._from_symbol('F16') + f17 = KeyCode._from_symbol('F17') + f18 = KeyCode._from_symbol('F18') + f19 = KeyCode._from_symbol('F19') + f20 = KeyCode._from_symbol('F20') + home = KeyCode._from_symbol('Home') + left = KeyCode._from_symbol('Left') + page_down = KeyCode._from_symbol('Page_Down') + page_up = KeyCode._from_symbol('Page_Up') + right = KeyCode._from_symbol('Right') + shift = KeyCode._from_symbol('Shift_L') + shift_l = KeyCode._from_symbol('Shift_L') + shift_r = KeyCode._from_symbol('Shift_R') + space = KeyCode._from_symbol('space', char=' ') + tab = KeyCode._from_symbol('Tab') + up = KeyCode._from_symbol('Up') + + media_play_pause = KeyCode._from_media('Play') + media_volume_mute = KeyCode._from_media('Mute') + media_volume_down = KeyCode._from_media('LowerVolume') + media_volume_up = KeyCode._from_media('RaiseVolume') + media_previous = KeyCode._from_media('Prev') + media_next = KeyCode._from_media('Next') + + insert = KeyCode._from_symbol('Insert') + menu = KeyCode._from_symbol('Menu') + num_lock = KeyCode._from_symbol('Num_Lock') + pause = KeyCode._from_symbol('Pause') + print_screen = KeyCode._from_symbol('Print') + scroll_lock = KeyCode._from_symbol('Scroll_Lock') +# pylint: enable=W0212 + + +class Controller(NotifierMixin, _base.Controller): + _KeyCode = KeyCode + _Key = Key + + #: The shift mask for :attr:`Key.ctrl` + CTRL_MASK = Xlib.X.ControlMask + + #: The shift mask for :attr:`Key.shift` + SHIFT_MASK = Xlib.X.ShiftMask + + def __init__(self, *args, **kwargs): + super(Controller, self).__init__(*args, **kwargs) + self._display = Xlib.display.Display() + self._keyboard_mapping = None + self._borrows = {} + self._borrow_lock = threading.RLock() + + # pylint: disable=C0103; this is treated as a class scope constant, but + # we cannot set it in the class scope, as it requires a Display instance + self.ALT_MASK = alt_mask(self._display) + self.ALT_GR_MASK = alt_gr_mask(self._display) + # pylint: enable=C0103 + + def __del__(self): + if hasattr(self, '_display'): + self._display.close() + + @property + def keyboard_mapping(self): + """A mapping from *keysyms* to *key codes*. + + Each value is the tuple ``(key_code, shift_state)``. By sending an + event with the specified *key code* and shift state, the specified + *keysym* will be touched. + """ + if not self._keyboard_mapping: + self._update_keyboard_mapping() + return self._keyboard_mapping + + def _handle(self, key, is_press): + """Resolves a key identifier and sends a keyboard event. + + :param int key: The key to handle. + :param bool is_press: Whether this is a press. + """ + event = Xlib.display.event.KeyPress if is_press \ + else Xlib.display.event.KeyRelease + keysym = self._keysym(key) + + # Make sure to verify that the key was resolved + if keysym is None: + raise self.InvalidKeyException(key) + + # If the key has a virtual key code, use that immediately with + # fake_input; fake input,being an X server extension, has access to + # more internal state that we do + if key.vk is not None: + with display_manager(self._display) as dm: + Xlib.ext.xtest.fake_input( + dm, + Xlib.X.KeyPress if is_press else Xlib.X.KeyRelease, + dm.keysym_to_keycode(key.vk)) + + # Otherwise use XSendEvent; we need to use this in the general case to + # work around problems with keyboard layouts + else: + try: + keycode, shift_state = self.keyboard_mapping[keysym] + self._send_key(event, keycode, shift_state) + + except KeyError: + with self._borrow_lock: + keycode, index, count = self._borrows[keysym] + self._send_key( + event, + keycode, + index_to_shift(self._display, index)) + count += 1 if is_press else -1 + self._borrows[keysym] = (keycode, index, count) + + # Notify any running listeners + self._emit('_on_fake_event', key, is_press) + + def _keysym(self, key): + """Converts a key to a *keysym*. + + :param KeyCode key: The key code to convert. + """ + return self._resolve_dead(key) if key.is_dead else None \ + or self._resolve_special(key) \ + or self._resolve_normal(key) \ + or self._resolve_borrowed(key) \ + or self._resolve_borrowing(key) + + def _send_key(self, event, keycode, shift_state): + """Sends a single keyboard event. + + :param event: The *X* keyboard event. + + :param int keycode: The calculated keycode. + + :param int shift_state: The shift state. The actual value used is + :attr:`shift_state` or'd with this value. + """ + with display_manager(self._display) as dm, self.modifiers as modifiers: + # Under certain cimcumstances, such as when running under Xephyr, + # the value returned by dm.get_input_focus is an int + window = dm.get_input_focus().focus + send_event = getattr( + window, + 'send_event', + lambda event: dm.send_event(window, event)) + send_event(event( + detail=keycode, + state=shift_state | self._shift_mask(modifiers), + time=0, + root=dm.screen().root, + window=window, + same_screen=0, + child=Xlib.X.NONE, + root_x=0, root_y=0, event_x=0, event_y=0)) + + def _resolve_dead(self, key): + """Tries to resolve a dead key. + + :param str identifier: The identifier to resolve. + """ + # pylint: disable=W0702; we want to ignore errors + try: + keysym, _ = SYMBOLS[CHARS[key.combining]] + except: + return None + # pylint: enable=W0702 + + if keysym not in self.keyboard_mapping: + return None + + return keysym + + def _resolve_special(self, key): + """Tries to resolve a special key. + + A special key has the :attr:`~KeyCode.vk` attribute set. + + :param KeyCode key: The key to resolve. + """ + if not key.vk: + return None + + return key.vk + + def _resolve_normal(self, key): + """Tries to resolve a normal key. + + A normal key exists on the keyboard, and is typed by pressing + and releasing a simple key, possibly in combination with a modifier. + + :param KeyCode key: The key to resolve. + """ + keysym = self._key_to_keysym(key) + if keysym is None: + return None + + if keysym not in self.keyboard_mapping: + return None + + return keysym + + def _resolve_borrowed(self, key): + """Tries to resolve a key by looking up the already borrowed *keysyms*. + + A borrowed *keysym* does not exist on the keyboard, but has been + temporarily added to the layout. + + :param KeyCode key: The key to resolve. + """ + keysym = self._key_to_keysym(key) + if keysym is None: + return None + + with self._borrow_lock: + if keysym not in self._borrows: + return None + + return keysym + + def _resolve_borrowing(self, key): + """Tries to resolve a key by modifying the layout temporarily. + + A borrowed *keysym* does not exist on the keyboard, but is temporarily + added to the layout. + + :param KeyCode key: The key to resolve. + """ + keysym = self._key_to_keysym(key) + if keysym is None: + return None + + mapping = self._display.get_keyboard_mapping(8, 255 - 8) + + def i2kc(index): + return index + 8 + + def kc2i(keycode): + return keycode - 8 + + #: Finds a keycode and index by looking at already used keycodes + def reuse(): + for _, (keycode, _, _) in self._borrows.items(): + keycodes = mapping[kc2i(keycode)] + + # Only the first four items are addressable by X + for index in range(4): + if not keycodes[index]: + return keycode, index + + #: Finds a keycode and index by using a new keycode + def borrow(): + for i, keycodes in enumerate(mapping): + if not any(keycodes): + return i2kc(i), 0 + + #: Finds a keycode and index by reusing an old, unused one + def overwrite(): + for keysym, (keycode, index, count) in self._borrows.items(): + if count < 1: + del self._borrows[keysym] + return keycode, index + + #: Registers a keycode for a specific key and modifier state + def register(dm, keycode, index): + i = kc2i(keycode) + + # Check for use of empty mapping with a character that has upper + # and lower forms + lower = key.char.lower() + upper = key.char.upper() + if lower != upper and len(lower) == 1 and len(upper) == 1 and all( + m == Xlib.XK.NoSymbol + for m in mapping[i]): + lower = self._key_to_keysym(KeyCode.from_char(lower)) + upper = self._key_to_keysym(KeyCode.from_char(upper)) + if lower: + mapping[i][0] = lower + self._borrows[lower] = (keycode, 0, 0) + if upper: + mapping[i][1] = upper + self._borrows[upper] = (keycode, 1, 0) + else: + mapping[i][index] = keysym + self._borrows[keysym] = (keycode, index, 0) + dm.change_keyboard_mapping(keycode, mapping[i:i + 1]) + + try: + with display_manager(self._display) as dm, self._borrow_lock as _: + # First try an already used keycode, then try a new one, and + # fall back on reusing one that is not currently pressed + register(dm, *( + reuse() or + borrow() or + overwrite())) + return keysym + + except TypeError: + return None + + def _key_to_keysym(self, key): + """Converts a character key code to a *keysym*. + + :param KeyCode key: The key code. + + :return: a keysym if found + :rtype: int or None + """ + # If the key code already has a VK, simply return it + if key.vk is not None: + return key.vk + + # If the character has no associated symbol, we try to map the + # character to a keysym + symbol = CHARS.get(key.char, None) + if symbol is None: + return char_to_keysym(key.char) + + # Otherwise we attempt to convert the symbol to a keysym + # pylint: disable=W0702; we want to ignore errors + try: + return symbol_to_keysym(symbol) + except: + try: + return SYMBOLS[symbol][0] + except: + return None + # pylint: enable=W0702 + + def _shift_mask(self, modifiers): + """The *X* modifier mask to apply for a set of modifiers. + + :param set modifiers: A set of active modifiers for which to get the + shift mask. + """ + return ( + 0 + | (self.ALT_MASK + if Key.alt in modifiers else 0) + + | (self.ALT_GR_MASK + if Key.alt_gr in modifiers else 0) + + | (self.CTRL_MASK + if Key.ctrl in modifiers else 0) + + | (self.SHIFT_MASK + if Key.shift in modifiers else 0)) + + def _update_keyboard_mapping(self): + """Updates the keyboard mapping. + """ + with display_manager(self._display) as dm: + self._keyboard_mapping = keyboard_mapping(dm) + + +@Controller._receiver +class Listener(ListenerMixin, _base.Listener): + _EVENTS = ( + Xlib.X.KeyPress, + Xlib.X.KeyRelease) + + #: A mapping from keysym to special key + _SPECIAL_KEYS = { + key.value.vk: key + for key in Key} + + #: A mapping from numeric keypad keys to keys + _KEYPAD_KEYS = { + KEYPAD_KEYS['KP_0']: KeyCode.from_char('0'), + KEYPAD_KEYS['KP_1']: KeyCode.from_char('1'), + KEYPAD_KEYS['KP_2']: KeyCode.from_char('2'), + KEYPAD_KEYS['KP_3']: KeyCode.from_char('3'), + KEYPAD_KEYS['KP_4']: KeyCode.from_char('4'), + KEYPAD_KEYS['KP_5']: KeyCode.from_char('5'), + KEYPAD_KEYS['KP_6']: KeyCode.from_char('6'), + KEYPAD_KEYS['KP_7']: KeyCode.from_char('7'), + KEYPAD_KEYS['KP_8']: KeyCode.from_char('8'), + KEYPAD_KEYS['KP_9']: KeyCode.from_char('9'), + KEYPAD_KEYS['KP_Add']: KeyCode.from_char('+'), + KEYPAD_KEYS['KP_Decimal']: KeyCode.from_char(','), + KEYPAD_KEYS['KP_Delete']: Key.delete, + KEYPAD_KEYS['KP_Divide']: KeyCode.from_char('/'), + KEYPAD_KEYS['KP_Down']: Key.down, + KEYPAD_KEYS['KP_End']: Key.end, + KEYPAD_KEYS['KP_Enter']: Key.enter, + KEYPAD_KEYS['KP_Equal']: KeyCode.from_char('='), + KEYPAD_KEYS['KP_F1']: Key.f1, + KEYPAD_KEYS['KP_F2']: Key.f2, + KEYPAD_KEYS['KP_F3']: Key.f3, + KEYPAD_KEYS['KP_F4']: Key.f4, + KEYPAD_KEYS['KP_Home']: Key.home, + KEYPAD_KEYS['KP_Insert']: Key.insert, + KEYPAD_KEYS['KP_Left']: Key.left, + KEYPAD_KEYS['KP_Multiply']: KeyCode.from_char('*'), + KEYPAD_KEYS['KP_Page_Down']: Key.page_down, + KEYPAD_KEYS['KP_Page_Up']: Key.page_up, + KEYPAD_KEYS['KP_Right']: Key.right, + KEYPAD_KEYS['KP_Space']: Key.space, + KEYPAD_KEYS['KP_Subtract']: KeyCode.from_char('-'), + KEYPAD_KEYS['KP_Tab']: Key.tab, + KEYPAD_KEYS['KP_Up']: Key.up} + + def __init__(self, *args, **kwargs): + super(Listener, self).__init__(*args, **kwargs) + self._keyboard_mapping = None + + def _run(self): + with self._receive(): + super(Listener, self)._run() + + def _initialize(self, display): + # Get the keyboard mapping to be able to translate event details to + # key codes + min_keycode = display.display.info.min_keycode + keycode_count = display.display.info.max_keycode - min_keycode + 1 + self._keyboard_mapping = display.get_keyboard_mapping( + min_keycode, keycode_count) + + def _handle_message(self, display, event, injected): + # Convert the event to a KeyCode; this may fail, and in that case we + # pass None + try: + key = self._event_to_key(display, event) + except IndexError: + key = None + + if event.type == Xlib.X.KeyPress: + self.on_press(key, injected) + + elif event.type == Xlib.X.KeyRelease: + self.on_release(key, injected) + + def _suppress_start(self, display): + display.screen().root.grab_keyboard( + self._event_mask, Xlib.X.GrabModeAsync, Xlib.X.GrabModeAsync, + Xlib.X.CurrentTime) + + def _suppress_stop(self, display): + display.ungrab_keyboard(Xlib.X.CurrentTime) + + def _on_fake_event(self, key, is_press): + """The handler for fake press events sent by the controllers. + + :param KeyCode key: The key pressed. + + :param bool is_press: Whether this is a press event. + """ + (self.on_press if is_press else self.on_release)( + self._SPECIAL_KEYS.get(key.vk, key), True) + + def _keycode_to_keysym(self, display, keycode, index): + """Converts a keycode and shift state index to a keysym. + + This method uses a simplified version of the *X* convention to locate + the correct keysym in the display table: since this method is only used + to locate special keys, alphanumeric keys are not treated specially. + + :param display: The current *X* display. + + :param keycode: The keycode. + + :param index: The shift state index. + + :return: a keysym + """ + keysym = display.keycode_to_keysym(keycode, index) + if keysym: + return keysym + elif index & 0x2: + return self._keycode_to_keysym(display, keycode, index & ~0x2) + elif index & 0x1: + return self._keycode_to_keysym(display, keycode, index & ~0x1) + else: + return 0 + + def _event_to_key(self, display, event): + """Converts an *X* event to a :class:`KeyCode`. + + :param display: The current *X* display. + + :param event: The event to convert. + + :return: a :class:`pynput.keyboard.KeyCode` + + :raises IndexError: if the key code is invalid + """ + keycode = event.detail + index = shift_to_index(display, event.state) + + # First try special keys... + keysym = self._keycode_to_keysym(display, keycode, index) + if keysym in self._SPECIAL_KEYS: + return self._SPECIAL_KEYS[keysym] + elif keysym in self._KEYPAD_KEYS: + # We must recalculate the index if numlock is active; index 1 is the + # one to use + try: + return self._KEYPAD_KEYS[ + self._keycode_to_keysym( + display, + keycode, + bool(event.state & numlock_mask(display)))] + except KeyError: + # Since we recalculated the key, this may happen + pass + + # ...then try characters... + name = KEYSYMS.get(keysym, None) + if name is not None and name in SYMBOLS: + char = SYMBOLS[name][1].upper() if index & 1 else SYMBOLS[name][1] + if char in DEAD_KEYS: + return KeyCode.from_dead(DEAD_KEYS[char], vk=keysym) + else: + return KeyCode.from_char(char, vk=keysym) + + # ...and fall back on a virtual key code + return KeyCode.from_vk(keysym) diff --git a/PortablePython/Lib/site-packages/pynput/mouse/__init__.py b/PortablePython/Lib/site-packages/pynput/mouse/__init__.py new file mode 100644 index 0000000..3456629 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/mouse/__init__.py @@ -0,0 +1,107 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The module containing mouse classes. + +See the documentation for more information. +""" + +# pylint: disable=C0103 +# Button, Controller and Listener are not constants + +from pynput._util import backend, Events + + +backend = backend(__name__) +Button = backend.Button +Controller = backend.Controller +Listener = backend.Listener +del backend + + +class Events(Events): + """A mouse event listener supporting synchronous iteration over the events. + + Possible events are: + + :class:`Events.Move` + The mouse was moved. + + :class:`Events.Click` + A mouse button was pressed or released. + + :class:`Events.Scroll` + The device was scrolled. + """ + _Listener = Listener + + class Move(Events.Event): + """A move event. + """ + def __init__(self, x, y, injected): + #: The X screen coordinate. + self.x = x + + #: The Y screen coordinate. + self.y = y + + #: Whether this event is synthetic. + self.injected = injected + + class Click(Events.Event): + """A click event. + """ + def __init__(self, x, y, button, pressed, injected): + #: The X screen coordinate. + self.x = x + + #: The Y screen coordinate. + self.y = y + + #: The button. + self.button = button + + #: Whether the button was pressed. + self.pressed = pressed + + #: Whether this event is synthetic. + self.injected = injected + + class Scroll(Events.Event): + """A scroll event. + """ + def __init__(self, x, y, dx, dy, injected): + #: The X screen coordinate. + self.x = x + + #: The Y screen coordinate. + self.y = y + + #: The number of horisontal steps. + self.dx = dx + + #: The number of vertical steps. + self.dy = dy + + #: Whether this event is synthetic. + self.injected = injected + + def __init__(self): + super(Events, self).__init__( + on_move=self.Move, + on_click=self.Click, + on_scroll=self.Scroll) diff --git a/PortablePython/Lib/site-packages/pynput/mouse/_base.py b/PortablePython/Lib/site-packages/pynput/mouse/_base.py new file mode 100644 index 0000000..186a7d7 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/mouse/_base.py @@ -0,0 +1,281 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +This module contains the base implementation. + +The actual interface to mouse classes is defined here, but the implementation +is located in a platform dependent module. +""" + +# pylint: disable=R0903 +# We implement stubs + +import enum + +from pynput._util import AbstractListener, prefix +from pynput import _logger + + +class Button(enum.Enum): + """The various buttons. + + The actual values for these items differ between platforms. Some + platforms may have additional buttons, but these are guaranteed to be + present everywhere. + """ + #: An unknown button was pressed + unknown = 0 + + #: The left button + left = 1 + + #: The middle button + middle = 2 + + #: The right button + right = 3 + + +class Controller(object): + """A controller for sending virtual mouse events to the system. + """ + def __init__(self): + self._log = _logger(self.__class__) + + @property + def position(self): + """The current position of the mouse pointer. + + This is the tuple ``(x, y)``, and setting it will move the pointer. + """ + return self._position_get() + + @position.setter + def position(self, pos): + self._position_set(pos) + + def scroll(self, dx, dy): + """Sends scroll events. + + :param int dx: The horizontal scroll. The units of scrolling is + undefined. + + :param int dy: The vertical scroll. The units of scrolling is + undefined. + + :raises ValueError: if the values are invalid, for example out of + bounds + """ + self._scroll(dx, dy) + + def press(self, button): + """Emits a button press event at the current position. + + :param Button button: The button to press. + """ + self._press(button) + + def release(self, button): + """Emits a button release event at the current position. + + :param Button button: The button to release. + """ + self._release(button) + + def move(self, dx, dy): + """Moves the mouse pointer a number of pixels from its current + position. + + :param int dx: The horizontal offset. + + :param int dy: The vertical offset. + + :raises ValueError: if the values are invalid, for example out of + bounds + """ + self.position = tuple(sum(i) for i in zip(self.position, (dx, dy))) + + def click(self, button, count=1): + """Emits a button click event at the current position. + + The default implementation sends a series of press and release events. + + :param Button button: The button to click. + + :param int count: The number of clicks to send. + """ + with self as controller: + for _ in range(count): + controller.press(button) + controller.release(button) + + def __enter__(self): + """Begins a series of clicks. + + In the default :meth:`click` implementation, the return value of this + method is used for the calls to :meth:`press` and :meth:`release` + instead of ``self``. + + The default implementation is a no-op. + """ + return self + + def __exit__(self, exc_type, value, traceback): + """Ends a series of clicks. + """ + pass + + def _position_get(self): + """The implementation of the getter for :attr:`position`. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def _position_set(self, pos): + """The implementation of the setter for :attr:`position`. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def _scroll(self, dx, dy): + """The implementation of the :meth:`scroll` method. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def _press(self, button): + """The implementation of the :meth:`press` method. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def _release(self, button): + """The implementation of the :meth:`release` method. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + +# pylint: disable=W0223; This is also an abstract class +class Listener(AbstractListener): + """A listener for mouse events. + + Instances of this class can be used as context managers. This is equivalent + to the following code:: + + listener.start() + try: + listener.wait() + with_statements() + finally: + listener.stop() + + This class inherits from :class:`threading.Thread` and supports all its + methods. It will set :attr:`daemon` to ``True`` when created. + + All callback arguments are optional; a callback may take less arguments + than actually passed, but not more, unless they are optional. + + :param callable on_move: The callback to call when mouse move events occur. + + It will be called with the arguments ``(x, y, injected)``, where ``(x, + y)`` is the new pointer position and ``injected`` whether the event was + injected and thus not generated by an actual input device. If this + callback raises :class:`StopException` or returns ``False``, the + listener is stopped. + + Please note that not all backends support ``injected`` and will always + set it to ``False``. + + :param callable on_click: The callback to call when a mouse button is + clicked. + + It will be called with the arguments ``(x, y, button, pressed, + injected)``, where ``(x, y)`` is the new pointer position, ``button`` + is one of the :class:`Button`, ``pressed`` is whether the button was + pressed and ``injected`` whether the event was injected and thus not + generated by an actual input device. + + If this callback raises :class:`StopException` or returns ``False``, + the listener is stopped. + + Please note that not all backends support ``injected`` and will always + set it to ``False``. + + :param callable on_scroll: The callback to call when mouse scroll + events occur. + + It will be called with the arguments ``(x, y, dx, dy, injected)``, + where ``(x, y)`` is the new pointer position, and ``(dx, dy)`` is the + scroll vector and ``injected`` whether the event was injected and thus + not generated by an actual input device. + + If this callback raises :class:`StopException` or returns ``False``, + the listener is stopped. + + Please note that not all backends support ``injected`` and will always + set it to ``False``. + + :param bool suppress: Whether to suppress events. Setting this to ``True`` + will prevent the input events from being passed to the rest of the + system. + + :param kwargs: Any non-standard platform dependent options. These should be + prefixed with the platform name thus: ``darwin_``, ``xorg_`` or + ``win32_``. + + Supported values are: + + ``darwin_intercept`` + A callable taking the arguments ``(event_type, event)``, where + ``event_type`` is any mouse related event type constant, and + ``event`` is a ``CGEventRef``. + + This callable can freely modify the event using functions like + ``Quartz.CGEventSetIntegerValueField``. If this callable does not + return the event, the event is suppressed system wide. + + ``win32_event_filter`` + A callable taking the arguments ``(msg, data)``, where ``msg`` is + the current message, and ``data`` associated data as a + `MSLLHOOKSTRUCT `_. + + If this callback returns ``False``, the event will not + be propagated to the listener callback. + + If ``self.suppress_event()`` is called, the event is suppressed + system wide. + """ + def __init__(self, on_move=None, on_click=None, on_scroll=None, + suppress=False, **kwargs): + self._log = _logger(self.__class__) + option_prefix = prefix(Listener, self.__class__) + self._options = { + key[len(option_prefix):]: value + for key, value in kwargs.items() + if key.startswith(option_prefix)} + super(Listener, self).__init__( + on_move=self._wrap(on_move, 3), + on_click=self._wrap(on_click, 5), + on_scroll=self._wrap(on_scroll, 5), + suppress=suppress) +# pylint: enable=W0223 diff --git a/PortablePython/Lib/site-packages/pynput/mouse/_darwin.py b/PortablePython/Lib/site-packages/pynput/mouse/_darwin.py new file mode 100644 index 0000000..b5dbe3c --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/mouse/_darwin.py @@ -0,0 +1,212 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The mouse implementation for *macOS*. +""" + +# pylint: disable=C0111 +# The documentation is extracted from the base classes + +# pylint: disable=R0903 +# We implement stubs + +import enum +import Quartz + +from AppKit import NSEvent + +from pynput._util.darwin import ( + ListenerMixin) +from . import _base + + +def _button_value(base_name, mouse_button): + """Generates the value tuple for a :class:`Button` value. + + :param str base_name: The base name for the button. This should be a string + like ``'kCGEventLeftMouse'``. + + :param int mouse_button: The mouse button ID. + + :return: a value tuple + """ + return ( + tuple( + getattr(Quartz, '%sMouse%s' % (base_name, name)) + for name in ('Down', 'Up', 'Dragged')), + mouse_button) + + +class Button(enum.Enum): + """The various buttons. + """ + unknown = None + left = _button_value('kCGEventLeft', 0) + middle = _button_value('kCGEventOther', 2) + right = _button_value('kCGEventRight', 1) + + +class Controller(_base.Controller): + #: The scroll speed + _SCROLL_SPEED = 10 + + def __init__(self, *args, **kwargs): + super(Controller, self).__init__(*args, **kwargs) + self._click = None + self._drag_button = None + + def _position_get(self): + pos = NSEvent.mouseLocation() + + return pos.x, Quartz.CGDisplayPixelsHigh(0) - pos.y + + def _position_set(self, pos): + try: + (_, _, mouse_type), mouse_button = self._drag_button.value + except AttributeError: + mouse_type = Quartz.kCGEventMouseMoved + mouse_button = 0 + + Quartz.CGEventPost( + Quartz.kCGHIDEventTap, + Quartz.CGEventCreateMouseEvent( + None, + mouse_type, + pos, + mouse_button)) + + def _scroll(self, dx, dy): + dx = int(dx) + dy = int(dy) + + Quartz.CGEventPost( + Quartz.kCGHIDEventTap, + Quartz.CGEventCreateScrollWheelEvent( + None, + Quartz.kCGScrollEventUnitPixel, + 2, + dy * self._SCROLL_SPEED, + dx * self._SCROLL_SPEED)) + + def _press(self, button): + (press, _, _), mouse_button = button.value + event = Quartz.CGEventCreateMouseEvent( + None, + press, + self.position, + mouse_button) + + # If we are performing a click, we need to set this state flag + if self._click is not None: + self._click += 1 + Quartz.CGEventSetIntegerValueField( + event, + Quartz.kCGMouseEventClickState, + self._click) + + Quartz.CGEventPost(Quartz.kCGHIDEventTap, event) + + # Store the button to enable dragging + self._drag_button = button + + def _release(self, button): + (_, release, _), mouse_button = button.value + event = Quartz.CGEventCreateMouseEvent( + None, + release, + self.position, + mouse_button) + + # If we are performing a click, we need to set this state flag + if self._click is not None: + Quartz.CGEventSetIntegerValueField( + event, + Quartz.kCGMouseEventClickState, + self._click) + + Quartz.CGEventPost(Quartz.kCGHIDEventTap, event) + + if button == self._drag_button: + self._drag_button = None + + def __enter__(self): + self._click = 0 + return self + + def __exit__(self, exc_type, value, traceback): + self._click = None + + +class Listener(ListenerMixin, _base.Listener): + #: The events that we listen to + _EVENTS = ( + Quartz.CGEventMaskBit(Quartz.kCGEventMouseMoved) | + Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDown) | + Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseUp) | + Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDragged) | + Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDown) | + Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseUp) | + Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDragged) | + Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDown) | + Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseUp) | + Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDragged) | + Quartz.CGEventMaskBit(Quartz.kCGEventScrollWheel)) + + def __init__(self, *args, **kwargs): + super(Listener, self).__init__(*args, **kwargs) + self._intercept = self._options.get( + 'intercept', + None) + + def _handle_message(self, _proxy, event_type, event, _refcon, injected): + """The callback registered with *macOS* for mouse events. + + This method will call the callbacks registered on initialisation. + """ + try: + (px, py) = Quartz.CGEventGetLocation(event) + except AttributeError: + # This happens during teardown of the virtual machine + return + + # Quickly detect the most common event type + if event_type == Quartz.kCGEventMouseMoved: + self.on_move(px, py, injected) + + elif event_type == Quartz.kCGEventScrollWheel: + dx = Quartz.CGEventGetIntegerValueField( + event, + Quartz.kCGScrollWheelEventDeltaAxis2) + dy = Quartz.CGEventGetIntegerValueField( + event, + Quartz.kCGScrollWheelEventDeltaAxis1) + self.on_scroll(px, py, dx, dy, injected) + + else: + for button in Button: + try: + (press, release, drag), _ = button.value + except TypeError: + # Button.unknown cannot be enumerated + continue + + # Press and release generate click events, and drag + # generates move events + if event_type in (press, release): + self.on_click(px, py, button, event_type == press, injected) + elif event_type == drag: + self.on_move(px, py, injected) diff --git a/PortablePython/Lib/site-packages/pynput/mouse/_dummy.py b/PortablePython/Lib/site-packages/pynput/mouse/_dummy.py new file mode 100644 index 0000000..c0a8a74 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/mouse/_dummy.py @@ -0,0 +1,22 @@ +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +This module contains a dummy implementation. + +It cannot be used, but importing it will not raise any exceptions. +""" + +from ._base import Button, Controller, Listener diff --git a/PortablePython/Lib/site-packages/pynput/mouse/_win32.py b/PortablePython/Lib/site-packages/pynput/mouse/_win32.py new file mode 100644 index 0000000..1c0dbb5 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/mouse/_win32.py @@ -0,0 +1,226 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The mouse implementation for *Windows*. +""" + +# pylint: disable=C0111 +# The documentation is extracted from the base classes + +# pylint: disable=R0903 +# We implement stubs + +import ctypes +import enum + +from ctypes import ( + windll, + wintypes) + +from pynput._util import NotifierMixin +from pynput._util.win32 import ( + INPUT, + INPUT_union, + ListenerMixin, + MOUSEINPUT, + SendInput, + SystemHook) +from . import _base + +#: A constant used as a factor when constructing mouse scroll data. +WHEEL_DELTA = 120 + + +class Button(enum.Enum): + """The various buttons. + """ + unknown = None + left = (MOUSEINPUT.LEFTUP, MOUSEINPUT.LEFTDOWN, 0) + middle = (MOUSEINPUT.MIDDLEUP, MOUSEINPUT.MIDDLEDOWN, 0) + right = (MOUSEINPUT.RIGHTUP, MOUSEINPUT.RIGHTDOWN, 0) + x1 = (MOUSEINPUT.XUP, MOUSEINPUT.XDOWN, MOUSEINPUT.XBUTTON1) + x2 = (MOUSEINPUT.XUP, MOUSEINPUT.XDOWN, MOUSEINPUT.XBUTTON2) + + +class Controller(NotifierMixin, _base.Controller): + __GetCursorPos = windll.user32.GetCursorPos + __SetCursorPos = windll.user32.SetCursorPos + + def __init__(self, *args, **kwargs): + super(Controller, self).__init__(*args, **kwargs) + + def _position_get(self): + point = wintypes.POINT() + if self.__GetCursorPos(ctypes.byref(point)): + return (point.x, point.y) + else: + return None + + def _position_set(self, pos): + pos = int(pos[0]), int(pos[1]) + self.__SetCursorPos(*pos) + self._emit('on_move', *pos, True) + + def _scroll(self, dx, dy): + if dy: + SendInput( + 1, + ctypes.byref(INPUT( + type=INPUT.MOUSE, + value=INPUT_union( + mi=MOUSEINPUT( + dwFlags=MOUSEINPUT.WHEEL, + mouseData=int(dy * WHEEL_DELTA))))), + ctypes.sizeof(INPUT)) + + if dx: + SendInput( + 1, + ctypes.byref(INPUT( + type=INPUT.MOUSE, + value=INPUT_union( + mi=MOUSEINPUT( + dwFlags=MOUSEINPUT.HWHEEL, + mouseData=int(dx * WHEEL_DELTA))))), + ctypes.sizeof(INPUT)) + + if dx or dy: + px, py = self._position_get() + self._emit('on_scroll', px, py, dx, dy, True) + + def _press(self, button): + SendInput( + 1, + ctypes.byref(INPUT( + type=INPUT.MOUSE, + value=INPUT_union( + mi=MOUSEINPUT( + dwFlags=button.value[1], + mouseData=button.value[2])))), + ctypes.sizeof(INPUT)) + + def _release(self, button): + SendInput( + 1, + ctypes.byref(INPUT( + type=INPUT.MOUSE, + value=INPUT_union( + mi=MOUSEINPUT( + dwFlags=button.value[0], + mouseData=button.value[2])))), + ctypes.sizeof(INPUT)) + + +@Controller._receiver +class Listener(ListenerMixin, _base.Listener): + #: The Windows hook ID for low level mouse events, ``WH_MOUSE_LL`` + _EVENTS = 14 + + WM_LBUTTONDOWN = 0x0201 + WM_LBUTTONUP = 0x0202 + WM_MBUTTONDOWN = 0x0207 + WM_MBUTTONUP = 0x0208 + WM_MOUSEMOVE = 0x0200 + WM_MOUSEWHEEL = 0x020A + WM_MOUSEHWHEEL = 0x020E + WM_RBUTTONDOWN = 0x0204 + WM_RBUTTONUP = 0x0205 + WM_XBUTTONDOWN = 0x20B + WM_XBUTTONUP = 0x20C + + MK_XBUTTON1 = 0x0020 + MK_XBUTTON2 = 0x0040 + + XBUTTON1 = 1 + XBUTTON2 = 2 + + #: A mapping from messages to button events + CLICK_BUTTONS = { + WM_LBUTTONDOWN: (Button.left, True), + WM_LBUTTONUP: (Button.left, False), + WM_MBUTTONDOWN: (Button.middle, True), + WM_MBUTTONUP: (Button.middle, False), + WM_RBUTTONDOWN: (Button.right, True), + WM_RBUTTONUP: (Button.right, False)} + + #: A mapping from message to X button events. + X_BUTTONS = { + WM_XBUTTONDOWN: { + XBUTTON1: (Button.x1, True), + XBUTTON2: (Button.x2, True)}, + WM_XBUTTONUP: { + XBUTTON1: (Button.x1, False), + XBUTTON2: (Button.x2, False)}} + + #: A mapping from messages to scroll vectors + SCROLL_BUTTONS = { + WM_MOUSEWHEEL: (0, 1), + WM_MOUSEHWHEEL: (1, 0)} + + _HANDLED_EXCEPTIONS = ( + SystemHook.SuppressException,) + + class _MSLLHOOKSTRUCT(ctypes.Structure): + """Contains information about a mouse event passed to a ``WH_MOUSE_LL`` + hook procedure, ``MouseProc``. + """ + LLMHF_INJECTED = 0x00000001 + LLMHF_LOWER_IL_INJECTED = 0x00000002 + _fields_ = [ + ('pt', wintypes.POINT), + ('mouseData', wintypes.DWORD), + ('flags', wintypes.DWORD), + ('time', wintypes.DWORD), + ('dwExtraInfo', ctypes.c_void_p)] + + #: A pointer to a :class:`_MSLLHOOKSTRUCT` + _LPMSLLHOOKSTRUCT = ctypes.POINTER(_MSLLHOOKSTRUCT) + + def __init__(self, *args, **kwargs): + super(Listener, self).__init__(*args, **kwargs) + self._event_filter = self._options.get( + 'event_filter', + lambda msg, data: True) + + def _handle_message(self, code, msg, lpdata): + if code != SystemHook.HC_ACTION: + return + + data = ctypes.cast(lpdata, self._LPMSLLHOOKSTRUCT).contents + injected = data.flags & (0 + | self._MSLLHOOKSTRUCT.LLMHF_INJECTED + | self._MSLLHOOKSTRUCT.LLMHF_LOWER_IL_INJECTED) != 0 + + # Suppress further propagation of the event if it is filtered + if self._event_filter(msg, data) is False: + return + + if msg == self.WM_MOUSEMOVE: + self.on_move(data.pt.x, data.pt.y,injected) + + elif msg in self.CLICK_BUTTONS: + button, pressed = self.CLICK_BUTTONS[msg] + self.on_click(data.pt.x, data.pt.y, button, pressed, injected) + + elif msg in self.X_BUTTONS: + button, pressed = self.X_BUTTONS[msg][data.mouseData >> 16] + self.on_click(data.pt.x, data.pt.y, button, pressed, injected) + + elif msg in self.SCROLL_BUTTONS: + mx, my = self.SCROLL_BUTTONS[msg] + dd = wintypes.SHORT(data.mouseData >> 16).value // WHEEL_DELTA + self.on_scroll(data.pt.x, data.pt.y, dd * mx, dd * my, injected) diff --git a/PortablePython/Lib/site-packages/pynput/mouse/_xorg.py b/PortablePython/Lib/site-packages/pynput/mouse/_xorg.py new file mode 100644 index 0000000..93b0a98 --- /dev/null +++ b/PortablePython/Lib/site-packages/pynput/mouse/_xorg.py @@ -0,0 +1,184 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2024 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The keyboard implementation for *Xorg*. +""" + +# pylint: disable=C0111 +# The documentation is extracted from the base classes + + +# pylint: disable=E1101,E1102 +# We dynamically generate the Button class + +# pylint: disable=R0903 +# We implement stubs + +# pylint: disable=W0611 +try: + import pynput._util.xorg +except Exception as e: + raise ImportError('failed to acquire X connection: {}'.format(str(e)), e) +# pylint: enable=W0611 + +import enum +import Xlib.display +import Xlib.ext +import Xlib.ext.xtest +import Xlib.X +import Xlib.protocol + +from pynput._util.xorg import ( + display_manager, + ListenerMixin) +from . import _base + + +# pylint: disable=C0103 +Button = enum.Enum( + 'Button', + module=__name__, + names=[ + ('unknown', None), + ('left', 1), + ('middle', 2), + ('right', 3), + ('scroll_up', 4), + ('scroll_down', 5), + ('scroll_left', 6), + ('scroll_right', 7)] + [ + ('button%d' % i, i) + for i in range(8, 31)]) +# pylint: enable=C0103 + + +class Controller(_base.Controller): + def __init__(self, *args, **kwargs): + super(Controller, self).__init__(*args, **kwargs) + self._display = Xlib.display.Display() + + def __del__(self): + if hasattr(self, '_display'): + self._display.close() + + def _position_get(self): + with display_manager(self._display) as dm: + qp = dm.screen().root.query_pointer() + return (qp.root_x, qp.root_y) + + def _position_set(self, pos): + px, py = self._check_bounds(*pos) + with display_manager(self._display) as dm: + Xlib.ext.xtest.fake_input(dm, Xlib.X.MotionNotify, x=px, y=py) + + def _scroll(self, dx, dy): + dx, dy = self._check_bounds(dx, dy) + if dy: + self.click( + button=Button.scroll_up if dy > 0 else Button.scroll_down, + count=abs(dy)) + + if dx: + self.click( + button=Button.scroll_right if dx > 0 else Button.scroll_left, + count=abs(dx)) + + def _press(self, button): + with display_manager(self._display) as dm: + Xlib.ext.xtest.fake_input(dm, Xlib.X.ButtonPress, button.value) + + def _release(self, button): + with display_manager(self._display) as dm: + Xlib.ext.xtest.fake_input(dm, Xlib.X.ButtonRelease, button.value) + + def _check_bounds(self, *args): + """Checks the arguments and makes sure they are within the bounds of a + short integer. + + :param args: The values to verify. + """ + if not all( + (-0x7fff - 1) <= number <= 0x7fff + for number in args): + raise ValueError(args) + else: + return tuple(int(p) for p in args) + + +class Listener(ListenerMixin, _base.Listener): + #: A mapping from button values to scroll directions + _SCROLL_BUTTONS = { + Button.scroll_up.value: (0, 1), + Button.scroll_down.value: (0, -1), + Button.scroll_right.value: (1, 0), + Button.scroll_left.value: (-1, 0)} + + _EVENTS = ( + Xlib.X.ButtonPressMask, + Xlib.X.ButtonReleaseMask) + + def __init__(self, *args, **kwargs): + super(Listener, self).__init__(*args, **kwargs) + + def _handle_message(self, dummy_display, event, injected): + px = event.root_x + py = event.root_y + + if event.type == Xlib.X.ButtonPress: + # Scroll events are sent as button presses with the scroll + # button codes + scroll = self._SCROLL_BUTTONS.get(event.detail, None) + if scroll: + self.on_scroll( + px, py, scroll[0], scroll[1], injected) + else: + self.on_click( + px, py, self._button(event.detail), True, injected) + + elif event.type == Xlib.X.ButtonRelease: + # Send an event only if this was not a scroll event + if event.detail not in self._SCROLL_BUTTONS: + self.on_click( + px, py, self._button(event.detail), False, injected) + + else: + self.on_move(px, py, injected) + + + def _suppress_start(self, display): + display.screen().root.grab_pointer( + True, self._event_mask, Xlib.X.GrabModeAsync, Xlib.X.GrabModeAsync, + 0, 0, Xlib.X.CurrentTime) + + def _suppress_stop(self, display): + display.ungrab_pointer(Xlib.X.CurrentTime) + + # pylint: disable=R0201 + def _button(self, detail): + """Creates a mouse button from an event detail. + + If the button is unknown, :attr:`Button.unknown` is returned. + + :param detail: The event detail. + + :return: a button + """ + try: + return Button(detail) + except ValueError: + return Button.unknown + # pylint: enable=R0201 diff --git a/PortablePython/Lib/site-packages/six-1.17.0.dist-info/INSTALLER b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/PortablePython/Lib/site-packages/six-1.17.0.dist-info/LICENSE b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/LICENSE new file mode 100644 index 0000000..1cc22a5 --- /dev/null +++ b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2010-2024 Benjamin Peterson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/PortablePython/Lib/site-packages/six-1.17.0.dist-info/METADATA b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/METADATA new file mode 100644 index 0000000..cfde03c --- /dev/null +++ b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/METADATA @@ -0,0 +1,43 @@ +Metadata-Version: 2.1 +Name: six +Version: 1.17.0 +Summary: Python 2 and 3 compatibility utilities +Home-page: https://github.com/benjaminp/six +Author: Benjamin Peterson +Author-email: benjamin@python.org +License: MIT +Classifier: Development Status :: 5 - Production/Stable +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.* +License-File: LICENSE + +.. image:: https://img.shields.io/pypi/v/six.svg + :target: https://pypi.org/project/six/ + :alt: six on PyPI + +.. image:: https://readthedocs.org/projects/six/badge/?version=latest + :target: https://six.readthedocs.io/ + :alt: six's documentation on Read the Docs + +.. image:: https://img.shields.io/badge/license-MIT-green.svg + :target: https://github.com/benjaminp/six/blob/master/LICENSE + :alt: MIT License badge + +Six is a Python 2 and 3 compatibility library. It provides utility functions +for smoothing over the differences between the Python versions with the goal of +writing Python code that is compatible on both Python versions. See the +documentation for more information on what is provided. + +Six supports Python 2.7 and 3.3+. It is contained in only one Python +file, so it can be easily copied into your project. (The copyright and license +notice must be retained.) + +Online documentation is at https://six.readthedocs.io/. + +Bugs can be reported to https://github.com/benjaminp/six. The code can also +be found there. diff --git a/PortablePython/Lib/site-packages/six-1.17.0.dist-info/RECORD b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/RECORD new file mode 100644 index 0000000..603afd7 --- /dev/null +++ b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/RECORD @@ -0,0 +1,8 @@ +__pycache__/six.cpython-313.pyc,, +six-1.17.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +six-1.17.0.dist-info/LICENSE,sha256=Q3W6IOK5xsTnytKUCmKP2Q6VzD1Q7pKq51VxXYuh-9A,1066 +six-1.17.0.dist-info/METADATA,sha256=ViBCB4wnUlSfbYp8htvF3XCAiKe-bYBnLsewcQC3JGg,1658 +six-1.17.0.dist-info/RECORD,, +six-1.17.0.dist-info/WHEEL,sha256=pxeNX5JdtCe58PUSYP9upmc7jdRPgvT0Gm9kb1SHlVw,109 +six-1.17.0.dist-info/top_level.txt,sha256=_iVH_iYEtEXnD8nYGQYpYFUvkUW9sEO1GYbkeKSAais,4 +six.py,sha256=xRyR9wPT1LNpbJI8tf7CE-BeddkhU5O--sfy-mo5BN8,34703 diff --git a/PortablePython/Lib/site-packages/six-1.17.0.dist-info/WHEEL b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/WHEEL new file mode 100644 index 0000000..104f387 --- /dev/null +++ b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.6.0) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/PortablePython/Lib/site-packages/six-1.17.0.dist-info/top_level.txt b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/top_level.txt new file mode 100644 index 0000000..ffe2fce --- /dev/null +++ b/PortablePython/Lib/site-packages/six-1.17.0.dist-info/top_level.txt @@ -0,0 +1 @@ +six diff --git a/PortablePython/Lib/site-packages/six.py b/PortablePython/Lib/site-packages/six.py new file mode 100644 index 0000000..3de5969 --- /dev/null +++ b/PortablePython/Lib/site-packages/six.py @@ -0,0 +1,1003 @@ +# Copyright (c) 2010-2024 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Utilities for writing code that runs on Python 2 and 3""" + +from __future__ import absolute_import + +import functools +import itertools +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.17.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + +if PY34: + from importlib.util import spec_from_loader +else: + spec_from_loader = None + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def find_spec(self, fullname, path, target=None): + if fullname in self.known_modules: + return spec_from_loader(fullname, self) + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + + def create_module(self, spec): + return self.load_module(spec.name) + + def exec_module(self, module): + pass + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + + """Lazy loading of moved objects""" + __path__ = [] # mark as package + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("getoutput", "commands", "subprocess"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections", "IterableUserDict", "UserDict"), + MovedAttribute("UserList", "UserList", "collections"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_thread"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("splitvalue", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") + + +class Module_six_moves_urllib_error(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), + MovedAttribute("parse_http_list", "urllib2", "urllib.request"), + MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"), +] +if sys.version_info[:2] < (3, 14): + _urllib_request_moved_attributes.extend( + [ + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + ] + ) +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + def create_unbound_method(func, cls): + return func + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") + + +if PY3: + def b(s): + return s.encode("latin-1") + + def u(s): + return s + unichr = chr + import struct + int2byte = struct.Struct(">B").pack + del struct + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + del io + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" + _assertNotRegex = "assertNotRegex" +else: + def b(s): + return s + # Workaround for standalone backslash + + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + + def byte2int(bs): + return ord(bs[0]) + + def indexbytes(buf, i): + return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) + import StringIO + StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + +def assertNotRegex(self, *args, **kwargs): + return getattr(self, _assertNotRegex)(*args, **kwargs) + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + def reraise(tp, value, tb=None): + try: + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None + tb = None + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + exec_("""def reraise(tp, value, tb=None): + try: + raise tp, value, tb + finally: + tb = None +""") + + +if sys.version_info[:2] > (3,): + exec_("""def raise_from(value, from_value): + try: + raise value from from_value + finally: + value = None +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() + +_add_doc(reraise, """Reraise an exception.""") + +if sys.version_info[0:2] < (3, 4): + # This does exactly the same what the :func:`py3:functools.update_wrapper` + # function does on Python versions after 3.2. It sets the ``__wrapped__`` + # attribute on ``wrapper`` object and it doesn't raise an error if any of + # the attributes mentioned in ``assigned`` and ``updated`` are missing on + # ``wrapped`` object. + def _update_wrapper(wrapper, wrapped, + assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + for attr in assigned: + try: + value = getattr(wrapped, attr) + except AttributeError: + continue + else: + setattr(wrapper, attr, value) + for attr in updated: + getattr(wrapper, attr).update(getattr(wrapped, attr, {})) + wrapper.__wrapped__ = wrapped + return wrapper + _update_wrapper.__doc__ = functools.update_wrapper.__doc__ + + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + return functools.partial(_update_wrapper, wrapped=wrapped, + assigned=assigned, updated=updated) + wraps.__doc__ = functools.wraps.__doc__ + +else: + wraps = functools.wraps + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(type): + + def __new__(cls, name, this_bases, d): + if sys.version_info[:2] >= (3, 7): + # This version introduced PEP 560 that requires a bit + # of extra care (we mimic what is done by __build_class__). + resolved_bases = types.resolve_bases(bases) + if resolved_bases is not bases: + d['__orig_bases__'] = bases + else: + resolved_bases = bases + return meta(name, resolved_bases, d) + + @classmethod + def __prepare__(cls, name, this_bases): + return meta.__prepare__(name, bases) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + if hasattr(cls, '__qualname__'): + orig_vars['__qualname__'] = cls.__qualname__ + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + +def ensure_binary(s, encoding='utf-8', errors='strict'): + """Coerce **s** to six.binary_type. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> encoded to `bytes` + - `bytes` -> `bytes` + """ + if isinstance(s, binary_type): + return s + if isinstance(s, text_type): + return s.encode(encoding, errors) + raise TypeError("not expecting type '%s'" % type(s)) + + +def ensure_str(s, encoding='utf-8', errors='strict'): + """Coerce *s* to `str`. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + # Optimization: Fast return for the common case. + if type(s) is str: + return s + if PY2 and isinstance(s, text_type): + return s.encode(encoding, errors) + elif PY3 and isinstance(s, binary_type): + return s.decode(encoding, errors) + elif not isinstance(s, (text_type, binary_type)): + raise TypeError("not expecting type '%s'" % type(s)) + return s + + +def ensure_text(s, encoding='utf-8', errors='strict'): + """Coerce *s* to six.text_type. + + For Python 2: + - `unicode` -> `unicode` + - `str` -> `unicode` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + if isinstance(s, binary_type): + return s.decode(encoding, errors) + elif isinstance(s, text_type): + return s + else: + raise TypeError("not expecting type '%s'" % type(s)) + + +def python_2_unicode_compatible(klass): + """ + A class decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) diff --git a/PortablePython/_asyncio.pyd b/PortablePython/_asyncio.pyd new file mode 100644 index 0000000..dd16af6 Binary files /dev/null and b/PortablePython/_asyncio.pyd differ diff --git a/PortablePython/_bz2.pyd b/PortablePython/_bz2.pyd new file mode 100644 index 0000000..efcfffe Binary files /dev/null and b/PortablePython/_bz2.pyd differ diff --git a/PortablePython/_ctypes.pyd b/PortablePython/_ctypes.pyd new file mode 100644 index 0000000..018eda1 Binary files /dev/null and b/PortablePython/_ctypes.pyd differ diff --git a/PortablePython/_decimal.pyd b/PortablePython/_decimal.pyd new file mode 100644 index 0000000..7dfb600 Binary files /dev/null and b/PortablePython/_decimal.pyd differ diff --git a/PortablePython/_elementtree.pyd b/PortablePython/_elementtree.pyd new file mode 100644 index 0000000..c0436ac Binary files /dev/null and b/PortablePython/_elementtree.pyd differ diff --git a/PortablePython/_hashlib.pyd b/PortablePython/_hashlib.pyd new file mode 100644 index 0000000..c240327 Binary files /dev/null and b/PortablePython/_hashlib.pyd differ diff --git a/PortablePython/_lzma.pyd b/PortablePython/_lzma.pyd new file mode 100644 index 0000000..98781ed Binary files /dev/null and b/PortablePython/_lzma.pyd differ diff --git a/PortablePython/_multiprocessing.pyd b/PortablePython/_multiprocessing.pyd new file mode 100644 index 0000000..1cb0d95 Binary files /dev/null and b/PortablePython/_multiprocessing.pyd differ diff --git a/PortablePython/_overlapped.pyd b/PortablePython/_overlapped.pyd new file mode 100644 index 0000000..5ea0657 Binary files /dev/null and b/PortablePython/_overlapped.pyd differ diff --git a/PortablePython/_queue.pyd b/PortablePython/_queue.pyd new file mode 100644 index 0000000..7632b73 Binary files /dev/null and b/PortablePython/_queue.pyd differ diff --git a/PortablePython/_remote_debugging.pyd b/PortablePython/_remote_debugging.pyd new file mode 100644 index 0000000..f29eddd Binary files /dev/null and b/PortablePython/_remote_debugging.pyd differ diff --git a/PortablePython/_socket.pyd b/PortablePython/_socket.pyd new file mode 100644 index 0000000..e249f30 Binary files /dev/null and b/PortablePython/_socket.pyd differ diff --git a/PortablePython/_sqlite3.pyd b/PortablePython/_sqlite3.pyd new file mode 100644 index 0000000..52e4bc2 Binary files /dev/null and b/PortablePython/_sqlite3.pyd differ diff --git a/PortablePython/_ssl.pyd b/PortablePython/_ssl.pyd new file mode 100644 index 0000000..2bacca6 Binary files /dev/null and b/PortablePython/_ssl.pyd differ diff --git a/PortablePython/_uuid.pyd b/PortablePython/_uuid.pyd new file mode 100644 index 0000000..9e447e0 Binary files /dev/null and b/PortablePython/_uuid.pyd differ diff --git a/PortablePython/_wmi.pyd b/PortablePython/_wmi.pyd new file mode 100644 index 0000000..2ac520f Binary files /dev/null and b/PortablePython/_wmi.pyd differ diff --git a/PortablePython/_zoneinfo.pyd b/PortablePython/_zoneinfo.pyd new file mode 100644 index 0000000..db27d13 Binary files /dev/null and b/PortablePython/_zoneinfo.pyd differ diff --git a/PortablePython/_zstd.pyd b/PortablePython/_zstd.pyd new file mode 100644 index 0000000..2a3fe89 Binary files /dev/null and b/PortablePython/_zstd.pyd differ diff --git a/PortablePython/libcrypto-3.dll b/PortablePython/libcrypto-3.dll new file mode 100644 index 0000000..b7a525a Binary files /dev/null and b/PortablePython/libcrypto-3.dll differ diff --git a/PortablePython/libffi-8.dll b/PortablePython/libffi-8.dll new file mode 100644 index 0000000..8ebbbe8 Binary files /dev/null and b/PortablePython/libffi-8.dll differ diff --git a/PortablePython/libssl-3.dll b/PortablePython/libssl-3.dll new file mode 100644 index 0000000..5a53a34 Binary files /dev/null and b/PortablePython/libssl-3.dll differ diff --git a/PortablePython/pyexpat.pyd b/PortablePython/pyexpat.pyd new file mode 100644 index 0000000..f884b92 Binary files /dev/null and b/PortablePython/pyexpat.pyd differ diff --git a/PortablePython/python.cat b/PortablePython/python.cat new file mode 100644 index 0000000..b8125f9 Binary files /dev/null and b/PortablePython/python.cat differ diff --git a/PortablePython/python.exe b/PortablePython/python.exe new file mode 100644 index 0000000..d478dae Binary files /dev/null and b/PortablePython/python.exe differ diff --git a/PortablePython/python3.dll b/PortablePython/python3.dll new file mode 100644 index 0000000..0868b22 Binary files /dev/null and b/PortablePython/python3.dll differ diff --git a/PortablePython/python314._pth b/PortablePython/python314._pth new file mode 100644 index 0000000..b764f7c --- /dev/null +++ b/PortablePython/python314._pth @@ -0,0 +1,5 @@ +python314.zip +. + +# Uncomment to run site.main() automatically +#import site diff --git a/PortablePython/python314.dll b/PortablePython/python314.dll new file mode 100644 index 0000000..1a69300 Binary files /dev/null and b/PortablePython/python314.dll differ diff --git a/PortablePython/python314.zip b/PortablePython/python314.zip new file mode 100644 index 0000000..ec77c58 Binary files /dev/null and b/PortablePython/python314.zip differ diff --git a/PortablePython/pythonw.exe b/PortablePython/pythonw.exe new file mode 100644 index 0000000..e70b3da Binary files /dev/null and b/PortablePython/pythonw.exe differ diff --git a/PortablePython/select.pyd b/PortablePython/select.pyd new file mode 100644 index 0000000..723a38b Binary files /dev/null and b/PortablePython/select.pyd differ diff --git a/PortablePython/sqlite3.dll b/PortablePython/sqlite3.dll new file mode 100644 index 0000000..8227b2e Binary files /dev/null and b/PortablePython/sqlite3.dll differ diff --git a/PortablePython/unicodedata.pyd b/PortablePython/unicodedata.pyd new file mode 100644 index 0000000..ba9a464 Binary files /dev/null and b/PortablePython/unicodedata.pyd differ diff --git a/PortablePython/vcruntime140.dll b/PortablePython/vcruntime140.dll new file mode 100644 index 0000000..411009b Binary files /dev/null and b/PortablePython/vcruntime140.dll differ diff --git a/PortablePython/vcruntime140_1.dll b/PortablePython/vcruntime140_1.dll new file mode 100644 index 0000000..2071f28 Binary files /dev/null and b/PortablePython/vcruntime140_1.dll differ diff --git a/PortablePython/winsound.pyd b/PortablePython/winsound.pyd new file mode 100644 index 0000000..7cc5224 Binary files /dev/null and b/PortablePython/winsound.pyd differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..656fdf6 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# df-scope-hold + +三角洲行动(Delta Force)开镜自动屏息小工具——按住鼠标右键时自动按下指定按键(默认 `F12`),松开时释放。让你只用右键瞄准就能稳枪,不必再额外按一个键。 + +## 工作原理 + +监听鼠标右键事件,在右键按下时模拟键盘按下「屏息」键、右键松开时释放。脚本通过进程名识别游戏,只有游戏在运行时才激活监听,游戏关闭自动停止,重新进入会自动恢复。 + +纯本地按键映射,等价于支持宏的游戏鼠标硬件功能;不读写游戏内存、不修改任何游戏文件、不与游戏服务器通信。 + +## 使用方法 + +### 1. 游戏内绑定屏息键 + +打开三角洲行动设置,把「屏息」绑定到一个不会和其他操作冲突的按键,例如 `F12`。记住这个按键。 + +### 2. 修改配置文件 + +打开 `config.ini`,按需调整: + +```ini +[config] +key = f12 ; 与游戏内屏息绑定保持一致 +delay_press = 5 ; 右键按下到模拟按键的延迟(毫秒) +program_running = DeltaForceClient-Win64-Shipping.exe ; 游戏进程名(区分大小写) +``` + +### 3. 运行 + +双击 `run.bat`,脚本会在命令行窗口提示当前配置并等待游戏启动。游戏运行起来后开始监听,按 `Ctrl+C` 退出。 + +## 配置说明 + +| 字段 | 含义 | 取值示例 | +|---|---|---| +| `key` | 模拟的按键。普通字符直接写(`a`、`1`),系统功能键用 [pynput](https://pynput.readthedocs.io/) 的名称(`shift`、`ctrl`、`f12` 等) | `f12` | +| `delay_press` | 右键按下后等待多少毫秒再模拟按键。游戏场景建议 5–20ms,过低可能被游戏判定为指令冲突 | `5` | +| `program_running` | 游戏进程名(任务管理器里看到的 `.exe` 全名,区分大小写) | `DeltaForceClient-Win64-Shipping.exe` | + +## 运行环境 + +仓库已内置 `PortablePython/`(约 25MB),包含运行所需的 Python 3 解释器与依赖(`pynput`、`psutil`),**无需另外安装 Python**,下载即用。 + +仅在 Windows 上验证过(三角洲行动客户端为 Windows 独占)。 + +## 免责声明 + +- 本脚本是纯本地的鼠标→键盘映射工具,原理与游戏鼠标厂商提供的宏功能完全一致 +- 但游戏厂商对第三方输入工具的政策可能变化,**使用本脚本所产生的任何后果(包括但不限于封号)由使用者自行承担** +- 仅供个人自用与学习交流,请勿用于排位、赛事等竞技场景 + +## License + +MIT diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..426a493 --- /dev/null +++ b/config.ini @@ -0,0 +1,27 @@ +; ----------------------------------------------- +; 鼠标按键映射工具配置文件 +; 注意:修改后需重启脚本生效 +; ----------------------------------------------- + +[config] +; 模拟的目标按键(支持普通字符或系统键) +; 格式要求: +; - 普通字符键: 直接写字母(例如 a, b, 1, 2) +; - 系统功能键: 使用 pynput 的 Key 属性名(例如 shift, ctrl, alt) +; 示例: shift (默认值) / ctrl / a +key = f12 + +; 按键触发延迟(单位:毫秒) +; 作用:按下鼠标右键后,等待多少毫秒再触发目标键 +; 建议值: +; - 游戏场景: 5-20ms +; - 办公场景: 10-50ms +; 警告:设置低于5ms可能导致操作冲突 +delay_press = 5 + +; 需要监控的应用程序进程名(区分大小写) +; 格式要求: +; - 必须与任务管理器中的进程名完全一致(包括扩展名) +; - 示例:notepad.exe / chrome.exe / photoshop.exe +; 功能说明:只有该程序运行时才会激活鼠标监听 +program_running = DeltaForceClient-Win64-Shipping.exe \ No newline at end of file diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..404604b --- /dev/null +++ b/run.bat @@ -0,0 +1,7 @@ +@echo off +REM 设置便携式Python路径和脚本路径 +set PYTHON_PATH="%~dp0PortablePython\python.exe" +set SCRIPT_PATH="%~dp0script.py" + +REM 动态传递所有参数给Python脚本 +%PYTHON_PATH% %SCRIPT_PATH% \ No newline at end of file diff --git a/script.py b/script.py new file mode 100644 index 0000000..8fcf540 --- /dev/null +++ b/script.py @@ -0,0 +1,145 @@ +import os +import sys +current_dir = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(current_dir, "PortablePython", "Lib", "site-packages")) + +import time +from pynput import mouse, keyboard +from pynput.keyboard import Key, KeyCode +import configparser +import psutil + +# 全局变量 +target_key = Key.shift +delay_press_ms = 10 +key_pressed = False +listener = None +keyboard_controller = keyboard.Controller() +program_name = "" +exit_flag = False + +def is_program_running(program_name): + """检查程序是否运行(不区分大小写)""" + program_name_lower = program_name.lower() + for proc in psutil.process_iter(['name']): + proc_name = proc.info['name'] + if proc_name and proc_name.lower() == program_name_lower: + return True + return False + +def read_config(): + """读取配置文件(显式指定UTF-8编码)""" + config_dir = os.path.join(current_dir, "config.ini") + config = configparser.ConfigParser() + config.read(config_dir, encoding='utf-8') + return { + 'key': config.get('config', 'key'), + 'delay_press': config.getint('config', 'delay_press'), + 'program_running': config.get('config', 'program_running') + } + +def get_key(key_name: str): + """解析按键名称""" + try: + return getattr(Key, key_name) + except AttributeError: + if len(key_name) == 1: + return KeyCode(char=key_name.lower()) + else: + raise ValueError(f"无效按键名称: {key_name}") + +def on_click(x, y, button, pressed): + """鼠标点击回调""" + global key_pressed + if button == mouse.Button.right: + if pressed: + time.sleep(delay_press_ms / 1000) + keyboard_controller.press(target_key) + key_pressed = True + else: + if key_pressed: + keyboard_controller.release(target_key) + key_pressed = False + +def init_listener(): + """初始化监听器""" + global listener + listener = mouse.Listener(on_click=on_click) + listener.start() + +def cleanup(): + """资源清理函数""" + global listener, key_pressed + if key_pressed: + keyboard_controller.release(target_key) + key_pressed = False + if listener and listener.is_alive(): + listener.stop() + listener.join() + +def wait_for_program(): + """等待程序启动""" + global exit_flag + print(f"\n等待程序 '{program_name}' 启动... (Ctrl+C 永久退出)") + try: + while not exit_flag and not is_program_running(program_name): + print(".", end="", flush=True) + time.sleep(2) + except KeyboardInterrupt: + exit_flag = True + print("\n收到退出指令") + +def main_loop(): + """主监控循环""" + global exit_flag + while not exit_flag: + if is_program_running(program_name): + print("\n程序已运行,开始监听!") + init_listener() + + try: + # 保持监听状态 + while not exit_flag and is_program_running(program_name): + time.sleep(1) + except KeyboardInterrupt: + exit_flag = True + print("\n收到退出指令") + finally: + cleanup() + if not exit_flag: + print("检测到程序关闭,停止监听...") + + # 等待程序重新启动 + if not exit_flag: + wait_for_program() + +def main(): + global target_key, delay_press_ms, program_name + args = read_config() + program_name = args['program_running'] + + try: + target_key = get_key(args['key']) + except ValueError as e: + print(f"错误: {e}") + return + + delay_press_ms = args['delay_press'] + + key_type = "特殊键" if isinstance(target_key, Key) else "字符键" + print(f"\n当前配置:") + print(f" 模拟按键: {target_key} ({key_type})") + print(f" 按下延迟: {delay_press_ms}ms") + print(f" 当前监控程序: {program_name}") + # print("持续运行模式已启用,程序关闭后会自动等待重启") + print("-" * 40) + + wait_for_program() + main_loop() + +if __name__ == "__main__": + try: + main() + finally: + cleanup() + print("\n资源已释放,脚本完全退出") \ No newline at end of file