././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692073911.458956 portpicker-1.6.0/0000750015404402575230000000000014466577667014122 5ustar00gpsprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1620784740.0 portpicker-1.6.0/CONTRIBUTING.md0000640015404402575230000000261014046633144016324 0ustar00gpsprimarygroup# How To Contribute Want to contribute? Great! First, read this page (including the small print at the end). ### Before you contribute Before we can use your code, you must sign the [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1) (CLA), which you can do online. The CLA is necessary mainly because you own the copyright to your changes, even after your contribution becomes part of our codebase, so we need your permission to use and distribute your code. We also need to be sure of various other things—for instance that you'll tell us if you know that your code infringes on other people's patents. You don't have to sign the CLA until after you've submitted your code for review and a member has approved it, but you must do it before we can put your code into our codebase. Before you start working on a larger contribution, you should get in touch with us first through the issue tracker with your idea so that we can help out and possibly guide you. Coordinating up front makes it much easier to avoid frustration later on. ### Code reviews All submissions, including submissions by project members, require review. We use Github pull requests for this purpose. ### The small print Contributions made by corporations are covered by a different agreement than the one above, the Software Grant and Corporate Contributor License Agreement. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692073734.0 portpicker-1.6.0/ChangeLog.md0000640015404402575230000000647414466577406016276 0ustar00gpsprimarygroup## 1.6.0 * Resolve an internal source of potential flakiness on the bind/close port checks when used in active environments by calling `.shutdown()` before `.close()`. ## 1.6.0b1 * Add `-h` and `--help` text to the command line tool. * The command line interface now defaults to associating the returned port with its parent process PID (usually the calling script) when no argument was given as that makes more sense. * When portpicker is used as a command line tool from a script, if a port is chosen without a portserver it can now be kept bound to a socket by a child process for a user specified timeout. When successful, this helps minimize race conditions as subsequent portpicker CLI invocations within the timeout window cannot choose the same port. * Some pylint based refactorings to portpicker and portpicker\_test. * Drop 3.6 from our CI test matrix and metadata. It probably still works there, but expect our unittests to include 3.7-ism's in the future. We'll *attempt* to avoid modern constructs in portpicker.py itself but zero guarantees. Using an old Python? Use an old portpicker. ## 1.5.2 * Do not re-pick a known used (not-yet-returned) port when running stand alone without a portserver. ## 1.5.1 * When not using a portserver *(you really should)*, try the `bind(0)` approach before hunting for random unused ports. More reliable per https://github.com/google/python_portpicker/issues/16. ## 1.5.0 * Add portserver support to Windows using named pipes. To create or connect to a server, prefix the name of the server with `@` (e.g. `@unittest-portserver`). ## 1.4.0 * Use `async def` instead of `@asyncio.coroutine` in order to support 3.10. * The portserver now checks for and rejects pid values that are out of range. * Declare a minimum Python version of 3.6 in the package config. * Rework `portserver_test.py` to launch an actual portserver process instead of mocks. ## 1.3.9 * No portpicker or portserver code changes * Fixed the portserver test on recent Python 3.x versions. * Switched to setup.cfg based packaging. * We no longer declare ourselves Python 2.7 or 3.3-3.5 compatible. ## 1.3.1 * Fix a race condition in `pick_unused_port()` involving the free ports set. ## 1.3.0 * Adds an optional `portserver_address` parameter to `pick_unused_port()` so that callers can specify their own regardless of `os.environ`. * `pick_unused_port()` now raises `NoFreePortFoundError` when no available port could be found rather than spinning in a loop trying forever. * Fall back to `socket.AF_INET` when `socket.AF_UNIX` support is not available to communicate with a portserver. ## 1.2.0 * Introduced `add_reserved_port()` and `return_port()` APIs to allow ports to be recycled and allow users to bring ports of their own. ## 1.1.1 * Changed default port range to 15000-24999 to avoid ephemeral ports. * Portserver bugfix. ## 1.1.0 * Renamed portpicker APIs to use PEP8 style function names in code and docs. * Legacy CapWords API name compatibility is maintained (and explicitly tested). ## 1.0.1 * Code reindented to use 4 space indents and run through [YAPF](https://github.com/google/yapf) for consistent style. * Not packaged for release. ## 1.0.0 * Original open source release. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1620784740.0 portpicker-1.6.0/LICENSE0000640015404402575230000002367514046633144015116 0ustar00gpsprimarygroup Apache License Version 2.0, January 2004 http://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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1621896581.0 portpicker-1.6.0/MANIFEST.in0000640015404402575230000000026314053026605015627 0ustar00gpsprimarygroupinclude src/port*.py include src/tests/port*.py include README.md include LICENSE include CONTRIBUTING.md include ChangeLog.md include setup.py include test.sh exclude package.sh ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692073911.458956 portpicker-1.6.0/PKG-INFO0000640015404402575230000000264314466577667015225 0ustar00gpsprimarygroupMetadata-Version: 2.1 Name: portpicker Version: 1.6.0 Summary: A library to choose unique available network ports. Home-page: https://github.com/google/python_portpicker Maintainer: Google LLC Maintainer-email: greg@krypto.org License: Apache 2.0 Platform: POSIX Platform: Windows Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: Apache Software License Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=3.6 License-File: LICENSE Portpicker provides an API to find and return an available network port for an application to bind to. Ideally suited for use from unittests or for test harnesses that launch local servers. It also contains an optional portserver that can be used to coordinate allocation of network ports on a single build/test farm host across all processes willing to use a port server aware port picker library such as this one. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1654722335.0 portpicker-1.6.0/README.md0000640015404402575230000000514214250207437015354 0ustar00gpsprimarygroup# Python portpicker module [![PyPI version](https://badge.fury.io/py/portpicker.svg)](https://badge.fury.io/py/portpicker) [![GH Action Status](https://github.com/google/python_portpicker/actions/workflows/python-package.yml/badge.svg)](https://github.com/google/python_portpicker/actions) This module is useful for finding unused network ports on a host. If you need legacy Python 2 support, use the 1.3.x releases. This module provides a pure Python `pick_unused_port()` function. It can also be called via the command line for use in shell scripts. If your code can accept a bound TCP socket rather than a port number consider using `socket.bind(('localhost', 0))` to bind atomically to an available port rather than using this library at all. There is a race condition between picking a port and your application code binding to it. The use of a port server by all of your test code to avoid that problem is recommended on loaded test hosts running many tests at a time. Unless you are using a port server, subsequent calls to `pick_unused_port()` to obtain an additional port are not guaranteed to return a unique port. ### What is the optional port server? A port server is intended to be run as a daemon, for use by all processes running on the host. It coordinates uses of network ports by anything using a portpicker library. If you are using hosts as part of a test automation cluster, each one should run a port server as a daemon. You should set the `PORTSERVER_ADDRESS=@unittest-portserver` environment variable on all of your test runners so that portpicker makes use of it. A sample port server is included. This portserver implementation works but has not spent time in production. If you use it with good results please report back so that this statement can be updated to reflect that. :) A port server listens on a unix socket, reads a pid from a new connection, tests the ports it is managing and replies with a port assignment port for that pid. A port is only reclaimed for potential reassignment to another process after the process it was originally assigned to has died. Processes that need multiple ports can simply issue multiple requests and are guaranteed they will each be unique. ## Typical usage: ```python import portpicker test_port = portpicker.pick_unused_port() ``` Or from the command line: ```bash TEST_PORT=`/path/to/portpicker.py $$` ``` Or, if portpicker is installed as a library on the system Python interpreter: ```bash TEST_PORT=`python3 -m portpicker $$` ``` ## DISCLAIMER This is not an official Google product (experimental or otherwise), it is just code that happens to be owned by Google. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1655503196.0 portpicker-1.6.0/pyproject.toml0000640015404402575230000000073214253174534017015 0ustar00gpsprimarygroup[build-system] requires = ["setuptools >= 40.9.0", "wheel"] build-backend = "setuptools.build_meta" [tool.tox] legacy_tox_ini = """ [tox] envlist = py{37,38,39,310,311} isolated_build = true skip_missing_interpreters = true # minimum tox version minversion = 3.3.0 [testenv] deps = check-manifest >= 0.42 pytest commands = check-manifest --ignore 'src/tests/**' python -c 'from setuptools import setup; setup()' check -m -s py.test -vv -s {posargs} """ ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692073911.458956 portpicker-1.6.0/setup.cfg0000640015404402575230000000272414466577667015751 0ustar00gpsprimarygroup[metadata] name = portpicker version = 1.6.0 maintainer = Google LLC maintainer_email = greg@krypto.org license = Apache 2.0 license_files = LICENSE description = A library to choose unique available network ports. url = https://github.com/google/python_portpicker long_description = Portpicker provides an API to find and return an available network port for an application to bind to. Ideally suited for use from unittests or for test harnesses that launch local servers. It also contains an optional portserver that can be used to coordinate allocation of network ports on a single build/test farm host across all processes willing to use a port server aware port picker library such as this one. classifiers = Development Status :: 5 - Production/Stable License :: OSI Approved :: Apache Software License Intended Audience :: Developers Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy platforms = POSIX, Windows requires = [options] install_requires = psutil python_requires = >= 3.6 package_dir = =src py_modules = portpicker scripts = src/portserver.py [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692073911.454956 portpicker-1.6.0/src/0000750015404402575230000000000014466577667014711 5ustar00gpsprimarygroup././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692073911.454956 portpicker-1.6.0/src/portpicker.egg-info/0000750015404402575230000000000014466577667020565 5ustar00gpsprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692073911.0 portpicker-1.6.0/src/portpicker.egg-info/PKG-INFO0000640015404402575230000000264314466577667021670 0ustar00gpsprimarygroupMetadata-Version: 2.1 Name: portpicker Version: 1.6.0 Summary: A library to choose unique available network ports. Home-page: https://github.com/google/python_portpicker Maintainer: Google LLC Maintainer-email: greg@krypto.org License: Apache 2.0 Platform: POSIX Platform: Windows Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: Apache Software License Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=3.6 License-File: LICENSE Portpicker provides an API to find and return an available network port for an application to bind to. Ideally suited for use from unittests or for test harnesses that launch local servers. It also contains an optional portserver that can be used to coordinate allocation of network ports on a single build/test farm host across all processes willing to use a port server aware port picker library such as this one. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692073911.0 portpicker-1.6.0/src/portpicker.egg-info/SOURCES.txt0000640015404402575230000000056614466577667022461 0ustar00gpsprimarygroupCONTRIBUTING.md ChangeLog.md LICENSE MANIFEST.in README.md pyproject.toml setup.cfg test.sh src/portpicker.py src/portserver.py src/portpicker.egg-info/PKG-INFO src/portpicker.egg-info/SOURCES.txt src/portpicker.egg-info/dependency_links.txt src/portpicker.egg-info/requires.txt src/portpicker.egg-info/top_level.txt src/tests/portpicker_test.py src/tests/portserver_test.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692073911.0 portpicker-1.6.0/src/portpicker.egg-info/dependency_links.txt0000640015404402575230000000000114466577667024634 0ustar00gpsprimarygroup ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692073911.0 portpicker-1.6.0/src/portpicker.egg-info/requires.txt0000640015404402575230000000000714466577667023163 0ustar00gpsprimarygrouppsutil ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692073911.0 portpicker-1.6.0/src/portpicker.egg-info/top_level.txt0000640015404402575230000000001314466577667023312 0ustar00gpsprimarygroupportpicker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692073734.0 portpicker-1.6.0/src/portpicker.py0000640015404402575230000004366514466577406017453 0ustar00gpsprimarygroup#!/usr/bin/python3 # # Copyright 2007 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """Pure python code for finding unused ports on a host. This module provides a pick_unused_port() function. It can also be called via the command line for use in shell scripts. When called from the command line, it takes one optional argument, which, if given, is sent to portserver instead of portpicker's PID. To reserve a port for the lifetime of a bash script, use $BASHPID as this argument. There is a race condition between picking a port and your application code binding to it. The use of a port server to prevent that is recommended on loaded test hosts running many tests at a time. If your code can accept a bound socket as input rather than being handed a port number consider using socket.bind(('localhost', 0)) to bind to an available port without a race condition rather than using this library. Typical usage: test_port = portpicker.pick_unused_port() """ # pylint: disable=consider-using-f-string # Some people still use this on old Pythons despite our test matrix and # supported versions. Be kind for now, until it gets in our way. from __future__ import print_function import logging import os import random import socket import sys import time _winapi = None # pylint: disable=invalid-name if sys.platform == 'win32': try: import _winapi except ImportError: _winapi = None # The legacy Bind, IsPortFree, etc. names are not exported. __all__ = ('bind', 'is_port_free', 'pick_unused_port', 'return_port', 'add_reserved_port', 'get_port_from_port_server') _PROTOS = [(socket.SOCK_STREAM, socket.IPPROTO_TCP), (socket.SOCK_DGRAM, socket.IPPROTO_UDP)] # Ports that are currently available to be given out. _free_ports = set() # Ports that are reserved or from the portserver that may be returned. _owned_ports = set() # Ports that we chose randomly that may be returned. _random_ports = set() class NoFreePortFoundError(Exception): """Exception indicating that no free port could be found.""" def add_reserved_port(port): """Add a port that was acquired by means other than the port server.""" _free_ports.add(port) def return_port(port): """Return a port that is no longer being used so it can be reused.""" if port in _random_ports: _random_ports.remove(port) elif port in _owned_ports: _owned_ports.remove(port) _free_ports.add(port) elif port in _free_ports: logging.info("Returning a port that was already returned: %s", port) else: logging.info("Returning a port that wasn't given by portpicker: %s", port) def bind(port, socket_type, socket_proto): """Try to bind to a socket of the specified type, protocol, and port. This is primarily a helper function for PickUnusedPort, used to see if a particular port number is available. For the port to be considered available, the kernel must support at least one of (IPv6, IPv4), and the port must be available on each supported family. Args: port: The port number to bind to, or 0 to have the OS pick a free port. socket_type: The type of the socket (ex: socket.SOCK_STREAM). socket_proto: The protocol of the socket (ex: socket.IPPROTO_TCP). Returns: The port number on success or None on failure. """ return _bind(port, socket_type, socket_proto) def _bind(port, socket_type, socket_proto, return_socket=None, return_family=socket.AF_INET6): """Internal implementation of bind. Args: port, socket_type, socket_proto: see bind(). return_socket: If supplied, a list that we will append an open bound reuseaddr socket on the port in question to. return_family: The socket family to return in return_socket. Returns: The port number on success or None on failure. """ # Our return family must come last when returning a bound socket # as we cannot keep it bound while testing a bind on the other # family with many network stack configurations. if return_socket is None or return_family == socket.AF_INET: socket_families = (socket.AF_INET6, socket.AF_INET) elif return_family == socket.AF_INET6: socket_families = (socket.AF_INET, socket.AF_INET6) else: raise ValueError('unknown return_family %s' % return_family) got_socket = False for family in socket_families: try: sock = socket.socket(family, socket_type, socket_proto) got_socket = True except socket.error: continue try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', port)) if socket_type == socket.SOCK_STREAM: sock.listen(1) port = sock.getsockname()[1] except socket.error: return None finally: if return_socket is None or family != return_family: try: # Adding this resolved 1 in ~500 flakiness that we were # seeing from an integration test framework managing a set # of ports with is_port_free(). close() doesn't move the # TCP state machine along quickly. sock.shutdown(socket.SHUT_RDWR) except OSError: pass sock.close() if return_socket is not None and family == return_family: return_socket.append(sock) break # Final iteration due to pre-loop logic; don't close. return port if got_socket else None def is_port_free(port): """Check if specified port is free. Args: port: integer, port to check Returns: bool, whether port is free to use for both TCP and UDP. """ return _is_port_free(port) def _is_port_free(port, return_sockets=None): """Internal implementation of is_port_free. Args: port: integer, port to check return_sockets: If supplied, a list that we will append open bound sockets on the port in question to rather than closing them. Returns: bool, whether port is free to use for both TCP and UDP. """ return (_bind(port, *_PROTOS[0], return_socket=return_sockets) and _bind(port, *_PROTOS[1], return_socket=return_sockets)) def pick_unused_port(pid=None, portserver_address=None): """Picks an unused port and reserves it for use by a given process id. Args: pid: PID to tell the portserver to associate the reservation with. If None, the current process's PID is used. portserver_address: The address (path) of a unix domain socket with which to connect to a portserver, a leading '@' character indicates an address in the "abstract namespace". OR On systems without socket.AF_UNIX, this is an AF_INET address. If None, or no port is returned by the portserver at the provided address, the environment will be checked for a PORTSERVER_ADDRESS variable. If that is not set, no port server will be used. If no portserver is used, no pid based reservation is managed by any central authority. Race conditions and duplicate assignments may occur. Returns: A port number that is unused on both TCP and UDP. Raises: NoFreePortFoundError: No free port could be found. """ return _pick_unused_port(pid, portserver_address) def _pick_unused_port(pid=None, portserver_address=None, noserver_bind_timeout=0): """Internal implementation of pick_unused_port. Args: pid, portserver_address: See pick_unused_port(). noserver_bind_timeout: If no portserver was used, this is the number of seconds we will attempt to keep a child process around with the ports returned open and bound SO_REUSEADDR style to help avoid race condition port reuse. A non-zero value attempts os.fork(). Do not use it in a multithreaded process. """ try: # Instead of `if _free_ports:` to handle the race condition. port = _free_ports.pop() except KeyError: pass else: _owned_ports.add(port) return port # Provide access to the portserver on an opt-in basis. if portserver_address: port = get_port_from_port_server(portserver_address, pid=pid) if port: return port if 'PORTSERVER_ADDRESS' in os.environ: port = get_port_from_port_server(os.environ['PORTSERVER_ADDRESS'], pid=pid) if port: return port return _pick_unused_port_without_server(bind_timeout=noserver_bind_timeout) def _spawn_bound_port_holding_daemon(port, bound_sockets, timeout): """If possible, fork()s a daemon process to hold bound_sockets open. Emits a warning to stderr if it cannot. Args: port: The port number the sockets are bound to (informational). bound_sockets: The list of bound sockets our child process will hold open. If the list is empty, no action is taken. timeout: A positive number of seconds the child should sleep for before closing the sockets and exiting. """ if bound_sockets and timeout > 0: try: fork_pid = os.fork() # This concept only works on POSIX. except Exception as err: # pylint: disable=broad-except print('WARNING: Cannot timeout unbinding close of port', port, ' closing on exit. -', err, file=sys.stderr) else: if fork_pid == 0: # This child process inherits and holds bound_sockets open # for bind_timeout seconds. try: # Close the stdio fds as may be connected to # a pipe that will cause a grandparent process # to wait on before returning. (cl/427587550) os.close(sys.stdin.fileno()) os.close(sys.stdout.fileno()) os.close(sys.stderr.fileno()) time.sleep(timeout) for held_socket in bound_sockets: held_socket.close() finally: os._exit(0) def _pick_unused_port_without_server(bind_timeout=0): """Pick an available network port without the help of a port server. This code ensures that the port is available on both TCP and UDP. This function is an implementation detail of PickUnusedPort(), and should not be called by code outside of this module. Args: bind_timeout: number of seconds to attempt to keep a child process process around bound SO_REUSEADDR style to the port. If we cannot do that we emit a warning to stderr. Returns: A port number that is unused on both TCP and UDP. Raises: NoFreePortFoundError: No free port could be found. """ # Next, try a few times to get an OS-assigned port. # Ambrose discovered that on the 2.6 kernel, calling Bind() on UDP socket # returns the same port over and over. So always try TCP first. port = None bound_sockets = [] if bind_timeout > 0 else None for _ in range(10): # Ask the OS for an unused port. port = _bind(0, socket.SOCK_STREAM, socket.IPPROTO_TCP, bound_sockets) # Check if this port is unused on the other protocol. if (port and port not in _random_ports and _bind(port, socket.SOCK_DGRAM, socket.IPPROTO_UDP, bound_sockets)): _random_ports.add(port) _spawn_bound_port_holding_daemon(port, bound_sockets, bind_timeout) return port if bound_sockets: for held_socket in bound_sockets: held_socket.close() del bound_sockets[:] # Try random ports as a last resort. rng = random.Random() for _ in range(10): port = int(rng.randrange(15000, 25000)) if port not in _random_ports: if _is_port_free(port, bound_sockets): _random_ports.add(port) _spawn_bound_port_holding_daemon( port, bound_sockets, bind_timeout) return port if bound_sockets: for held_socket in bound_sockets: held_socket.close() del bound_sockets[:] # Give up. raise NoFreePortFoundError() def _posix_get_port_from_port_server(portserver_address, pid): # An AF_UNIX address may start with a zero byte, in which case it is in the # "abstract namespace", and doesn't have any filesystem representation. # See 'man 7 unix' for details. # The convention is to write '@' in the address to represent this zero byte. if portserver_address[0] == '@': portserver_address = '\0' + portserver_address[1:] try: # Create socket. if hasattr(socket, 'AF_UNIX'): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) else: # fallback to AF_INET if this is not unix sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: # Connect to portserver. sock.connect(portserver_address) # Write request. sock.sendall(('%d\n' % pid).encode('ascii')) # Read response. # 1K should be ample buffer space. return sock.recv(1024) finally: sock.close() except socket.error as error: print('Socket error when connecting to portserver:', error, file=sys.stderr) return None def _windows_get_port_from_port_server(portserver_address, pid): if portserver_address[0] == '@': portserver_address = '\\\\.\\pipe\\' + portserver_address[1:] try: handle = _winapi.CreateFile( portserver_address, _winapi.GENERIC_READ | _winapi.GENERIC_WRITE, 0, 0, _winapi.OPEN_EXISTING, 0, 0) _winapi.WriteFile(handle, ('%d\n' % pid).encode('ascii')) data, _ = _winapi.ReadFile(handle, 6, 0) return data except FileNotFoundError as error: print('File error when connecting to portserver:', error, file=sys.stderr) return None def get_port_from_port_server(portserver_address, pid=None): """Request a free a port from a system-wide portserver. This follows a very simple portserver protocol: The request consists of our pid (in ASCII) followed by a newline. The response is a port number and a newline, 0 on failure. This function is an implementation detail of pick_unused_port(). It should not normally be called by code outside of this module. Args: portserver_address: The address (path) of a unix domain socket with which to connect to the portserver. A leading '@' character indicates an address in the "abstract namespace." On systems without socket.AF_UNIX, this is an AF_INET address. pid: The PID to tell the portserver to associate the reservation with. If None, the current process's PID is used. Returns: The port number on success or None on failure. """ if not portserver_address: return None if pid is None: pid = os.getpid() if _winapi: buf = _windows_get_port_from_port_server(portserver_address, pid) else: buf = _posix_get_port_from_port_server(portserver_address, pid) if buf is None: return None try: port = int(buf.split(b'\n')[0]) except ValueError: print('Portserver failed to find a port.', file=sys.stderr) return None _owned_ports.add(port) return port # Legacy APIs. # pylint: disable=invalid-name Bind = bind GetPortFromPortServer = get_port_from_port_server IsPortFree = is_port_free PickUnusedPort = pick_unused_port # pylint: enable=invalid-name def main(argv): """If passed an arg, treat it as a PID, otherwise we use getppid(). A second optional argument can be a bind timeout in seconds that will be used ONLY if no portserver is found. We attempt to leave a process around holding the port open and bound with SO_REUSEADDR set for timeout seconds. If the timeout bind was not possible, a warning is emitted to stderr. #!/bin/bash port="$(python -m portpicker $$ 1.23)" test_my_server "$port" This will pick a port for your script's PID and assign it to $port, if no portserver was used, it attempts to keep a socket bound to $port for 1.23 seconds after the portpicker process has exited. This is a convenient hack to attempt to prevent port reallocation during scripts outside of portserver managed environments. Older versions of the portpicker CLI ignore everything beyond the first arg. Older versions also used getpid() instead of getppid(), so script users are strongly encouraged to be explicit and pass $$ or your languages equivalent to associate the port with the PID of the controlling process. """ # Our command line is trivial so I avoid an argparse import. If we ever # grow more than 1-2 args, switch to a using argparse. if '-h' in argv or '--help' in argv: print(argv[0], 'usage:\n') import inspect print(inspect.getdoc(main)) sys.exit(1) pid=int(argv[1]) if len(argv) > 1 else os.getppid() bind_timeout=float(argv[2]) if len(argv) > 2 else 0 port = _pick_unused_port(pid=pid, noserver_bind_timeout=bind_timeout) if not port: sys.exit(1) print(port) if __name__ == '__main__': main(sys.argv) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1626053922.0 portpicker-1.6.0/src/portserver.py0000640015404402575230000003476114072716442017465 0ustar00gpsprimarygroup#!/usr/bin/python3 # # Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """A server to hand out network ports to applications running on one host. Typical usage: 1) Run one instance of this process on each of your unittest farm hosts. 2) Set the PORTSERVER_ADDRESS environment variable in your test runner environment to let the portpicker library know to use a port server rather than attempt to find ports on its own. $ /path/to/portserver.py & $ export PORTSERVER_ADDRESS=@unittest-portserver $ # ... launch a bunch of unittest runners using portpicker ... """ import argparse import asyncio import collections import logging import signal import socket import sys import psutil import subprocess from datetime import datetime, timezone, timedelta log = None # Initialized to a logging.Logger by _configure_logging(). _PROTOS = [(socket.SOCK_STREAM, socket.IPPROTO_TCP), (socket.SOCK_DGRAM, socket.IPPROTO_UDP)] def _get_process_command_line(pid): try: return psutil.Process(pid).cmdline() except psutil.NoSuchProcess: return '' def _get_process_start_time(pid): try: return psutil.Process(pid).create_time() except psutil.NoSuchProcess: return 0.0 # TODO: Consider importing portpicker.bind() instead of duplicating the code. def _bind(port, socket_type, socket_proto): """Try to bind to a socket of the specified type, protocol, and port. For the port to be considered available, the kernel must support at least one of (IPv6, IPv4), and the port must be available on each supported family. Args: port: The port number to bind to, or 0 to have the OS pick a free port. socket_type: The type of the socket (ex: socket.SOCK_STREAM). socket_proto: The protocol of the socket (ex: socket.IPPROTO_TCP). Returns: The port number on success or None on failure. """ got_socket = False for family in (socket.AF_INET6, socket.AF_INET): try: sock = socket.socket(family, socket_type, socket_proto) got_socket = True except socket.error: continue try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', port)) if socket_type == socket.SOCK_STREAM: sock.listen(1) port = sock.getsockname()[1] except socket.error: return None finally: sock.close() return port if got_socket else None def _is_port_free(port): """Check if specified port is free. Args: port: integer, port to check Returns: boolean, whether it is free to use for both TCP and UDP """ return _bind(port, *_PROTOS[0]) and _bind(port, *_PROTOS[1]) def _should_allocate_port(pid): """Determine if we should allocate a port for use by the given process id.""" if pid <= 0: log.info('Not allocating a port to invalid pid') return False if pid == 1: # The client probably meant to send us its parent pid but # had been reparented to init. log.info('Not allocating a port to init.') return False if not psutil.pid_exists(pid): log.info('Not allocating a port to a non-existent process') return False return True async def _start_windows_server(client_connected_cb, path): """Start the server on Windows using named pipes.""" def protocol_factory(): stream_reader = asyncio.StreamReader() stream_reader_protocol = asyncio.StreamReaderProtocol( stream_reader, client_connected_cb) return stream_reader_protocol loop = asyncio.get_event_loop() server, *_ = await loop.start_serving_pipe(protocol_factory, address=path) return server class _PortInfo(object): """Container class for information about a given port assignment. Attributes: port: integer port number pid: integer process id or 0 if unassigned. start_time: Time in seconds since the epoch that the process started. """ __slots__ = ('port', 'pid', 'start_time') def __init__(self, port): self.port = port self.pid = 0 self.start_time = 0.0 class _PortPool(object): """Manage available ports for processes. Ports are reclaimed when the reserving process exits and the reserved port is no longer in use. Only ports which are free for both TCP and UDP will be handed out. It is easier to not differentiate between protocols. The pool must be pre-seeded with add_port_to_free_pool() calls after which get_port_for_process() will allocate and reclaim ports. The len() of a _PortPool returns the total number of ports being managed. Attributes: ports_checked_for_last_request: The number of ports examined in order to return from the most recent get_port_for_process() request. A high number here likely means the number of available ports with no active process using them is getting low. """ def __init__(self): self._port_queue = collections.deque() self.ports_checked_for_last_request = 0 def num_ports(self): return len(self._port_queue) def get_port_for_process(self, pid): """Allocates and returns port for pid or 0 if none could be allocated.""" if not self._port_queue: raise RuntimeError('No ports being managed.') # Avoid an infinite loop if all ports are currently assigned. check_count = 0 max_ports_to_test = len(self._port_queue) while check_count < max_ports_to_test: # Get the next candidate port and move it to the back of the queue. candidate = self._port_queue.pop() self._port_queue.appendleft(candidate) check_count += 1 if (candidate.start_time == 0.0 or candidate.start_time != _get_process_start_time(candidate.pid)): if _is_port_free(candidate.port): candidate.pid = pid candidate.start_time = _get_process_start_time(pid) if not candidate.start_time: log.info("Can't read start time for pid %d.", pid) self.ports_checked_for_last_request = check_count return candidate.port else: log.info( 'Port %d unexpectedly in use, last owning pid %d.', candidate.port, candidate.pid) log.info('All ports in use.') self.ports_checked_for_last_request = check_count return 0 def add_port_to_free_pool(self, port): """Add a new port to the free pool for allocation.""" if port < 1 or port > 65535: raise ValueError( 'Port must be in the [1, 65535] range, not %d.' % port) port_info = _PortInfo(port=port) self._port_queue.append(port_info) class _PortServerRequestHandler(object): """A class to handle port allocation and status requests. Allocates ports to process ids via the dead simple port server protocol when the handle_port_request asyncio.coroutine handler has been registered. Statistics can be logged using the dump_stats method. """ def __init__(self, ports_to_serve): """Initialize a new port server. Args: ports_to_serve: A sequence of unique port numbers to test and offer up to clients. """ self._port_pool = _PortPool() self._total_allocations = 0 self._denied_allocations = 0 self._client_request_errors = 0 for port in ports_to_serve: self._port_pool.add_port_to_free_pool(port) async def handle_port_request(self, reader, writer): client_data = await reader.read(100) self._handle_port_request(client_data, writer) writer.close() def _handle_port_request(self, client_data, writer): """Given a port request body, parse it and respond appropriately. Args: client_data: The request bytes from the client. writer: The asyncio Writer for the response to be written to. """ try: if len(client_data) > 20: raise ValueError('More than 20 characters in "pid".') pid = int(client_data) except ValueError as error: self._client_request_errors += 1 log.warning('Could not parse request: %s', error) return log.info('Request on behalf of pid %d.', pid) log.info('cmdline: %s', _get_process_command_line(pid)) if not _should_allocate_port(pid): self._denied_allocations += 1 return port = self._port_pool.get_port_for_process(pid) if port > 0: self._total_allocations += 1 writer.write('{:d}\n'.format(port).encode('utf-8')) log.debug('Allocated port %d to pid %d', port, pid) else: self._denied_allocations += 1 def dump_stats(self): """Logs statistics of our operation.""" log.info('Dumping statistics:') stats = [] stats.append( 'client-request-errors {}'.format(self._client_request_errors)) stats.append('denied-allocations {}'.format(self._denied_allocations)) stats.append('num-ports-managed {}'.format(self._port_pool.num_ports())) stats.append('num-ports-checked-for-last-request {}'.format( self._port_pool.ports_checked_for_last_request)) stats.append('total-allocations {}'.format(self._total_allocations)) for stat in stats: log.info(stat) def _parse_command_line(): """Configure and parse our command line flags.""" parser = argparse.ArgumentParser() parser.add_argument( '--portserver_static_pool', type=str, default='15000-24999', help='Comma separated N-P Range(s) of ports to manage (inclusive).') parser.add_argument( '--portserver_address', '--portserver_unix_socket_address', # Alias to be backward compatible type=str, default='@unittest-portserver', help='Address of AF_UNIX socket on which to listen on Unix (first @ is ' 'a NUL) or the name of the pipe on Windows (first @ is the ' r'\\.\pipe\ prefix).') parser.add_argument('--verbose', action='store_true', default=False, help='Enable verbose messages.') parser.add_argument('--debug', action='store_true', default=False, help='Enable full debug messages.') return parser.parse_args(sys.argv[1:]) def _parse_port_ranges(pool_str): """Given a 'N-P,X-Y' description of port ranges, return a set of ints.""" ports = set() for range_str in pool_str.split(','): try: a, b = range_str.split('-', 1) start, end = int(a), int(b) except ValueError: log.error('Ignoring unparsable port range %r.', range_str) continue if start < 1 or end > 65535: log.error('Ignoring out of bounds port range %r.', range_str) continue ports.update(set(range(start, end + 1))) return ports def _configure_logging(verbose=False, debug=False): """Configure the log global, message format, and verbosity settings.""" overall_level = logging.DEBUG if debug else logging.INFO logging.basicConfig( format=('{levelname[0]}{asctime}.{msecs:03.0f} {thread} ' '{filename}:{lineno}] {message}'), datefmt='%m%d %H:%M:%S', style='{', level=overall_level) global log log = logging.getLogger('portserver') # The verbosity controls our loggers logging level, not the global # one above. This avoids debug messages from libraries such as asyncio. log.setLevel(logging.DEBUG if verbose else overall_level) def main(): config = _parse_command_line() if config.debug: # Equivalent of PYTHONASYNCIODEBUG=1 in 3.4; pylint: disable=protected-access asyncio.tasks._DEBUG = True _configure_logging(verbose=config.verbose, debug=config.debug) ports_to_serve = _parse_port_ranges(config.portserver_static_pool) if not ports_to_serve: log.error('No ports. Invalid port ranges in --portserver_static_pool?') sys.exit(1) request_handler = _PortServerRequestHandler(ports_to_serve) if sys.platform == 'win32': asyncio.set_event_loop(asyncio.ProactorEventLoop()) event_loop = asyncio.get_event_loop() if sys.platform == 'win32': # On Windows, we need to periodically pause the loop to allow the user # to send a break signal (e.g. ctrl+c) def listen_for_signal(): event_loop.call_later(0.5, listen_for_signal) event_loop.call_later(0.5, listen_for_signal) coro = _start_windows_server( request_handler.handle_port_request, path=config.portserver_address.replace('@', '\\\\.\\pipe\\', 1)) else: event_loop.add_signal_handler( signal.SIGUSR1, request_handler.dump_stats) # pylint: disable=no-member old_py_loop = {'loop': event_loop} if sys.version_info < (3, 10) else {} coro = asyncio.start_unix_server( request_handler.handle_port_request, path=config.portserver_address.replace('@', '\0', 1), **old_py_loop) server_address = config.portserver_address server = event_loop.run_until_complete(coro) log.info('Serving on %s', server_address) try: event_loop.run_forever() except KeyboardInterrupt: log.info('Stopping due to ^C.') server.close() if sys.platform != 'win32': # PipeServer doesn't have a wait_closed() function event_loop.run_until_complete(server.wait_closed()) event_loop.remove_signal_handler(signal.SIGUSR1) # pylint: disable=no-member event_loop.close() request_handler.dump_stats() log.info('Goodbye.') if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692073911.458956 portpicker-1.6.0/src/tests/0000750015404402575230000000000014466577667016053 5ustar00gpsprimarygroup././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1657221941.0 portpicker-1.6.0/src/tests/portpicker_test.py0000640015404402575230000005405314261631465021632 0ustar00gpsprimarygroup#!/usr/bin/python3 # # Copyright 2007 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """Unittests for portpicker.""" # pylint: disable=invalid-name,protected-access,missing-class-docstring,missing-function-docstring from contextlib import ExitStack import errno import os import socket import subprocess import sys import time import unittest from unittest import mock import portpicker _winapi = portpicker._winapi # pylint: disable=invalid-name,protected-access,missing-class-docstring,missing-function-docstring class CommonTestMixin: def IsUnusedTCPPort(self, port): return self._bind(port, socket.SOCK_STREAM, socket.IPPROTO_TCP) def IsUnusedUDPPort(self, port): return self._bind(port, socket.SOCK_DGRAM, socket.IPPROTO_UDP) def setUp(self): super().setUp() # So we can Bind even if portpicker.bind is stubbed out. self._bind = portpicker.bind portpicker._owned_ports.clear() portpicker._free_ports.clear() portpicker._random_ports.clear() @unittest.skipIf( ('PORTSERVER_ADDRESS' not in os.environ) and not hasattr(socket, 'AF_UNIX'), 'no existing port server; test launching code requires AF_UNIX.') class PickUnusedPortTestWithAPortServer(CommonTestMixin, unittest.TestCase): @classmethod def setUpClass(cls): cls.portserver_process = None if 'PORTSERVER_ADDRESS' not in os.environ: # Launch a portserver child process for our tests to use if we are # able to. Obviously not host-exclusive, but good for integration # testing purposes on CI without a portserver of its own. cls.portserver_address = '@pid%d-test-ports' % os.getpid() try: cls.portserver_process = subprocess.Popen( ['portserver.py', # Installed in PATH within the venv. '--portserver_address=%s' % cls.portserver_address]) except EnvironmentError as err: raise unittest.SkipTest( 'Unable to launch portserver.py: %s' % err) linux_addr = '\0' + cls.portserver_address[1:] # The @ means 0. # loop for a few seconds waiting for that socket to work. err = '???' for _ in range(123): time.sleep(0.05) try: ps_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) ps_sock.connect(linux_addr) except socket.error as err: # pylint: disable=unused-variable continue ps_sock.close() break else: # The socket failed or never accepted connections, assume our # portserver setup attempt failed and bail out. if cls.portserver_process.poll() is not None: cls.portserver_process.kill() cls.portserver_process.wait() cls.portserver_process = None raise unittest.SkipTest( 'Unable to connect to our own portserver.py: %s' % err) # Point child processes at our shiny portserver process. os.environ['PORTSERVER_ADDRESS'] = cls.portserver_address @classmethod def tearDownClass(cls): if cls.portserver_process: if os.environ.get('PORTSERVER_ADDRESS') == cls.portserver_address: del os.environ['PORTSERVER_ADDRESS'] if cls.portserver_process.poll() is None: cls.portserver_process.kill() cls.portserver_process.wait() cls.portserver_process = None def testPickUnusedCanSuccessfullyUsePortServer(self): with mock.patch.object(portpicker, '_pick_unused_port_without_server'): portpicker._pick_unused_port_without_server.side_effect = ( Exception('eek!') ) # Since _PickUnusedPortWithoutServer() raises an exception, if we # can successfully obtain a port, the portserver must be working. port = portpicker.pick_unused_port() self.assertTrue(self.IsUnusedTCPPort(port)) self.assertTrue(self.IsUnusedUDPPort(port)) def testPickUnusedCanSuccessfullyUsePortServerAddressKwarg(self): with mock.patch.object(portpicker, '_pick_unused_port_without_server'): portpicker._pick_unused_port_without_server.side_effect = ( Exception('eek!') ) # Since _PickUnusedPortWithoutServer() raises an exception, and # we've temporarily removed PORTSERVER_ADDRESS from os.environ, if # we can successfully obtain a port, the portserver must be working. addr = os.environ.pop('PORTSERVER_ADDRESS') try: port = portpicker.pick_unused_port(portserver_address=addr) self.assertTrue(self.IsUnusedTCPPort(port)) self.assertTrue(self.IsUnusedUDPPort(port)) finally: os.environ['PORTSERVER_ADDRESS'] = addr def testGetPortFromPortServer(self): """Exercise the get_port_from_port_server() helper function.""" for _ in range(10): port = portpicker.get_port_from_port_server( os.environ['PORTSERVER_ADDRESS']) self.assertTrue(self.IsUnusedTCPPort(port)) self.assertTrue(self.IsUnusedUDPPort(port)) class PickUnusedPortTest(CommonTestMixin, unittest.TestCase): def testPickUnusedPortActuallyWorks(self): """This test can be flaky.""" for _ in range(10): port = portpicker.pick_unused_port() self.assertTrue(self.IsUnusedTCPPort(port)) self.assertTrue(self.IsUnusedUDPPort(port)) def testSendsPidToPortServer(self): with ExitStack() as stack: if _winapi: create_file_mock = mock.Mock() create_file_mock.return_value = 0 read_file_mock = mock.Mock() write_file_mock = mock.Mock() read_file_mock.return_value = (b'42768\n', 0) stack.enter_context( mock.patch('_winapi.CreateFile', new=create_file_mock)) stack.enter_context( mock.patch('_winapi.WriteFile', new=write_file_mock)) stack.enter_context( mock.patch('_winapi.ReadFile', new=read_file_mock)) port = portpicker.get_port_from_port_server( 'portserver', pid=1234) write_file_mock.assert_called_once_with(0, b'1234\n') else: server = mock.Mock() server.recv.return_value = b'42768\n' stack.enter_context( mock.patch.object(socket, 'socket', return_value=server)) port = portpicker.get_port_from_port_server( 'portserver', pid=1234) server.sendall.assert_called_once_with(b'1234\n') self.assertEqual(port, 42768) def testPidDefaultsToOwnPid(self): with ExitStack() as stack: stack.enter_context( mock.patch.object(os, 'getpid', return_value=9876)) if _winapi: create_file_mock = mock.Mock() create_file_mock.return_value = 0 read_file_mock = mock.Mock() write_file_mock = mock.Mock() read_file_mock.return_value = (b'52768\n', 0) stack.enter_context( mock.patch('_winapi.CreateFile', new=create_file_mock)) stack.enter_context( mock.patch('_winapi.WriteFile', new=write_file_mock)) stack.enter_context( mock.patch('_winapi.ReadFile', new=read_file_mock)) port = portpicker.get_port_from_port_server('portserver') write_file_mock.assert_called_once_with(0, b'9876\n') else: server = mock.Mock() server.recv.return_value = b'52768\n' stack.enter_context( mock.patch.object(socket, 'socket', return_value=server)) port = portpicker.get_port_from_port_server('portserver') server.sendall.assert_called_once_with(b'9876\n') self.assertEqual(port, 52768) @mock.patch.dict(os.environ,{'PORTSERVER_ADDRESS': 'portserver'}) def testReusesPortServerPorts(self): with ExitStack() as stack: if _winapi: read_file_mock = mock.Mock() read_file_mock.side_effect = [ (b'12345\n', 0), (b'23456\n', 0), (b'34567\n', 0), ] stack.enter_context(mock.patch('_winapi.CreateFile')) stack.enter_context(mock.patch('_winapi.WriteFile')) stack.enter_context( mock.patch('_winapi.ReadFile', new=read_file_mock)) else: server = mock.Mock() server.recv.side_effect = [b'12345\n', b'23456\n', b'34567\n'] stack.enter_context( mock.patch.object(socket, 'socket', return_value=server)) self.assertEqual(portpicker.pick_unused_port(), 12345) self.assertEqual(portpicker.pick_unused_port(), 23456) portpicker.return_port(12345) self.assertEqual(portpicker.pick_unused_port(), 12345) @mock.patch.dict(os.environ,{'PORTSERVER_ADDRESS': ''}) def testDoesntReuseRandomPorts(self): ports = set() for _ in range(10): try: port = portpicker.pick_unused_port() except portpicker.NoFreePortFoundError: # This sometimes happens when not using portserver. Just # skip to the next attempt. continue ports.add(port) portpicker.return_port(port) self.assertGreater(len(ports), 5) # Allow some random reuse. def testReturnsReservedPorts(self): with mock.patch.object(portpicker, '_pick_unused_port_without_server'): portpicker._pick_unused_port_without_server.side_effect = ( Exception('eek!')) # Arbitrary port. In practice you should get this from somewhere # that assigns ports. reserved_port = 28465 portpicker.add_reserved_port(reserved_port) ports = set() for _ in range(10): port = portpicker.pick_unused_port() ports.add(port) portpicker.return_port(port) self.assertEqual(len(ports), 1) self.assertEqual(ports.pop(), reserved_port) @mock.patch.dict(os.environ,{'PORTSERVER_ADDRESS': ''}) def testFallsBackToRandomAfterRunningOutOfReservedPorts(self): # Arbitrary port. In practice you should get this from somewhere # that assigns ports. reserved_port = 23456 portpicker.add_reserved_port(reserved_port) self.assertEqual(portpicker.pick_unused_port(), reserved_port) self.assertNotEqual(portpicker.pick_unused_port(), reserved_port) def testRandomlyChosenPorts(self): # Unless this box is under an overwhelming socket load, this test # will heavily exercise the "pick a port randomly" part of the # port picking code, but may never hit the "OS assigns a port" # code. ports = 0 for _ in range(100): try: port = portpicker._pick_unused_port_without_server() except portpicker.NoFreePortFoundError: # Without the portserver, pick_unused_port can sometimes fail # to find a free port. Check that it passes most of the time. continue self.assertTrue(self.IsUnusedTCPPort(port)) self.assertTrue(self.IsUnusedUDPPort(port)) ports += 1 # Getting a port shouldn't have failed very often, even on machines # with a heavy socket load. self.assertGreater(ports, 95) def testOSAssignedPorts(self): self.last_assigned_port = None def error_for_explicit_ports(port, socket_type, socket_proto): # Only successfully return a port if an OS-assigned port is # requested, or if we're checking that the last OS-assigned port # is unused on the other protocol. if port in (0, self.last_assigned_port): self.last_assigned_port = self._bind(port, socket_type, socket_proto) return self.last_assigned_port return None with mock.patch.object(portpicker, 'bind', error_for_explicit_ports): # Without server, this can be little flaky, so check that it # passes most of the time. ports = 0 for _ in range(100): try: port = portpicker._pick_unused_port_without_server() except portpicker.NoFreePortFoundError: continue self.assertTrue(self.IsUnusedTCPPort(port)) self.assertTrue(self.IsUnusedUDPPort(port)) ports += 1 self.assertGreater(ports, 70) def pickUnusedPortWithoutServer(self): # Try a few times to pick a port, to avoid flakiness and to make sure # the code path we want was exercised. for _ in range(5): try: port = portpicker._pick_unused_port_without_server() except portpicker.NoFreePortFoundError: continue else: self.assertTrue(self.IsUnusedTCPPort(port)) self.assertTrue(self.IsUnusedUDPPort(port)) return self.fail("Failed to find a free port") def testPickPortsWithoutServer(self): # Test the first part of _pick_unused_port_without_server, which # tries a few random ports and checks is_port_free. self.pickUnusedPortWithoutServer() # Now test the second part, the fallback from above, which asks the # OS for a port. def mock_port_free(unused_port): return False with mock.patch.object(portpicker, 'is_port_free', mock_port_free): self.pickUnusedPortWithoutServer() def checkIsPortFree(self): """This might be flaky unless this test is run with a portserver.""" # The port should be free initially. port = portpicker.pick_unused_port() self.assertTrue(portpicker.is_port_free(port)) cases = [ (socket.AF_INET, socket.SOCK_STREAM, None), (socket.AF_INET6, socket.SOCK_STREAM, 1), (socket.AF_INET, socket.SOCK_DGRAM, None), (socket.AF_INET6, socket.SOCK_DGRAM, 1), ] # Using v6only=0 on Windows doesn't result in collisions if not _winapi: cases.extend([ (socket.AF_INET6, socket.SOCK_STREAM, 0), (socket.AF_INET6, socket.SOCK_DGRAM, 0), ]) for (sock_family, sock_type, v6only) in cases: # Occupy the port on a subset of possible protocols. try: sock = socket.socket(sock_family, sock_type, 0) except socket.error: print('Kernel does not support sock_family=%d' % sock_family, file=sys.stderr) # Skip this case, since we cannot occupy a port. continue if not hasattr(socket, 'IPPROTO_IPV6'): v6only = None if v6only is not None: try: sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, v6only) except socket.error: print('Kernel does not support IPV6_V6ONLY=%d' % v6only, file=sys.stderr) # Don't care; just proceed with the default. # Socket may have been taken in the mean time, so catch the # socket.error with errno set to EADDRINUSE and skip this # attempt. try: sock.bind(('', port)) except socket.error as e: if e.errno == errno.EADDRINUSE: raise portpicker.NoFreePortFoundError raise # The port should be busy. self.assertFalse(portpicker.is_port_free(port)) sock.close() # Now it's free again. self.assertTrue(portpicker.is_port_free(port)) def testIsPortFree(self): # This can be quite flaky on a busy host, so try a few times. for _ in range(10): try: self.checkIsPortFree() except portpicker.NoFreePortFoundError: pass else: return self.fail("checkPortIsFree failed every time.") def testIsPortFreeException(self): port = portpicker.pick_unused_port() with mock.patch.object(socket, 'socket') as mock_sock: mock_sock.side_effect = socket.error('fake socket error', 0) self.assertFalse(portpicker.is_port_free(port)) def testThatLegacyCapWordsAPIsExist(self): """The original APIs were CapWords style, 1.1 added PEP8 names.""" self.assertEqual(portpicker.bind, portpicker.Bind) self.assertEqual(portpicker.is_port_free, portpicker.IsPortFree) self.assertEqual(portpicker.pick_unused_port, portpicker.PickUnusedPort) self.assertEqual(portpicker.get_port_from_port_server, portpicker.GetPortFromPortServer) def get_open_listen_tcp_ports(): netstat = subprocess.run(['netstat', '-lnt'], capture_output=True, encoding='utf-8') if netstat.returncode != 0: raise unittest.SkipTest('Unable to run netstat -lnt to list binds.') rows = (line.split() for line in netstat.stdout.splitlines()) listen_addrs = (row[3] for row in rows if row[0].startswith('tcp')) listen_ports = [int(addr.split(':')[-1]) for addr in listen_addrs] return listen_ports @unittest.skipUnless((sys.executable and os.access(sys.executable, os.X_OK)) or (os.environ.get('TEST_PORTPICKER_CLI') and os.access(os.environ['TEST_PORTPICKER_CLI'], os.X_OK)), 'sys.executable portpicker.__file__ not launchable and ' ' no TEST_PORTPICKER_CLI supplied.') class PortpickerCommandLineTests(unittest.TestCase): def setUp(self): self.main_py = portpicker.__file__ def _run_portpicker(self, pp_args, env_override=None): env = dict(os.environ) if env_override: env.update(env_override) if os.environ.get('TEST_PORTPICKER_CLI'): pp_command = [os.environ['TEST_PORTPICKER_CLI']] else: pp_command = [sys.executable, '-m', 'portpicker'] return subprocess.run(pp_command + pp_args, capture_output=True, env=env, encoding='utf-8', check=False) def test_command_line_help(self): cmd = self._run_portpicker(['-h']) self.assertNotEqual(0, cmd.returncode) self.assertIn('usage', cmd.stdout) self.assertIn('passed an arg', cmd.stdout) cmd = self._run_portpicker(['--help']) self.assertNotEqual(0, cmd.returncode) self.assertIn('usage', cmd.stdout) self.assertIn('passed an arg', cmd.stdout) def test_command_line_help_text_dedented(self): cmd = self._run_portpicker(['-h']) self.assertNotEqual(0, cmd.returncode) self.assertIn('\nIf passed an arg', cmd.stdout) self.assertIn('\n #!/bin/bash', cmd.stdout) self.assertIn('\nOlder versions ', cmd.stdout) def test_command_line_interface(self): cmd = self._run_portpicker([str(os.getpid())]) cmd.check_returncode() port = int(cmd.stdout) self.assertNotEqual(0, port, msg=cmd) listen_ports = sorted(get_open_listen_tcp_ports()) self.assertNotIn(port, listen_ports, msg='expected nothing to be bound to port.') def test_command_line_interface_no_portserver(self): cmd = self._run_portpicker([str(os.getpid())], env_override={'PORTSERVER_ADDRESS': ''}) cmd.check_returncode() port = int(cmd.stdout) self.assertNotEqual(0, port, msg=cmd) listen_ports = sorted(get_open_listen_tcp_ports()) self.assertNotIn(port, listen_ports, msg='expected nothing to be bound to port.') def test_command_line_interface_no_portserver_bind_timeout(self): # This test is timing sensitive and leaves that bind process hanging # around consuming resources until it dies on its own unless the test # runner kills the process group upon exit. timeout = 9.5 before = time.monotonic() cmd = self._run_portpicker([str(os.getpid()), str(timeout)], env_override={'PORTSERVER_ADDRESS': ''}) self.assertEqual(0, cmd.returncode, msg=(cmd.stdout, cmd.stderr)) port = int(cmd.stdout) self.assertNotEqual(0, port, msg=cmd) if 'WARNING' in cmd.stderr: raise unittest.SkipTest('bind timeout not supported on this platform.') listen_ports = sorted(get_open_listen_tcp_ports()) self.assertIn(port, listen_ports, msg='expected port to be bound. ' '%f seconds elapsed of %f bind timeout.' % (time.monotonic() - before, timeout)) if __name__ == '__main__': unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1655506110.0 portpicker-1.6.0/src/tests/portserver_test.py0000640015404402575230000003644314253202276021661 0ustar00gpsprimarygroup#!/usr/bin/python3 # # Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """Tests for the example portserver.""" import asyncio import os import signal import socket import subprocess import sys import time import unittest from unittest import mock from multiprocessing import Process import portpicker # On Windows, portserver.py is located in the "Scripts" folder, which isn't # added to the import path by default if sys.platform == 'win32': sys.path.append(os.path.join(os.path.split(sys.executable)[0])) try: import portserver except ImportError: # Or if testing from a third_party/py/portpicker/ style installed # package tree find it this way. from portpicker import portserver def setUpModule(): portserver._configure_logging(verbose=True) def exit_immediately(): os._exit(0) class PortserverFunctionsTest(unittest.TestCase): @classmethod def setUp(cls): cls.port = portpicker.PickUnusedPort() def test_get_process_command_line(self): portserver._get_process_command_line(os.getpid()) def test_get_process_start_time(self): self.assertGreater(portserver._get_process_start_time(os.getpid()), 0) def test_is_port_free(self): """This might be flaky unless this test is run with a portserver.""" # The port should be free initially. self.assertTrue(portserver._is_port_free(self.port)) cases = [ (socket.AF_INET, socket.SOCK_STREAM, None), (socket.AF_INET6, socket.SOCK_STREAM, 1), (socket.AF_INET, socket.SOCK_DGRAM, None), (socket.AF_INET6, socket.SOCK_DGRAM, 1), ] # Using v6only=0 on Windows doesn't result in collisions if sys.platform != 'win32': cases.extend([ (socket.AF_INET6, socket.SOCK_STREAM, 0), (socket.AF_INET6, socket.SOCK_DGRAM, 0), ]) for (sock_family, sock_type, v6only) in cases: # Occupy the port on a subset of possible protocols. try: sock = socket.socket(sock_family, sock_type, 0) except socket.error: print('Kernel does not support sock_family=%d' % sock_family, file=sys.stderr) # Skip this case, since we cannot occupy a port. continue if not hasattr(socket, 'IPPROTO_IPV6'): v6only = None if v6only is not None: try: sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, v6only) except socket.error: print('Kernel does not support IPV6_V6ONLY=%d' % v6only, file=sys.stderr) # Don't care; just proceed with the default. sock.bind(('', self.port)) # The port should be busy. self.assertFalse(portserver._is_port_free(self.port)) sock.close() # Now it's free again. self.assertTrue(portserver._is_port_free(self.port)) def test_is_port_free_exception(self): with mock.patch.object(socket, 'socket') as mock_sock: mock_sock.side_effect = socket.error('fake socket error', 0) self.assertFalse(portserver._is_port_free(self.port)) def test_should_allocate_port(self): self.assertFalse(portserver._should_allocate_port(0)) self.assertFalse(portserver._should_allocate_port(1)) self.assertTrue(portserver._should_allocate_port, os.getpid()) p = Process(target=exit_immediately) p.start() child_pid = p.pid p.join() # This test assumes that after waitpid returns the kernel has finished # cleaning the process. We also assume that the kernel will not reuse # the former child's pid before our next call checks for its existence. # Likely assumptions, but not guaranteed. self.assertFalse(portserver._should_allocate_port(child_pid)) def test_parse_command_line(self): with mock.patch.object( sys, 'argv', ['program_name', '--verbose', '--portserver_static_pool=1-1,3-8', '--portserver_unix_socket_address=@hello-test']): portserver._parse_command_line() def test_parse_port_ranges(self): self.assertFalse(portserver._parse_port_ranges('')) self.assertCountEqual(portserver._parse_port_ranges('1-1'), {1}) self.assertCountEqual(portserver._parse_port_ranges('1-1,3-8,375-378'), {1, 3, 4, 5, 6, 7, 8, 375, 376, 377, 378}) # Unparsable parts are logged but ignored. self.assertEqual({1, 2}, portserver._parse_port_ranges('1-2,not,numbers')) self.assertEqual(set(), portserver._parse_port_ranges('8080-8081x')) # Port ranges that go out of bounds are logged but ignored. self.assertEqual(set(), portserver._parse_port_ranges('0-1138')) self.assertEqual(set(range(19, 84 + 1)), portserver._parse_port_ranges('1138-65536,19-84')) def test_configure_logging(self): """Just code coverage really.""" portserver._configure_logging(False) portserver._configure_logging(True) _test_socket_addr = f'@TST-{os.getpid()}' @mock.patch.object( sys, 'argv', ['PortserverFunctionsTest.test_main', f'--portserver_unix_socket_address={_test_socket_addr}'] ) @mock.patch.object(portserver, '_parse_port_ranges') def test_main_no_ports(self, *unused_mocks): portserver._parse_port_ranges.return_value = set() with self.assertRaises(SystemExit): portserver.main() @unittest.skipUnless(sys.executable, 'Requires a stand alone interpreter') @unittest.skipUnless(hasattr(socket, 'AF_UNIX'), 'AF_UNIX required') def test_portserver_binary(self): """Launch python portserver.py and test it.""" # Blindly assuming tree layout is src/tests/portserver_test.py # with src/portserver.py. portserver_py = os.path.join( os.path.dirname(os.path.dirname(__file__)), 'portserver.py') anon_addr = self._test_socket_addr.replace('@', '\0') conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) with self.assertRaises( ConnectionRefusedError, msg=f'{self._test_socket_addr} should not listen yet.'): conn.connect(anon_addr) conn.close() server = subprocess.Popen( [sys.executable, portserver_py, f'--portserver_unix_socket_address={self._test_socket_addr}'], stderr=subprocess.PIPE, ) try: # Wait a few seconds for the server to start listening. start_time = time.monotonic() while True: time.sleep(0.05) try: conn.connect(anon_addr) conn.close() except ConnectionRefusedError: delta = time.monotonic() - start_time if delta < 4: continue else: server.kill() self.fail('Failed to connect to portserver ' f'{self._test_socket_addr} within ' f'{delta} seconds. STDERR:\n' + server.stderr.read().decode('utf-8')) else: break ports = set() port = portpicker.get_port_from_port_server( portserver_address=self._test_socket_addr) ports.add(port) port = portpicker.get_port_from_port_server( portserver_address=self._test_socket_addr) ports.add(port) with subprocess.Popen('exit 0', shell=True) as quick_process: quick_process.wait() # This process doesn't exist so it should be a denied alloc. # We use the pid from the above quick_process under the assumption # that most OSes try to avoid rapid pid recycling. denied_port = portpicker.get_port_from_port_server( portserver_address=self._test_socket_addr, pid=quick_process.pid) # A now unused pid. self.assertIsNone(denied_port) self.assertEqual(len(ports), 2, msg=ports) # Check statistics from portserver server.send_signal(signal.SIGUSR1) # TODO implement an I/O timeout for line in server.stderr: if b'denied-allocations ' in line: denied_allocations = int( line.split(b'denied-allocations ', 2)[1]) self.assertEqual(1, denied_allocations, msg=line) elif b'total-allocations ' in line: total_allocations = int( line.split(b'total-allocations ', 2)[1]) self.assertEqual(2, total_allocations, msg=line) break rejected_port = portpicker.get_port_from_port_server( portserver_address=self._test_socket_addr, pid=99999999999999999999999999999999999) # Out of range. self.assertIsNone(rejected_port) # Done. shutdown gracefully. server.send_signal(signal.SIGINT) server.communicate(timeout=2) finally: server.kill() server.wait() class PortPoolTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.port = portpicker.PickUnusedPort() def setUp(self): self.pool = portserver._PortPool() def test_initialization(self): self.assertEqual(0, self.pool.num_ports()) self.pool.add_port_to_free_pool(self.port) self.assertEqual(1, self.pool.num_ports()) self.pool.add_port_to_free_pool(1138) self.assertEqual(2, self.pool.num_ports()) self.assertRaises(ValueError, self.pool.add_port_to_free_pool, 0) self.assertRaises(ValueError, self.pool.add_port_to_free_pool, 65536) @mock.patch.object(portserver, '_is_port_free') def test_get_port_for_process_ok(self, mock_is_port_free): self.pool.add_port_to_free_pool(self.port) mock_is_port_free.return_value = True self.assertEqual(self.port, self.pool.get_port_for_process(os.getpid())) self.assertEqual(1, self.pool.ports_checked_for_last_request) @mock.patch.object(portserver, '_is_port_free') def test_get_port_for_process_none_left(self, mock_is_port_free): self.pool.add_port_to_free_pool(self.port) self.pool.add_port_to_free_pool(22) mock_is_port_free.return_value = False self.assertEqual(2, self.pool.num_ports()) self.assertEqual(0, self.pool.get_port_for_process(os.getpid())) self.assertEqual(2, self.pool.num_ports()) self.assertEqual(2, self.pool.ports_checked_for_last_request) @mock.patch.object(portserver, '_is_port_free') @mock.patch.object(os, 'getpid') def test_get_port_for_process_pid_eq_port(self, mock_getpid, mock_is_port_free): self.pool.add_port_to_free_pool(12345) self.pool.add_port_to_free_pool(12344) mock_is_port_free.side_effect = lambda port: port == os.getpid() mock_getpid.return_value = 12345 self.assertEqual(2, self.pool.num_ports()) self.assertEqual(12345, self.pool.get_port_for_process(os.getpid())) self.assertEqual(2, self.pool.ports_checked_for_last_request) @mock.patch.object(portserver, '_is_port_free') @mock.patch.object(os, 'getpid') def test_get_port_for_process_pid_ne_port(self, mock_getpid, mock_is_port_free): self.pool.add_port_to_free_pool(12344) self.pool.add_port_to_free_pool(12345) mock_is_port_free.side_effect = lambda port: port != os.getpid() mock_getpid.return_value = 12345 self.assertEqual(2, self.pool.num_ports()) self.assertEqual(12344, self.pool.get_port_for_process(os.getpid())) self.assertEqual(2, self.pool.ports_checked_for_last_request) @mock.patch.object(portserver, '_get_process_command_line') @mock.patch.object(portserver, '_should_allocate_port') @mock.patch.object(portserver._PortPool, 'get_port_for_process') class PortServerRequestHandlerTest(unittest.TestCase): def setUp(self): portserver._configure_logging(verbose=True) self.rh = portserver._PortServerRequestHandler([23, 42, 54]) def test_stats_reporting(self, *unused_mocks): with mock.patch.object(portserver, 'log') as mock_logger: self.rh.dump_stats() mock_logger.info.assert_called_with('total-allocations 0') def test_handle_port_request_bad_data(self, *unused_mocks): self._test_bad_data_from_client(b'') self._test_bad_data_from_client(b'\n') self._test_bad_data_from_client(b'99Z\n') self._test_bad_data_from_client(b'99 8\n') self.assertEqual([], portserver._get_process_command_line.mock_calls) def _test_bad_data_from_client(self, data): mock_writer = mock.Mock(asyncio.StreamWriter) self.rh._handle_port_request(data, mock_writer) self.assertFalse(portserver._should_allocate_port.mock_calls) def test_handle_port_request_denied_allocation(self, *unused_mocks): portserver._should_allocate_port.return_value = False self.assertEqual(0, self.rh._denied_allocations) mock_writer = mock.Mock(asyncio.StreamWriter) self.rh._handle_port_request(b'5\n', mock_writer) self.assertEqual(1, self.rh._denied_allocations) def test_handle_port_request_bad_port_returned(self, *unused_mocks): portserver._should_allocate_port.return_value = True self.rh._port_pool.get_port_for_process.return_value = 0 mock_writer = mock.Mock(asyncio.StreamWriter) self.rh._handle_port_request(b'6\n', mock_writer) self.rh._port_pool.get_port_for_process.assert_called_once_with(6) self.assertEqual(1, self.rh._denied_allocations) def test_handle_port_request_success(self, *unused_mocks): portserver._should_allocate_port.return_value = True self.rh._port_pool.get_port_for_process.return_value = 999 mock_writer = mock.Mock(asyncio.StreamWriter) self.assertEqual(0, self.rh._total_allocations) self.rh._handle_port_request(b'8', mock_writer) portserver._should_allocate_port.assert_called_once_with(8) self.rh._port_pool.get_port_for_process.assert_called_once_with(8) self.assertEqual(1, self.rh._total_allocations) self.assertEqual(0, self.rh._denied_allocations) mock_writer.write.assert_called_once_with(b'999\n') if __name__ == '__main__': unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636424248.0 portpicker-1.6.0/test.sh0000750015404402575230000000056314142355070015411 0ustar00gpsprimarygroup#!/bin/sh -ex unset PYTHONPATH python3 -m venv build/venv . build/venv/bin/activate pip install --upgrade pip pip install tox # We should really do this differently, test from a `pip install .` so that # testing relies on the setup.cfg install_requires instead of listing it here. pip install psutil tox -e "py3$(python -c 'import sys; print(sys.version_info.minor)')"