pax_global_header00006660000000000000000000000064141614730130014512gustar00rootroot0000000000000052 comment=40336cb0d47c326ebdd498d7e3afb9a34e1045fe Pyro4-4.82/000077500000000000000000000000001416147301300124645ustar00rootroot00000000000000Pyro4-4.82/.gitattributes000066400000000000000000000001401416147301300153520ustar00rootroot00000000000000* text=auto *.py text=auto *.rst text=auto *.txt text=auto *.sh text=lf *.png -text *.jpg -text Pyro4-4.82/.github/000077500000000000000000000000001416147301300140245ustar00rootroot00000000000000Pyro4-4.82/.github/workflows/000077500000000000000000000000001416147301300160615ustar00rootroot00000000000000Pyro4-4.82/.github/workflows/main-ci.yml000066400000000000000000000020401416147301300201150ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python application on: push: branches: [ master ] pull_request: branches: [ master ] # allow manual trigger workflow_dispatch: jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10'] steps: - name: Checkout source uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r test_requirements.txt - name: build and install run: pip install . - name: run unit tests run: | cd tests python -E -Wall -bb run_testsuite.py Pyro4-4.82/.gitignore000066400000000000000000000005661416147301300144630ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info .idea dist build eggs .eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox coverage.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # java stuff *.class # logfiles *.log tests/coverage.xml Pyro4-4.82/.lgtm.yml000066400000000000000000000002001416147301300142200ustar00rootroot00000000000000# Format of this file: https://lgtm.com/help/lgtm/lgtm.yml-configuration-file path_classifiers: documentation: - examples Pyro4-4.82/LICENSE000066400000000000000000000020561416147301300134740ustar00rootroot00000000000000MIT License Copyright (c) 2016 Irmen de Jong 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. Pyro4-4.82/MANIFEST.in000066400000000000000000000006651416147301300142310ustar00rootroot00000000000000include LICENSE include MANIFEST.in include README.md include tox.ini recursive-include tests * recursive-include examples * recursive-include docs * recursive-include contrib * recursive-include certs * global-exclude */.svn/* global-exclude */.idea/* global-exclude *.class global-exclude *.pyc global-exclude *.pyo global-exclude *.coverage global-exclude .git global-exclude .gitignore global-exclude .tox global-exclude __pycache__ Pyro4-4.82/Makefile000066400000000000000000000024031416147301300141230ustar00rootroot00000000000000.PHONY: all sdist wheel docs install upload clean test lint PYTHON=python3 all: @echo "targets include sdist, wheel, docs, upload, install, clean, lint" lint: pycodestyle test: $(PYTHON) setup.py test sdist: $(PYTHON) setup.py sdist @echo "Look in the dist/ directory" wheel: $(PYTHON) setup.py bdist_wheel @echo "Look in the dist/ directory" docs: $(PYTHON) setup.py build_sphinx upload: sdist wheel twine upload dist/* @echo "Don't forget to check the doc builds on RTD!" install: $(PYTHON) setup.py install clean: @echo "Removing tox dirs, logfiles, Pyro URI dumps, .pyo/.pyc files..." rm -rf .tox find . -name __pycache__ -print0 | xargs -0 rm -rf find . -name \*_log -print0 | xargs -0 rm -f find . -name \*.log -print0 | xargs -0 rm -f find . -name \*_URI -print0 | xargs -0 rm -f find . -name \*.pyo -print0 | xargs -0 rm -f find . -name \*.pyc -print0 | xargs -0 rm -f find . -name \*.class -print0 | xargs -0 rm -f find . -name \*.DS_Store -print0 | xargs -0 rm -f find . -name \.coverage -print0 | xargs -0 rm -f find . -name \coverage.xml -print0 | xargs -0 rm -f rm -f MANIFEST rm -rf build rm -rf tests/test-reports find . -name '.#*' -print0 | xargs -0 rm -f find . -name '#*#' -print0 | xargs -0 rm -f @echo "clean!" Pyro4-4.82/README.md000066400000000000000000000114611416147301300137460ustar00rootroot00000000000000[![Latest Version](https://img.shields.io/pypi/v/Pyro4.svg)](https://pypi.python.org/pypi/Pyro4/) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/pyro4/badges/version.svg)](https://anaconda.org/conda-forge/pyro4) PYRO - Python Remote Objects ============================ Pyro enables you to build applications in which objects can talk to each other over the network, with minimal programming effort. You can just use normal Python method calls to call objects on other machines. Pyro is a pure Python library so it runs on many different platforms and Python versions. This software is copyright (c) by Irmen de Jong (irmen@razorvine.net). This software is released under the MIT software license. This license, including disclaimer, is available in the 'LICENSE' file. Pyro5 ----- Pyro4 is considered feature complete and new development is frozen. Only very important bug fixes (such as security issues) will still be made to Pyro4. New development, improvements and new features will only be available in its successor Pyro5: https://pyro5.readthedocs.io New code should strongly consider using Pyro5 unless a feature of Pyro4 is strictly required. Older code should consider migrating to Pyro5. It provides a (simple) backwards compatibility api layer to make the porting easier. Documentation ============= Documentation can be found online at: http://pyro4.readthedocs.io (or unformatted here in the repo at: docs/source/intro.rst) Feature overview ================ Pyro is a library that enables you to build applications in which objects can talk to each other over the network, with minimal programming effort. You can just use normal Python method calls, with almost every possible parameter and return value type, and Pyro takes care of locating the right object on the right computer to execute the method. It is designed to be very easy to use, and to generally stay out of your way. But it also provides a set of powerful features that enables you to build distributed applications rapidly and effortlessly. Pyro is a pure Python library and runs on many different platforms and Python versions. Here's a quick overview of Pyro's features: - written in 100% Python so extremely portable, runs on Python 2.7, Python 3.5 and newer, IronPython, Pypy 2 and 3. - works between different system architectures and operating systems. - able to communicate between different Python versions transparently. - defaults to a safe serializer (serpent https://pypi.python.org/pypi/serpent ) that supports many Python data types. - supports different serializers (serpent, json, marshal, msgpack, pickle, cloudpickle, dill). - can use IPv4, IPv6 and Unix domain sockets. - optional secure connections via SSL/TLS (encryption, authentication and integrity), including certificate validation on both ends (2-way ssl). - lightweight client library available for .NET and Java native code ('Pyrolite', provided separately). - designed to be very easy to use and get out of your way as much as possible, but still provide a lot of flexibility when you do need it. - name server that keeps track of your object's actual locations so you can move them around transparently. - yellow-pages type lookups possible, based on metadata tags on registrations in the name server. - support for automatic reconnection to servers in case of interruptions. - automatic proxy-ing of Pyro objects which means you can return references to remote objects just as if it were normal objects. - one-way invocations for enhanced performance. - batched invocations for greatly enhanced performance of many calls on the same object. - remote iterator on-demand item streaming avoids having to create large collections upfront and transfer them as a whole. - you can define timeouts on network communications to prevent a call blocking forever if there's something wrong. - asynchronous invocations if you want to get the results 'at some later moment in time'. Pyro will take care of gathering the result values in the background. - remote exceptions will be raised in the caller, as if they were local. You can extract detailed remote traceback information. - http gateway available for clients wanting to use http+json (such as browser scripts). - stable network communication code that works reliably on many platforms. - can hook onto existing sockets created for instance with socketpair() to communicate efficiently between threads or sub-processes. - possibility to use Pyro's own event loop, or integrate it into your own (or third party) event loop. - three different possible instance modes for your remote objects (singleton, one per session, one per call). - many simple examples included to show various features and techniques. - large amount of unit tests and high test coverage. - reliable and established: built upon more than 15 years of existing Pyro history, with ongoing support and development. Pyro4-4.82/certs/000077500000000000000000000000001416147301300136045ustar00rootroot00000000000000Pyro4-4.82/certs/client_cert.pem000066400000000000000000000024161416147301300166050ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDjzCCAnegAwIBAgIJAJv9mHLZbwZsMA0GCSqGSIb3DQEBCwUAMF4xCzAJBgNV BAYTAk5MMRMwEQYDVQQIDApTb21lLVN0YXRlMRYwFAYDVQQKDA1SYXpvcnZpbmUu bmV0MQ4wDAYDVQQLDAVQeXJvNDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTE3MDgw MzE0MDc0MloXDTIxMDkxMTE0MDc0MlowXjELMAkGA1UEBhMCTkwxEzARBgNVBAgM ClNvbWUtU3RhdGUxFjAUBgNVBAoMDVJhem9ydmluZS5uZXQxDjAMBgNVBAsMBVB5 cm80MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw ggEKAoIBAQDnlY+rpRE61q8uCOONDFPYJZjcW+IRjIjq/Aone84CUFv0kl0UR2VO Cihq4offI3A/ILZHnYUw4xb/ThXM00y+7z4691i0qYn1rac02Un/r5UgqJbo/JQP OGRwxVhwfltC9aDMoJbNgJ/1z04P3ENvPClXVF2mPtcscPG+Ot1v+Ov1yeBzCkuO 7CIB0TKIL0ShmscPKccxbKnSpEqeJp+2xGlasKZGSCs67+grMg0H+Y3aoDmjB65o 6JH5aFeqndZHbzsxQuKCSxy4Y+o75Xm1QgleP8UYHHMBIbPkf0TYvDfrnSw7dXdI rNbgW9xhbJwG42/kIA+TO2XrlPfEsfD1AgMBAAGjUDBOMB0GA1UdDgQWBBQJb0wj f8O5fNvWScUYFjOHFPLmgzAfBgNVHSMEGDAWgBQJb0wjf8O5fNvWScUYFjOHFPLm gzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAfJBJVu21tbfTv8Ucs qKf8qoOZUHa+fyhLOdzXSISesh5nUsAjkwh7jCqx93Q7EJB7SHWss8PDEQQ3orcM W+kjSpuxWIxP/C2F4Gn/RGM9w1aorD34DkLrGV2Ej7tKak2BrL5rUHkdvTzoIi8j 70RqV5yeWV6WpclqWWbXrJXquB0u6O5nR2gs3IwDbbmGpcDBHIBeyZc0syCzC0t1 RtmGYXoLXxUYHnTVYBAF6Th6hEXO4SXKSf3F01HThVL2VWuKhGv+6UdmPW5wDDs8 nvf8IgvIqGJhzWXk8REeT05iIC3t+8iEHAHTilN1lFFitSKBP0U5xqTbJFUFjkoF mb4c -----END CERTIFICATE----- Pyro4-4.82/certs/client_key.pem000066400000000000000000000032501416147301300164350ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDnlY+rpRE61q8u COONDFPYJZjcW+IRjIjq/Aone84CUFv0kl0UR2VOCihq4offI3A/ILZHnYUw4xb/ ThXM00y+7z4691i0qYn1rac02Un/r5UgqJbo/JQPOGRwxVhwfltC9aDMoJbNgJ/1 z04P3ENvPClXVF2mPtcscPG+Ot1v+Ov1yeBzCkuO7CIB0TKIL0ShmscPKccxbKnS pEqeJp+2xGlasKZGSCs67+grMg0H+Y3aoDmjB65o6JH5aFeqndZHbzsxQuKCSxy4 Y+o75Xm1QgleP8UYHHMBIbPkf0TYvDfrnSw7dXdIrNbgW9xhbJwG42/kIA+TO2Xr lPfEsfD1AgMBAAECggEAG37JWQK3Ifo9QETz51owVjhYhskt9RFvbB7Sgc+ULPgn gskh3feAEpzoIZCVuyt09znGCoisOJqtr1AxDGmF8iFrjQqy8Hg0lgOxF3g47e+F 0CPUDwN1/LygSDdWAb0uzqtlFQZ4ARwlYcTkKsPnCbzl16V/nO5XE8KxIPnSMqns T712i2WmOm+AZf5C4vOBlBYwJpv7cYWuT0Mow5rUMS2tkT3yD3MnVDGJfhTGVyrf M5V6ijGaPS1xLuEv9tWkAo1CMyQCG6/YIaf6zIzc56rsPiNFvCE7C+vw6nu3er2l IH6GT2I2w1DbSvQ13eLMcLhkod+x2jHWT+dPdz8lnQKBgQD/aiLZbb89WrcVzz40 HTCN4ovxhE144k24rPCkSpPCrS8NdRRJZAGM9ReuZVLpQlsNsqtMz/l5Hzgf2kO0 UYO/Qj352+nX83vOEzXFHtmZjgCV4jf5xNRNLSHr4XheMxi4d84+0h2cGFYBiPrL zjXxt9MQuvtv48OVF3klfGxJqwKBgQDoHXFS/i4l7CnmWHQ8VHpet7IlKy5bv7Pr 82oEZa60PYmwBspp7rSL74z1lRqkpU3AESrHhp9ib04gc56r96FZgZS42SyEul8w EGZbcqo+j45xDD/+JMLtXZcQWK3wVnFUJolAD5sZrsGLBmTjGmnQp2hej0nxLxmS U2nqWOZP3wKBgQCXWJsB3+g8QO7QO5eOZeWJjb6DBHSrtt17Gu8VSyO3bcu926yD uIC6t9iqfFve4HT37vFWeL5JKVimdz07MjoxMN1smwU784lfGT75aUhjlyN9rSii FiH/AUlibp5Wo0x4snVCAFuPTVRZYIPMFIseimDFPycSrBIO3HPq5Il+NQKBgCyP AmNBQlCrXnvGvUGbogYu03cJLBQW4A5Koy6G6pvVOGpfU1o0pdo7OV8nqX6z8RIO +Zxl/pDh9yiJqYsvtXy+QHOf1UkBkiZi75Nclsv9uQWAqYQ7QGRa1BYiP/nkTksu Pqjalha/Eo6CwrlKJ8gTaxjD/xjaxtjtRGblfAkXAoGAfsn7EyKe/iaRASLeypML chsPlFiVm32F/IkJZmuor5jCBSgekahYWw5eJGvhybW8juNmvC61Wq+dmYTDf7YW Uy1e252KWguFABFdax66o3hy/pXrfF/xk3oQMGST3PwqhCHIgp4jjWnWQffBDcTz TUducolvUDE0xfqqLlgUBKA= -----END PRIVATE KEY----- Pyro4-4.82/certs/readme.txt000066400000000000000000000011321416147301300155770ustar00rootroot00000000000000These SSL/TLS certificates are self-signed and have no CA trust chain. They contain some info to see that they're for Pyro4 (O=Razorvine.net, OU=Pyro4, CN=localhost) They're meant to be used for testing purposes. There is no key password. It's easy to make your own certs by the way, it's mentioned in the docs of the ssl module: https://docs.python.org/3/library/ssl.html#self-signed-certificates $ openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout key.pem It's also possible to make your own CA certs and sign your client and server certs with them, but that is a lot more elaborate. Pyro4-4.82/certs/server_cert.pem000066400000000000000000000024161416147301300166350ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDjzCCAnegAwIBAgIJANFjq4K4t03mMA0GCSqGSIb3DQEBCwUAMF4xCzAJBgNV BAYTAk5MMRMwEQYDVQQIDApTb21lLVN0YXRlMRYwFAYDVQQKDA1SYXpvcnZpbmUu bmV0MQ4wDAYDVQQLDAVQeXJvNDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTE3MDgw MzE0MDcxNFoXDTIxMDkxMTE0MDcxNFowXjELMAkGA1UEBhMCTkwxEzARBgNVBAgM ClNvbWUtU3RhdGUxFjAUBgNVBAoMDVJhem9ydmluZS5uZXQxDjAMBgNVBAsMBVB5 cm80MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw ggEKAoIBAQDpXbWk/AkfiR0Dqs9S84CLQ8sNmpgj7IYDmNgUFuj+a7JQOpHjtfMK JS7WtWXbWDY0CvzSWE1bxmVru0uOBywd1eUH8BHfYR+AQzEJ3IFU9K2yxZyT1nZ3 DPY3gmsT9D2J1NMvy+drsDL/qH/MRcMRmhBmw7QSuGrViox2Vu5dFhxx+3cEMPTj OU+dXtOYmsWQp94lE7LvOWdstTQmAqR/eKj8+qmewi9dFsozkSfaI541s9OHMNVF ECx0qpKiTyeDA9CLcHe8WCHL0TvIFXTXjUUJQUUiCueZ6ub1ZqhRhWz7hm9iz6NI aV4GIicfEfr0+ocZO7peidwpTVKdBWfTAgMBAAGjUDBOMB0GA1UdDgQWBBTo4uUt 4+xM4sXDBqmw7Js3IGk5czAfBgNVHSMEGDAWgBTo4uUt4+xM4sXDBqmw7Js3IGk5 czAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBlB7wPTI9EAWKoigEW zkXMMBGSn7wqdCquaU2CgEi+N2LlaitfD1/sH36Xj8AuyEujT7frrs6q/P6RkRlE PRuHlAV07GUWPOiqskT86hYMbxIl5ygvkCYSwcvrBkFGvVN2hUPTqtwqckQRUyzK +w1m2nfldlG9/kto7lyV3MuCc8f0WUYgS3e74uwnl3Qr1Sjrr74+pdK39uMm1dXw ho8rfaDH+fEqBymwI+3DA9QVoi1vTUTLPbP4SK8nvLa1CiV2DaRmmw+eOv87qDGl fh3llz7+UJFNco6uquuXHeDtKvcNCVlj3mUfAhP1SzGG4mSvAQeEjwSjTEZQTBOc XAue -----END CERTIFICATE----- Pyro4-4.82/certs/server_key.pem000066400000000000000000000032501416147301300164650ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDpXbWk/AkfiR0D qs9S84CLQ8sNmpgj7IYDmNgUFuj+a7JQOpHjtfMKJS7WtWXbWDY0CvzSWE1bxmVr u0uOBywd1eUH8BHfYR+AQzEJ3IFU9K2yxZyT1nZ3DPY3gmsT9D2J1NMvy+drsDL/ qH/MRcMRmhBmw7QSuGrViox2Vu5dFhxx+3cEMPTjOU+dXtOYmsWQp94lE7LvOWds tTQmAqR/eKj8+qmewi9dFsozkSfaI541s9OHMNVFECx0qpKiTyeDA9CLcHe8WCHL 0TvIFXTXjUUJQUUiCueZ6ub1ZqhRhWz7hm9iz6NIaV4GIicfEfr0+ocZO7peidwp TVKdBWfTAgMBAAECggEAH3s1yuj5LhjoIhuD1nPk7mSZ2t71ETuSJKAU3jjs+415 w5rzHV3pSIAVnn9gG0P+I+0riBDDdtL/0ZW1rpZHHEtDl1uKfbDzd/EYh3UIuXY8 jHZeFg+DcbH88SYV3d5AAv0D9Fm0gXGGn7iSbPMoSpKXDdXuz+uy9dW39yrpAwN7 piW/4M4QVYtl6yAoqzemwf7QQi9T1fe+K0y4EknX825JfO0rYhK0RYA4XQPTh/Fs jaH10c5IfcBW7LEbJCooemXdR4qFUxgKP3C2R2GgcocgAaZKqVL+TT5/lfeR1KN2 nv8GR+JJvNbKLLRE598S6VOoLVTdTE3wuqc64DD28QKBgQD2R0Ygr9a9GjusjryK tnnta8I+SzVrL3nnvX9pAjMyg9lWk3Wpq8U2EVHxoow7yG3FQCO5HMezz72z/CYq u5L1uGubN/QHjokdeItn0YAN/lAWPMgj2q3TRuCErht45v16RWFLsQzX5xgZ4APN NbSJSo2fxrSbAs3KoXBiQpw0CwKBgQDyk/OtwrSV50lX7h01eM5ZcWHWm11AcYzS YatQr0s5p9EZgekQEhWa+veq7M3j0spOFakFbelPcH3MU5GHG6h09AVLfqLlScUE ZfNXKP+s3BbM9oI2kpKDmGzIBDK5J1+iQihYdHHQwyT9+WXMBeqNieuZuHsO/klm 2MvW6ufwWQKBgQDrnwM0EKdlYqzHCgTOvYhnoxpDGRdxhgmesoksgSNUaBrOnuQK wcE7WlTI1thN9hM3jYbf+u4BVXbMtSga6DzRBHFHHsBFXpRGcucFG8XlHXOn4t0k mHqH0Z7KsfysrmrCyZAtp6V/BqTjVJSuh3xDgeV/gM+YIpWbENuB1vK1QwKBgCf0 3N+TQcGrXjZgn2kOZBbxiScbYknaKlMBCYH5zc9KaDPmZShjgjGMAz2hUeyj7PTd toCRcSvHoMEGRuNVV/MjureLvXM9Knml/WYu96ZWfl72f85TaFTKx6hOoLhE1wjY EuM3EwRLEI3RBcaMdu0neapRa9u1YQvSzAizms7JAoGBALI4qL+MFsaumjDDUWzU 8Wvqj+NTId4/SNypLr3iG2FHLeGtBTro9aHx/H/OWMZNgghGOZAimZKnSXNHd1f8 tJzBsFbqEden93kw9PMR3uvzjoVVj6HjHWkJhqWQMRvNU1QSzTHAFfsrDjNF1MVw 0lNd6vPevCXhdZ7dS/qCcJAJ -----END PRIVATE KEY----- Pyro4-4.82/contrib/000077500000000000000000000000001416147301300141245ustar00rootroot00000000000000Pyro4-4.82/contrib/init.d/000077500000000000000000000000001416147301300153115ustar00rootroot00000000000000Pyro4-4.82/contrib/init.d/pyro4-nsd000077500000000000000000000045321416147301300171020ustar00rootroot00000000000000#!/bin/sh ### BEGIN INIT INFO # Provides: pyro4-nsd # Required-Start: $time $local_fs $remote_fs $network # Required-Stop: $time $local_fs $remote_fs $network # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Pyro4 name server daemon # Description: Debian init script for pyro4-nsd (Pyro4 name server daemon) ### END INIT INFO # ------------------------------------------------------------------------- # # Copyright (C) <2011> - ppacory@gmail.com # Licensed under the "MIT Software License" for inclusion in Pyro4. # Improvements for Debian by Laszlo Boszormenyi # ------------------------------------------------------------------------- LISTEN_ADDRESS=0.0.0.0 LISTEN_PORT=9090 MESSAGEDIR=/var/log/Pyro4 MESSAGELOG=/var/log/Pyro4/NameServer.log PID=/var/run/Pyro4-NameServer.pid # Defaults - don't touch, edit /etc/default/pyro-nsd ENABLED=0 if [ -f /etc/default/pyro4-nsd ] ; then . /etc/default/pyro4-nsd fi if [ "$ENABLED" = "0" ]; then echo "pyro4-nsd: disabled, see /etc/default/pyro4-nsd" exit 0 fi # Add Pyro Config # here you can add others ... export PYRO_LOGFILE="$MESSAGELOG" export PYRO_LOGLEVEL=DEBUG . /lib/lsb/init-functions # Check the script is being run by root user if [ "$(id -u)" != "0" ]; then echo 1>&2 "ERROR: The $0 script must be run as root" exit 1 fi # Create the PID File touch $PID # Detect if Python 2.x or Python 3.y is installed PYTHON=python3 [ -d /usr/lib/python3/dist-packages/Pyro4 ] || PYTHON=python case "$1" in start) # create the log directory if not exist [ ! -d "$MESSAGEDIR" ] && mkdir -p "$MESSAGEDIR" echo "Starting Pyro4 Name Server" $PYTHON # test if not already running if [ ! -f "/proc/$(cat $PID)/exe" ]; then $PYTHON -m Pyro4.naming -n "$LISTEN_ADDRESS" -p "$LISTEN_PORT" >/dev/null 2>&1 & echo $!>"$PID" else echo "Pyro4 Name Server already running" fi ;; stop) echo "Stopping Pyro4 Name Server" # test if running if [ -f "/proc/$(cat $PID)/exe" ]; then kill -9 "$(cat $PID)" rm -rf "$PID" else echo "Pyro4 Name Server already stopped" fi ;; restart) $0 stop $0 start ;; force-reload) $0 stop $0 start ;; *) echo "usage: $0 {start|stop|restart|force-reload}" esac exit 0 Pyro4-4.82/docs/000077500000000000000000000000001416147301300134145ustar00rootroot00000000000000Pyro4-4.82/docs/Makefile000066400000000000000000000107641416147301300150640ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../build/sphinx # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pyro.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pyro.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Pyro" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pyro" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." Pyro4-4.82/docs/make.bat000066400000000000000000000106511416147301300150240ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=../build/sphinx set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Pyro.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Pyro.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end Pyro4-4.82/docs/source/000077500000000000000000000000001416147301300147145ustar00rootroot00000000000000Pyro4-4.82/docs/source/_static/000077500000000000000000000000001416147301300163425ustar00rootroot00000000000000Pyro4-4.82/docs/source/_static/css/000077500000000000000000000000001416147301300171325ustar00rootroot00000000000000Pyro4-4.82/docs/source/_static/css/customize.css000066400000000000000000000004151416147301300216660ustar00rootroot00000000000000.wy-nav-content { max-width: 1000px; } /* override table width restrictions */ .wy-table-responsive table td, .wy-table-responsive table th { white-space: normal; } .wy-table-responsive { margin-bottom: 24px; max-width: 100%; overflow: visible; } Pyro4-4.82/docs/source/_static/flammable.png000066400000000000000000000176371416147301300210060ustar00rootroot00000000000000PNG  IHDR=bsRGBgAMA a pHYsodtEXtSoftwarePaint.NET v3.5.9@<IDATx^ M׍dVQ,S/ ()M4zzMPk(O=!I+Z'}GTؽ{wMݨ>O9{@!^8蠃X)<-[9.>_'MZ{a`~5UV£R {*4^lC}e{o ^ׅN:/_VZRKٲeWXRS}obDF Tzt޽\H㏦MeѳAsev}INjg"TzP#`ФI I3T5kYLVjvVȌh!qt<"]KT_,PƱ7oN 4XjUҥE=`ߏ e ~x`JI :kFh#ƥNTC;*T("i0g4k"O=*P_OnrI3SKڵMDZtc ]*U+^ܹsRD4C8qH}DR_Ce[!f,M5Wh:*zjr;VˌߝGWP# @Z2pq^]I|/u2?aDJE]Y=oŅʗ/" |kfчDg;5$]'A3HKAl߾NivIDOIsUNV<.3Y>x57< ogH)ժVuj JU(ɓ'':jü/SׯH2շ&H}=);DҌMYRȢ=>ZXBbs0'WiF=߾K0`*&0ܯ\2!vSx͙-".HfD7t^-i@<E49E^FZ:s m<(q̧DO"E|Aoצ#e Z=QvIKw90v< Z4oQtQ_Mj_t&b֡R^J&{mAгgOqA |1V!F#,xyԫw|A(_~P?_~uO_'媶v{d ]$Ď1~bya Ĩ Y7 >}g pӎa~Kgguڞ:50 "(H\)3o9)@_- Of(quՋOY~>k(ܳvWO'qHuMelS,X_X@ 6Xʈ#tAUe~XrFc"xN+/;l޼y M6)# }Jѕ@ P^6ڦ 6 ?߸I7(DS sʔ)mwݻwIVKWm)SCX 㣽/̜0E<~(!zʎ$ H b 0uOx7A ^Zzwy"TgnnUnA;Ĕve]VbuR%M-/<\=E$QB94եKRmj׮-~SQ2$@@ ȝetGBoMVb.b?b&rI "d\k-_*[ZqKe v,Ⱦ_ ?E)ISuJ@q~_bMxk_ %TΜMiN6f*1cZ"#랔`UNl8o͗3LM#qAj{~>?' u]2BK͛]f- ((ojEϔou} -6tHZ LrV25 &ng7''Ok7֏n8? HZ>t-i+:*q>RQ  ~FN[rZ7u݋{lٲfcw ^Aizv+oqFs,Ϧŋ|hxDjrZ#zƒo75;/Fp} :g6"Zƣ)D_0 ķʚFj+Ngo4sGʮECt*Z鯅f Dώ;@- KGD*5OGax)Sdht;~DlGְaðP\V9=&ͅ#O#T%bjx(xg3%jv<؆ JdiL=an~{ 5,bS&<4aZ5_N#j'"iv Uxr[}w8]4<0aQS|ѕbn!h\TvVgidi "2~`qFg!]JغB/,=zd&Wg0oPj/[,NHi($=HxNIb ] _{%bBbXF x<^1m\|G#Ƅۅ^d^d ޢY3cw5$@fN@571Aͻ^\ {"ΘZ2 )=:oi,C#ÈgZ _yv]ڪlp< iG];< Nmmp7߬H6sES[Sa޿=`Ds$f@cҿcr,es\MqȃӃ^2yq{0rb{&6 %a0^w3:+l$0 lyf*O:N~X !odi94m`&#ꑣ(?F01cU]dC]hQ [{£BXP\zXpaRTЫ꤆Q &']TT@̙3&+x[e~L''i#*s/lrfոQ}E \9Eӿ5jXۢC5eذa@ ]&(ݮ'K Q;fSz9_GQ*!3'dL:WCZw޾(\EH“=Sq$(]ҥ_90f@^t^Km:"`VM.%h8'իW9&lP૯q!'7nX!,% R@x3up:ɒ4y la)a*;eiCʕ-ry0,_SZزjԨѵkW*iղ-y c6xղ$C,P/{@dط=wd)sV=uA{s7w(_/#Fl %PTGՃr, dH!vۍYdH) @ G& 6vy{S-g45@6φqKBF`6pT3BCIi9C|ëTI+3XBFK SXؤCM/;3vBoYneݣ)8ZAޚs( o/r :FduaQ`3W l/&7'')fa2wgkrl <9H3mS _+NWc^sݯ DƘ|d5uj;鿨@1v ,G' 8 d*h=H"m9:&s7s urNE|ճ o|QG[{ݟoDK jJQ!k&@>XzqKv7wF@,img֬ӎͰ ή^I6 ),uk4f">K0S ֔G:3MIhD|*UPĆfOxaPb. b2 *@^]K' Һ9bͲ?,[ّ"MPE βH?:[ Ε&)}^n`^(F2)zx$۫ɺ TlhD{z2%)v0Y $ۃQ5X*ier1}eIcly_|TxC- UΨ1Ja6vu9yjpg6$23#O |"~j/-?/XJjS2sV7T֚mfQu*@kR Σ2@uX Ą_g*~Pld~*ȇ`âM EQ(?jFϬs)$}e(63;9@f@,i& @tl2g=:ޛ3m;w37I~ǩBD-7.:L1SLB*bᮠ5jyzlR>{RqǡV^'}$.W9s`)Şz{*P}*2zWًsE\?d󈇜LBeӉuM;9?{O @L{Ǵr󵖿&9Ov9Mʡ@:\@πяX#$CoPqFhn( fA)}w۸An\[񛪞^ƨGkpT-΍dH_YFT2&90@x "}WցwF,F[/g_Z-?J9yls#{7f/ZYoU8 0hGg7y A_ K `>b]z@}sOßGl.w ^jZ$ƢG:& isi9"SWXϬvdF2BF({ ^{X%1PE>ף+<{+ ׿3R2,E U`qэ>jG[SW~TL9lw uA" d< ড়7NDЈcappLaRjobT8 "ƛu@ G7._xsH`_85:K@s  vbSQ i}lN{ƾnH$m˶~a3”| [ Mjf  ) @3XfLVյg~spͥ*g$5EddVs=a˱˱B!ȌAo|~=E~[r,r,eŁ$If’YaZ˷K9c9c9RLF^g_{=|ҘN0N23[~c9c++Z|>~fϼα?(R᱆u,r,G$I _~&= C,{c:c9[Jd5Q $Bf-AGk`2<c9c3L9So x co|+Z=iZOX"@+A/K - pyC׷k ?c9c DkM) Aw6|oEH*<==`gkݽ}zI.c:c9#ER("*Fjyڦomʦtvm $IǴc9c9RH)rjU!?XuJ:SW5j{SyxZD kFu8c97Xk1d EΟ !@iIT+8҄w|X>`||OBHhz:c97&eh%0!1ӆ }HiCJ!$)bM45di;ܸó%子.wyw{hO@2X $1()_^WA5P|rgn?dQ裵dH)=rɒf$IIwkGL]9SE!AࡵBI,=h˱_Zւ  :P_rm?fׇXIDZ!3Q\X@ Z ZOyKuii|ZǴc97dEڔ5MXco}oO? MS QJjf"o_6#)i˓Gm;C5c97X,#cĀ\d;}|O6h~2a!ɠr+i!`oySԥxD.Mc:cyc ~Bv <9ʏG9Srz´)7iҍaRTz%<)N4A6r BƔwer,o)AIZ[C~8Al%$Y+˻oojw Q~yH>RF߹Heyd'}whw s X $.Y>T{-ޜD,=$+@b+WolۢIͅ$|F~hqHY$!he>_ZWju$I2S96 XpL,&h =h 5XZV{ ~Hix,ydZ>x eK)ںS9c97LA-_^ᚭҺ"|ZA@3e$OitZ GXHRC9=Ij+i o. 0rCvr Xr,oc%B"!v&W8kɄzOqNRd{=CNfHpHV$;~ވg c:cO@u;܀pL0OcJtF(.7"e|z!]task-qj)\jYv3 NS!Z֝ǀu,W\HK 1H;03Z2@ IYУH ,f,^0f;>AH`d@3vyI^9,4Aix Xr,ɋ1)3Hзvy7wx['K=Rv#$¦m#5`XU{AyѤ$1iviR67D%( @X1lc'o{O֐?搷|iɬ/M WB@ďܐ e1!QjE%:5 mBj wz 쥊M" Z_(`9 p|uFȣ&f'w/B({wMF2umYNj%\ ;dZX1G/,il.1U)&nYbϝW4d؉ =}Ƌ/ah!3ήdUXEsX2Sqԝ5/ւp}w}moԾ;XKY ^Jz]29.R8̋>-2 w5.Ҷ!] 2a责f᨟X̠!>/t||^M"Gd^ZZwREG,an>$cA+)|Ujw+z 9C 5ƒ7%/xnnHA)\&ӫf{H;% VEɏ =;չҔˮn{oewwDI3OJd ԁ@Z(%XJǜ]B H?'Ca^D͏5rarrrq7eN-̒g2# *C8W%+eE=TC=v+\)$SJRȍBR6,|ץ2JDQbod=Ur#K<9IVTr9Zȋ}J$jd{ * <5~5(!eqM;GZ&~5,kiKW6ݏ~}i^\=e櫆(6AbV {}>E>K^̻MgMF"T,[e k-B:^+I(b|me~~n'>0\FEJak%}{f +$_tqRlf5G}a%D6/%  OZnb$y3hvE1w Ml"X`sEnPN*/[<О3 OAf׺&_Y'9c='RzS y(>Weayݻ 1F(h*dQR}{h崬\MK-fl  t{ C(ԑҫvi7Z/~8\ .Z;nxGi47mb2OD R3{ _}f<2kܳTb'h:ZO 2Mʥ|+f &4ovHN$NLWS{uRv?#/S =|-0o6KkT0 #TmiJ O>{|)|.r4ИfiQ#3QZa=mYo~ O (h#OZ^ާ:|!Ԓ/ tQ~'J.^R:M.}[J*Mn'Z"T1(.sZ0YFt;{7xnX%ҩYSTA-;+P(ŋ;\il2p3 I/2ԪJ G!绵$iL\pRý~rD9'MmR^`i_}΃TQn& esE5高:&)(A z:fܻ\FAYD۳,s뗮mo\ٷ9[pWlf}Cٗ=hs@nVgF-!Kx Z⩖Z;fTU6rѥ^h'Ei%MSzq0^fv<0VKW ~枧rMkPM3*;DJő@=JcG/ 14e 7V=8ZWA+r&`5k{O [?LU65`QoԴsu&W _(hu\˛NL)aon|*P5*-iu ]HYcy7Ԗ ʾs9 )ܻV)~԰{1uak[{]+$Vi=S7CܚS72M Q6HJXM&ď|Rֲg6 M$a9˳ IJz< R4eL)&*W)8brutܦ=r?z %[gn9*6t/^cT,?hyC OPJ~?~+ %{?k>L޿{^s+qI%6#'{|o|&6z G`4\&IjbYl:_84;?2z*ia x|X 1g&,Wr.176[<+ @j7T*eeYu585t>Ϸ$4{6JJDߧqɝ?|?]._ۥY/9-0; _Ǎ?~gVC0%th8x@-JHNe,53Y wz@)^'w.\pw0"K%_JN2})]W_9*~g?._{_4*6}({'uϕ[[-:>qe-wRݘÐ*+Kb_1L<2.%Jimoc$}&(B:oW,3vÐ]!Ii8GX*%EmDw-NnU#ͲrQBb,h*zK1 ){*If}K((5!r(M2;?̅!͒&3E㶦5b=L'\٦I2:3}MNv":tӤeO@K!F%:eGAXXFSmL5q"40=õ7?lqv9%3u*6vrŜԼRO\L\ڽjN !Uo}\J9mx:4Wy9R*oZvLX E,wT$w˩>K)]i fRg/H@v C!'=I#ڂ?Ksdx3ٚ4(Yߏ{5C{%w?,.{ma)8@{dq~stnm*1Vhh9G45v!w$Iw;ȝ8)zTO k_ N/qe/+ԉ`g-64ۚo&#ѻ;:~/5H&WAVyS`<6}/og 8z~ `O -]S{ ^Ekw/7CtR P)E!s*ͥ%޺q.]#F#r>`4g RiEx27}NCBe5Z{;#6LuV (#mI,ۦ~?s6&%Mf/=)Bpu;mN>})u'uȩp$C"`+a~|ӑGʣXkIgwFeD4Ѓ w0{fS;H^湍07ҢLyV:GC͇O7zYpVaa&2 -xF8^&3/'4 ]5l9>J;&I~_afMG!FWQ.E45+˼Εdյ\U-sug=檔JT23y+0W6/m(B{v_܄~}MH,tV{]z %`eӒKW6_eߗzzïW01yQB>滾CHLoۉRN6iYR^/y fTIvxSc;4KtVtbRcQZf"w IDAT9x'*|0vp D)MOߡ*3Rw;RcE[8QD%&YnifG+0JXWЌSΦXxxˏ5yٛeV+ezyw{ol,p'@ڌWb¹- ԪeG5 9eE*omxh;%E!Hk*݌vk^Odi>0E?s0 Yl'~~,/P8^LFo0_wGy£붳hU*tm19wjgWRÄwyogvNKP콖aZk 8a2;Ԧ#F>s>y R{L2=Ҁ@g#:7s.UX+FZ o |Vswh,EZkoe kr̷4Uʥhi>J4~)\r=VH\0e6;VnO5TZzrT`KbjY=Z2~L?`0(ULK ^[gZ WL/4OZ.o7ZNJ[s4hq1 dd5Yl+d`kRL /=MviBiY:J^!xO:8+02ԄrWZA,Qds6+)1^2cI`2+VŐc"rsLNpy pQ0GH6}r):-{ $aYlʞ?ZEVvtT0R9<)AЬ8[q&̶ߥ3$iJ02 i+9kB  ithX o!QfCrKނ@Hlw ʦ!>V _ {00gIЗ<|yOeG\Q4~p7n9%p߾ٻF?u( mcdm9mjү7>m!BFL<`xe%ۺ{0BPl0ݸI >Փs4K;tjN#Ơ4 O?āEZ%&|2~\x~KBZ|'9_vZEa‘X,r}s&Xo3za'oᅡ|ri8laPXvZ\7&Ľ֐%Wx`{Ή]bD(KW `߫YqmI:HVM@e`-]C0cR9x?X#snlGPĝqLv:Jzvp_$.|iHcMW>MgnAQSOrf!JwO3>K4#r*XfsJ3;~iMx] 2ck/̅6@Nv1c`tڊ d#ðsĮY"Va;) @ U) (T("%z(Ha"9cŚЬA) j|C 8^İӊ (tGRoހQEyb޻H{C"܁L^rߜ$- vۂn۵1r-\F yvKl0BASVt t&_0_R-S]ZR.(w)Şw9XPSAXJ4Jѽ #]Nפ换Zʳ|?r)bai?LT:(MS]>}}DJ>Z9 uUN8͌V~Ẻj1ЦIS=L">8-gfyZߧ' Шѽ.|d:X /}|ZR峛t;@Z0֋ -5TJ"x.heӦ(lo2{w4%G#ˁcmX{1ؤlhh6jU2c N^-/qٜ!%4(*mTA! 1\snq9sS^l嗀˵XYA61G_;dY1YME a,_ZوyGꡯh,/, %‡~UJ4=GQ1WcnJVR)2M:R"lQ <4C-9LAFxj2y~>s*zZB\.?.2Clm_M6\oc0SUcmgH*'te66GY$aA<(^Jv7+⩩E>=%m  )nSeSo/BB>yO`qq ,.4h6׫a0NiuW-)o*v2\-9"L&#UZLD'(Rʯy?'VY^Ydq>bfN^SX5 but{;:>6?>֊<ǹ2kQV-XPǺ0"-~*#rR$ }RZ^L?5=GU5t l )S( Y.u'.`֓6pz7p;Kf0ցi$>8!I)IXAܗγaVr;F64j>K++XduuՓ,/-0??GV&IO [:vvZ\hИ[runib#(ίXmP /zě_ IGϑ&.Bf4eԮ} \buu+,-6i4j˹IQ$I t=Zm,,6x|o`rk0Tnw k_dqgz20h9-KD'^Bk 8^Bhuu{ .pTǹ ~BK];H -6쨏DiB>qjHx X)BX>³|RxD8⩝!$譧hT}N<+;'8ybz^0T Ɍ^~\6k{b%{HE\!,]$ƹ oF+Am_fpsn} HH8籛h5GarZDfJkZ[XiuըScFJkrtZt~"vy3M7@ݹ HLGt30װtdrfbk0̘R҉=I,g'NO5ufL#%7{-N,zr{9Źs8{$KK՜H5EZk|O/ F/"\t͎lCSK| 8֭Yx)i.^aJe?Mv)HOӀ4 ?Al_ mĭF~{_$ω+=}sNsSX1?G<.xۭ+m~%I+$&؟5ڗCT? vN.s)s%TS+,f@T#gQ" vZD~Z޵y=:NTR۱^{J% LXO - kYvwgE@i6faf ^OPwp넔~"X Ȁ}B$vuIԍ ,;EΜ930@9Pk߉XkְM1#qیwM\,Q)/?sN= MWXYYda\Rԅ3FmBhq%M>R*:YR[>G|ofKMvʯZZ~/p)/zR0T3v`RuZ4]tEi4T*t8j12 W|a]֚awgeqiŅ&++K,//K#ͪh$=z'lJݦ& C`Pt;zc(?ڕRDbhffrXZЏSZda>('%R+QcA(R1q|`|{*Ǒ kXZdؖB^{GL%0m* ,;MUaݭõΑXUX^j5@}7mZK*|)irLYgGܪ!fP3{C{gVM͸FfM{2 jscA*L-wtb-xDYy,.4i6XƧue P޽cu^eiefsnĵ䑷^qE\h7E>~ \@AuM2S’±}4YijfZH*=#m&i130T?)6$#IQ5hhd'DC"kB[z8%?dtF^ѨlԩrݑNE*iJQHZ܉:!ҝHXҌrLb,wr^}u-zn?ϱ:\YKQ>ܿ_$qT"3]suRD|V8'ԁPhl&A{؏3ыKLWf|ʹIM+ȓjd<0G'uN;"Bk^Ho ݥ(nv?=#t|l2'GE UR'[rR95k/mZmOqQ+e!go)jB^ri1k1 ` RB%I]9<<355W-͌:c@ >9\Xl:J:s0ֺp.ޠy,7*k*2Vì;^c\%ZPɫ6ON,>2-q:*ZJfmQpcGVIeboŽ`f0WqJ v-J+*%p`4;(dR:y՗M'T_F눯bb?P~ŗyy{@-n;e~48\JR+W%vRpFb5m <|`'!a8RR)QKVsu!yK$yFHO#ЂfiXEK$:k}[ߢ\^;IKG 7=~J_ɗ/שq:%D=^\]f1WTbFJ CƇ*%qUj0a;A3xzfR,g.SQx\' <\C`~d$#pF0:v/$Z(Iv{VMAǴ5i2%M$IILPi#QcytT=LL4sSS$Qq߫<z؃ydm u#M?iZƔ IDATQr3R{.~TKK6sY,[ڕ|?}g?wFɡ{"JKvlgQu>9wڔD%o}`/~VFQR7>{AvM{~XM)/<զsČ($}ּZ!38K͟>e=4P˳QPxټF))5N\LTS&,qr=8jhr}3:J'˵Uj A`f/n|voyޮ?x4LuB$YS)L)!D˜[#ֆa6O-a6R )ۇRLV4S)J![KcCՠmlV)"͋:28ZB NQ r%(\!R|]u5w%ay9gT<,de±.aޮ߳ݻپJ0+O9l^`$' ]歲89ᜍ^eӎw?kSnͽ!G0pgѧe&׌R˲q]8 pAon=-fwa,&7<:sf *Ц6v4UAQ'FQ(5k+^=~ؔ9NImNqSkI*[LQ{j9T$mi¨;N#{-W*7HRUx? +`vYPM|ίvFғГ/z"J{:?|b+%8wn^ZbdqMݤR-8=,8sVh_ijncED. G1M ͥvpcbEaJIRc75L- 9, E@Gm;bFIᑡ+W2xg}L5t$$P^+(OXfuy:c4!!clu|8i6]~ŀ/t9 vꖥƭᅧz۴{=z&k(b<—)V<9Rl]#uk A2NJe:.sxp8* [wLʔ _֌"]0֟&do ;3X節T*vFyr7M\:8@{5 B`%É:F>&oR)Tl=N_;"YT+Y"%kǏ`ϼfJv2bV"HY9<3LS[m:,/-ԥiR(^da翞Uв(9DMg+Z( j/3NR~vQ2X%IžpIt&JfDU } t'50l#)-UV*;Obpw3kƳ %9jaB^{t 0Vja)"ϧ^1_>/ @[9}(ܟ w[N]|#C*n%J‚P9gߏK[Rbew?dlX]^ȅ8\Oɖ}HM̓;f,uVbn~*F /LuemM .>+20UY>HG:2QV(9Ou-.c0̥2r寔Ɔdc`XC-ph۹bDF4Obw8  d5cN&?{}I*uJqք̃VISM$?ާ>goKpyZ(}*V^l0VYISg~TV1SdccC[4hT v8N1_2],8CYEJ*_?5sW~W?dOOzN+YaYYi?Y#3+UT'O:)I5v9 {'N>X(%C=*uAf,1'GQ;'feZ-ǹF&?sK.iB#LdVfoM*K+K\^Yʕ.]ҪF( plG{g^=eMF_"#bfW0tec1\b+<~fǴ9)6Rl*$z iԉHP*rB"8sϱ0V˱rmY&BH;4O`w1ԤCw,:ׇ?;`;%&-< )YaCv_O-UO74S簒D;!ۦ aMuV{БDqLn("릒'LqUˤn [eű__0#{]\`q˥E^K=zFGxb0N.D1upy}$$W%l>TP}RyH%WX5{_͌I;ѐl.RbUή'T:5e34kU00 (.Y)T {)#Du 8}9je$X=PT@c\95ЊXy5s1Pl% .)"svZmtIk[ #?&yJ@>ISqtOY>ws=s9Zt"{O]Ş:shԯ=Bx>5Ǵ/bue˗|i.vJ9ol^:{>'@lץ9X/K_t{=!f Db[6/ {IUQ3g3qc0BO疊DB"SWfT/E|vש\8$^Yk1I6SC!S/Wyv32clxW,*<m pqX&`(EXs? *p{7u7nQ/zL1zy"LilƽK>_0 I%FT\3J()ˣÄ;R=qeؽЍ}TzͧcF)Dnv۸$g",DY PؖSpc?au^fS/5)^IJ8;-ɷ_X^nƺ(uJKZ#J1A՗vZS.Dh>{w.]! `$tWtb'cIv:ÏG;DH`qҒ.tMb`2tY,WEgT ظajX<䡳#RR)̸ qDQO F \"k[Y袄hSȰz %qáb,:JR,&%Md%7`].QTTiɧ; {QT2F&kΌ:$*ppm &=$*R#=8S=; K*LqX1)^F#1o߽DP+40%y럙(e~IDuaZLgc;ShNγm|3%[sGJ1jڃۻ>abHgGCR*';w vGTc!,iOڏ?y4uZjc)58V:?zM~ž(1\ETuN`\ر5NYX.56j.^rHTZu:ZV{mMz6P!0zu^y Wuz;x^Ƒ F EjTid*\BVj8tL0!f`@ A*#%KRЗVt/Qꞹ,pH+ï>CK/]QlԨVJoN+=+phq8\_)-mqp|H\^! 5-L`) n?d7/k9l/:kÄDVH ؓJeJyzPDx.n3l٬<](:08 F GÄ85JRɢg8j&Q!eڭ:fzLxLJB߻%r9^jXbcctŞX2yu1Re\Z\Q֡ffAݠjjթy8*L}V碑gyZđJ}ևM N%H D)HԠS(K UZ./R;;J9?Z i3}qS@>!7w%]TWN>%tqDb[8$6Ol eوu\9TTvJIsH v*cܔpx8M^ˬ,qjYKF'^Z.Dr@Q#Nyoit("j.l^ek*{ _ĒCUY!u|,tE1Ÿx\=XWRp +Nޟn aP9 8U+sV VXޛq[V=#2Qt}觯(ttu{{_3täw?{Ly%ʥR>MJr8LxPq$=zwP祸fi5bzȞֲ(+C* K|k]cXD!5nD߿I*(%8֕F؛&j(Jdzae8-jFbbŅ6f]ZR-džٌơLA[}I슼ڻ?'W``z}Cnoq, [Tc."1 |$p}ZWB Uĩ/$ݸ{u^Vv[kCsLOg² ID2q)V]=*ϲ* 3P}bv7 ~wtCc",,!ZE8W#D2iPu!}S 5kOmm>JL{Tc0\߿o?(}SDQ [aDJ4^oluT2RhR9!oGK,-vX]]byy!OuteHbw?CL:BE_\ 岮>S@;Cz0 [#BJea9b);nX>J_pܘ00׏EXs}AegS zQ>kcRC$>&f"VtyE-&ndNïq!x~)œ'5;)ժ ]%• Л+(&Zx4f4h|gAG`)P l%p4UAYB7Kk]aI9X3qZÌeh {c7YyEVz._Zҥ-k(Y/P((NJ"yRJfcXG-fccTƿ}pkiZH]cYg7F: u|Ȯ(kWp|T5Zry+\RC6k?>f )d*uZE`A Ѵ4U!)SnU6o (ed,-tjt$ݡO?K2ZtZKfԆ4GaE9pXzE{\^++v[: N[,IDAT%1.E3qSo5{xafBYR)It%q,;x@hOWxDr)'+S^LA}6"ߥ$p=' %`E{*-NsDZNo}^LS@Z%y| T]IkeŅVz\%zybF^wV\ 8x#@XqoVzZuz{~G|o֠{ }@خǧ㍻$_MK(9F& B;Qڶ4K njW:xS,ʈY*&)vQl* .ˋ._^ҥ%Vv[je0qܙj&RZ?poRL/ UIQlIgDx9~Umk{?zX7(]ZR)Q)#_J]R Fc^+:j3 }]!4*]ifWo6 B!O0\V괵Ws,R.ʚ~nv)ȩ䟗RǾ~)JXaOdWHYkxBnzJG}qkfbe/'&#1#TfFF/ɜ&L/lGwv?_1Ψꋗv,.riRVi\o//Hp}?kߙ4OcW) |EVzMi[qmCL4%}gS.U˔U䚥T(oyIt²t; 8NqXԫx~b?;FJ]9)a2{a#JG}u9WY^겼Rɡ҄Vg11;8xjM+%A X~_nR%]ͪt5qu"q Eyzd|wn͛?bʗ)¯@(T:&Mg*%)a 9~",р_U-V&fjdi|jp{7 o;Xx׫ԫ%jr;4vЎE=6:TGT>.QGZy'VvcLb31oZ6BȃX{FDKסѬi^Caۢ Ygӄ(j0eef aF0ޱgI>/rlhw,,vjK20)a80zVI 8"xa{西Cy%Z]D)4wX=24cO댷SYyeZNb^bN;,Ly R)!yP*~lԼLa=_|nI3LFF`ϟ}h;Jc?@zkjղܓqQ«7Y:(w( 9k  ,\x PJ䓀3+%Σ+&s,nQi[fl5A +^FO\8?`po[ٺ1{3x|RTk5_zzLӌ[Yt4U* q,i&V)W7Suծ$=v,,je0d2KS'No-xwDT*Q79K>;8Bqctv@ elgZ ,|pe9*z̍)dJr#l9)cGA1jFJUjni6/*!M+~^KT ~RvXfopHÈ<~yA@vJFz"L\r,~G.Gq#JAHkv/Y^Z`iB; M*diOOy`SjL^ժ[dscu]e;l E?.U|$hHrtpg>\TbU2rDqmShQU+ q/drINJ8yF㿡BwbWf-gUl۶P<'J)]έ [;+kh? 袀mmbߡ=-"(̪N NM uS;׈J:mOslF9e&E䊞[q]|Cyn@TZ+QUת4U L^% sx0+K_{FGG\x݃}24;d4K%Mŧ A qO/,~ 8T.8].hTtt;:m5h[2Tbڏ&lx ׂk8QVv{&ۻ]vvg}oe41ȴ0!8q"dduT*%j z|R)DM/?Jy `kT X{)kt^9UML vEWJ;%u}j)LK`a{-HM;B KhiO؟2Yu#%U C?:Q@)GQ:䏃vqk˚w@|Rii'ut4 nj#IB$qZWs# <0 C64*|#4kO;B`ܽlG슈{|N"ݼj -]Iû_~2#ɷ %﮸-|MP$جIa8\h^쐜G^94U\BEZ"'M)Jf\#y#oɛHjN?F4!L'uV ݥDrD-2 CfsH$9)^/$D~L- qx\f4p}Gc&)+L$Dz:5{.r\:f_,֟y|7'=LISI*SD)qRinv1-+-M7Ƨ;fy&bJLei"Fh.FVYM4J(zu'*Wh6tZZ-`VI&ٻ t٥&wr/[l>Skv%ltr$VbPժ 8 F8,!Y4dB)纄i@6hYiEX3s=M+!;HB(ي׏ eflJe >ȮSQ8Opnc<>}r̢PCHwVtڍz, |_5:tZ ђ(CIp⣇z}ȳH\b !(\1qhTT3ѴÚ?fRrBR2@ l @X܁U5>e@X]6֋ ϔ֐wd iMGVlZ9BBn>LT+%-gӚ.09OvL4|IƜf=$aY?9wnWwi4a<G=t+Y(vXdwfFӦiRW h~RZ&g(eqB]ݯVt42!!3+&ʩTJ7tw(Svh6tR)F+G?wgw8_engiyZ&XhjE6YK؅t5Ky-[~!U")Wרs|ՠR-MpcƔ2C),HCpU<3p{ca h`K+ d@|xz3ni7iL#ep~6ggYeY6ʶp3H?M%R l)AIs>$%_^5mAa.NK8ÄW^Oۿڣܹ}ǏCqݐ(nSVh5kt-zI#eNivNiT_}⟿9f/(o fK+ Z>OJTP5JPhGxT+1zM)@g:j7/Ib>+8hTQ@{Kix45yVP62OmnsM|>$k.]mJ.^BJLnL (Ri#~wQhmRrr)V]0J(|œazcz.~j5'Jh4B [*4`fF9ULoA7L&@+ǣ4btpDt-VtZIJJ+RpmO%I0zJfJ?vqXaSWEI*EQHqHFczClHӔ)qliZ-*%ĵ/Y٠X/96c,G45هO prS3q73~O?"s&x(1OSm~[wq M>.1ĥF:}X̀VSpB QE Yfioe*(YceYA@$C}ϨԙRjLoo#T6մf~rvycX@FJEgZ&c?Ӵ40zuʷDd 4nJk+7GלD-4fUeu곭9'Ke'EmnsI",7=#5#8Qh wP-e{=R4.֨6+ iLCi`|&o8h}~Id0ZE!4fa05*mnsY$ :q,R~]D =3Bi4YlNv)-uqs XT1V]On֩_Y ;mngR `<?|&o=[ ů:O8٬mn71b|fq;mngZ* G|Maj:8A45-|r<{8amns;Ӕyu0d(v_Eq0L zBFDOZs4 KenݒA@Vbfۢ٪S&gݮ7wXsNl6cxp6޷J1jY\ln7U+DFyPkns۩65²"L*E!К<|6wXs4!QlVbLC" JLeӛÚv !pDZUJKAWZs-KӔ8a$4]13!es5\RiZ);-IY9= `4YLOV$ O$sjQMV8U6zz YO=]i]G^Zy"RE@fNd-ߨڦׯa>E#,pqIoN8`qJX|nqp3P:-yFv $gfu fm"raںzԵ4-פ/_-l}XlOMhy$‘_`ZVyEmDdQFT!dk!X@Uq6;J2{A8WcqIcz1pC݂t:}X70QI L ËZ8k{3 B5ְm2 lj V'uq&F&y1U##cO866ؚ)o"dWYE"<3ojlUsO0.5jn~Ɋ-&iH|uM_PIصiG 2y" B`h9 ś 6J,dOGACR/{^gD򨧮cFoε|7֫W:׍qaIY9@DYUZSϙĮSO=$8m.XG`QO"ǜa mٶOk1v߀TWznNڠr&2/\EUP TWMWѢѦI 67IyHXCD0w&f5+mV\PӊzYāEoRCzk:F&Qo5aoFkOFhXޠ4+Eo?CU!|kRzz,IŔU( v$2ccay¿8\[%SKvU|UW;4vvt Rj}E%{6"B4,('j!RWvuHXƭcы񏩭k{i}$p@ԨNh]JN] b'G6uDq.|K\ ?9y9s)%HY_{ɚ߸1yXӿվvmM5ê) -^E`5q:|Q yLZj"DX8Z6k%i$iمBfb9ᚦ†R|l<]}ã/'_Aj(!ωRjbs뫚ʇ l{?xp|{h?3 -gb|8EJ|>8^9ZV[..nVねENj!\E, 8jN?IkHH({>=74::6Kjl>xGnfZA ,[GKcrJOwskh!{W*?DG^p2BqD5ٵʻle{]]n/M׈Z&g𴤾su3m5׊]䟜Ҍ-m~ރ|I.q#;zo2}iȂiwwB +ao_ Oh4WyT!BϪתru%uM5M m-]-ߣ:G|  |MY%ccTf؛#.E}=bdb&u-Ki.nށ \ &&JZw9 P`?䐝U|J‘pEFy JEDE2~g|VV֕T V B<tְٱaSf-N ~% pNz߉쒊v߈Q﹕0݌㺯v_RI /k9]PPYݠ[oWG-Vgb^~i_c5ѩbnfT9D@2-PڮJ-y`Tg`Q._zW[X_M_۱ikv o`U0Ϳ?d񼽩{rM}IF@xm/N)~9GwAy!;Ry>r|SRzդ*ITש@DQOH?#Gȹ(­08aJAgD6-3/3YV`=UGu 7c[ٔj< Hlii })S*nw_Kҫ:=H齗{'R `NiLjB{⳶$g`hU8xsɁM6^D$ԛ-^MWǭg(&$d?G0Ţk{4-վڢV o-7?h Tpǣ?XsM+m;X rzy\%hX\E.y jhhe5. kJZVuihjꘖ^4 YwѣjqI1i Y)ٸ6W 1XӨ"抢Yȥu;+TAA7"I'b8Ѝۉ=_{522t ?heidTӀ@,^4Kn)m. H 8#QЮ`(`]!k1ZPRR д%{U3iQeA)B(4/q_فE͛}WT!fbTmKX߉+XfX?!e';>H>dJgsX,6.{z\b6 "}#[S:nͲ'Y5ӔF)ͽ*Pħ0JJ7GZagmg,TjWBSRĶqk?~pX?Qc5qSf'|놦v,ϹTaTy,RK2ɋU_ AK˨*'NAJifKhiY%&A\$l|ֺŬd"8A^^s\]Q?H LsEP5e Gl㒔K͗#:6 .L뷫ޛ$]hftQ -Zv%eiꃷ-^q1*تۉ/р*X !GeӐkGW͈OluQ=Z5\1=,4+0TKv𦤌b$&Ƕ[S8dB#<gkZVBZ9L$& X|zY :n IY@->j XC$} ϸήl R*^_RXlŐwE,XpY& s7# QKb=Ķ:a䤝c$d@q6k(.`⪩0>!p`5N!XX$$XI= {6a`r5 * m=AD`6DzWqs]``\*= FGGC=a$a4aI(9hQƖ*|b Z2ab9_EE妞 |\u~P-Tԑ=BXj5 WUaA;8)-XEǦ_߇ŚXTk!ַUݥnhR?x,i`U ~}tkZz\Eu1\_vScXx528fj:B,tLcY'fAѽ[^/9 l,20BaWUפp~FM:)iY-GiuwrLO.I_|A<=|Qt|?AZ`M3w -6VB[b5ѝa`U.ݧLogNJ봈5W JiX`LvQr{: ) ~JDh(_qtCw9s^遪)C⡪GD%EF'ƥC.L+;,UT"AU9Zٗ^2dOd1>-\QXU_Hg-E]욘q/ߺQJDCEDzFKʿ3h:Ux}Ci%gp-ƙY9%S"'yPlz{XWYx&*ȊS5pyO,*`!urDv:_VߺS\Feb:R56!1tx F,1vgU ,OBޯz`I2 Te|tT!{}0e(s'kF,M3Ȟ e5U#jy;x=CG}V7B:nu C, OTִɩjO4u!HWKEXu9]>*)9j~HBKмXV1i:0 LQ^I ;baнY s ><2UT(m+KXD"|jyx]X39-v>/{]k`Jj.a2X zIi6M]'͠Suh Iq5*?#M-;ϙR'[CMa ~a$sQӐP#}j.ѻmmd2w#Z<[rӱcrjfAe8SD]}/WzoF AX,U ՏGm?r\/ jm 8=p,'f-i(uܟ![0ZO s7W%xܢ%qmmEll! *Sq;RX|BU6Iben ]C{Ep4x$Tl|UzV;6]X[ڢ݆+|B'4 )!@M4>޼~@9**:Y/Ǭ %bgdX|$sh byb-Y-uC5= 0ǬΤKb튻,֭}"׮fܩ$Fk+{_Rײs+;J7M}χuH0~P3q"AhŽf>` YU|\*[|!hԎi!xL,\Az¿~LZH򀊏[@6Xï҈hXc V(p/7:fQK9 AK> f ^T]K++Kc!Q(jmwZ5k|F lzDlbˎ gX?;:D!,0xTw[`P *4Z0ֈZz``%$J;kU[24|7lB.`-5yMؕҲj4%5S&+ePIh@)ʀ 69~ cV|%eT|2>dt4U1/š`l=*%)"MFw I(Sx LPUS=89cQFp͞e02);} 6[-s%E|!MEl,x/{\,PܵE4dU)€zk{{z +$ ;^!e9b!XXOJ'bq?f܆F'$s܋[ ~XTk`jg܍CZ`p nR$봸+- vJrA< nv?4r:GD ttvÑڽID^ IE QmPoM h %Pu9WV#;G LKcir~?-Ps}q` h&(E&qSJ5ED+3cbA )yX]X'~)s6 O ɫdcS'qa) ŝ֝6"rErǿf}`UOImY5-|RgpL+l5ܟXkFn=sˤx^9 S@OYhtdD3d`e0X(@Q+zO^U GE=-&&"%!\*rAH]"!jP^!-%O];$+,n}e"uCdu R.0 E!2'PI9`k랚O+D%x992 ZR[%,pS^}J)$ดsw"-/NO[\PQմ5Ś2Jq֏sH~AA_ت#Jic,uK+c &N)yX<མVJD'wb{ՐGIKtmeC&ᕭmiձ:咪H1gu| ,ѫEJn` eT҅bZPڶ95cjS0#DjԲ<%UR=vINɥE(@^?7( ,Ս;eXgE_@=zQIRQ..V8/BAY]!X~exn៛^brbp10WmlV1k , S ݴuuhxWw-*ͳooEM ;XUCb[x-0Bx%.^~Ez\AęYJkX"W3saZoݠ*n`kpI([g@K\; &3X}ݪ֖ Yȷ 3U(c~Q^XZ ϙr=؟.e~d,z0zq,S.[gK{Z)jwcV,?rI ``y/׈;uY Yd"^oyYqUA?bA Mylb,9HGo'Mwg0[n&iN쌎ԯgzZV-ʰ8.K(=!q`)jxU+@E԰{ BF%-#V\}V^Ru AFIW%ϔkXrA+Y~=dХWތ`b\ጨ"* 'R<}’fc4JB@Lig*G5 Q@*]c.*Ϗȫ;} Ъh(o+<*FFf!7maMnq;j dUD cqXyX2:e5Bq گdn4訢z6I P\r|.,?0R":PG?kAlYR1l7#V݊Z5? \8uF)%-Vyir upyQ[z{6cٝRd%嗀Ѱdx'Dǃ S)ȻDl*<+jtMXwR,%3,Y!FqrZZB ,ĀF! T-xmS[N^"*oV0gUO;}Qk wIY2'o/o:B_.W8X7l- Xk,sy%*3ϙb_QGLTW- e`!@>jEIh m-74;$9rQ~7vWPzhd8ܫ܀T{Ѝx9%5-mܼq}}5|\yh~HYY_(du\oz|{i$K[ӮG2)GUXX8#F޹ eW9cO)*" >1'  HHdV\qfkP]9m"ٮ6XFZ1x閫[D|SP ZgkF-z!3 5 IR:42I`H fP4Q-BXci]m"$xbeK ah9ft괜-GǂlYԽFS`fYwtEƍM09zU/x/ .jjY&>9|tN[?tKN%Tn/2LBsU7$c'dLޢA2꒗J0)X4*HYꬸتp"Zv{q:PiuwVܢ,IЎMI-tH'䞒dHYHfiI'!#ORJ%cc2ZdU'Kzc'[HU}!Xh ZÕ=rVRʞasdliSBڢl5usITQzncJ>r,cZ{mrvI i}P߰u}ŚQ6ñfO sIYzݪsc1]W$̆@+B܋~;{K6_0]vs?94#V݌$Q!, auY6 J:> Ц,SG{7h=t ZU?Xpm!*pOI82 dL-l~#EgXz3 %|τtb:l B6>T cXxшʆNfV$vLuLD' A$a#{UOC+ƇH늊1s+W|:q]uo\ӈ򆵭'fqEu{PF{.޹g[ClɩhۦY-nAx, e c=; 㢌so( {7Sg蚊Y `( &W󎬎-.mt plp pаd'PWTseٮz )ŁXJQ+~5y²7IV^)/bJ38kZ,N-n&~mSYF y=(OG[ǟ6e5t>Ǵ#Pёqu&4?#C&VfJRwOvl__nQrƗ[5ڦvw|Sk]Wh~f X,8wuCG&p%Xaƨ^o4Rb *HYJ+.; o9yYbx/ִbd}_/( \@怋}֯L4G:yCiز'"uYFc +J_`xX?hH Ю{A@=9jŷ ସLxDJ^Oz֣`%c5)ر_I)9qx%J/>Àn2˷9>* gg_)𤊗 QC"5 2 #Ng!-;q>18yeL4@j{P AI|+Q`0 0zcSPix>eXY'qYlfmfj2  ,Y&6SCCDg )3M`(,F- Z ^@6T\ACKh8VU襆vEJݟvڰai3g ǧI *{0Eg-#"fΠ-l2h|2[+h1;ڌ,sZ8.X3f7b =>nB'.] W]u`8.㙠RJ9(l Z'*#Mk9|6`Mw`˴trI'XB? >F0Ppr&|h"@lP`YH93?ABlW*HuLm'$*d;U{ƞ}7Bف>MQ\QӰ@QaJp c9hj8=#SܐyM˕SŨ:86^Ycf֎9c3=V*:6@?ϵ^Eu ސIH)ԜjɟwŚG?ύ'\0>wJPr3:GqCbP+ N 𽬹wM ` KYt=UƂ|d!m-K8JDK9 Q!HiGS?xђgxDҵI*ʵ oSW,p,wdux`ehMYyL{zVº=uw|RQQ;8DB9wZRZ.<$LQn)PB @D\++Lz-E-gw]@ <r L2S v=xmnsxeo^rLm Bsw%C tq%7"Ee'S`kLk!QhX=d/kbwяa_l[d,±FJh Ic,S+sNDtȁ+ܠr]{kh[ )ƚvklRR LϘF] h%7I!<#8 ɖч_Q̎GuKO*/ta Tefґ̜A.}4pg{~`re&`]aQ&[{Id(8^vտf ;bNO왙<=яq$  >c{aNƴr.EI]4WU֨a&;k_N'T#@s9 o ,:{ ezjdfq↎E? ] q0{/JHjڠP7{tEҼb/ _c Yq-h\0*HaV( rCʛ+oEȸ+)i%;?Lje󨧬U_qB{5߉,ADqDx uHh+j"O[%^oPddcd#Zϻ @V@3kLl*:QO};K+mŢdԜwL$\[1iKk7<不F2Ut,Jc$~q gF`$‪/q(Ⱦ\k Xܳ6|N⫵p{^y~!?bD@z ϼ(rqDªzS:ޅvIeE>'s&+i/;2"ˏ>e(|N_e K8 $G9W6[61t޺kDKEw.3MOћZSӄo釓goFnJ[n9?$1I|§vq, r~QhC|z*zZX+auvy0MTi9&.t*Ace3tyLB =k:[#ninlu1,8Cqt0 ~0O axÜYյM?_=QZNWQR\!0*FPX1ǭ eQd9G qXAĎȆVad(PҲ*:+ XdbG5a@`ybHIkHx4r]+yTFw?|k0^Oᘫ7^={su|Wv=%%tj)Y*K7K>:2lƻ[` `$L \vb3N^Ģ9 hƪc2aXf /9Ff$kS!dxb} [S*i"{EZ(CɜnzjauA^TL L1BxkP |t[^Vo$x"t9Ww6oW`1ٙh'Nc^E?1Z1p:gcx>>G!1&O(4;ըWA:`՘@,ӨlWyblD٘v~u79bKY -9{I1Q4?8o] sfS=!AU>Fc7/^#7\7._PsEҹx!izp c7nsn~#ڲ] 9698YA5<̣8(uI`NnqG'@AXQi!ƨ͠b>M!L%ޭؼ4Fbp9(B7 )?v]auۍsh'F&}; 6<2LDɍZTA~X\Z<,mvݘLC8 3<"` HQP+yX*|NHMMw2C42vNd[ת]`yW6` so`AS3Pe@B S񑩽Oj|Bk ÁN~!NGZVn&xAmI35>i5MtO2<2mh$.[Pe|6'8fFF9M:>P~2lt q| ^Fs'p "7Y=pK;RVtLʽ#=/> cIx[V݋?LsЇsX#􁲑CT ›}qΧ^GV?i[N= ꖪ N$tg`+)fk`gT+ä́;,kDy8ª|֧pN3i&4~$YMQ~GTg̪ϢcOpWC˷)ƃ2`4SGN,fWN nԟU5}Ő61~|*|_ {aC.F%tdT2P0v̱>AdSbJ^(eLQvl`U dg9>D_<%d&C~qwIENDB`Pyro4-4.82/docs/source/_static/pyro5.png000066400000000000000000000567711416147301300201460ustar00rootroot00000000000000PNG  IHDRhDpzTXtRaw profile type exifxڵir%9cZhhN]]Of$eV%˸gpp|z,TmSFiM?~Z,߿ߟ___$^|??{>FYON|s~{~^ߟH?O~ Ʃ/n(|Kuo>Gaᣏ1䡳L5~J+jW_cMnwcϓN>@@8gyRJzQk/k_d7f5ZϬ0IUX*F]r9,C5ʪS`y D?[EROM6 RS2~\4S?5ovLm}{ar9S{ԓmvf4 7͚ 7O'"?:+EizG,ӖAN'$D~MBC:VD3u4hGqרy`z 4. 5(\s;ʳ*ዙ&@b:z\g{[ !e$(S`%>/; {Z%pbZ%!U^9Dy(=LqѝfWdv0d{[ܟh'K_GtW^t$B"IZNYc"qbB[Puڳ+NX_4MyaMs5(_nJ/J n[t1&WuI5 \GZ0) 0@ %:/*ʼ@fS/ [" mƽAX:k 1@ 4 ,8<~5P6TiW׋ix! AUSi~ sXb'>cNN }^9@m+ "<j DLl@Cp5/9;D a"-꿂x DzqMp> ő(AԿ,[Be9wypԆPW<#eΑĀrpTfbY6W# ZJ31e$o Fl )T0ҾRh.x`OnǎjTגVZ'_g*SQCO-$;>C%}Ҝw÷){@h2OMq ff^ju; !%}#)8RX̚\Ou1lE#ʦn8pGq +M邷Ǎ{CxNB9Zz7ZT EI]#.K{_ؠ-B  Jb:XYěQe9i!,HqH{q-2?p栴! G$&8®pF2v?M7yQޱK e:[Cݠ `HAG^<3 "ᏸQ^%Ol꨷R$^HH()<  FzrIIօ7J+#"ێ o2+ oYL,Si m[iIO2BH*zאLgEĄJn6S"&~{ #*pCrG`v\o-XY&FW' zϹ93anKv<dzW-ZuPbFR:ᢥ: <`4s _@]q#n8q h6?qaACA$P P Q<³Aua`19R^(n]W 4PkSX[X.,%OshOG)D\Igv`#YϵQe'cv#f T86m[oAOls63bEN(5d4#L333Q)o\4at,z80)ѤH ^joZ  ~8K-]{5e55'RtM>JWR|E0NjU:M;Ɔjc4IGB!jT!.co"JAz U- _z`/AB^E0o,9tAY4qQdZ"|HPrf@|]!N|\VĐ(v\EkN|/z%V9!5ůq\;~hm*Bˬtop`E 3\z χc`og6x?$Bm0ӡo·(&:UR bl㕨þ;ngԏIDd ǍzfsỞiy.M~ ݏ(2t~@mi2uad8V,)Y)@1P܍إk>Ԅ"c dғC[pe[:/dM~`E<| fQK} VxyAw5gѓc ^v@P PtD Fets cYU@v*5&4m*/KuO#~ld`v a |k j:(,:q 9B?GA5NZ=s>c!bMsyX@^ɚT `ę)Bl+"V%E+md9j( ΀*i`+t% Q[9Zo ^~f碮O 걇W0bXϷweӉ]^:L{Fc+-_Q4iRd+l\?i$T`MX+h}䜈iT2mmK]i]qP;7: \RW- \3=oy$b7j8L84ҨLB\ =՟E}nA"JƢN&KQv5h>@ uY E},CP)_[HJ;YU@TB*=\0G'*"W.A7qDP78PUz<ӍH,ЦI̠q,16uSJUaqYբ4Blw |**S/WI{d6PӴCi`Nur.KьjG4[2vt 9|_AT'$ a}8jwnPdAn TUS m]"4<&?BA!_:b GW}Zׄz@UUlÚ&vBm:A?*(n,cgo}@@حJ>zTe |g mަ>tsh!dKqP1P[@:Uo}nlIG\KV`0Cp?_4% )HE. !7p\P-΅(M l4\ -Brn[WD.:5;"Og $  PC'X:xj'1ooe@o?ev?iCCPICC profilex}=H@_[E+v鐡v *U(BP+`rФ!Iqq\ ~,V\uupW'E)IExwq7*L5UL*)+B=Q%f곢w }ѯL:Ԧs'tAG.q.9## ; x8s.+8kݓ0\ЖN3d԰ ,$hH1ɵFyTBr[81&@m]YcngJk `z[u[`I ɑ4"~FߔoU>N,ux<3~JrӨTbKGDWF)E pHYs oytIME %7 IDATxWdy;YYUYu ܒrAJK)JzЃ̋"(nPAvi@pA @`f03iJל{ӕ6>Uy͹Zb?CkRFBϳbz_t?{U|ϣ(x|1/OPZJZ{.G>%p1M, bd Hrϥ ᄉ7?XeooFE*Ő'~A?X0p~[koy 4[-0xjw녋bD!it-~~ɽ=H؛$ $\R \ǦP33Q,S4[ܯI~KW;bZGQ:_@,$M3V.q~fhnW ~}A>q &~1 aH&)%dJxvuwC|eI0lN%RIG=B@^R?d!JbV7kdҥT,!-4xN>/7q\ױ0N' Ưʯʋb|T#B>yPJrѪ2iHY)%i`ˇ۾Eb*N,VE b|G\X@ih) nKŵQQJs_ůw7k1 AHLg iYuMa#`DQʧq2LnUYmZ4ŴӤ0%:Xs(R8c>EsΡĿBQ GOq]: 4>Tﳁu31L:.E| Zu+CPYoq0Sp40 È5,֚vkTUf@x"W'g&Di i|ƲnPQBmn3)ځ"`J]Aڒ\cXV\beEM ~?jii0\ȓLH)m\.8?hrz52|2!1&pa<˹ޱxoӛ sd2=<(if{e+ZxAH4J $LfY?1ύ29!E$]ZhyƗ?Xd82YLo4| ӲbΏSPYo'oh@{1TJJ&y=k=w{ mi4T^tG%#֔bw!J !#r m#Dl)qsA5ִ&萘B`KM%%SlAfyzCN@„FKx|5/LֱL˲m+嘜o/d˲c^ZB|'}t'M/뛼quK&ّXJVo SS{Br u>JKCo> PZ=o0n5)Q,8ր(-ԺʦcT@b,_#sAz?Z,!p,c H@:BPՃ6EĔ`&Rp ШPp4B&~\'J RH4 lTZ(8#d3n3+> i Jy8K&쮗S *. Ak$v/d%9I@%zݪy&M#m؂|흇0`A!A6¶g&՟\sUYc60190kGZOEA7o~e=;U@xY* |LVyol%)M؆ eFV#"Jŷ.h7yX52v5 9V`:YplaO"DS=@f`$2\q5.{yn0?lfH%PxHˡBx>޹gɻO?Il>?V7՛~+E*p1w|e66axek^Oc1p]BTdJk;oOLBD D[Xa<ܼCs,giaZ2 $,kIVyn͖KXה`-tܚ7٥lwaR8C,3SlFCiI~mA nurCy>VvșA=ip`}}j>~ Cplʅ $m-yزۧZ (Q1ǽ\ޗ>.HTp@i~kw̄ݚ@N X-6/\~Pe}uJ y#s[k;{X|OjJE @_gAW.q&{ ^wşANcе]t@Kt?c \S!hIl'vKп`]סTbvvO8;:S)N.{{Z-/ΤFHT@_9Vw*Tj^$gJNPVkAo?B@Pq?w1:!S=%[\7oEيBJ;>qNv)zV|6Vnm0cDEUlH?ĴlxpI{3}ϵĀ86% Ukn_g{gXv4f&OM$h!{4z0;&BO{ˠ&,h6@2ّ$ *aSb!O|7/?yG8M;7}s8dwj]]bo< {W߽J[kc%&5*}$g%\0BB+E寒_f#\a N K1ZD+D1b;&KTgPm"M3=^z_!4G- CmR9~A0MRK)bmK z^|cyRBcђjuZ-ok~A/˘|̓@D0Ԍ~Q}FҌPk `FV-TB4G4qDJk MjA7 ?bqCZh[K] P!kl~`t۱qi&P`H0ȱ"NbHV,?\{H6|9t>>~7ut**h(ݵ x.82.3L&Umcgg7J[YsyJ=JN\9* ay˯2J4Ps 븦4i04acQqbMyuͷ]_7Ml7q:-l6۴[8`AӅ',!ۤD4]{QQ C$ f&ϐ^dܧPS+;2u>x-|l>\ohdd&¥9WX|c!HtK4 M?tO*C`&7.jҞɤmUOdhܫZڥhJ&1HٖE:Ln۵Ͳrl/Sql ÈiGbk{MϢhWWZ\(o"|DVS %UR= :5:@kMB GyrA@юDݧ cњ{¸ f&tq)&)d0f!@* y"U]!" R`kuۿd2?7TtLˎz%Ҵmj ,/Wʥ_&C P*| Jpߕ,o$@1=: }nF_Jm&WTZ-ʠu=-B%z{hJ5-G|Y|&c 񗙙g\"Lv,D?l#ʍlUܬFz !0 " Ank}p~O~"3KRIU5aCG?_.cA(%Z`)A&[0kgPkN%X$kц^`rpPa4 I+[HQoъ`٤EΕ$jy=ejvRۻU._[4NYhNKgxcx9JsAױq]t*r 5vÏՏ9Ml7(qLVLKJfM:|~-4+lSL^cۯq]2IڑiI|W{X]ߣţ0$S,,L311 5'/hÓT*IT N GR^r|Nl"+-oެE S* |`ĺ]"th䡴8^ V355f'-#dZ-Dwe $=s357zr3 MQ.I' GRb&L"- | WN𹼾Ud#9h1%13sBL02R㋥(؜-Z^|6ԣWcx/`ksJ? ح?r;X3;$*+9Ftdarj2RtVԠ€UUr#oظwq&'˔Jdӟ/ێpGG ~ ib^VG7MZjz؆a;B?@Z4ZmZ6Aؗa!% xq K$iK  ^4WjˇyIKS* .Kq"* ) XfT\:]U$P0늇uyJ;/]o 0_(mq!/ͻBm=&ߥMRB&M1)R*I-- CDt[o[g4A0::H@6¶'Fݽ;9,ڠVArGĵuznx? 0>+ IDATdl /j̓$b+@0 Қz;% C|\.pBJcnO$,9eVGVxg5>X ƶLBix!|&_`3n;֨]^TF7N! Dr-LwJy^: e~ @|6J;m,3IKn8 >  Iݎѳ uSc(GUxӃVAHaS  Ca0 @+E( OYD`[Ʉzl5ˆMԕR@2!;5TN] Gk> C&iQk)6Ucԕ)Q^LM1::| O\&LD# =/0IeR ۱OM3n< &$e'x:aٰn&kRI y.h:iVmcs=Ϗc8:j0J ŀ+m*7C4V"%N7G#\:̼3"e;4e`[V$`hpF`Z6L&lgۏ[M+eA.Ц`1#ˑE#ʠNxЙNϛtrLTww ] 'J>zV <ϔD׏)!g'ƝZch6N0| E7LFƌ:;RނU}qk{Q(ɓhi(:>^AX;Av,ҏtYy'5(n2 i=^}VnF5LCQ$ 6(055Ͻ4«CM$ǥ6pc̳ UI3М?;lTeN%h:Z\J'jQ5(Xa7#xG8:([n )ӡw>_DnUu21Q+cUd1sAxVk V:1.F>n<*)rĬ\1nTI8L&( M 򼜭8t`# Yu.[…fg&X4qdzciWMS#@U4JCѵ0qAqnr_$PO,$Zk0d_ ZZF牑y8Q*A#k{T«a2+oZjtPIC+PQ]e3/f=ڞϯ2Y }R4H#["J&B9t~<#97Otz %/+8UAc֓\4C2BJ1571V B Be^=ɄNR4>W(^0fT:]iAcm G3.IfY!5.vҨ*NCu2*FYf`3vڿmR4[;|G_.YWf$_G03=5Hns9/Q_Ep[aqq3>6J6bGO?:;*&Fҝ*=PSEX%Wǧhkk[$bOp<P(E4A!K="ݺ\>G6ɩ#Q+r%-WtQ!1 W/݈F`ض0 j!KhCmޕxL,n!䷾~~-ô5MS[eh!O#JDC"p!aJŹk \xlb V.3SqY.[ҥLN3ض' ? >0 Is>jyBYh&a2$9{ml8t۲!yi(<(0ƏTH_g(!qLM~vЋD*T)j2nAkMV 1F ǶhњZ+bH`V棋^Hޣ3 .׮'܇X[2ꀑYMd?Qpy$.i1rgKYe;6' }=)Q!X9[3wni>9禘'McvFbX'$ SHZaxJȂxT5@CZGs~XDa(} LIi6*)D, IL5ЩxT%; qRq\u/[5pqq>C{׫ւFϾΗ߫H1]tq9 5}ŐǰGgj;&'19>p!XGRJl յ-ncI IIE {|Hɹ2c%f&(37;=ʱxdH>'p.ʃ-1PKRc*!Rx紈QCR`YX <-:] tW8|c Y,/YqʥJGppBڦ>N}\6E.bA#N;J"* JH&](eXk =lxwNf|ssBca3Xj}aeEqw8Ř+σ{'~ g&gKO1>1J.:PAuI(C +&Gf" dHE.SSe QL$:rċUBgzxI&\}9| #h "}!V*j%q_OGitJH(A,,QGR]>"ss?JFtqZ)mo/ъ8iSը_fx\vhSaHF E6dӸ!wLEF_ rc.O0tsjn6qa*#3 3,M122 ?IJ Rl0m.-_a0>>s | 2<':QTCp\J"6f96C,WKF(vBe!Jj)5lmP,N]%kt]cETwvYg,d83?C,k RpSpaz#( Qk#6w5C24L ?Wp{m$bƘNb he{B Usl@(?JFI7oPb3 \8$C@ҖeaQq] +B&cdc _6DiqQ~\6DK;gk1[o~tkhCjen'b^UT:ٱ+ձ$𔦲ŵ:EWpB}:Q&%mF^>b}FGKqKp'NDJŕ‘ĭo#E}d8hz`ȉ&N"-j3xb ru\PiK[75/t3 ӌq`&E8!r l"VgZH$V[wI溻= -Ţ`}anfo>Lke㔈Bi D+LK wI֠saOtb@>\%^8xK%:ozȸIae5?FX t< X>IfЃwNFf)-2$Ht}ݭDk1J#y98Goޢ/0ͫŋg' G++w}@|E\^'fhht*!(e=O'%Tsi, ڵENeDTQJ8Bj$"ѝʸXaBE%K!YK \8@\"N ;B"/Tiޏ2L?vTI,x{)~{>لp!O691]Ulj+tZͣ"qԃdJ͞V# B&r=/p.|)r4ϸZQza[g;!xvیN-?0J)0ͨoRiEۼi} G9*^-ք ͰFze8XWl\?MfeJp~ȵLHBtݠk:smnfBĜvMZk7iB}& yҩA 6W8R3`Kk>ي>$}CG*.XR]&͝㯾s, mȩOPs*Nc-Z4~1Qcn%&s8\0 f gqqCn|5G.`\@e2aDjӈHb7K5} :*ϱ[̌8sfO~e2;3{HcqyοR }# }B阯+tJiZmި(݇du yRdck5V=N2U IwZ{1IrcW`LlSy,pY&'ʔ|s)f3YZ?s8wG_a$e,0>>o15B>Dɤyꇬ-]5qSgpRI :IAw ރXJGz`[ [t r\>'wn~,.m?qɱ(pRTE Jaodh(H|>j F(@qU2$M$yb$q;*D:h Y+8:"3c?cdi1/Y0G}'\os)6svmƒM1c,rc"Q&[n)ӐQ\-v.pdg|N28]k'ӧф~@{Զy-jnQ"^晘,36ahsqAl=m>նHVW8 f.oЫW߿WE AYp-,L:.\,}AHD@.Vmo{a&Q8ŒqM~zwݔuk:eɑd#eDOj>Xc*d,83\x3ӌɦ88fX`X`*k&tTL#-a^ݤW#8Pd2cc%様e||\.C*IJ#jڱ"u;}f#:(#h[KUl4}Dr-([;wSgyw逵OJ TMw[eJ ҂\:M2bH *e%,TT*pB!XDX\(J<P%>U{7lT` xR.Bdt ԲiKg\8aY&ǘevvR)̶]IIU۶E~(K8kkl쳱4jzaT巄$e[Y21>RێmwƏr<ʚض0$NôZys2 {fD=G׵9v8,~t{;{FųNvIхqo5$7o0~nqFFI8J)ZmLr4N#>EߜNezj,ʸ~]f(14.[0NͱbR<<24Z\.#Ew!u ƱYyɄR TPTDPK=Phxr(P.|v$ ˥`x(n͕TmV*~PB2=Yfbl$*犵j 2(䇲$\r.J(UFG&ءT`x(eYҮǡo:Gg1jܧ|t+ 1J8>0uq P{|ZH6׮0y~)FJ ^n#dh2df(m+ΚGҸՏ]CUF&r˥5= _INH˲H(0bf۱JknlHufی`b|4NxRU3j4)r<"@8&rZˌe}秙X"g~J4+3y1X`xn0ʣ)ltToՊ;dO\@a(gX\ezj!xp?} |c7d-$'YZ31W>$})秘(wg:\yX۷UVP* 1R,NydonB *d#4ϟ \|y[/64Q7;Ohy>bH]"y绌O9~33 b3+͌e(-4҃14̨RSLҬs=vwvu9 e^~ LMX|@%\ @;aNj Y3,.P.}{ F拗7ygJJ̄똎MT42L&~f| 'lm@6bb|_{gTOmmӗ]۬̽t1`pa` ?Ĩ\pE4^L@^nc]sm Xal}.O{~OyEA<'.cJG|(I}d}f6l ڵEr !}մ5eHdBC.$dMP<ރt׈aJʑNv+\K>uTu8]t28.^J{nBWd~9@gMk04zbD7 IDAT/+s7c||TF%Ql& L0U~$İ0tNMsUqYT\BQU`nRxuq2=% M0C]s#<=j:$mG9r47ct űCe9b9*+{CKN ƱҴ*$UU&:Դb7_ f xݯHӏq&6UԴj='nrE]7?VFz=D,qop޾!@(IbDVb8\vjkCD!>B x>8}[v}I||NgX_oa 9n%&d0>nNdM' r鰚{mD%\S=8i /DˠGm)6R"XNU,+#34< ߸AWOy *vDH'&ٿ+W ks/EG}[8t "*􀏶6R+*\C:Wï*M46K7QVYs!%N" í ? ۄl3R &D.B @TN/٠ =IENDB`Pyro4-4.82/docs/source/_static/tf_pyrotaunt.png000066400000000000000000001055761416147301300216240ustar00rootroot00000000000000PNG  IHDRX cHRMz%u0`:o_F pHYs  ~tEXtSoftwarePaint.NET v3.5.87;]IDATx^}T ;  I L}w߽&Xַ>}׵Oٮ; w@x;1;~:⣊w ߁=_~inoeO_fihh똣G;]~믿*֤oOg=y佟7o~pŏ._7aݽ{H{[l鑖moo_VIIi{N:չX=%44իW{oڴ8=#3+.!2$,7 }d`pX9s-Ϝ9s۶mc/]\o9W%ׯ_yf(]Yrr pGOvJ]>:#-xEM֭[wVwqq2d.]<ӯ9 +#Zzoê+WL=zHNIdHVAMNAUFNMEE ?҄{GO51:X##5? уwvJ}}SΝHMUe˜1?y޽{kkk>3667V^.Oݺu#u_~9qd-ɓ9;;?`bbdAPHȔ1# ߕkSTPzM//Wijݸq'z8q 3~ġCfffN>կ_ r}}LUTJJJ<OĘu9}iO?4>ŵ=[ZZ*ںuku!pH;A˨NLVF),---=:zΝ;9F{{{fn4Drw\]dgi:u>ӿ:#G|Tomc3_N^"|ғ>0z@[tC<޽?>}Ago _u׎ރwӧ~:0R/}vAz=>IAbI Oݺ|H`N^СzlatRI_5nkk{ٳg>|X {;v dsSseqQ"lݩ~k.?*$*sBg/o^93#=Nd3FCMeIIA ЏzE=zvXiTߖm/tujVTP8$!1GgϞca}q 'w`f @}}?'%%%^OGow*;?{z>0 ;wq9nJs}>˦T$L*O?DG}2a=EE9}ٿ"bctu)*(>7o޼n}tZpAMH//1GHп/ ߇q`?^?;n`Ύ(+\tO*Y Kk+jj'{^fG$%$b} M'4H޵KW(ԩS> @8a/6}!gm=+3KczIG IzP_cb@@?Tõǭ>7#VሻjBі[GW]S{}Neep)+0?z_~Çzli /022\}JYI񞼜3!4pѿ^A)|C i) L!]aB}L~GOݺϷr5)!OX0>>ݧw[޽ Z>Q7޻cGُfLahaj1YAFOd$.]` (:u;گ x`Ѐ=<ӿBҪUTT/8i6b/ ɵ=#5D#G6aԩN~~~n+ +>U"Y"9i!4/hx>p ~@ >|P^$;$I {ӫ'u!̱޽E!4&&&  'Flu 0 cۇ͂9ғ0;O6M0Q$BZrpC.]G0ۣn ҽ{^]qLW*?z/I_|w:y%^ [srsdfez5=~»[ zZ55 m3ƿ8~KmMHSS}ܕmh鉻 }{BAf#O긛k+*C"3 E[UDVhNN0d^e w,]5y2Q##m;Y鞓ƍQ XgfGC&{&)++]*/J@{nGCր Fn!$b4@ JU6$SSOag a10H79yh8,y<#A o<.\pƗ0.#yF Cō ס_ovH4$%%-oUWW'-m[n4c_afjyv8cݳ{ 949dhdW~A3'ʸY\UTpRo*MAX4("ZSfر\r߆` yXNJoLgqb35#CT6DGf-N|ZzGeDZe @ & |SI~ f^o<ɾ 9rB?뒥alq ; }t+yPxEERlT`MdblH1!T\B]<)++2#?pwgH5_قx9VGԟ6^r{ĉʡaJ*j&\8JʼnJ ZBIH\LavYX4! ]EE3Et?wrqpBu*^zK3gA'_nw6lx_QiIn ]Ν9F8DI[{h1?>jñ-N$먓eCYk +rsASؑ'EzP\d%SFR8eQvzx%dmaN^TUOaA.lgInLQdbͤGϧ)..+ݺm[7BI96_pWں99[J"7UZmL}DR) ZpKji &X^zņ{ܾ.-)qG۷T_/79p˜A#/YnffvjحމP F"-"Vi*bdӭj%׮s6o٪ /v-\LpJ)si~TPP}񒯫gA@>RTT<*HB,4rMdeNvVVdcn,;dgE~'*܃"w22ЃSC9aTVL%TWO͵E%4ZbX]Scn$(bጱxDZ0m MH!AF~.<,;-a}Kš,X@;)1)tYM .#֞`b'~ͣs>X;+)ʒ*rCE|id>z'q颂Ĕd9sܹs ^~'駟?=իW!BrT$h#/w\H$ %ytSMqA !M_Y0H pI #_֖zQ9!Mêh|K-M@'6ќ)hFjΧƜpZ:̞ hэUTQ%#:d H21yn&;ZVv߭|l>>>p[TWS9In9oc@ŜG La78ٜccJ:M Ny43?"+x@!8~Poǧ@Q^QERRL'Qfb %B೨?qUTJSSs ;xD")>&j !E ɶO Hi9{'_؝logizK.uH aX8 ~T!ϝlLQĞh<ptdxviq.JR^hH&x>]{9H:Y] *NEʹ JOTZK(G:ZODVFp](jΠ'O7'9hhZ6w2?,FkN F0 0J))ؙҢ})9&.p"kDP+;rnKm[8 );ʃUEШl֜0}O' }5`ذaSqC2 s63܃@kGV/hx`v;W|&V"sR@AԐ@H9'{ќ1F`Bq.chcw<;KfϞ=->^p38-7#ePKEi@) g|}!.k\ Cz!My)rDZh#h ʦdÓ)a`7[HL! Gb+2 Wj(Єt14&VP&ƺOhed^dwVKSm;̡[#7 @!/\P!>JY ! K_GQOjI_`=t8C`!{J-11k#W ۹g J#F TT?bit#262JsS"TQ@'US|pX0 (;QF;"!yj3L֋H}du3@ePc#G[R:Pjb}T ~_qRϕw/LY.ei0U0M TS=ub>;4ATS{v@O0}Flpz#G !UTUUy<{N¦_E6pԭc}{~reDF03zdoFIq4~D!_ r"~T 6ƤTvh 4H>.sppKNN6]pĊ˓%uKIyVKr`rE"@k6Oh$ ,S_$?\De ZBD>ئyĶ{yzy)Z7HH9{{ٲewЗ(H|1Jg=~A[}]Ϝ -R??(1!~3s=|JYv| Gn\cWX A=aǎж&9ckA pf$E{f̰>\v+XQ _] PG04548yҤFgka)[lV?ɛuH6Px+edgEeul9*:2pIA ,u鈈jjeˀ>]ڛ < #6Nڨ郐? *@yaaa VG\$t@D<,n#P9 '(.: T M%QW.,dgZZ h>BO>ޞӏ=Rr*yg߁w ܾs> ItRk;5Hk螨֒ъyyy2/~Xz\73C_gsr>ٴi3fʁ8)g!!0.~; 8g8G}Sj`zzyddd"-^,B!/ -\m5\ՊpUP7Wiem\#{V ٳgOߒbٽK<e5LH R|Bbʍf LTI|0yy iTqAKx!M&&@;y*2p'rFn٤I\d0h pwYli|؏3? @;]n<[!RMl\hg3]9P̏( \ƵΜ Il |kme&m.EOGG>Ԧ&;Zbx`F'ikx߱YQuzΣY{x4 쏶<.iR@GzvYډM֞OMË)ILU.khutZx}ş;tRqI Pz5L-G'6;ȞФʺ6fd⎊2S)WSU r+W.=OkBGvnowq2P__YZY_]Y0{T߻]uw#XWA H͡`8qQNc-b"\Y F004h/$2 s3s iDM1;2,H5kGL,#C}8j+IT _@_@nY[el675%;:+ki2r\dm_WkF X5 X΀`]ٵ쉗Xm#?h?B'>&3e\3j GV,><nzfF.4n@a{'u`mJ:4k"XKPo &0L3H.sMC])NmȽPv,Do2S$=!\00R 7 :Y!@$tF/AkVvx;Dr?<"|HdsY`̆&1&[yp a^+W0r9Lrpssi*@##ctְAct MWA Zqs,-up̕g4%*8h+XXF˜;p̟rεPMsk#`"rzZ"7"4!G0Qtť DOtut!*(0}~:;;R9"Q@H ZF.Bu[4-6&:o Wzi8͝;vΠ_/3$ФX υ^#0/2R@e׬zA nn3dKGD|Ԅ2lDTl;w!G^R#1W3Ӕ3Umb٪ĉ#{p_ٸ8'D ]+rg 0rx0ŸO`b򫉞93Uh~m?XIp4QhRR_6XYBb0hDǿ:xi|ȃtGu;e%KZtDc46E8H=GCBf:sbuDx#uZ%%#%ꃮ޳;=c'-]F.T#}raG}7d\uBR̵57ܸ~mKfiNZhLo J S 75qf||E4PPWV KJ֖ք`:h?]yuv¢>! qn.?q.?* - Q'p2{S, ZWRR4 1cZS@c[δTj)]FI7a⺆Sy.Q]Fh.3'1Iݎ2 NQt:SSzʧ25485g \{NCw0114MML<=Pc(>y֖;]Ϛ9FPխ"@!j)ёQ@k 6X(Nv.Y@ *A+6@k0(FkEXQC=%y6: P`ΩܜI(gdp\{Yl j_0pS!l̠ܰz8LeKJR pMfpٹ673"jmff߻l  th1B϶ Μ= TC_'aè!k!m0ݸ"LFE\7k" @QObbw-+#=;sf͚eA8X]Koߵ`6:}( 'fΜ$W+*p'6!=P퍨:ٍ&S]pF۪)+/ڸqc?xk,BVCk S0oZmv8(~YC4f깸zWTEOI-#c 绶4&eG闞 E?`:Fw$3PT8/Z1FƐS94펙4q |[> wCA˃@J>x ^U?4'#E*NqҎڈ>6Ӻ0:*2;1>t1gϜ96֖dfu=8 rd0TY/KN@@,̟p+ $;[4ۜA8gϞ>{MM:Q:'++4}O( m85D ٘hѬEhd L&ޕ$y;>M3HIԥnC`r8Xc`y|z\H||00]1CeTp uKȈoyhfnzͥnum?lX2Gpy 7VU@ao{MF614XX_ay+1@ uOOM]S$ vRy4W83ȫMb>SD.HN3pt aFIC]g8(3̙FP>"?u8Jf4=Q kN047$KA~0aVA|`V>X~{fnU*MPuo͌-dgK_Gjժ\kp5 x@T By\ ЂJCh_B@B+~C:W%'gkǍggeio1PLM!ƀ1KΎدχ h탮}{ z74Ah> W3\RaֹS҄b 4Yb#T0 ?&5a2S8/59 c0Go*?'H[<sd鯯Ɏ S'OJ._XeƏQ_BŽz23Ouc 'صkHO-C4 攊`b 9H^Zjs"':_͝=Gy?ο:NQ}B`^Aƻx鎨oPF04$4%0n=Muf| }r-eQsEUqW1&B/ KtآW՘ "seR$?RA@xσ_@8%6 Pk>nwqԷnT7{j}}Uc!\NFJf+7)f^s̶%ʸΉB ^ A@7!/;9ɔS k--ff-,pqs9"6>6+;7[#cvsu;ί6Zg–椙.lf]c(vXò"8ΰ~ƑX.h1Md&CaJR;RXE9T LP0Y˰n a4xHGay)a=-ƈ⃽ͨm,(mT$BB/E[ݙ;sÝ;?(޿]@>쎀IKd5X Źeh<af/;e3=*'V;D(+)S{c81y4;T}-Zd" ' M0\4ԛ~^=ϟQ`Hee$/ƌLtkBw"ɏߣHgc_O2) ..Gs3Sa0`f 4 =7htMǷ di鯏_>lFVfӿ@ >''&%!I0ᨸ&(ǎ}MȰG\Ѹs0a2\;zZyl,_h~?#V?_>ۺCB !JEXFbO+S}n\Å&F3c 1L4=C'6PkINakF<}; JOC:JwRo" Ѭv,[]'k(fH' z!q.VZh!ï.^uvE3fL'$@S[Yu㽫W.w_4u넚IJTw Iv"L<#Rj-/{RVꆅ7j*U6xLɻz@ itڷ_niO0cA0'7; mu)h,h-ɜcۖbb5ݘF _ЁE"/"Qis݈tA6nx"L0c/OgM>L^aNС4jHA[;**(0k9dHacY)[ky7ў{w.Gd…Pi7e9\G~_QKs,vV bB) 61ff/S'Mz)J7߮y΢'i7 &]^Xy סkNDr# NCQHR)tiXJ_[˃f͚A(!ׯ8B!IF螎QFKv7@' +-\k .]RRT揶"{;[{.m-4Q wJ|?x!snG(X2,$ & ] L[b: L-KY:k  ,"j 0@`r0}  MP#fD}5R_)IP`LyThBh}ake%B$t7L+6w ͑6&B`4fÇ\p*B^.9q}"f@2a+7gV|uy=H|>o 'oWۙ9ci+S0t n+L~Kfqt/)nFҥ@7: p:`Eˬ-h*,1H3W5 #|+9@J0@8eX%ja6¿ȓTTHjћkx;@;gϞ}aH>\xMgPW4A%-1Tb|B~-weaԳRirQw&Z~(u~la\>}TՉaH~t*ԇb ޹%3yي[+̐&#;fLe'``fU 4 +F/  A¯ Lp[wGv.aM $D[p9̰ɂ*,\I^d];lF"_vKS}͑҂okm13\lMʈI)6z"W49}ѣ|2NDG> 0B( "y 7H1F @Zȯ8a^Ioe9 %U* m +-ZU0#1#ph\\fS-C)Ph U,zE^8r#R~<@{W.~O4)OK|sg]=x@Y3]S#D;S `t4fmx8xf,(fx}kxpbayVmS~ 㒇-vabsO7ա)F`4 QURmQNzPP)⢢Motoz~vNKK32J A]Kp~fz~H@LG6f9^YrKs㣣?m2Λꗥz?1a>ߕ'M5ڴ-*ЇI^}yR ]ç# 0pB3Lth*gС5{Tᑵ,Ԕj,LO-(8oo_Cog^ZZ\`o\'k"!+6T ,YHCۊGKQ=#HXygDb;}KKv՘-va@CǏuNvEGy{lZ K捣TWyGO_` xéw#/V>`Zbx<蜻-tag:hgF h6Mj(4F-섳yѬ2VZdtisR y(J5b T}x=W !B;;1@C *vX_A[G S]0ɒO^qmͪOjw?nݺqcR&M0qKd JIYC!\K&VRiQad-Uӌb| ]s>t׉V8tA'sj7_Zц8;*N%t}R%4:bkeIAsvL]aKSAoߑD*Yod Oسn] k_yЬ31ɑ T5"ps 6~ Mvj`@긻 h F/]h8P,"Aw֮G5p, 0Ňatϑn@+MG (/+Jjjh?:tϕniaOt9.%w]@LEBg۷Sm~&hH)g6\ z5) E YϟS8O/X6<?6ND16TiIa( 1Tĥ!A)T*f$ᕏlfՀނ#.X͡_WgK?S'ѽKQK{S3FVl?ͭDT &+62::+-^tύaQ/;:fEGbFQ"j)GOoΞ'ñށ':uؙ'+4ִT]kG`|uwC|I:J_޽sˢ1cgPc*< 1\XN(2S8UNF)fG6xG,0YW#'x 4T'Gҭ%s1t~DA/ K/'' _S,"N2y.4ڹ@`@{\C|eaM;)ĂxYjâ#QTtȟ+G>F =P: Ä @P DrB"?h`kiѾ-[>z]GyuN>02tSk[$ eTF D?# ~$I)zseM 0* tn"?N?Cw]z2nxt#}IѨǙG4rÆ gr# ƨPJ,gy]isa=Otl9эCD ҈FrM3c5-@&8 T>@%93Q~a߄8je֦#i%E4g,JMЧ "IG0T4-)GJS_Bw$@2 A-NNF?/[BЩz] ]ϡ[9FӯkE$FR$WhLlO8`ȅn@{\qpG:)4 @VA[[ӈ(eD"N%94ٍ;mYӧ|o 50^?J-{~C9v\ٱ=y 1tߧu  TM>VV^ a!Z,{%58$>b.<]G%x$֖ Ɗetm\sjr<DyӯǸH@00!ȝn,@[PEDQI+W-MKҼ1A=nGܲ*HBΗ#iB/J)>/I+YN7Wv=zԍ,OCD$ GԀJ*,E(U"=29IH/wjܬc½ peG;G4% Kt kȐ {!^z0_C9sRޖ݁dm71@~@ W< ehPJաh2MYq4'ݗFR8yңfw^0NQ B-:` DxYNowI^ٵWsf-}8ƒ1@yjY 6{F2(`ӦR&̭Rh&1u(aգuP#f^[UNg/ @'DŘp@?$7Ҥrs@tyJbr\@. ]h%4Σ<#RRRi9t#t{VJഇQ ̱ޘ~` ҥKQd0`z)pBKo..)Svt~Y=}11:jjDErmr24nwSQabV_tfjFp(k]" ״H:8c}n- &ٔx:BwatK|= MF d $QH'Zo 2@MiFƽ(-OcˈN!%DYSj;aZjm޼=wE&o7,h8DBdeߵa9U:~/?;VYeB=ӵF͝S?5%mu'Oty믝.[Dݙav 94on+# 5%L A.ϥPlq'? "h3E`nb朇'v̫\7n --ؘX-=H#}D곑6•~). SnN.:$9Q4>iٔ`f;LkUdK3R0ZHӣYP#E&؏{S̫h8%ɓQ2֯M7 4`_?*B1h7їS$L"5G{ʏ0Ȕl L(Ɓ tj=0т ?K nbNFT V4Tߔ +JNbe`xS;O?}$'lnjҵo6m_7cet"2y?:-}j 1W"ftaIID F3D4k:^Lm>^RN7/mg<)0*[ iDlhE$ )x% 5 Q;OXYS{;7_Cgǎ#hD}i:KG1`e17#bmJ|hBX@+vaf]l6].hmJKh-Oy` DO[*1c>Рz.c3e!4op;phsvqo@*+#Ax|t2+Ks 4Cc DʐSݞV'&OMqCi2DA|n6;ӏ D%K !]Ooۚrj>5#MF@!j74g >)[>đ :Q $DF}3%s T<,8I} 'B".3ɡPj `$]98h/=mryTFOf̘A&LD9W6"s::4P2 @6BE>GVځ'ggLiI/ț \hr!ȁX%ۙr*P olJ\4@°6ZG$ʈGW'P,:)FED'Ԃ!/T4PdmT<71@2jwW>텥iE B4KX] d,}̸ ^Z`} XEPO֝HO?Kߟ>HG G;xt C]' bh)U?^]4 Eh4,&Pɓ+jwPo<@߫+[m&F[|i;-Cӷymp7aXD9ʈNx^Mv܄Xr1t)W$҆ СCHF$bŠ3:AVH2O|HV-ei÷zfO}pzk_fd="SCheh3G\MM13Х"g[ZH<(3΍DӚ5k(%9u%#mM bjiRibLSn!z-ӈ"< Ξ׾"wV|9vUThɎq#X줛a:FȕQ,nLX))T_WOÛA bfI&fTD=Ct~,'ҧ \;J+|e;ӹ6mf _&Cۊ#deآ{ȋL`قeƐ7*NOMpr8RL}=ZH 8CWiF]ϱ0Dm*px~lC_,ށށ| LB[k#?>C5a:puѳK_ TyBCLؔJy L.EB0Ԕ y5輼a,gfsxrjWkm^~ Ѭ-(Z ՞69٣a9œrDjs"'dqur0'Wn+1W(#C1F*k0H7G]mÂfB̲@ఏiwk9@'xaZltBEi{L2GD wp@~ٷa:gʇ,[ UxO ֠# o.ulo>GV}fy,AGE'F> ,6q2Fਖ9 m y@Ӆͨ- JЄ Q hx>qD>/# ^n+$>w{GRQAC *~!_؀.h ' CTCk |x^aaD[́X[M h!@ agD p0@fdpo6{{ۿ_8sMT4Otd٬G Opȣ]*Qq~07BKl@stJ܁yy #}S(:@xC 2] ; LGvASrkAyww;,^|А Pҽ0Yhu閸s.Ĕi'0J5d|-o4LuZg @XZ<_@ k6u3py g4 tNp{1/ܹ?oG`=9A.)6ȅ96 a:mx5Q6p@bqkوdeua\.vBfpX3i ae|sr.D,rwu!;[=ϦNoa6Ř76)zheBX kL5Cϱc4 lPڧ 'YBHVo@2 )]FCKhk>8Jkr?SUV,@9 ep42 k]]~Y@V\ ZXkZ Y2aT`xP9dT2ӵdx8 =Ǽ&[$>wy0ۦM78&o!#G-fh ֣֒Qjd—yR~(-L? ڤZ(Z ,Uhpܛe127Ə Ξ|U_'O:oۺu`A%4kA܁s sh:f qq c 'CHqvѩG3[5tAQ]AFܠEJI% jU }t4nXWGw{/[zvIU%MCk"ln%M;AMAx H-X l hǛ"5c d8ۓowF0RXQj=OV}a46ؾ:<]E^Р_?{3YF >E#`19 0Q0v)&M̈́~5|ѫ=j Dv[k2GF#҅_m8KV(#}R0|R%#$Z bkZna@]rf?Ab ܚ+tG FpaYPFtbpXZiqw+Hz~8wHCL=G aw'Pݍ!((hQm":U /IUg;ž#S ;;;ڃ_ &т K"BCg;{RudsUUUAw fŚ!e`HSUbI2hٓ0dBT$0(BLsb@xVXsŸ᥇E_O; I i}j`w_ށ{ja󙘠@7R8"ӑ09#!ih"]Sa>oǣ+@`RU#c0x8L/1?L˝r̈́AFG/ax^8!BƯp:"SUD`qe!yh'6*Jʤ -y"ܛ픩DFب*H޵Khxh@ Hܙ#Dk6{f-pu!]O s玼!t&6D LeMa 󍋮tMb_Cri6cfA=<  spp!ޞmgm2?E0a L%{{EB6H( #i:!>cd(]T{|>/+_|80CruF5Q'+h3xƀ}KL/ "LA9Xq$CdB a29" Tzc|5]C[Ca a,Z( Lc󐒦-=JJ_F܁3;w(y6zp*-O*Ufd1F $J4zh̏& z J%Sa n7*L71CKGS KYSe晋6B+@[W wiه/$Wr_V$ ' % >0>M L?⫠HN-19}e{lVZXOӒh( Bb Z@50c&}Hdއ#J5 i@:~G7c9aYi=ƯbTht"MJJDH8V4"9[e6L|6}ʦ(uq0Lbpb7ywLfX/ -2 p845\4A/1cA"2L/ fW&̷2`.c,|4{ 捜 mBE-Q}5Wy⸚Oˊ,,ȡ1T:ԛ7>IF(8V70 DnP7iPx_QTE J?-*\#ɆBz)2xphZE$^Ոͻ7/\蹪i9x;#=GR5 jѱd9`N%TTFv= j.`)ȯ` CkT`bhXhocF<*$)[qL 4I4X"\pMdJMPEnKm[l}<=ȟjסaZC4$K3x4tj`abPmQsW-R Qz.zhzB_nl֜XYxLkixL{h"Q 3&7Zn y:lKxT'LYPSEcid|,5yS=Amy5V ,C)T B[ Ӫ5PhZCzh:|/Ί,Βs)Ybxeoi`[mOa9 #-OZ -nݼ)~{{֯3_:n)%Nã" ¡0wj-3~`OksQQөQ*UcJK+g#e*B4$k4> ?3vCԍHJMœ^y7̝o)m .8QH 4H)UB+qg2H< U  hjhj}U8XS-b8Oj !,[ i $+FmJ$ay(."ꚬAHCJSoہoOm钺g=[:aʠ*: 0и];w<H$x^Q: Ŏ85h*W ө QX%9?BLԬg T(QH\,'rYݼdwk̦Ûilq1UԶYtGd!. (){F (OQT**I(|f/)`rL^O`I@{ I Cx LaS$5IYijoشfM׷_Q;XnE-8ѰLo/j"Aa/(cžG/@ c °Ŝ (g Eq` `@ȋ"Bz$e% i` 'J7# z8A "HH${ܸv]Pz~pnEg<7y2MZO0Q26|) Ry0up.z)l*0(X[H! 0'&G?%&[0 BW>8\tyH2rc<saB1@ Pt%XQy_QZ[&v9_E LiiQuuTTH)ɔAi=RQ?S?xA Ys(ƋU Rp1 Ʌ9'ȥ1/,+c`±s=9,ɕH^n>@DVE w Ž`-1䁿SUmo|i©S.YUK񔅉(xJ)S&U. !l*cċX,y,H6TXgv+t\/OJ4|>d;3I,K6,8삳B<H4̬|&gZl_ԽIH;#]j:d/Y<|Zˈ'eTJhCqäIB85Ba`CG5^9,LOg&.W< l}3`𘅕ג^`&p>cILt ht|yXh(2|V0$ GzuҲ6`0:xCH[rЋ.<hڜlʇSYq;␹N΄P26OF;8pwgs(݌4wA&%vW3P;fMb± BN27T&8v? $1+7qG4H$sg ?@,g4y0>P4  f@}-դ8FkR*p^ @C`$ 5"(3KBD*B a;$@08^$ GPCp.0Wm`M@>|*>[GPY>gu44; ##( O& 1wl&k ` @(5Dܱ! 84{A!S LAh3@* m* 'C.=^\[0 7?} |gKa%J9s߃מ ùQ. s &j cyK#.c禍'OG5y $fnz&"/k 0 wk6m 0 w '8u['„purقr&_ObQvԻ`a$(Qط4_oJBH7`ǹH*a0@q 8ACȑ r%HGH[rC_~x;͠'&P+Hz[H@I0 L&!\ ߀AEP5(t8`JEBC V!C~pUQ9裣3)+y)bw\kv $Mh{#PRSM5T(ù 18S ;k+C>xB9 d1>h8"vy~qo?[QT fJV=ؾO[`8pW@x6 3͚l4x8ZUTZXΈvrQܾn}[}ǝsy%yѧ77?Bune|2 'r(p}0h 1%o,Oh'UЃ j-j䥋q˗/4uĊy3ߝ3i"BS{9gda2X \ / 0@h6P Ch˚ N;Ƀ4ѣ?3'ȑWFZs̋&yƀ s LT pQy2HdWDJJ򢳕mmy;e^իW??ztXS]ݏfΤ-#2C)$e#$@ d ѥH B$@{j>Mm3~kqGk/w7mƉ#FN9F`$u~L! m'8U!C$˗GXOYz[lÆFMILBmxNy&i 0BfkʥK^;Jnj9ɹ(Şbv&$IeaX0i^rbknٳ^D|.:uJzk&¥nAyc{KځIN)&OKL$WC#2=@b'ݬP̑$4(i#M2XOML kVz~.;ɓJKKsrssEEG!Z꤂"'5E{N.a˹󨩬,+.>]QZJ$+}8EC5K3͑1*cM$`t>q~qFuoЈ445HI^`Mb\]^0vL~v(r]^;lA(@1F#@S-3%Sy]@|^p߫ssuu=f;>i ȒIKH=ISx)=sTS^mȤEX 9ku̸&D&zڿݳ[񥜌 oE"Qf;@ȕr doks)7o%ٳg{O011yVphBCEѼMCQ22bࠠ6K80,@oGw#:9|)/ACL>{#&d6ZE0XFYj~ ⧎ɓ&%%&$C}8ڤJ+duh /HG o1=z^MulK+g h kB-p@SyPUnwҥK5N:;;`D/$H2@4@sGgkDy߳E([z簮6L ҂sngnvwW*Luu3}J@,C43g-Ю(#$ 9O绦DSD?YYnKALp7y&o%D!{gJ TH"k@`^ 50_}ou,1S%"ܞyص> r)q_My۶ǎOII }c@4v gGmo۵۷X4w̤¬ yuei%*H~ܒ;w_[SS1"YЖ  p *)~xގ}ve˖η07BhXJ[KIB|\޽_?*l޼y@ff4$zikjڣ1cc?'ށfΟ??rcQCu&r@SMʻ5-7s~u1++*rAE hH]||ϛyUK"z+rrsژI[[[[`)LL3:7^J|wq@Ԕڰadt4F!LQy#r5,Pr,I]fdսPV,Vlbbgg'dܹN.]kkk-[őC4p`>Jpn֬Y~̩PyH|[ZFqtr"3 aw :d&ܬ>cYش>BTT_&y(ځ3OJhY!9B:"I8e0fL ^__{sW|.]л8o $ ma98SYYK# m666M"@^M^)>x\;m֖ N^<򭬬TAU:Zmxm''qMMMv}x"K|;n*̄YnN_= ^@a"!9RRryv{0RPy}m"a?a---nWK݁{u:᏿=wôclZMĀ9` srt  2ǬncU'Ny`ܼqKUINMx5'4w\ t̬LBFruq4q3~.H|2x;v-5AMͬCDKF϶&_ C11/|kYFxovhs1\!Ρa$55c*+/#32LU}-/H|Rx;pF6fF ӪlSX$5554|JJJ`Yd:~5W'`_ serialization library manually (this is a dependency). Check that you can ``import serpent`` to make sure it is installed. .. index:: IronPython IronPython ---------- `IronPython `_ is a Python implementation running on the .NET virtual machine. - Pyro runs with IronPython 2.7.5. Older versions may or may not work, and can lack required modules such as zlib. - IronPython cannot properly serialize exception objects, which could lead to problems when dealing with Pyro's enhanced tracebacks. For now, Pyro contains a workaround for this `bug `_. - You may have to use the ``-X:Frames`` command line option when starting Ironpython. (one of the libraries Pyro4 depends on when running in Ironpython, requires this) .. index:: Pypy Pypy ---- `Pypy `_ is a Python implementation written in Python itself, and it usually is quite a lot faster than the default implementation because it has a :abbr:`JIT (Just in time)`-compiler. Pyro runs happily on recent versions of Pypy. Pyro4-4.82/docs/source/api.rst000066400000000000000000000010071416147301300162150ustar00rootroot00000000000000***************** Pyro4 library API ***************** This chapter describes Pyro's library API. All Pyro classes and functions are defined in sub packages such as :mod:`Pyro4.core`, but for ease of use, the most important ones are also placed in the :mod:`Pyro4` package scope. .. toctree:: api/main.rst api/core.rst api/naming.rst api/util.rst api/message.rst api/constants.rst api/config.rst api/errors.rst api/echoserver.rst api/flame.rst api/futures.rst api/socketserver.rst Pyro4-4.82/docs/source/api/000077500000000000000000000000001416147301300154655ustar00rootroot00000000000000Pyro4-4.82/docs/source/api/config.rst000066400000000000000000000011371416147301300174660ustar00rootroot00000000000000``Pyro4.config`` --- Configuration items ======================================== Pyro's configuration is available in the ``Pyro4.config`` object. Detailed information about the API of this object is available in the :doc:`/config` chapter. .. note:: creation of the ``Pyro4.config`` object This object is constructed when you import Pyro4. It is an instance of the :class:`Pyro4.configuration.Configuration` class. The package initializer code creates it and the initial configuration is determined (from defaults and environment variable settings). It is then assigned to ``Pyro4.config``. Pyro4-4.82/docs/source/api/constants.rst000066400000000000000000000012501416147301300202310ustar00rootroot00000000000000:mod:`Pyro4.constants` --- Constant value definitions ===================================================== .. module:: Pyro4.constants .. attribute:: VERSION The library version string (currently "|version|"). .. attribute:: DAEMON_NAME Standard object name for the Daemon itself, preferred over hardcoding it as a string literal. .. attribute:: NAMESERVER_NAME Standard object name for the Name server itself, preferred over hardcoding it as a string literal. .. attribute:: FLAME_NAME Standard object name for the Flame server, preferred over hardcoding it as a string literal. .. attribute:: PROTOCOL_VERSION Pyro's network protocol version number. Pyro4-4.82/docs/source/api/core.rst000066400000000000000000000004071416147301300171500ustar00rootroot00000000000000:mod:`Pyro4.core` --- core Pyro logic ===================================== .. automodule:: Pyro4.core :members: URI, Proxy, Daemon, DaemonObject, callback, batch, asyncproxy, expose, behavior, oneway, current_context, _StreamResultIterator, SerializedBlob Pyro4-4.82/docs/source/api/echoserver.rst000066400000000000000000000003121416147301300203600ustar00rootroot00000000000000:mod:`Pyro4.test.echoserver` --- Built-in echo server for testing purposes ========================================================================== .. automodule:: Pyro4.test.echoserver :members: Pyro4-4.82/docs/source/api/errors.rst000066400000000000000000000010531416147301300175320ustar00rootroot00000000000000:mod:`Pyro4.errors` --- Exception classes ========================================= The exception hierarchy is as follows:: Exception | +-- PyroError | +-- NamingError +-- DaemonError +-- SecurityError +-- CommunicationError | +-- ConnectionClosedError +-- TimeoutError +-- ProtocolError | +-- SerializeError .. automodule:: Pyro4.errors :members: Pyro4-4.82/docs/source/api/flame.rst000066400000000000000000000002761416147301300173100ustar00rootroot00000000000000:mod:`Pyro4.utils.flame` --- Foreign Location Automatic Module Exposer ====================================================================== .. automodule:: Pyro4.utils.flame :members: Pyro4-4.82/docs/source/api/futures.rst000066400000000000000000000002321416147301300177110ustar00rootroot00000000000000:mod:`Pyro4.futures` --- asynchronous calls =========================================== .. automodule:: Pyro4.futures :members: Future, FutureResult Pyro4-4.82/docs/source/api/main.rst000066400000000000000000000034571416147301300171540ustar00rootroot00000000000000:mod:`Pyro4` --- Main API package ================================= .. module:: Pyro4 :mod:`Pyro4` is the main package of Pyro4. It imports most of the other packages that it needs and provides shortcuts to the most frequently used objects and functions from those packages. This means you can mostly just ``import Pyro4`` in your code to start using Pyro. The classes and functions provided are: =================================== ========================== symbol in :mod:`Pyro4` referenced location =================================== ========================== .. py:data:: config the current configuration settings, :data:`Pyro4.configuration.config` .. py:data:: current_context the current client context, :data:`Pyro4.core.current_context` .. py:class:: URI :class:`Pyro4.core.URI` .. py:class:: Proxy :class:`Pyro4.core.Proxy` .. py:class:: Daemon :class:`Pyro4.core.Daemon` .. py:class:: Future :class:`Pyro4.futures.Future` .. py:function:: callback :func:`Pyro4.core.callback` .. py:function:: batch :func:`Pyro4.core.batch` .. py:function:: asyncproxy :func:`Pyro4.core.asyncproxy` .. py:function:: locateNS :func:`Pyro4.naming.locateNS` .. py:function:: resolve :func:`Pyro4.naming.resolve` .. py:function:: expose :func:`Pyro4.core.expose` (decorator ``@expose``) .. py:function:: oneway :func:`Pyro4.core.oneway` (decorator ``@oneway``) .. py:function:: behavior :func:`Pyro4.core.behavior` (decorator ``@behavior``) =================================== ========================== .. seealso:: Module :mod:`Pyro4.core` The core Pyro classes and functions. Module :mod:`Pyro4.naming` The Pyro name server logic. Pyro4-4.82/docs/source/api/message.rst000077500000000000000000000005371416147301300176530ustar00rootroot00000000000000:mod:`Pyro4.message` --- Pyro wire protocol message =================================================== .. automodule:: Pyro4.message :members: .. attribute:: MSG_* (*int*) The various message type identifiers .. attribute:: FLAGS_* (*int*) Various bitflags that specify the characteristics of the message, can be bitwise or-ed together Pyro4-4.82/docs/source/api/naming.rst000066400000000000000000000003451416147301300174720ustar00rootroot00000000000000:mod:`Pyro4.naming` --- Pyro name server ======================================== .. automodule:: Pyro4.naming :members: .. autoclass:: Pyro4.naming.NameServer :members: .. automodule:: Pyro4.naming_storage :members: Pyro4-4.82/docs/source/api/socketserver.rst000066400000000000000000000044001416147301300207340ustar00rootroot00000000000000Socket server API contract ************************** For now, this is an internal API, used by the Pyro Daemon. The various servers in Pyro4.socketserver implement this. .. py:class:: SocketServer_API **Methods:** .. py:method:: init(daemon, host, port, unixsocket=None) Must bind the server on the given host and port (can be None). daemon is the object that will receive Pyro invocation calls (see below). When host or port is None, the server can select something appropriate itself. If possible, use ``Pyro4.config.COMMTIMEOUT`` on the sockets (see :doc:`config`). Set ``self.sock`` to the daemon server socket. If unixsocket is given the name of a Unix domain socket, that type of socket will be created instead of a regular tcp/ip socket. .. py:method:: loop(loopCondition) Start an endless loop that serves Pyro requests. loopCondition is an optional function that is called every iteration, if it returns False, the loop is terminated and this method returns. .. py:method:: events(eventsockets) Called from external event loops: let the server handle events that occur on one of the sockets of this server. eventsockets is a sequence of all the sockets for which an event occurred. .. py:method:: shutdown() Initiate shutdown of a running socket server, and close it. .. py:method:: close() Release resources and close a stopped server. It can no longer be used after calling this, until you call initServer again. .. py:method:: wakeup() This is called to wake up the :meth:`requestLoop` if it is in a blocking state. **Properties:** .. py:attribute:: sockets must be the list of all sockets used by this server (server socket + all connected client sockets) .. py:attribute:: sock must be the server socket itself. .. py:attribute:: locationStr must be a string of the form ``"serverhostname:serverport"`` can be different from the host:port arguments passed to initServer. because either of those can be None and the server will choose something appropriate. If the socket is a Unix domain socket, it should be of the form ``"./u:socketname"``. Pyro4-4.82/docs/source/api/util.rst000066400000000000000000000004441416147301300171760ustar00rootroot00000000000000:mod:`Pyro4.util` --- Utilities and serializers =============================================== .. automodule:: Pyro4.util :members: :mod:`Pyro4.socketutil` --- Socket related utilities ==================================================== .. automodule:: Pyro4.socketutil :members: Pyro4-4.82/docs/source/changelog.rst000066400000000000000000000360521416147301300174030ustar00rootroot00000000000000********** Change Log ********** **Pyro 4.82** - fixed @expose issue on static method/classmethod due to API change in Python 3.10 - switched from travis to using github actions for CI builds and tests - Python 3.10 is now included in the unit test runs **Pyro 4.81** - fix some typo's in docs - corrected some minor other things in docs - Python 3.9 is now included in the unit test runs - No actual code changes. **Pyro 4.80** - fix possible race condition when creating instances with instancemode "single" **Pyro 4.79** - cython compatibility fix - removed explicit version checks of dependencies such as serpent. This fixes crash error when dealing with prerelease versions that didn't match the pattern. **Pyro 4.78** - stick to an older serpent library version to install as a dependency when installing on old python versions - explain the delay and threading in the distributed-computing2 example **Pyro 4.77** - dropped support for Python 3.4 (which has reached end-of-life status). Supported Python versions are now 2.7, and 3.5 or newer. (the life cycle status of the Python versions can be seen here https://devguide.python.org/#status-of-python-branches) - URIs now allow spaces in the location part. Useful for unix domain sockets. **Pyro 4.76** - corrected bogus space in worker thread name - thread server can now be cleanly stopped with SIGINT / Ctrl-C on Windows (if the selectors module is available which is also used by the multiplex server) - the behavior of the NATPORT config item has been corrected to be in line with the API behavior of the Daemon: if you leave this at 0 (the default), it will now correctly replicate the internal port number as NAT port (instead of crashing with a configuration error) - certs are now included in sdist archive so the ssl unit tests also run as intended - now correctly checks for write access to the correct logfile location, instead of assuming the current directory **Pyro 4.75** - fixed distributed-mandelbrot example to actually run multiple concurrent calculations. - CI build process now using more modern Python versions. - missing API method doc added on NameServer.count() **Pyro 4.74** - serpent 1.27 required to avoid regression in previous version - fixed marshal serializer problem that prevented it to even call register() in the name server. Its dumpsCall is now able to use the class_to_dict conversion for unmarshallable types (in simple situations, not recursively). Previously, you would get a ValueError: unmarshallable object. - msgpack, json and marshal serializers now understand how to serialize array.array the same way serpent already did **Pyro 4.73** - include LICENSE file in distribution - avoid decode error when dealing with memoryview annotations **Pyro 4.72** - (source files: normalized line endings to LF) - the -k command line option to supply a HMAC encryption key on the command line for the name server, nsc, echoserver, flameserver and httpgateway tools is now deprecated (and will print a warning if used). It is a security issue because the key used is plainly visible. If you require proper security, use Pyro's 2-way SSL feature. Alternatively, set the HMAC key in the (new) environment variable PYRO_HMAC_KEY if you still have to use it before launching the aforementioned tools. **Pyro 4.71** - updated ``msgpack`` dependency (was ``msgpack-python`` but that name is now deprecated) - fixed restart and force reload commands of the contrib/init.d/pyro4-nsd script, and changed its port binding from 9999 back to 9090 which is Pyro's default. - serpent 1.24 library now required to fix some api deprecation warnings when using Python 3.7 or newer. - updated sphinx documentation theme **Pyro 4.70** - bump to version 4.70 to emphasize the following change: - **incompatible API change** for python 3.7 compatibility: renaming of ``async`` function and keyword arguments in the API: Renamed ``Pyro4.core.async`` to ``Pyro4.core.asyncproxy`` (and its occurrence in ``Pyro4``) and the ``async`` keyword argument in some methods to ``asynchronous``. This had to be done because ``async`` (and ``await``) are now parsed as keywords in Python 3.7 and using them otherwise will result in a SyntaxError when loading the module. It is suggested you stop using the ``asyncproxy`` function and instead create asynchronous proxies using the ``_pyroAsync`` method on the regular proxy. - For existing code running on Python *older than 3.7*, a backwards compatibility feature is present to still provide the ``async`` function and keyword arguments as they were supported on previous Pyro versions. But also for that older environments, it's advised to migrate away from them and start using the new names. - Proxy and Daemon have a new 'connected_socket' parameter. You can set it to a user-supplied connected socket that must be used by them instead of creating a new socket for you. Connected sockets can be created using the socket.socketpair() function for instance, and allow for easy and efficient communication over an internal socket between parent-child processes or threads, using Pyro. Also see the new 'socketpair' example. - dropped support for Python 3.3 (which has reached end-of-life status). Supported Python versions are now 2.7, and 3.4 or newer. (the life cycle status of the Python versions can be seen here https://devguide.python.org/#status-of-python-branches) **Pyro 4.63** - fixed bug in autoproxy logic where it registered the wrong type if daemon.register() was called with a class instead of an object (internal register_type_replacement method) - added check in @expose method to validate the order of decorators on a method (@expose should come last, after @classmethod or @staticmethod). - added resource tracking feature (see 'Automatically freeing resources when client connection gets closed' in the Tips & Tricks chapter) - the warning about a class not exposing anything now actually tells you the correct class **Pyro 4.62** - **major new feature: SSL/TLS support added** - a handful of new config items ('SSL' prefixed), supports server-only certificate and also 2-way-ssl (server+client certificates). For testing purposes, self-signed server and client certificates are available in the 'certs' directory. SSL/TLS in Pyro is supported on Python 2.7.11+ or Python 3.4.4+ (these versions have various important security related changes such as disabling vulnerable cyphers or protocols by default) - added SSL example that shows how to configure 2-way-SSL in Pyro and how to do certificate verification on both sides. - added cloudpickle serialization support (https://github.com/cloudpipe/cloudpickle/) - added a small extended-pickle example that shows what dill and cloudpickle can do (send actual functions) - daemon is now more resilient to exceptions occurring with socket communications (it logs them but is otherwise not interrupted) (this was required to avoid errors occurring in the SSL layer stopping the server) - some small bugs fixed (crash when logging certain errors in thread server, invalid protected members showing up on pypy3) - the ``raise data`` line in a traceback coming from Pyro now has a comment after it, telling you that you probably should inspect the remote traceback as well. - *note*: if you're using Python 3 only and are interested in a modernized version of Pyro, have a look at Pyro5: https://github.com/irmen/Pyro5 It's experimental work in progress, but it works pretty well. - *note*: Pyro4 is reaching a state where I consider it "feature complete": I'm considering not adding more new features but only doing bug-fixes. New features (if any) will then appear only in Pyro5. **Pyro 4.61** - serpent 1.23 library now required. - Pyro4.utils.flame.connect now has an optional ``hmac_key`` argument. You can now use this utility function to connect to a flame server running with a hmac_key. (Previously it didn't let you specify the client hmac_key so you had to create a flame proxy manually, on which you then had to set the _pyroHmacKey property). - main documentation is now http://pyro4.readthedocs.io instead of http://pythonhosted.org/Pyro4/ **Pyro 4.60** - ``Pyro4.core.async()`` and ``proxy._pyroAsync()`` now return ``None``, instead of the proxy object. This means you'll have to change your code that expects a proxy as return value, for instance by creating a copy of the proxy yourself first. This change was done to avoid subtle errors where older code still assumed it got a *copy* of the proxy, but since 4.57 that is no longer done and it is handed back the same proxy. By returning ``None`` now, at least the old code will now crash with a clear error, instead of silently continuing with the possibility of failing in weird ways later. **Pyro 4.59** - Fixed pyro4-check-config script. **Pyro 4.58** - Added feature to be able to pass through serialized arguments unchanged via ``Pyro4.core.SerializedBlob``, see example 'blob-dispatch' - Fixed a fair amount of typos in the manual and readme texts. - The stockquotes tutorial example now also has a 'phase 3' just like the warehouse tutorial example, to show how to run it on different machines. **Pyro 4.57** - Pyro4.core.async() and proxy._pyroAsync() no longer return a copy of the proxy but rather modify the proxy itself, in an attempt to reduce the number of socket connections to a server. They still return the proxy object for api compatibility reasons. - async result now internally retries connection after a short delay, if it finds that the server has no free worker threads to accept the connection. If COMMTIMEOUT has been set, it retries until the timeout is exceeded. Otherwise it retries indefinitely util it gets a connection. - _StreamResultIterator now stops all communication as soon as StopIteration occurred, this avoids unnecessary close calls to remote iterators. **Pyro 4.56** - optional msgpack serializer added (requires msgpack library, see https://pypi.python.org/pypi/msgpack ) - fixed possible crash in closing of remote iterators (they could crash the proxy by screwing up the internal sequence number). - json serializer can now serialize uuid.UUID, datetime and decimal objects (into strings, like serpent does) - serializers can now deal with memoryview and bytearray serialized data input types. - serpent library dependency updated to 1.19 to be able to deal with memoryview and bytearray inputs. - added ``response_annotations`` on the call context object to be able to access annotations more easily than having to subclass Proxy or Daemon. - ``Proxy._pyroAnnotations`` and ``Daemon.annotations`` no longer needs to call super, the annotations you return here are now automatically merged with whatever Pyro uses internally. - Proxy and Daemon now contain the ip address family in their repr string. - Pyro now logs the ip address family for proxy or daemon socket connections. - ipv6 doesn't have broadcasts, so Pyro no longer uses them when ipv6 is in use. - improved the docs about binary data transfer a bit. - documentation is now also available on ReadTheDocs: http://pyro4.readthedocs.io/ - fixed various examples **Pyro 4.55** - *CRITICAL FIX:* serpent library dependency updated to 1.17 to fix issues with encoding and parsing strings containing 0-bytes. Note that if you don't want to upgrade Pyro itself yet, you should manually upgrade the serpent library to get this fix. - Prefer selectors2 over selectors34 if it is available (Python 3.4 or older, to have better semantics of failing syscalls) - Removed THREADING2 config item and Pyro4.threadutil module. (the threading2 third party module is old and seems unmaintained and wasn't useful for Pyro anyway) - Improved module structure; fixed various circular import dependencies. This also fixed the RuntimeWarning about sys.modules, when starting the name server. - To achieve the previous item, had to move ``resolve`` and ``locateNS`` from ``Pyro4.naming`` to ``Pyro4.core`` . They're still available on their old location for backwards compatibility for now. Of course, they're also still on their old "shortcut" location in ``Pyro4`` directly. - Removed the publicly visible serializer id numbers from the message module. They're internal protocol details, user code should always refer to serializers by their name. - When a connection cannot be made, the address Pyro tries to connect to is now also included in the error message. - Added overridable ``Daemon.housekeeping()`` method. - Improved error message in case of invalid ipv6 uri. - Fixed various examples, and made the Pyro4 main api package documentation page complete again. **Pyro 4.54** - Serpent serializer: floats with value NaN will now be properly serialized and deserialized into a float again, instead of the class dict ``{'__class__':'float', 'value':'nan'}`` Note that you can achieve the same for older versions of Pyro by manually registering a custom converter: ``Pyro4.util.SerializerBase.register_dict_to_class("float", lambda _, d: float(d["value"]))`` - Removed platform checks when using dill serializer, latest Pypy version + latest dill (0.2.6) should work again. Other platforms might still expose problems when trying to use dill (IronPython), but they are now considered to be the user's problem if they attempt to use this combination. - Applied version detection patch from Debian package to contrib/init.d/pyro4-nsd - Don't crash immediately at importing Pyro4 when the 'selectors' or 'selectors34' module is not available. Rationale: This is normally a required dependency so the situation should usually not occur at all. But it can be problematic on Debian (and perhaps other distributions) at this time, because this module may not be packaged/not be available. So we now raise a proper error message, but only when an attempt is made to actually create a multiplex server (all other parts of Pyro4 are still usable just fine in this case). The selectors module is available automatically on Python 3.4 or newer, for older Pythons you have to install it manually or via the python2-selectors34 package if that is available. - Fixed crash when trying to print the repr or string form of a Daemon that was serialized. - Changed uuid.uuid1() calls to uuid.uuid4() because of potential issues with uuid1 (obscure resource leak on file descriptors on /var/lib/libuuid/clock.txt). Pyro4 already used uuid4() for certain things, it now exclusively uses uuid4(). - Fixed a few IronPython issues with several unit tests. - Improved the installation chapter in the docs. **Pyro 4.53** - *CRITICAL FIX:* serpent library dependency updated to 1.16 to fix floating point precision loss error on older python versions. Note that if you don't want to upgrade Pyro itself yet, you should manually upgrade the serpent library to get this fix. - added unittest to check that float precision is maintained in the serializers - fixed some typos in docs and docstrings, improved daemon metadata doc. - mailing list (``pyro@freelists.org``) has been discontinued. **Earlier versions** Change history for earlier versions is available by looking at older versions of this documentation. One way to do that is looking at previous versions in the Github source repository. Pyro4-4.82/docs/source/clientcode.rst000066400000000000000000001063121416147301300175620ustar00rootroot00000000000000.. index:: client code, calling remote objects ******************************* Clients: Calling remote objects ******************************* This chapter explains how you write code that calls remote objects. Often, a program that calls methods on a Pyro object is called a *client* program. (The program that provides the object and actually runs the methods, is the *server*. Both roles can be mixed in a single program.) Make sure you are familiar with Pyro's :ref:`keyconcepts` before reading on. .. index:: object discovery, location, object name .. _object-discovery: Object discovery ================ To be able to call methods on a Pyro object, you have to tell Pyro where it can find the actual object. This is done by creating an appropriate URI, which contains amongst others the object name and the location where it can be found. You can create it in a number of ways. .. index:: PYRO protocol type * directly use the object name and location. This is the easiest way and you write an URI directly like this: ``PYRO:someobjectid@servername:9999`` It requires that you already know the object id, servername, and port number. You could choose to use fixed object names and fixed port numbers to connect Pyro daemons on. For instance, you could decide that your music server object is always called "musicserver", and is accessible on port 9999 on your server musicbox.my.lan. You could then simply use:: uri_string = "PYRO:musicserver@musicbox.my.lan:9999" # or use Pyro4.URI("...") for an URI object instead of a string Most examples that come with Pyro simply ask the user to type this in on the command line, based on what the server printed. This is not very useful for real programs, but it is a simple way to make it work. You could write the information to a file and read that from a file share (only slightly more useful, but it's just an idea). * use a logical name and look it up in the name server. A more flexible way of locating your objects is using logical names for them and storing those in the Pyro name server. Remember that the name server is like a phone book, you look up a name and it gives you the exact location. To continue on the previous bullet, this means your clients would only have to know the logical name "musicserver". They can then use the name server to obtain the proper URI:: import Pyro4 nameserver = Pyro4.locateNS() uri = nameserver.lookup("musicserver") # ... uri now contains the URI with actual location of the musicserver object You might wonder how Pyro finds the Name server. This is explained in the separate chapter :doc:`nameserver`. * use a logical name and let Pyro look it up in the name server for you. Very similar to the option above, but even more convenient, is using the *meta*-protocol identifier ``PYRONAME`` in your URI string. It lets Pyro know that it should lookup the name following it, in the name server. Pyro should then use the resulting URI from the name server to contact the actual object. See :ref:`nameserver-pyroname`. This means you can write:: uri_string = "PYRONAME:musicserver" # or Pyro4.URI("PYRONAME:musicserver") for an URI object You can use this URI everywhere you would normally use a normal uri (using ``PYRO``). Everytime Pyro encounters the ``PYRONAME`` uri it will use the name server automatically to look up the object for you. [#pyroname]_ * use object metadata tagging to look it up (yellow-pages style lookup). You can do this directly via the name server for maximum control, or use the ``PYROMETA`` protocol type. See :ref:`nameserver-pyrometa`. This means you can write:: uri_string = "PYROMETA:metatag1,metatag2" # or Pyro4.URI("PYROMETA:metatag1,metatag2") for an URI object You can use this URI everywhere you would normally use a normal uri. Everytime Pyro encounters the ``PYROMETA`` uri it will use the name server automatically to find a random object for you with the given metadata tags. [#pyroname]_ .. [#pyroname] this is not very efficient if it occurs often. Have a look at the :doc:`tipstricks` chapter for some hints about this. .. index:: double: Proxy; calling methods Calling methods =============== Once you have the location of the Pyro object you want to talk to, you create a Proxy for it. Normally you would perhaps create an instance of a class, and invoke methods on that object. But with Pyro, your remote method calls on Pyro objects go trough a proxy. The proxy can be treated as if it was the actual object, so you write normal python code to call the remote methods and deal with the return values, or even exceptions:: # Continuing our imaginary music server example. # Assume that uri contains the uri for the music server object. musicserver = Pyro4.Proxy(uri) try: musicserver.load_playlist("90s rock") musicserver.play() print("Currently playing:", musicserver.current_song()) except MediaServerException: print("Couldn't select playlist or start playing") For normal usage, there's not a single line of Pyro specific code once you have a proxy! .. index:: single: object serialization double: serialization; pickle double: serialization; cloudpickle double: serialization; dill double: serialization; serpent double: serialization; marshal double: serialization; msgpack double: serialization; json .. index:: double: Proxy; remote attributes Accessing remote attributes =========================== You can access exposed attributes of your remote objects directly via the proxy. If you try to access an undefined or unexposed attribute, the proxy will raise an AttributeError stating the problem. Note that direct remote attribute access only works if the metadata feature is enabled (``METADATA`` config item, enabled by default). :: import Pyro4 p = Pyro4.Proxy("...") velo = p.velocity # attribute access, no method call print("velocity = ", velo) See the :file:`attributes` example for more information. .. _object-serialization: Serialization ============= Pyro will serialize the objects that you pass to the remote methods, so they can be sent across a network connection. Depending on the serializer that is being used, there will be some limitations on what objects you can use. * **serpent**: serializes into Python literal expressions. Accepts quite a lot of different types. Many will be serialized as dicts. You might need to explicitly translate literals back to specific types on the receiving end if so desired, because most custom classes aren't dealt with automatically. Requires third party library module, but it will be installed automatically as a dependency of Pyro. This serializer is the default choice. * **json**: more restricted as serpent, less types supported. Part of the standard library. Not particularly fast, so you might want to look for a faster 3rd party implementation (such as simplejson). Be sure to benchmark before switching! Use the `JSON_MODULE` config item to tell Pyro to use the other module instead. Note that it has to support the advanced parameters such as `default`, not all 3rd party implementations do that. * **marshal**: a very limited but fast serializer. Can deal with a small range of builtin types only, no custom classes can be serialized. Part of the standard library. * **msgpack**: See https://pypi.python.org/pypi/msgpack Reasonably fast serializer (and a lot faster if you're using the C module extension). Can deal with many builtin types, but not all. Not enabled by default because it's optional, but it's safe to add to the accepted serializers config item if you have it installed. * **pickle**: the legacy serializer. Fast and supports almost all types. Part of the standard library. Has security problems, so it's better to avoid using it. * **cloudpickle**: See https://pypi.python.org/pypi/cloudpickle It is similar to pickle serializer, but more capable. Extends python's 'pickle' module for serializing and de-serializing python objects to the majority of the built-in python types. Has security problems though, just as pickle. * **dill**: See https://pypi.python.org/pypi/dill It is similar to pickle serializer, but more capable. Extends python's 'pickle' module for serializing and de-serializing python objects to the majority of the built-in python types. Has security problems though, just as pickle. .. index:: SERIALIZER, PICKLE_PROTOCOL_VERSION, SERIALIZERS_ACCEPTED, DILL_PROTOCOL_VERSION You select the serializer to be used by setting the ``SERIALIZER`` config item. (See the :doc:`/config` chapter). The valid choices are the names of the serializer from the list mentioned above. If you're using pickle or dill, and need to control the protocol version that is used, you can do so with the ``PICKLE_PROTOCOL_VERSION`` or ``DILL_PROTOCOL_VERSION`` config items. If you're using cloudpickle, you can control the protocol version with ``PICKLE_PROTOCOL_VERSION`` as well. By default Pyro will use the highest one available. It is possible to override the serializer on a particular proxy. This allows you to connect to one server using the default serpent serializer and use another proxy to connect to a different server using the json serializer, for instance. Set the desired serializer name in ``proxy._pyroSerializer`` to override. .. note:: Since Pyro 4.20 the default serializer is "``serpent``". Serpent is secure but cannot serialize all types (by design). Some types are serialized into a different form such as a string or a dict. Strings are serialized/deserialized into unicode at all times -- be aware of this if you're using Python 2.x (strings in Python 3.x are always unicode already). .. note:: The serializer(s) that a Pyro server/daemon accepts, is controlled by a different config item (``SERIALIZERS_ACCEPTED``). This can be a set of one or more serializers. By default it accepts the set of 'safe' serializers, so "``pickle``", "``cloudpickle``" and "``dill``" are excluded. If the server doesn't accept the serializer that you configured for your client, it will refuse the requests and respond with an exception that tells you about the unsupported serializer choice. If it *does* accept your requests, the server response will use the same serializer that was used for the request. .. note:: Because the name server is just a regular Pyro server as well, you will have to tell it to allow the pickle, cloudpickle or dill serializers if your client code uses them. See :ref:`nameserver-pickle`. .. caution:: Pyro5 won't support insecure serializers such as pickle, cloudpickle and dill. If you want your code to be more easily ported to Pyro5 later, there's another reason to avoid using them. .. index:: deserialization, serializing custom classes, deserializing custom classes .. _customizing-serialization: Changing the way your custom classes are (de)serialized ------------------------------------------------------- .. sidebar:: Applicability The information in this paragraph does not apply to the pickle, cloudpickle or dill serialization protocols. They have their own ways of serializing custom classes. By default, custom classes are serialized into a dict. They are not deserialized back into instances of your custom class. This avoids possible security issues. An exception to this however are certain classes in the Pyro4 package itself (such as the URI and Proxy classes). They *are* deserialized back into objects of that certain class, because they are critical for Pyro to function correctly. There are a few hooks however that allow you to extend this default behaviour and register certain custom converter functions. These allow you to change the way your custom classes are treated, and allow you to actually get instances of your custom class back from the deserialization if you so desire. The hooks are provided via several classmethods: :py:meth:`Pyro4.util.SerializerBase.register_class_to_dict` and :py:meth:`Pyro4.util.SerializerBase.register_dict_to_class` and their unregister-counterparts: :py:meth:`Pyro4.util.SerializerBase.unregister_class_to_dict` and :py:meth:`Pyro4.util.SerializerBase.unregister_dict_to_class` Click on the method link to see its apidoc, or have a look at the :file:`ser_custom` example and the :file:`test_serialize` unit tests for more information. It is recommended to avoid using these hooks if possible, there's a security risk to create arbitrary objects from serialized data that is received from untrusted sources. Upgrading older code that relies on pickle ------------------------------------------ What do you have to do with code that relies on pickle, and worked fine in older Pyro versions, but now crashes? You have three options: #. Redesign remote interfaces #. Configure Pyro to eable the use of pickle again #. Stick to Pyro 4.18 (less preferable) You can redesign the remote interface to only include types that can be serialized (python's built-in types and exception classes, and a few Pyro specific classes such as URIs). That way you benefit from the new security that the alternative serializers provide. If you can't do this, you have to tell Pyro to enable pickle again. This has been made an explicit step because of the security implications of using pickle. Here's how to do this: Client code configuration Tell Pyro to use pickle as serializer for outgoing communication, by setting the ``SERIALIZER`` config item to ``pickle``. For instance, in your code: :code:`Pyro4.config.SERIALIZER = 'pickle'` or set the appropriate environment variable. Server code configuration Tell Pyro to accept pickle as incoming serialization format, by including ``pickle`` in the ``SERIALIZERS_ACCEPTED`` config item list. For instance, in your code: :code:`Pyro4.config.SERIALIZERS_ACCEPTED.add('pickle')`. Or set the appropriate environment variable, for instance: :code:`export PYRO_SERIALIZERS_ACCEPTED=serpent,json,marshal,pickle`. If your server also uses Pyro to call other servers, you may also need to configure it as mentioned above at 'client code'. This is because the incoming and outgoing serializer formats are configured independently. .. index:: release proxy connection .. index:: double: Proxy; cleaning up .. _client_cleanup: Proxies, connections, threads and cleaning up ============================================= Here are some rules: * Every single Proxy object will have its own socket connection to the daemon. * You can share Proxy objects among threads, it will re-use the same socket connection. * Usually every connection in the daemon has its own processing thread there, but for more details see the :doc:`servercode` chapter. * The connection will remain active for the lifetime of the proxy object. Hence, consider cleaning up a proxy object explicitly if you know you won't be using it again in a while. That will free up resources and socket connections. You can do this in two ways: 1. calling ``_pyroRelease()`` on the proxy. 2. using the proxy as a context manager in a ``with`` statement. *This is the preffered way of creating and using Pyro proxies.* This ensures that when you're done with it, or an error occurs (inside the with-block), the connection is released:: with Pyro4.Proxy(".....") as obj: obj.method() *Note:* you can still use the proxy object when it is disconnected: Pyro will reconnect it as soon as it's needed again. * At proxy creation, no actual connection is made. The proxy is only actually connected at first use, or when you manually connect it using the ``_pyroReconnect()`` or ``_pyroBind()`` methods. .. index:: double: oneway; client method call .. _oneway-calls-client: Oneway calls ============ Normal method calls always block until the response is returned. This can be any normal return value, ``None``, or an error in the form of a raised exception. The client code execution is suspended until the method call has finished and produced its result. Some methods never return any response or you are simply not interested in it (including errors and exceptions!), or you don't want to wait until the result is available but rather continue immediately. You can tell Pyro that calls to these methods should be done as *one-way calls*. For calls to such methods, Pyro will not wait for a response from the remote object. The return value of these calls is always ``None``, which is returned *immediately* after submitting the method invocation to the server. The server will process the call while your client continues execution. The client can't tell if the method call was successful, because no return value, no errors and no exceptions will be returned! If you want to find out later what - if anything - happened, you have to call another (non-oneway) method that does return a value. Note that this is different from :ref:`async-calls`: they are also executed while your client code continues with its work, but they *do* return a value (but at a later moment in time). Oneway calls are more efficient because they immediately produce ``None`` as result and that's it. .. index:: double: @Pyro4.oneway; client handling **How to make methods one-way:** You mark the methods of your class *in the server* as one-way by using a special *decorator*. See :ref:`decorating-pyro-class` for details on how to do this. See the :file:`oneway` example for some code that demonstrates the use of oneway methods. .. index:: batch calls .. _batched-calls: Batched calls ============= Doing many small remote method calls in sequence has a fair amount of latency and overhead. Pyro provides a means to gather all these small calls and submit it as a single 'batched call'. When the server processed them all, you get back all results at once. Depending on the size of the arguments, the network speed, and the amount of calls, doing a batched call can be *much* faster than invoking every call by itself. Note that this feature is only available for calls on the same proxy object. How it works: #. You create a batch proxy object for the proxy object. #. Call all the methods you would normally call on the regular proxy, but use the batch proxy object instead. #. Call the batch proxy object itself to obtain the generator with the results. You create a batch proxy using this: ``batch = Pyro4.batch(proxy)`` or this (equivalent): ``batch = proxy._pyroBatch()``. The signature of the batch proxy call is as follows: .. py:method:: batchproxy.__call__([oneway=False, asynchronous=False]) Invoke the batch and when done, returns a generator that produces the results of every call, in order. If ``oneway==True``, perform the whole batch as one-way calls, and return ``None`` immediately. If ``asynchronous==True``, perform the batch asynchronously, and return an asynchronous call result object immediately. **Simple example**:: batch = Pyro4.batch(proxy) batch.method1() batch.method2() # more calls ... batch.methodN() results = batch() # execute the batch for result in results: print(result) # process result in order of calls... **Oneway batch**:: results = batch(oneway=True) # results==None **Asynchronous batch** The result value of an asynchronous batch call is a special object. See :ref:`async-calls` for more details about it. This is some simple code doing an asynchronous batch:: results = batch(asynchronous=True) # do some stuff... until you're ready and require the results of the asynchronous batch: for result in results.value: print(result) # process the results See the :file:`batchedcalls` example for more details. .. index:: remote iterators/generators Remote iterators/generators =========================== Since Pyro 4.49 it is possible to simply iterate over a remote iterator or generator function as if it was a perfectly normal Python iterable. Pyro will fetch the items one by one from the server that is running the remote iterator until all elements have been consumed or the client disconnects. .. sidebar:: *Filter on the server* If you plan to filter the items that are returned from the iterator, it is strongly suggested to do that on the server and not in your client. Because otherwise it is possible that you first have to serialize and transfer all possible items from the server only to select a few out of them, which is very inefficient. *Beware of many small items* Pyro has to do a remote call to get every next item from the iterable. If your iterator produces lots of small individual items, this can be quite inefficient (many small network calls). Either chunk them up a bit or use larger individual items. So you can write in your client:: proxy = Pyro4.Proxy("...") for item in proxy.things(): print(item) The implementation of the ``things`` method can return a normal list but can also return an iterator or even be a generator function itself. This has the usual benefits of "lazy" generators: no need to create the full collection upfront which can take a lot of memory, possibility of infinite sequences, and spreading computation load more evenly. By default the remote item streaming is enabled in the server and there is no time limit set for how long iterators and generators can be 'alive' in the server. You can configure this however if you want to restrict resource usage or disable this feature altogether, via the ``ITER_STREAMING`` and ``ITER_STREAM_LIFETIME`` config items. Lingering when disconnected: the ``ITER_STREAM_LINGER`` config item controls the number of seconds a remote generator is kept alive when a disconnect happens. It defaults to 30 seconds. This allows you to reconnect the proxy and continue using the remote generator as if nothing happened (see :py:meth:`Pyro4.core.Proxy._pyroReconnect` or even :ref:`reconnecting`). If you reconnect the proxy and continue iterating again *after* the lingering timeout period expired, an exception is thrown because the remote generator has been discarded in the meantime. Lingering can be disabled completely by setting the value to 0, then all remote generators from a proxy will immediately be discarded in the server if the proxy gets disconnected or closed. Notice that you can also use this in your Java or .NET/C# programs that connect to Python via Pyrolite! Version 4.14 or newer of that library supports Pyro item streaming. It returns normal Java and .NET iterables to your code that you can loop over normally with foreach or other things. There are several examples that use the remote iterator feature. Have a look at the :file:`stockquotes` tutorial example, or the :file:`filetransfer` example. .. index:: asynchronous call, future, call chaining .. _async-calls: Asynchronous ('future') remote calls & call chains ================================================== You can execute a remote method call and tell Pyro: "hey, I don't need the results right now. Go ahead and compute them, I'll come back later once I need them". The call will be processed in the background and you can collect the results at a later time. If the results are not yet available (because the call is *still* being processed) your code blocks but only at the line you are actually retrieving the results. If they have become available in the meantime, the code doesn't block at all and can process the results immediately. It is possible to define one or more callables (the "call chain") that should be invoked automatically by Pyro as soon as the result value becomes available. You set a proxy in asynchronous mode using this: ``Pyro4.asyncproxy(proxy)`` or (equivalent): ``proxy._pyroAsync()``. Every remote method call you make on the asynchronous proxy, returns a :py:class:`Pyro4.futures.FutureResult` object immediately. This object means 'the result of this will be available at some moment in the future' and has the following interface: .. py:attribute:: value This property contains the result value from the call. If you read this and the value is not yet available, execution is halted until the value becomes available. If it is already available you can read it as usual. .. py:attribute:: ready This property contains the readiness of the result value (``True`` meaning that the value is available). .. py:method:: wait([timeout=None]) Waits for the result value to become available, with optional wait timeout (in seconds). Default is None, meaning infinite timeout. If the timeout expires before the result value is available, the call will return ``False``. If the value has become available, it will return ``True``. .. py:method:: then(callable [, *args, **kwargs]) Add a callable to the call chain, to be invoked when the results become available. The result of the current call will be used as the first argument for the next call. Optional extra arguments can be provided via ``args`` and ``kwargs``. .. py:method:: iferror(errorhandler) Specify the exception handler to be invoked (with the exception object as only argument) when asking for the result raises an exception. If no exception handler is set, any exception result will be silently ignored (unless you explicitly ask for the value). Returns self so you can easily chain other calls. A simple piece of code showing an asynchronous method call:: proxy._pyroAsync() asyncresult = proxy.remotemethod() print("value available?", asyncresult.ready) # ...do some other stuff... print("resultvalue=", asyncresult.value) .. note:: :ref:`batched-calls` can also be executed asynchronously. Asynchronous calls are implemented using a background thread that waits for the results. Callables from the call chain are invoked sequentially in this background thread. .. note:: Be aware that the async setting is on a per-proxy basis (unless you make an exact copy of a proxy using ``copy.copy``). The async setting is not part of a serialized proxy object. So this means for instance if you're using auto proxy and use a method on an async proxy that returns a new proxy, those new proxies will *not* be async automatically as well. .. caution:: The async proxy concept is not a part of Pyro5. It has been removed in favor of an explicit user code solution such as using Python's ``concurrent.futures`` and not relying on a 'hidden' background thread. It is advised to not use this feature if you want your code to be easily portable to Pyro5 later. See the :file:`async` example for more details and example code for call chains. Async calls for normal callables (not only for Pyro proxies) ------------------------------------------------------------ The asynchrnous proxy discussed above is only available when you are dealing with Pyro proxies. It provides a convenient syntax to call the methods on the proxy asynchronously. For normal Python code it is sometimes useful to have a similar mechanism as well. Pyro provides this too, see :ref:`future-functions` for more information. .. index:: callback Pyro Callbacks ============== Usually there is a nice separation between a server and a client. But with some Pyro programs it is not that simple. It isn't weird for a Pyro object in a server somewhere to invoke a method call on another Pyro object, that could even be running in the client program doing the initial call. In this case the client program is a server itself as well. These kinds of 'reverse' calls are labeled *callbacks*. You have to do a bit of work to make them possible, because normally, a client program is not running the required code to also act as a Pyro server to accept incoming callback calls. In fact, you have to start a Pyro daemon and register the callback Pyro objects in it, just as if you were writing a server program. Keep in mind though that you probably have to run the daemon's request loop in its own background thread. Or make heavy use of oneway method calls. If you don't, your client program won't be able to process the callback requests because it is by itself still waiting for results from the server. .. index:: single: exception in callback single: @Pyro4.callback double: decorator; callback **Exceptions in callback objects:** If your callback object raises an exception, Pyro will return that to the server doing the callback. Depending on what the server does with it, you might never see the actual exception, let alone the stack trace. This is why Pyro provides a decorator that you can use on the methods in your callback object in the client program: ``@Pyro4.callback``. This way, an exception in that method is not only returned to the caller, but also logged locally in your client program, so you can see it happen including the stack trace (if you have logging enabled):: import Pyro4 class Callback(object): @Pyro4.expose @Pyro4.callback def call(self): print("callback received from server!") return 1//0 # crash! Also notice that the callback method (or the whole class) has to be decorated with ``@Pyro4.expose`` as well to allow it to be called remotely at all. See the :file:`callback` example for more details and code. .. index:: misc features Miscellaneous features ====================== Pyro provides a few miscellaneous features when dealing with remote method calls. They are described in this section. .. index:: error handling Error handling -------------- You can just do exception handling as you would do when writing normal Python code. However, Pyro provides a few extra features when dealing with errors that occurred in remote objects. This subject is explained in detail its own chapter: :doc:`errors`. See the :file:`exceptions` example for more details. .. index:: timeouts Timeouts -------- Because calls on Pyro objects go over the network, you might encounter network related problems that you don't have when using normal objects. One possible problems is some sort of network hiccup that makes your call unresponsive because the data never arrived at the server or the response never arrived back to the caller. By default, Pyro waits an indefinite amount of time for the call to return. You can choose to configure a *timeout* however. This can be done globally (for all Pyro network related operations) by setting the timeout config item:: Pyro4.config.COMMTIMEOUT = 1.5 # 1.5 seconds You can also do this on a per-proxy basis by setting the timeout property on the proxy:: proxy._pyroTimeout = 1.5 # 1.5 seconds There is also a server setting related to oneway calls, that says if oneway method calls should be executed in a separate thread or not. If this is set to ``False``, they will execute in the same thread as the other method calls. This means that if the oneway call is taking a long time to complete, the other method calls from the client may actually stall, because they're waiting on the server to complete the oneway call that came before them. To avoid this problem you can set this config item to True (which is the default). This runs the oneway call in its own thread (regardless of the server type that is used) and other calls can be processed immediately:: Pyro4.config.ONEWAY_THREADED = True # this is the default See the :file:`timeout` example for more details. Also, there is a automatic retry mechanism for timeout or connection closed (by server side), in order to use this automatically retry:: Pyro4.config.MAX_RETRIES = 3 # attempt to retry 3 times before raise the exception You can also do this on a pre-proxy basis by setting the max retries property on the proxy:: proxy._pyroMaxRetries = 3 # attempt to retry 3 times before raise the exception Be careful to use when remote functions have a side effect (e.g.: calling twice results in error)! See the :file:`autoretry` example for more details. .. index:: double: reconnecting; automatic .. _reconnecting: Automatic reconnecting ---------------------- If your client program becomes disconnected to the server (because the server crashed for instance), Pyro will raise a :py:exc:`Pyro4.errors.ConnectionClosedError`. You can use the automatic retry mechanism to handle this exception, see the :file:`autoretry` example for more details. Alternatively, it is also possible to catch this and tell Pyro to attempt to reconnect to the server by calling ``_pyroReconnect()`` on the proxy (it takes an optional argument: the number of attempts to reconnect to the daemon. By default this is almost infinite). Once successful, you can resume operations on the proxy:: try: proxy.method() except Pyro4.errors.ConnectionClosedError: # connection lost, try reconnecting obj._pyroReconnect() This will only work if you take a few precautions in the server. Most importantly, if it crashed and comes up again, it needs to publish its Pyro objects with the exact same URI as before (object id, hostname, daemon port number). See the :file:`autoreconnect` example for more details and some suggestions on how to do this. The ``_pyroReconnect()`` method can also be used to force a newly created proxy to connect immediately, rather than on first use. .. index:: proxy sharing Proxy sharing ------------- Due to internal locking you can freely share proxies among threads. The lock makes sure that only a single thread is actually using the proxy's communication channel at all times. This can be convenient *but* it may not be the best way to approach things. The lock essentially prevents parallelism. If you want calls to go in parallel, give each thread its own proxy. Here are a couple of suggestions on how to make copies of a proxy: #. use the :py:mod:`copy` module, ``proxy2 = copy.copy(proxy)`` #. create a new proxy from the uri of the old one: ``proxy2 = Pyro4.Proxy(proxy._pyroUri)`` #. simply create a proxy in the thread itself (pass the uri to the thread instead of a proxy) See the :file:`proxysharing` example for more details. .. index:: double: Daemon; Metadata .. _metadata: Metadata from the daemon ------------------------ A proxy contains some meta-data about the object it connects to. It obtains the data via the (public) :py:meth:`Pyro4.core.DaemonObject.get_metadata` method on the daemon that it connects to. This method returns the following information about the object (or rather, its class): what methods and attributes are defined, and which of the methods are to be called as one-way. This information is used to properly execute one-way calls, and to do client-side validation of calls on the proxy (for instance to see if a method or attribute is actually available, without having to do a round-trip to the server). Also this enables a properly working ``hasattr`` on the proxy, and efficient and specific error messages if you try to access a method or attribute that is not defined or not exposed on the Pyro object. Lastly the direct access to attributes on the remote object is also made possible, because the proxy knows about what attributes are available. For backward compatibility with old Pyro4 versions (4.26 and older) you can disable this mechanism by setting the ``METADATA`` config item to ``False`` (it's ``True`` by default). You can tell if you need to do this if you're getting errors in your proxy saying that 'DaemonObject' has no attribute 'get_metadata'. Either upgrade the Pyro version of the server, or set the ``METADATA`` config item to False in your client code. Pyro4-4.82/docs/source/commandline.rst000066400000000000000000000044331416147301300177400ustar00rootroot00000000000000.. index:: command line tools .. _command-line: ****************** Command line tools ****************** Pyro has several command line tools that you will be using sooner or later. They are generated and installed when you install Pyro. - :command:`pyro4-ns` (name server) - :command:`pyro4-nsc` (name server client tool) - :command:`pyro4-test-echoserver` (test echo server) - :command:`pyro4-check-config` (prints configuration) - :command:`pyro4-flameserver` (flame server) - :command:`pyro4-httpgateway` (http gateway server) If you prefer, you can also invoke the various "executable modules" inside Pyro directly, by using Python's "-m" command line argument. Some of these tools are described in detail in their respective sections of the manual: Name server tools: See :ref:`nameserver-nameserver` and :ref:`nameserver-nsc` for detailed information. HTTP gateway server: See :ref:`http-gateway` for detailed information. .. index:: double: echo server; command line .. _command-line-echoserver: Test echo server ================ :command:`python -m Pyro4.test.echoserver [options]` (or simply: :command:`pyro4-test-echoserver [options]`) This is a simple built-in server that can be used for testing purposes. It launches a Pyro object that has several methods suitable for various tests (see below). Optionally it can also directly launch a name server. This way you can get a simple Pyro server plus name server up with just a few keystrokes. A short explanation of the available options can be printed with the help option: .. program:: Pyro4.test.echoserver .. option:: -h, --help Print a short help message and exit. The echo server object is available by the name ``test.echoserver``. It exposes the following methods: .. method:: echo(argument) Simply returns the given argument object again. .. method:: error() Generates a run time exception. .. method:: shutdown() Terminates the echo server. .. index:: double: configuration check; command line Configuration check =================== :command:`python -m Pyro4.configuration` (or simply: :command:`pyro4-check-config`) This is the equivalent of:: >>> import Pyro4 >>> print(Pyro4.config.dump()) It prints the Pyro version, the location it is imported from, and a dump of the active configuration items. Pyro4-4.82/docs/source/conf.py000066400000000000000000000172221416147301300162170ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Pyro documentation build configuration file, created by # sphinx-quickstart on Thu Jun 16 22:20:40 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os if sys.version_info < (3, 0): raise RuntimeError("Use python 3.x to format the docs. It contains Python 3.x code blocks that won't be syntaxhighlighted otherwise.") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('../../src')) import Pyro4.constants # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '1.5.3' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Pyro' copyright = u'Irmen de Jong' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = Pyro4.constants.VERSION # The full version, including alpha/beta/rc tags. release = Pyro4.constants.VERSION # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. #import guzzle_sphinx_theme #html_theme_path = guzzle_sphinx_theme.html_theme_path() #html_theme = 'guzzle_sphinx_theme' html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { # "rightsidebar": True, # "bodyfont": "Tahoma,Helvetica,\"Helvetica Neue\",Arial,sans-serif", # "linkcolor": "#3070a0", # "visitedlinkcolor": "#3070a0", } # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "_static/pyro.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # (deprecated, use docutils.conf) # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. html_domain_indices = False # If false, no index is generated. html_use_index = True # If true, the index is split into individual pages for each letter. html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Pyrodoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Pyro.tex', u'Pyro Documentation', u'Irmen de Jong', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pyro', u'Pyro Documentation', [u'Irmen de Jong'], 1) ] def setup(app): # add custom css app.add_css_file("css/customize.css") from sphinx.ext.autodoc import cut_lines # skip the copyright line in every module docstring (last line of docstring) app.connect('autodoc-process-docstring', cut_lines(pre=0, post=1, what=['module'])) Pyro4-4.82/docs/source/config.rst000066400000000000000000000255361416147301300167260ustar00rootroot00000000000000.. index:: configuration **************** Configuring Pyro **************** Pyro can be configured using several *configuration items*. The current configuration is accessible from the ``Pyro4.config`` object, it contains all config items as attributes. You can read them and update them to change Pyro's configuration. (usually you need to do this at the start of your program). For instance, to enable message compression and change the server type, you add something like this to the start of your code:: Pyro4.config.COMPRESSION = True Pyro4.config.SERVERTYPE = "multiplex" .. index:: double: configuration; environment variables You can also set them outside of your program, using environment variables from the shell. **To avoid conflicts, the environment variables have a ``PYRO_`` prefix.** This means that if you want to change the same two settings as above, but by using environment variables, you would do something like:: $ export PYRO_COMPRESSION=true $ export PYRO_SERVERTYPE=multiplex (or on windows:) C:\> set PYRO_COMPRESSION=true C:\> set PYRO_SERVERTYPE=multiplex This environment defined configuration is simply used as initial values for Pyro's configuration object. Your code can still overwrite them by setting the items to other values, or by resetting the config as a whole. .. index:: reset config to default Resetting the config to default values -------------------------------------- .. method:: Pyro4.config.reset([useenvironment=True]) Resets the configuration items to their builtin default values. If `useenvironment` is True, it will overwrite builtin config items with any values set by environment variables. If you don't trust your environment, it may be a good idea to reset the config items to just the builtin defaults (ignoring any environment variables) by calling this method with `useenvironment` set to False. Do this before using any other part of the Pyro library. .. index:: current config, pyro4-check-config Inspecting current config ------------------------- To inspect the current configuration you have several options: 1. Access individual config items: ``print(Pyro4.config.COMPRESSION)`` 2. Dump the config in a console window: :command:`python -m Pyro4.configuration` (or simply :command:`pyro4-check-config`) This will print something like:: Pyro version: 4.6 Loaded from: E:\Projects\Pyro4\src\Pyro4 Active configuration settings: AUTOPROXY = True COMMTIMEOUT = 0.0 COMPRESSION = False ... 3. Access the config as a dictionary: ``Pyro4.config.asDict()`` 4. Access the config string dump (used in #2): ``Pyro4.config.dump()`` .. index:: configuration items .. _config-items: Overview of Config Items ------------------------ ========================= ======= ======================= ======= config item type default meaning ========================= ======= ======================= ======= AUTOPROXY bool True Enable to make Pyro automatically replace Pyro objects by proxies in the method arguments and return values of remote method calls. Doesn't work with marshal serializer. COMMTIMEOUT float 0.0 network communication timeout in seconds. 0.0=no timeout (infinite wait) COMPRESSION bool False Enable to make Pyro compress the data that travels over the network DETAILED_TRACEBACK bool False Enable to get detailed exception tracebacks (including the value of local variables per stack frame) HOST str localhost Hostname where Pyro daemons will bind on MAX_MESSAGE_SIZE int 0 Maximum size in bytes of the messages sent or received on the wire. If a message exceeds this size, a ProtocolError is raised. NS_HOST str *equal to HOST* Hostname for the name server. Used for locating in clients only (use the normal HOST config item in the name server itself) NS_PORT int 9090 TCP port of the name server. Used by the server and for locating in clients. NS_BCPORT int 9091 UDP port of the broadcast responder from the name server. Used by the server and for locating in clients. NS_BCHOST str None Hostname for the broadcast responder of the name server. Used by the server only. NS_AUTOCLEAN float 0.0 Specify a recurring period in seconds where the Name server checks its registrations and removes the ones that are not available anymore. (0=disabled, otherwise should be >=3) NATHOST str None External hostname in case of NAT (used by the server) NATPORT int 0 External port in case of NAT (used by the server) 0=replicate internal port number as NAT port BROADCAST_ADDRS str , 0.0.0.0 List of comma separated addresses that Pyro should send broadcasts to (for NS locating in clients) ONEWAY_THREADED bool True Enable to make oneway calls be processed in their own separate thread POLLTIMEOUT float 2.0 For the multiplexing server only: the timeout of the select or poll calls SERVERTYPE str thread Select the Pyro server type. thread=thread pool based, multiplex=select/poll/kqueue based SOCK_REUSE bool True Should SO_REUSEADDR be used on sockets that Pyro creates. PREFER_IP_VERSION int 4 The IP address type that is preferred (4=ipv4, 6=ipv6, 0=let OS decide). THREADPOOL_SIZE int 40 For the thread pool server: maximum number of threads running THREADPOOL_SIZE_MIN int 4 For the thread pool server: minimum number of threads running FLAME_ENABLED bool False Should Pyro Flame be enabled on the server SERIALIZER str serpent The wire protocol serializer to use for clients/proxies (one of: serpent, json, marshal, msgpack, pickle, cloudpickle, dill) SERIALIZERS_ACCEPTED set json,marshal,serpent The wire protocol serializers accepted in the server/daemon. In your code it should be a set of strings, use a comma separated string instead when setting the shell environment variable. PICKLE_PROTOCOL_VERSION int highest possible The pickle protocol version to use, if pickle is selected as serializer. Defaults to pickle.HIGHEST_PROTOCOL DILL_PROTOCOL_VERSION int highest possible The dill protocol version to use, if dill is selected as serializer. Defaults to dill.HIGHEST_PROTOCOL (-1 if dill is not installed) JSON_MODULE str json The json module to use for the json serializer. (json is included in the stdlib, simplejson is a possible 3rd party alternative). LOGWIRE bool False If wire-level message data should be written to the logfile (you may want to disable COMPRESSION) METADATA bool True Client: Get remote object metadata from server automatically on proxy connect (methods, attributes, oneways, etc) and use local checks in the proxy against it (set to False to use compatible behavior with Pyro 4.26 and earlier) REQUIRE_EXPOSE bool True Server: Is @expose required to make members remotely accessible. If False, everything is accessible (use this only for backwards compatibility). USE_MSG_WAITALL bool True (False if Some systems have broken socket MSG_WAITALL support. Set this item to False if your system is one of these. Pyro will then use another (but slower) piece of code to receive network data. on Windows) MAX_RETRIES int 0 Automatically retry network operations for some exceptions (timeout / connection closed), be careful to use when remote functions have a side effect (e.g.: calling twice results in error) ITER_STREAMING bool True Should iterator item streaming support be enabled in the server (default=True) ITER_STREAM_LIFETIME float 0.0 Maximum lifetime in seconds for item streams (default=0, no limit - iterator only stops when exhausted or client disconnects) ITER_STREAM_LINGER float 30.0 Linger time in seconds to keep an item stream alive after proxy disconnects (allows to reconnect to stream) SSL bool False Should SSL/TSL communication security be used? Enabling it also requires some other SSL config items to be set. SSL_SERVERCERT str *empty str* Location of the server's certificate file SSL_SERVERKEY str *empty str* Location of the server's private key file SSL_SERVERKEYPASSWD str *empty str* Password for the server's private key SSL_REQUIRECLIENTCERT bool False Should the server require clients to connect with their own certificate (2-way-ssl) SSL_CLIENTCERT str *empty str* Location of the client's certificate file SSL_CLIENTKEY str *empty str* Location of the client's private key file SSL_CLIENTKEYPASSWD str *empty str* Password for the client's private key ========================= ======= ======================= ======= .. index:: double: configuration items; logging There are two special config items that control Pyro's logging, and that are only available as environment variable settings. This is because they are used at the moment the Pyro4 package is being imported (which means that modifying them as regular config items after importing Pyro4 is too late and won't work). It is up to you to set the environment variable you want to the desired value. You can do this from your OS or shell, or perhaps by modifying ``os.environ`` in your Python code *before* importing Pyro4. ======================= ======= ============== ======= environment variable type default meaning ======================= ======= ============== ======= PYRO_LOGLEVEL string *not set* The log level to use for Pyro's logger (DEBUG, WARN, ...) See Python's standard :py:mod:`logging` module for the allowed values (https://docs.python.org/2/library/logging.html#levels). If it is not set, no logging is being configured. PYRO_LOGFILE string pyro.log The name of the log file. Use {stderr} to make the log go to the standard error output. ======================= ======= ============== ======= Pyro4-4.82/docs/source/docutils.conf000066400000000000000000000000541416147301300174100ustar00rootroot00000000000000[restructuredtext parser] smart_quotes=true Pyro4-4.82/docs/source/errors.rst000066400000000000000000000146231416147301300167700ustar00rootroot00000000000000.. index:: exceptions, remote traceback ******************************** Exceptions and remote tracebacks ******************************** There is an example that shows various ways to deal with exceptions when writing Pyro code. Have a look at the ``exceptions`` example in the :file:`examples` directory. Pyro exceptions --------------- Pyro's exception classes can be found in :mod:`Pyro4.errors`. They are used by Pyro if something went wrong inside Pyro itself or related to something Pyro was doing. .. index:: remote errors Remote exceptions ----------------- More interesting are the exceptions that occur in *your own* objects (the remote Pyro objects). Pyro is doing its best to make the remote objects appear as normal, local, Python objects. That also means that if they raise an error, Pyro will make it appear in the caller, as if the error occurred locally. Say you have a remote object that can divide arbitrary numbers. It will probably raise a ``ZeroDivisionError`` when you supply ``0`` as the divisor. This can be dealt with as follows:: import Pyro4 divider=Pyro4.Proxy( ... ) try: result = divider.div(999,0) except ZeroDivisionError: print("yup, it crashed") Just catch the exception as if you were writing code that deals with normal objects. But, since the error occurred in a *remote* object, and Pyro itself raises it again on the client side, you lose some information: the actual traceback of the error at the time it occurred in the server. Pyro fixes this because it stores the traceback information on a special attribute on the exception object (``_pyroTraceback``). The traceback is stored as a list of strings (each is a line from the traceback text, including newlines). You can use this data on the client to print or process the traceback text from the exception as it occurred in the Pyro object on the server. There is a utility function in :mod:`Pyro4.util` to make it easy to deal with this: :func:`Pyro4.util.getPyroTraceback` You use it like this:: import Pyro4.util try: result = proxy.method() except Exception: print("Pyro traceback:") print("".join(Pyro4.util.getPyroTraceback())) .. index:: exception hook Also, there is another function that you can install in ``sys.excepthook``, if you want Python to automatically print the complete Pyro traceback including the remote traceback, if any: :func:`Pyro4.util.excepthook` A full Pyro exception traceback, including the remote traceback on the server, looks something like this:: Traceback (most recent call last): File "client.py", line 50, in print(test.complexerror()) # due to the excepthook, the exception will show the pyro error File "E:\Projects\Pyro4\src\Pyro4\core.py", line 130, in __call__ return self.__send(self.__name, args, kwargs) File "E:\Projects\Pyro4\src\Pyro4\core.py", line 242, in _pyroInvoke raise data TypeError: unsupported operand type(s) for //: 'str' and 'int' +--- This exception occured remotely (Pyro) - Remote traceback: | Traceback (most recent call last): | File "E:\Projects\Pyro4\src\Pyro4\core.py", line 760, in handleRequest | data=method(*vargs, **kwargs) # this is the actual method call to the Pyro object | File "E:\projects\Pyro4\examples\exceptions\excep.py", line 17, in complexerror | x.crash() | File "E:\projects\Pyro4\examples\exceptions\excep.py", line 22, in crash | s.crash2('going down...') | File "E:\projects\Pyro4\examples\exceptions\excep.py", line 25, in crash2 | x=arg//2 | TypeError: unsupported operand type(s) for //: 'str' and 'int' +--- End of remote traceback As you can see, the first part is only the exception as it occurs locally on the client (raised by Pyro). The indented part marked with 'Remote traceback' is the exception as it occurred in the remote Pyro object. .. index:: traceback information Detailed traceback information ------------------------------ There is another utility that Pyro has to make it easier to debug remote object exceptions. If you enable the ``DETAILED_TRACEBACK`` config item on the server (see :ref:`config-items`), the remote traceback is extended with details of the values of the local variables in every frame:: +--- This exception occured remotely (Pyro) - Remote traceback: | ---------------------------------------------------- | EXCEPTION : unsupported operand type(s) for //: 'str' and 'int' | Extended stacktrace follows (most recent call last) | ---------------------------------------------------- | File "E:\Projects\Pyro4\src\Pyro4\core.py", line 760, in Daemon.handleRequest | Source code: | data=method(*vargs, **kwargs) # this is the actual method call to the Pyro object | ---------------------------------------------------- | File "E:\projects\Pyro4\examples\exceptions\excep.py", line 17, in TestClass.complexerror | Source code: | x.crash() | Local values: | self = | self._pyroDaemon = | self._pyroId = 'obj_c63d47dd140f44dca8782151643e0c55' | x = | ---------------------------------------------------- | File "E:\projects\Pyro4\examples\exceptions\excep.py", line 22, in Foo.crash | Source code: | self.crash2('going down...') | Local values: | self = | ---------------------------------------------------- | File "E:\projects\Pyro4\examples\exceptions\excep.py", line 25, in Foo.crash2 | Source code: | x=arg//2 | Local values: | arg = 'going down...' | self = | ---------------------------------------------------- | EXCEPTION : unsupported operand type(s) for //: 'str' and 'int' | ---------------------------------------------------- +--- End of remote traceback You can immediately see why the call produced a ``TypeError`` without the need to have a debugger running (the ``arg`` variable is a string and dividing that string by 2 is the cause of the error). Of course it is also possible to enable ``DETAILED_TRACEBACK`` on the client, but it is not as useful there (normally it is no problem to run the client code inside a debugger). Pyro4-4.82/docs/source/flame.rst000066400000000000000000000143361416147301300165410ustar00rootroot00000000000000.. index:: FLAME ************************************************ Flame: Foreign Location Automatic Module Exposer ************************************************ .. image:: _static/flammable.png :align: left Pyro Flame is an easy way of exposing remote modules and builtins, and even a remote interactive Python console. It is available since Pyro 4.10. With Flame, you don't need to write any server side code anymore, and still be able to call objects, modules and other things on the remote machine. Flame does this by giving a client direct access to any module or builtin that is available on the remote machine. Flame can be found in the :py:mod:`Pyro4.utils.flame` module. .. warning:: Be very sure about what you are doing before enabling Flame. Flame is disabled by default. You need to explicitly set a config item to true, and start a Flame server yourself, to make it available. This is because it allows client programs full access to *everything* on your system. Only use it if you fully trust your environment and the clients that can connect to your machines. (Flame is also symbolic for burning server machines that got totally owned by malicious clients.) .. index:: enabling Flame Enabling Flame ============== Flame is actually a special Pyro object that is exposed via a normal Pyro daemon. You need to start it explicitly in your daemon. This is done by calling a utility function with your daemon that you want to enable flame on:: import Pyro4.utils.flame Pyro4.utils.flame.start(daemon) Additionally, you have to make some configuration changes: * flame server: set the ``FLAME_ENABLED`` config item to True * flame server: set the ``SERIALIZERS_ACCEPTED`` config item to ``{"pickle"}`` * flame client: set the ``SERIALIZER`` config item to ``pickle`` You'll have to explicitly enable Flame. When you don't, you'll get an error when trying to start Flame. The config item is False by default to avoid unintentionally running Flame servers. Also, Flame requires the pickle serializer. It doesn't work when using one of the secure serializers, because it needs to be able to transfer custom python objects. .. index:: double: command line; Flame server Command line server =================== There's a little command line server program that will launch a flame enabled Pyro daemon, to avoid the hassle of having to write a custom server program yourself everywhere you want to provide a Flame server: :command:`python -m Pyro4.utils.flameserver` (or simply: :command:`pyro4-flameserver`) The command line arguments are similar to the echo server (see :ref:`command-line-echoserver`). Use ``-h`` to make it print a short help text. For the command line server you'll also have to set the ``FLAME_ENABLED`` config item to True, otherwise you'll get an error when trying to start it. Because we're talking about command line clients, the most convenient way to do so is probably by setting the environment variable in your shell: ``PYRO_FLAME_ENABLED=true``. .. index:: Flame object Flame object and examples ========================= A Flame server exposes a ``"Pyro.Flame"`` object (you can hardcode this name or use the constant :py:attr:`Pyro4.constants.FLAME_NAME`). Its interface is described in the API documentation, see :py:class:`Pyro4.utils.flame.Flame`. Connecting to the flame server can be done as usual (by creating a Pyro proxy yourself) or by using the convenience function :py:func:`Pyro4.utils.flame.connect`. It creates a Pyro proxy to the flame server for you using a slightly simpler interface. (If you started the flame server with a hmac key, you also must provide the ``hmac_key`` parameter here.) A little example follows. You have to have running flame server, and then you can write a client like this:: import Pyro4.utils.flame Pyro4.config.SERIALIZER = "pickle" # flame requires pickle serializer flame = Pyro4.utils.flame.connect("hostname:9999") # or whatever the server runs at socketmodule = flame.module("socket") osmodule = flame.module("os") print("remote host name=", socketmodule.gethostname()) print("remote server directory contents=", osmodule.listdir(".")) flame.execute("import math") root = flame.evaluate("math.sqrt(500)") print("calculated square root=", root) print("remote exceptions also work", flame.evaluate("1//0")) # print something on the remote std output flame.builtin("print")("Hello there, remote server stdout!") .. index:: Flame remote console A remote interactive console can be started like this:: with flame.console() as console: console.interact() # ... you can repeat sessions if you want ... which will print something like:: Python 2.7.2 (default, Jun 12 2011, 20:46:48) [GCC 4.2.1 (Apple Inc. build 5577)] on darwin (Remote console on charon:9999) >>> # type stuff here and it gets executed on the remote machine >>> import socket >>> socket.gethostname() 'charon.local' >>> ^D (Remote session ended) .. index:: getfile, sendfile .. note:: The ``getfile`` and ``sendfile`` functions can be used for *very* basic file transfer. The ``getmodule`` and ``sendmodule`` functions can be used to send module source files to other machines so it is possible to execute code that wasn't available before. This is a *very* experimental replacement of the mobile code feature that Pyro 3.x had. It also is a very easy way of totally owning the server because you can make it execute anything you like. Be very careful. .. note:: The remote module proxy that Flame provides does *not* support direct attribute access. For instance, you cannot do the following:: flame = Pyro4.utils.flame.connect("....") ros = flame.module("os") print(ros.name) # doesn't work as you might expect The ``flame.evaluate`` method (:py:meth:`Pyro4.utils.flame.Flame.evaluate`) provides an alternative though:: print(flame.evaluate("os.name")) # properly prints the os.name of the remote server .. note:: :doc:`pyrolite` also supports convenient access to a Pyro Flame server. This includes the remote interactive console. See the :file:`flame` example for example code including uploading module source code to the server. Pyro4-4.82/docs/source/index.rst000066400000000000000000000037271416147301300165660ustar00rootroot00000000000000**************************************** Pyro - Python Remote Objects - |version| **************************************** .. image:: _static/pyro-large.png :align: center :alt: PYRO logo .. index:: what is Pyro What is Pyro? ------------- It is a library that enables you to build applications in which objects can talk to each other over the network, with minimal programming effort. You can just use normal Python method calls to call objects on other machines. Pyro is a pure Python library and runs on many different platforms and Python versions. Pyro is copyright © Irmen de Jong (irmen@razorvine.net | http://www.razorvine.net). Please read :doc:`license`. It's on Pypi as `Pyro4 `_. Source on Github: https://github.com/irmen/Pyro4 and version 5 as `Pyro5 `__ (`Source `__) .. image:: _static/pyro5.png :align: right :width: 120px Pyro4 is considered feature complete and new development is frozen. Only very important bug fixes (such as security issues) will still be made to Pyro4. New development, improvements and new features will only be available in its successor `Pyro5 `_ . New code should use Pyro5 unless a feature of Pyro4 is strictly required. Older code should consider migrating to Pyro5. It provides a (simple) backwards compatibility api layer to make the porting easier. .. toctree:: :maxdepth: 2 :caption: Contents of this manual: intro.rst install.rst tutorials.rst commandline.rst clientcode.rst servercode.rst nameserver.rst security.rst errors.rst flame.rst tipstricks.rst config.rst api.rst alternative.rst pyrolite.rst changelog.rst license.rst Indices and tables ================== * :ref:`genindex` * :ref:`search` .. figure:: _static/tf_pyrotaunt.png :target: http://wiki.teamfortress.com/wiki/Pyro :alt: PYYYRRRROOOO :align: center Pyro4-4.82/docs/source/install.rst000066400000000000000000000115071416147301300171200ustar00rootroot00000000000000.. index:: installing Pyro *************** Installing Pyro *************** This chapter will show how to obtain and install Pyro. Pyro5 ----- .. image:: _static/pyro5.png :align: right :width: 120px Pyro4 is considered feature complete and new development is frozen. Only very important bug fixes (such as security issues) will still be made to Pyro4. New development, improvements and new features will only be available in its successor `Pyro5 `_ . New code should use Pyro5 unless a feature of Pyro4 is strictly required. Older code should consider migrating to Pyro5. It provides a (simple) backwards compatibility api layer to make the porting easier. .. index:: double: installing Pyro; requirements for Pyro Compatibility ------------- Pyro4 is written in 100% Python. It works on any recent operating system where a suitable supported Python implementation is available (2.7, or 3.5 and newer). It also works with Pypy (2 and 3). Maybe it also works with other Python implementations, but those are not tested. (if you only need to write *client* code in Jython/Java, consider using :doc:`pyrolite` instead!) .. note:: When Pyro is configured to use pickle, cloudpickle, dill or marshal as its serialization format, it is required to have the same *major* Python versions on your clients and your servers. Otherwise the different parties cannot decipher each others serialized data. This means you cannot let Python 2.x talk to Python 3.x with Pyro, when using those serializers. However it should be fine to have Python 3.5 talk to Python 3.6 for instance. The other protocols (serpent, json) don't have this limitation! .. index:: double: installing Pyro; obtaining Pyro Obtaining and installing Pyro ----------------------------- **Linux** Some Linux distributions offer Pyro4 through their package manager. Make sure you install the correct one for the python version that you are using. It may be more convenient to just pip install it instead in a virtualenv. **Anaconda** Anaconda users can install the Pyro4 package from conda-forge using ``conda install -c conda-forge pyro4`` **Pip install** ``pip install Pyro4`` should do the trick. Pyro is available `here `_ on pypi. **Manual installation from source** Download the source distribution archive (Pyro4-X.YZ.tar.gz) from Pypi or Github, extract and ``python setup.py install``. The `serpent `_ serialization library must also be installed. If you're using a version of Python older than 3.5, the `selectors2 `_ or `selectors34 `_ backported module must also be installed to be able to use the multiplex server type. **Github** Source is on Github: https://github.com/irmen/Pyro4 The required serpent serializer library is there as well: https://github.com/irmen/Serpent Third party libraries that Pyro4 uses ------------------------------------- `serpent `_ - required Should be installed automatically when you install Pyro4. `selectors34 `_ - required on Python 3.3 or older Should be installed automatically when you install Pyro4. `selectors2 `_ - optional on Python 3.4 or older Install this if you want better behavior for interrupted system calls on Python 3.4 or older. `dill `_ - optional Install this if you want to use the dill serializer. `cloudpickle `_ - optional Install this if you want to use the cloudpickle serializer. `msgpack `_ - optional Install this if you want to use the msgpack serializer. Stuff you get extra in the source distribution archive and not with packaged versions ------------------------------------------------------------------------------------- If you decide to download the distribution (.tar.gz) you have a bunch of extras over simply installing the Pyro library directly. It contains: docs/ the Sphinx/RST sources for this manual examples/ dozens of examples that demonstrate various Pyro features (highly recommended to examine these, many paragraphs in this manual refer to relevant examples here) tests/ the unittest suite that checks for correctness and regressions src/ The actual Pyro4 library's source code (only this part is installed if you install the ``Pyro4`` package) and a couple of other files: a setup script and other miscellaneous files such as the license (see :doc:`license`). If you don't want to download anything, you can view all of this `online on Github `_. Pyro4-4.82/docs/source/intro.rst000066400000000000000000000335311416147301300166060ustar00rootroot00000000000000***************** Intro and Example ***************** .. image:: _static/pyro-large.png :align: center This chapter contains a little overview of Pyro's features and a simple example to show how it looks like. .. index:: features About Pyro: feature overview ============================ Pyro is a library that enables you to build applications in which objects can talk to each other over the network, with minimal programming effort. You can just use normal Python method calls, with almost every possible parameter and return value type, and Pyro takes care of locating the right object on the right computer to execute the method. It is designed to be very easy to use, and to generally stay out of your way. But it also provides a set of powerful features that enables you to build distributed applications rapidly and effortlessly. Pyro is a pure Python library and runs on many different platforms and Python versions. Here's a quick overview of Pyro's features: - written in 100% Python so extremely portable, runs on Python 2.7, Python 3.5 and newer, IronPython, Pypy 2 and 3. - works between different system architectures and operating systems. - able to communicate between different Python versions transparently. - defaults to a safe serializer (`serpent `_) that supports many Python data types. - supports different serializers (serpent, json, marshal, msgpack, pickle, cloudpickle, dill). - support for all Python data types that are serializable when using the 'pickle', 'cloudpickle' or 'dill' serializers [1]_. - can use IPv4, IPv6 and Unix domain sockets. - optional secure connections via SSL/TLS (encryption, authentication and integrity), including certificate validation on both ends (2-way ssl). - lightweight client library available for .NET and Java native code ('Pyrolite', provided separately). - designed to be very easy to use and get out of your way as much as possible, but still provide a lot of flexibility when you do need it. - name server that keeps track of your object's actual locations so you can move them around transparently. - yellow-pages type lookups possible, based on metadata tags on registrations in the name server. - support for automatic reconnection to servers in case of interruptions. - automatic proxy-ing of Pyro objects which means you can return references to remote objects just as if it were normal objects. - one-way invocations for enhanced performance. - batched invocations for greatly enhanced performance of many calls on the same object. - remote iterator on-demand item streaming avoids having to create large collections upfront and transfer them as a whole. - you can define timeouts on network communications to prevent a call blocking forever if there's something wrong. - asynchronous invocations if you want to get the results 'at some later moment in time'. Pyro will take care of gathering the result values in the background. - remote exceptions will be raised in the caller, as if they were local. You can extract detailed remote traceback information. - http gateway available for clients wanting to use http+json (such as browser scripts). - stable network communication code that works reliably on many platforms. - can hook onto existing sockets created for instance with socketpair() to communicate efficiently between threads or sub-processes. - possibility to use Pyro's own event loop, or integrate it into your own (or third party) event loop. - three different possible instance modes for your remote objects (singleton, one per session, one per call). - many simple examples included to show various features and techniques. - large amount of unit tests and high test coverage. - reliable and established: built upon more than 15 years of existing Pyro history, with ongoing support and development. .. index:: history Pyro's history ^^^^^^^^^^^^^^ I started working on the first Pyro version in 1998, when remote method invocation technology such as Java's RMI and CORBA were quite popular. I wanted something like that in Python and there was nothing available, so I decided to write my own. Over the years it slowly gained features till it reached version 3.10 or so. At that point it was clear that the code base had become quite ancient and couldn't reliably support any new features, so Pyro4 was born in early 2010, written from scratch. ``Pyro`` is the package name of the old and no longer supported 3.x version of Pyro. ``Pyro4`` is the package name of the current version. Its concepts are similar to Pyro 3.x but it is not backwards compatible. To avoid conflicts, this version has a different package name. If you're somehow still interested in the old version, here is `its git repo `_ and it is also still `available on PyPi `_ -- but don't use it unless you really have to. .. index:: usage What can you use Pyro for? ========================== Essentially, Pyro can be used to distribute and integrate various kinds of resources or responsibilities: computational (hardware) resources (cpu, storage, printers), informational resources (data, privileged information) and business logic (departments, domains). An example would be a high performance compute cluster with a large storage system attached to it. Usually this is not accessible directly, rather, smaller systems connect to it and feed it with jobs that need to run on the big cluster. Later, they collect the results. Pyro could be used to expose the available resources on the cluster to other computers. Their client software connects to the cluster and calls the Python program there to perform its heavy duty work, and collect the results (either directly from a method call return value, or perhaps via asynchronous callbacks). Remote controlling resources or other programs is a nice application as well. For instance, you could write a simple remote controller for your media server that is running on a machine somewhere in a closet. A simple remote control client program could be used to instruct the media server to play music, switch playlists, etc. Another example is the use of Pyro to implement a form of `privilege separation `_. There is a small component running with higher privileges, but just able to execute the few tasks (and nothing else) that require those higher privileges. That component could expose one or more Pyro objects that represent the privileged information or logic. Other programs running with normal privileges can talk to those Pyro objects to perform those specific tasks with higher privileges in a controlled manner. Finally, Pyro can be a communication glue library to easily integrate various pars of a heterogeneous system, consisting of many different parts and pieces. As long as you have a working (and supported) Python version running on it, you should be able to talk to it using Pyro from any other part of the system. Have a look at the :file:`examples` directory in the source archive, perhaps one of the many example programs in there gives even more inspiration of possibilities. .. index:: example Simple Example ============== This example will show you in a nutshell what it's like to use Pyro in your programs. A much more extensive introduction is found in the :doc:`tutorials`. Here, we're making a simple greeting service that will return a personalized greeting message to its callers. First let's see the server code:: # saved as greeting-server.py import Pyro4 @Pyro4.expose class GreetingMaker(object): def get_fortune(self, name): return "Hello, {0}. Here is your fortune message:\n" \ "Behold the warranty -- the bold print giveth and the fine print taketh away.".format(name) daemon = Pyro4.Daemon() # make a Pyro daemon uri = daemon.register(GreetingMaker) # register the greeting maker as a Pyro object print("Ready. Object uri =", uri) # print the uri so we can use it in the client later daemon.requestLoop() # start the event loop of the server to wait for calls Open a console window and start the greeting server:: $ python greeting-server.py Ready. Object uri = PYRO:obj_edb9e53007ce4713b371d0dc6a177955@localhost:51681 Great, our server is running. Let's see the client code that invokes the server:: # saved as greeting-client.py import Pyro4 uri = input("What is the Pyro uri of the greeting object? ").strip() name = input("What is your name? ").strip() greeting_maker = Pyro4.Proxy(uri) # get a Pyro proxy to the greeting object print(greeting_maker.get_fortune(name)) # call method normally Start this client program (from a different console window):: $ python greeting-client.py What is the Pyro uri of the greeting object? <> What is your name? <> Hello, Irmen. Here is your fortune message: Behold the warranty -- the bold print giveth and the fine print taketh away. As you can see the client code called the greeting maker that was running in the server elsewhere, and printed the resulting greeting string. With a name server ^^^^^^^^^^^^^^^^^^ While the example above works, it could become tiresome to work with object uris like that. There's already a big issue, *how is the client supposed to get the uri, if we're not copy-pasting it?* Thankfully Pyro provides a *name server* that works like an automatic phone book. You can name your objects using logical names and use the name server to search for the corresponding uri. We'll have to modify a few lines in :file:`greeting-server.py` to make it register the object in the name server:: # saved as greeting-server.py import Pyro4 @Pyro4.expose class GreetingMaker(object): def get_fortune(self, name): return "Hello, {0}. Here is your fortune message:\n" \ "Tomorrow's lucky number is 12345678.".format(name) daemon = Pyro4.Daemon() # make a Pyro daemon ns = Pyro4.locateNS() # find the name server uri = daemon.register(GreetingMaker) # register the greeting maker as a Pyro object ns.register("example.greeting", uri) # register the object with a name in the name server print("Ready.") daemon.requestLoop() # start the event loop of the server to wait for calls The :file:`greeting-client.py` is actually simpler now because we can use the name server to find the object:: # saved as greeting-client.py import Pyro4 name = input("What is your name? ").strip() greeting_maker = Pyro4.Proxy("PYRONAME:example.greeting") # use name server object lookup uri shortcut print(greeting_maker.get_fortune(name)) The program now needs a Pyro name server that is running. You can start one by typing the following command: :command:`python -m Pyro4.naming` (or simply: :command:`pyro4-ns`) in a separate console window (usually there is just *one* name server running in your network). After that, start the server and client as before. There's no need to copy-paste the object uri in the client any longer, it will 'discover' the server automatically, based on the object name (:kbd:`example.greeting`). If you want you can check that this name is indeed known in the name server, by typing the command :command:`python -m Pyro4.nsc list` (or simply: :command:`pyro4-nsc list`), which will produce:: $ pyro4-nsc list --------START LIST Pyro.NameServer --> PYRO:Pyro.NameServer@localhost:9090 example.greeting --> PYRO:obj_663a31d2dde54b00bfe52ec2557d4f4f@localhost:51707 --------END LIST (Once again the uri for our object will be random) This concludes this simple Pyro example. .. note:: In the source archive there is a directory :file:`examples` that contains a truckload of example programs that show the various features of Pyro. If you're interested in them (it is highly recommended to be so!) you will have to download the Pyro distribution archive. Installing Pyro only provides the library modules. For more information, see :doc:`config`. Other means of creating connections ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The example above showed two of the basic ways to set up connections between your client and server code. There are various other options, have a look at the client code details: :ref:`object-discovery` and the server code details: :ref:`publish-objects`. The use of the name server is optional, see :ref:`name-server` for details. .. index:: performance, benchmark Performance =========== Pyro4 is pretty fast. On a typical networked system you can expect: - a few hundred new proxy connections per second to one server - similarly, a few hundred initial remote calls per second to one server - a few thousand remote method calls per second on a single proxy - tens of thousands batched or oneway remote calls per second - 10-100 Mb/sec data transfer Results do vary depending on many factors such as: - network speed - machine and operating system - I/O or CPU bound workload - contents and size of the pyro call request and response messages - the serializer being used - python version being used Experiment with the ``benchmark``, ``batchedcalls`` and ``hugetransfer`` examples to see what results you get on your own setup. .. rubric:: Footnotes .. [1] When configured to use the :py:mod:`pickle`, :py:mod:`cloudpickle` or :py:mod:`dill` serializer, your system may be vulnerable because of the security risks of these serialization protocols (possibility of arbitrary code execution). Pyro does have some security measures in place to mitigate this risk somewhat. They are described in the :doc:`security` chapter. It is strongly advised to read it. By default, Pyro is configured to use the safe `serpent` serializer, so you won't have to deal with these issues unless you configure it explicitly to use one of the other serializers. Pyro4-4.82/docs/source/license.rst000066400000000000000000000030671416147301300170760ustar00rootroot00000000000000.. index:: software license, license, disclaimer ******************************* Software License and Disclaimer ******************************* Pyro - Python Remote Objects - version 4.x Pyro is Copyright (c) by Irmen de Jong (irmen@razorvine.net). 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. This is the `MIT Software License `_ which is OSI-certified, and GPL-compatible. .. figure:: _static/tf_pyrotaunt.png :target: http://wiki.teamfortress.com/wiki/Pyro :alt: PYYYRRRROOOO :align: center Pyro4-4.82/docs/source/nameserver.rst000066400000000000000000000761451416147301300176320ustar00rootroot00000000000000.. index:: Name Server .. _name-server: *********** Name Server *********** The Pyro Name Server is a tool to help keeping track of your objects in your network. It is also a means to give your Pyro objects logical names instead of the need to always know the exact object name (or id) and its location. Pyro will name its objects like this:: PYRO:obj_dcf713ac20ce4fb2a6e72acaeba57dfd@localhost:51850 PYRO:custom_name@localhost:51851 It's either a generated unique object id on a certain host, or a name you chose yourself. But to connect to these objects you'll always need to know the exact object name or id and the exact hostname and port number of the Pyro daemon where the object is running. This can get tedious, and if you move servers around (or Pyro objects) your client programs can no longer connect to them until you update all URIs. Enter the *name server*. This is a simple phone-book like registry that maps logical object names to their corresponding URIs. No need to remember the exact URI anymore. Instead, you can ask the name server to look it up for you. You only need to give it the logical object name. .. note:: Usually you only need to run *one single instance* of the name server in your network. You can start multiple name servers but they are unconnected; you'll end up with a partitioned name space. **Example scenario:** Assume you've got a document archive server that publishes a Pyro object with several archival related methods in it. This archive server can register this object with the name server, using a logical name such as "Department.ArchiveServer". Any client can now connect to it using only the name "Department.ArchiveServer". They don't need to know the exact Pyro id and don't even need to know the location. This means you can move the archive server to another machine and as long as it updates its record in the name server, all clients won't notice anything and can keep on running without modification. .. index:: starting the name server double: name server; command line .. _nameserver-nameserver: Starting the Name Server ======================== The easiest way to start a name server is by using the command line tool. synopsys: :command:`python -m Pyro4.naming [options]` (or simply: :command:`pyro4-ns [options]`) Starts the Pyro Name Server. It can run without any arguments but there are several that you can use, for instance to control the hostname and port that the server is listening on. A short explanation of the available options can be printed with the help option. When it starts, it prints a message similar to this ('neptune' is the hostname of the machine it is running on):: $ pyro4-ns -n neptune Broadcast server running on 0.0.0.0:9091 NS running on neptune:9090 (192.168.1.100) URI = PYRO:Pyro.NameServer@neptune:9090 As you can see it prints that it started a broadcast server (and its location), a name server (and its location), and it also printed the URI that clients can use to access it directly. The nameserver uses a fast but volatile in-memory database by default. With a command line argument you can select a persistent storage mechanism (see below). If you're using that, your registrations will not be lost when the nameserver stops/restarts. The server will print the number of existing registrations at startup time if it discovers any. .. note:: Pyro by default binds its servers on localhost which means you cannot reach them from another machine on the network. This behavior also applies to the name server. If you want to be able to talk to the name server from other machines, you have to explicitly provide a hostname to bind on. There are several command line options for this tool: .. program:: Pyro4.naming .. option:: -h, --help Print a short help message and exit. .. option:: -n HOST, --host=HOST Specify hostname or ip address to bind the server on. The default is localhost, note that your name server will then not be visible from the network If the server binds on localhost, *no broadcast responder* is started either. Make sure to provide a hostname or ip address to make the name server reachable from other machines, if you want that. .. option:: -p PORT, --port=PORT Specify port to bind server on (0=random). .. option:: -u UNIXSOCKET, --unixsocket=UNIXSOCKET Specify a Unix domain socket name to bind server on, rather than a normal TCP/IP socket. .. option:: --bchost=BCHOST Specify the hostname or ip address to bind the broadcast responder on. Note: if the hostname where the name server binds on is localhost (or 127.0.x.x), no broadcast responder is started. .. option:: --bcport=BCPORT Specify the port to bind the broadcast responder on (0=random). .. option:: --nathost=NATHOST Specify the external host name to use in case of NAT .. option:: --natport=NATPORT Specify the external port use in case of NAT .. option:: -s STORAGE, --storage=STORAGE Specify the storage mechanism to use. You have several options: - ``memory`` - fast, volatile in-memory database. This is the default. - ``dbm:dbfile`` - dbm-style persistent database table. Provide the filename to use. This storage type does not support metadata. - ``sql:sqlfile`` - sqlite persistent database. Provide the filename to use. .. option:: -x, --nobc Don't start a broadcast responder. Clients will not be able to use the UDP-broadcast lookup to discover this name server. (The broadcast responder listens to UDP broadcast packets on the local network subnet, to signal its location to clients that want to talk to the name server) .. option:: -k, --key Specify hmac key to use. Deprecated: use SSL instead, or if you must, set the key via the PYRO_HMAC_KEY environment variable before starting the name server. Starting the Name Server from within your own code ================================================== Another way to start up a name server is by doing it from within your own code. This is more complex than simply launching it via the command line tool, because you have to integrate the name server into the rest of your program (perhaps you need to merge event loops?). For your convenience, two helper functions are available to create a name server yourself: :py:func:`Pyro4.naming.startNS` and :py:func:`Pyro4.naming.startNSloop`. Look at the :file:`eventloop` example to see how you can use this. **Custom storage mechanism:** The utility functions allow you to specify a custom storage mechanism (via the ``storage`` parameter). By default the in memory storage :py:class:`Pyro4.naming.MemoryStorage` is used. In the :py:mod:`Pyro4.naming_storage` module you can find the two other implementations (for the dbm and for the sqlite storage). You could also build your own, as long as it has the same interface. .. index:: double: name server; configuration items Configuration items =================== There are a couple of config items related to the nameserver. They are used both by the name server itself (to configure the values it will use to start the server with), and the client code that locates the name server (to give it optional hints where the name server is located). Often these can be overridden with a command line option or with a method parameter in your code. ================== =========== Configuration item description ================== =========== HOST hostname that the name server will bind on (being a regular Pyro daemon). NS_HOST the hostname or ip address of the name server. Used for locating in clients only. NS_PORT the port number of the name server. Used by the server and for locating in clients. NS_BCHOST the hostname or ip address of the name server's broadcast responder. Used only by the server. NS_BCPORT the port number of the name server's broadcast responder. Used by the server and for locating in clients. NATHOST the external hostname in case of NAT. Used only by the server. NATPORT the external port in case of NAT. Used only by the server. NS_AUTOCLEAN a recurring period in seconds where the Name server checks its registrations, and removes the ones that are no longer available. Defaults to 0.0 (off). ================== =========== .. index:: double: name server; name server control .. _nameserver-nsc: Name server control tool ======================== The name server control tool (or 'nsc') is used to talk to a running name server and perform diagnostic or maintenance actions such as querying the registered objects, adding or removing a name registration manually, etc. synopsis: :command:`python -m Pyro4.nsc [options] command [arguments]` (or simply: :command:`pyro4-nsc [options] command [arguments]`) .. program:: Pyro4.nsc .. option:: -h, --help Print a short help message and exit. .. option:: -n HOST, --host=HOST Provide the hostname or ip address of the name server. The default is to do a broadcast lookup to search for a name server. .. option:: -p PORT, --port=PORT Provide the port of the name server, or its broadcast port if you're doing a broadcast lookup. .. option:: -u UNIXSOCKET, --unixsocket=UNIXSOCKET Provide the Unix domain socket name of the name server, rather than a normal TCP/IP socket. .. option:: -k, --key Specify hmac key to use. Deprecated: use SSL instead, or if you must, set the key via the PYRO_HMAC_KEY environment variable before starting the nsc tool. .. option:: -v, --verbose Print more output that could be useful. The available commands for this tool are: list : list [prefix] List all objects registered in the name server. If you supply a prefix, the list will be filtered to show only the objects whose name starts with the prefix. listmatching : listmatching pattern List only the objects with a name matching the given regular expression pattern. lookup : lookup name Looks up a single name registration and prints the uri. listmeta_all : listmeta_all metadata [metadata...] List the objects having *all* of the given metadata tags listmeta_any : listmeta_any metadata [metadata...] List the objects having *any one* (or multiple) of the given metadata tags register : register name uri Registers a name to the given Pyro object :abbr:`URI (universal resource identifier)`. remove : remove name Removes the entry with the exact given name from the name server. removematching : removematching pattern Removes all entries matching the given regular expression pattern. setmeta : setmeta name [metadata...] Sets the new list of metadata tags for the given Pyro object. If you don't specify any metadata tags, the metadata of the object is cleared. ping Does nothing besides checking if the name server is running and reachable. Example:: $ pyro4-nsc ping Name server ping ok. $ pyro4-nsc list Pyro --------START LIST - prefix 'Pyro' Pyro.NameServer --> PYRO:Pyro.NameServer@localhost:9090 --------END LIST - prefix 'Pyro' .. index:: double: name server; locating the name server Locating the Name Server and using it in your code ================================================== The name server is a Pyro object itself, and you access it through a normal Pyro proxy. The object exposed is :class:`Pyro4.naming.NameServer`. Getting a proxy for the name server is done using the following function: :func:`Pyro4.naming.locateNS` (also available as :func:`Pyro4.locateNS`). .. index:: double: name server; broadcast lookup By far the easiest way to locate the Pyro name server is by using the broadcast lookup mechanism. This goes like this: you simply ask Pyro to look up the name server and return a proxy for it. It automatically figures out where in your subnet it is running by doing a broadcast and returning the first Pyro name server that responds. The broadcast is a simple UDP-network broadcast, so this means it usually won't travel outside your network subnet (or through routers) and your firewall needs to allow UDP network traffic. There is a config item ``BROADCAST_ADDRS`` that contains a comma separated list of the broadcast addresses Pyro should use when doing a broadcast lookup. Depending on your network configuration, you may have to change this list to make the lookup work. It could be that you have to add the network broadcast address for the specific network that the name server is located on. .. note:: You can only talk to a name server on a different machine if it didn't bind on localhost (that means you have to start it with an explicit host to bind on). The broadcast lookup mechanism only works in this case as well -- it doesn't work with a name server that binds on localhost. For instance, the name server started as an example in :ref:`nameserver-nameserver` was told to bind on the host name 'neptune' and it started a broadcast responder as well. If you use the default host (localhost) a broadcast responder will not be created. Normally, all name server lookups are done this way. In code, it is simply calling the locator function without any arguments. If you want to circumvent the broadcast lookup (because you know the location of the server already, somehow) you can specify the hostname. As soon as you provide a specific hostname to the name server locator (by using a host argument to the ``locateNS`` call, or by setting the ``NS_HOST`` config item, etc) it will no longer use a broadcast too try to find the name server. .. function:: locateNS([host=None, port=None, broadcast=True, hmac_key=key]) Get a proxy for a name server somewhere in the network. If you're not providing host or port arguments, the configured defaults are used. Unless you specify a host, a broadcast lookup is done to search for a name server. (api reference: :py:func:`Pyro4.naming.locateNS`) :param host: the hostname or ip address where the name server is running. Default is ``None`` which means it uses a network broadcast lookup. If you specify a host, no broadcast lookup is performed. :param port: the port number on which the name server is running. Default is ``None`` which means use the configured default. The exact meaning depends on whether the host parameter is given: * host parameter given: the port now means the actual name server port. * host parameter not given: the port now means the broadcast port. :param broadcast: should a broadcast be used to locate the name server, if no location is specified? Default is True. :param hmac_key: optional hmac key to use .. index:: PYRONAME protocol type .. _nameserver-pyroname: The PYRONAME protocol type ========================== To create a proxy and connect to a Pyro object, Pyro needs an URI so it can find the object. Because it is so convenient, the name server logic has been integrated into Pyro's URI mechanism by means of the special ``PYRONAME`` protocol type (rather than the normal ``PYRO`` protocol type). This protocol type tells Pyro to treat the URI as a logical object name instead, and Pyro will do a name server lookup automatically to get the actual object's URI. The form of a PYRONAME uri is very simple:: PYRONAME:some_logical_object_name PYRONAME:some_logical_object_name@nshostname # with optional host name PYRONAME:some_logical_object_name@nshostname:nsport # with optional host name + port where "some_logical_object_name" is the name of a registered Pyro object in the name server. When you also provide the ``nshostname`` and perhaps even ``nsport`` parts in the uri, you tell Pyro to look for the name server on that specific location (instead of relying on a broadcast lookup mechanism). (You can achieve more or less the same by setting the ``NS_HOST`` and ``NS_PORT`` config items) All this means that instead of manually resolving objects like this:: nameserver=Pyro4.locateNS() uri=nameserver.lookup("Department.BackupServer") proxy=Pyro4.Proxy(uri) proxy.backup() you can write this instead:: proxy=Pyro4.Proxy("PYRONAME:Department.BackupServer") proxy.backup() An additional benefit of using a PYRONAME uri in a proxy is that the proxy isn't strictly tied to a specific object on a specific location. This is useful in scenarios where the server objects might move to another location, for instance when a disconnect/reconnect occurs. See the :file:`autoreconnect` example for more details about this. .. note:: Pyro has to do a lookup every time it needs to connect one of these PYRONAME uris. If you connect/disconnect many times or with many different objects, consider using PYRO uris (you can type them directly or create them by resolving as explained in the following paragraph) or call :meth:`Pyro4.core.Proxy._pyroBind()` on the proxy to bind it to a fixed PYRO uri instead. .. index:: PYROMETA protocol type .. _nameserver-pyrometa: The PYROMETA protocol type ========================== Next to the ``PYRONAME`` protocol type there is another 'magic' protocol ``PYROMETA``. This protocol type tells Pyro to treat the URI as metadata tags, and Pyro will ask the name server for any (randomly chosen) object that has the given metadata tags. The form of a PYROMETA uri is:: PYROMETA:metatag PYROMETA:metatag1,metatag2,metatag3 PYROMETA:metatag@nshostname # with optional host name PYROMETA:metatag@nshostname:nsport # with optional host name + port So you can write this to connect to any random printer (given that all Pyro objects representing a printer have been registered in the name server with the ``resource.printer`` metadata tag):: proxy=Pyro4.Proxy("PYROMETA:resource.printer") proxy.printstuff() You have to explicitly add metadata tags when registering objects with the name server, see :ref:`nameserver-yellowpages`. Objects without metadata tags cannot be found via ``PYROMETA`` obviously. Note that the name server supports more advanced metadata features than what ``PYROMETA`` provides: in a PYROMETA uri you cannot use white spaces, and you cannot ask for an object that has one or more of the given tags -- multiple tags means that the object must have all of them. Metadata tags can be listed if you query the name server for registrations. .. index:: resolving object names, PYRONAME protocol type Resolving object names ====================== 'Resolving an object name' means to look it up in the name server's registry and getting the actual URI that belongs to it (with the actual object name or id and the location of the daemon in which it is running). This is not normally needed in user code (Pyro takes care of it automatically for you), but it can still be useful in certain situations. So, resolving a logical name can be done in several ways: #. The easiest way: let Pyro do it for you! Simply pass a ``PYRONAME`` URI to the proxy constructor, and forget all about the resolving happening under the hood:: obj = Pyro4.Proxy("PYRONAME:objectname") obj.method() #. obtain a name server proxy and use its ``lookup`` method (:meth:`Pyro4.naming.NameServer.lookup`). You could then use this resolved uri to get an actual proxy, or do other things with it:: ns = Pyro4.locateNS() uri = ns.lookup("objectname") # uri now is the resolved 'objectname' obj = Pyro4.Proxy(uri) obj.method() #. use a ``PYRONAME`` URI and resolve it using the ``resolve`` utility function :func:`Pyro4.naming.resolve` (also available as :func:`Pyro4.resolve`):: uri = Pyro4.resolve("PYRONAME:objectname") # uri now is the resolved 'objectname' obj = Pyro4.Proxy(uri) obj.method() #. use a ``PYROMETA`` URI and resolve it using the ``resolve`` utility function :func:`Pyro4.naming.resolve` (also available as :func:`Pyro4.resolve`):: uri = Pyro4.resolve("PYROMETA:metatag1,metatag2") # uri is now randomly chosen from all objects having the given meta tags obj = Pyro4.Proxy(uri) .. index:: double: name server; registering objects double: name server; unregistering objects .. _nameserver-registering: Registering object names ======================== 'Registering an object' means that you associate the URI with a logical name, so that clients can refer to your Pyro object by using that name. Your server has to register its Pyro objects with the name server. It first registers an object with the Daemon, gets an URI back, and then registers that URI in the name server using the following method on the name server proxy: .. py:method:: register(name, uri, safe=False) Registers an object (uri) under a logical name in the name server. :param name: logical name that the object will be known as :type name: string :param uri: the URI of the object (you get it from the daemon) :type uri: string or :class:`Pyro4.core.URI` :param safe: normally registering the same name twice silently overwrites the old registration. If you set safe=True, the same name cannot be registered twice. :type safe: bool You can unregister objects as well using the :py:meth:`unregister` method. The name server also supports automatically checking for registrations that are no longer available, for instance because the server process crashed or a network problem occurs. It will then automatically remove those registrations after a certain timeout period. This feature is disabled by default (it potentially requires the NS to periodically create a lot of network connections to check for each of the registrations if it is still available). You can enable it by setting the ``NS_AUTOCLEAN`` config item to a non zero value; it then specifies the recurring period in seconds for the nameserver to check all its registrations. Choose an appropriately large value, the minimum allowed is 3. .. index:: scaling Name Server connections Free connections to the NS quickly ================================== By default the Name server uses a Pyro socket server based on whatever configuration is the default. Usually that will be a threadpool based server with a limited pool size. If more clients connect to the name server than the pool size allows, they will get a connection error. It is suggested you apply the following pattern when using the name server in your code: #. obtain a proxy for the NS #. look up the stuff you need, store it #. free the NS proxy (See :ref:`client_cleanup`) #. use the uri's/proxies you've just looked up This makes sure your client code doesn't consume resources in the name server for an excessive amount of time, and more importantly, frees up the limited connection pool to let other clients get their turn. If you have a proxy to the name server and you let it live for too long, it may eventually deny other clients access to the name server because its connection pool is exhausted. So if you don't need the proxy anymore, make sure to free it up. There are a number of things you can do to improve the matter on the side of the Name Server itself. You can control its behavior by setting certain Pyro config items before starting the server: - You can set ``SERVERTYPE=multiplex`` to create a server that doesn't use a limited connection (thread) pool, but multiplexes as many connections as the system allows. However, the actual calls to the server must now wait on eachother to complete before the next call is processed. This may impact performance in other ways. - You can set ``THREADPOOL_SIZE`` to an even larger number than the default. - You can set ``COMMTIMEOUT`` to a certain value, which frees up unused connections after the given time. But the client code may now crash with a TimeoutError or ConnectionClosedError when it tries to use a proxy it obtained earlier. (You can use Pyro's autoreconnect feature to work around this but it makes the code more complex) .. index:: double: name server; pickle double: name server; cloudpickle double: name server; dill .. _nameserver-pickle: Using the name server with pickle, cloudpickle or dill serializers ================================================================== If you find yourself in the unfortunate situation where you absolutely have to use the pickle, cloudpickle or dill serializers, you have to pay attention when also using the name server. Because these serializers are disabled by default, the name server will not reply to messages from clients that are using them, unless you enable them in the name server as well. The symptoms are usually that your client code seems unable to contact the name server:: Pyro4.errors.NamingError: Failed to locate the nameserver The name server will show a user warning message on the console:: Pyro protocol error occurred: message used serializer that is not accepted And if you enable logging for the name server you will likely see in its logfile:: accepted serializers: {'json', 'marshal', 'serpent'} ... ... Pyro4.errors.ProtocolError: message used serializer that is not accepted: [4,5] The way to solve this is to stop using the these serializers, or if you must use them, tell the name server that it is okay to accept them. You do that by setting the ``SERIALIZERS_ACCEPTED`` config item to a set of serializers that includes them, and then restart the name server. For instance:: $ export PYRO_SERIALIZERS_ACCEPTED=serpent,json,marshal,pickle,cloudpickle,dill $ pyro4-ns If you enable logging you will then see that the name server says that pickle, cloudpickle and dill are among the accepted serializers. .. index:: double: name server; Yellow-pages double: name server; Metadata .. _nameserver-yellowpages: Yellow-pages ability of the Name Server (metadata tags) ======================================================= Since Pyro 4.40, it is possible to tag object registrations in the name server with one or more Metadata tags. These are simple strings but you're free to put anything you want in it. One way of using it, is to provide a form of Yellow-pages object lookup: instead of directly asking for the registered object by its unique name (telephone book), you're asking for any registration from a certain *category*. You get back a list of registered objects from the queried category, from which you can then choose the one you want. .. note:: Metadata tags are case-sensitive. As an example, imagine the following objects registered in the name server (with the metadata as shown): =================== ======================= ======== Name Uri Metadata =================== ======================= ======== printer.secondfloor PYRO:printer1@host:1234 printer printer.hallway PYRO:printer2@host:1234 printer storage.diskcluster PYRO:disks1@host:1234 storage storage.ssdcluster PYRO:disks2@host:1234 storage =================== ======================= ======== Instead of having to know the exact name of a required object you can query the name server for all objects having a certain set of metadata. So in the above case, your client code doesn't have to 'know' that it needs to lookup the ``printer.hallway`` object to get the uri of a printer (in this case the one down in the hallway). Instead it can just ask for a list of all objects having the ``printer`` metadata tag. It will get a list containing both ``printer.secondfloor`` and ``printer.hallway`` so you will still have to choose the object you want to use - or perhaps even use both. The objects tagged with ``storage`` won't be returned. Arguably the most useful way to deal with the metadata is to use it for Yellow-pages style lookups. You can ask for all objects having some set of metadata tags, where you can choose if they should have *all* of the given tags or only *any one* (or more) of the given tags. Additional or other filtering must be done in the client code itself. So in the above example, querying with ``metadata_any={'printer', 'storage'}`` will return all four objects, while querying with ``metadata_all={'printer', 'storage'}`` will return an empty list (because there are no objects that are both a printer and storage). **Setting metadata in the name server** Object registrations in the name server by default have an empty set of metadata tags associated with them. However the ``register`` method (:meth:`Pyro4.naming.NameServer.register`) has an optional ``metadata`` argument, you can set that to a set of strings that will be the metadata tags associated with the object registration. For instance:: ns.register("printer.secondfloor", "PYRO:printer1@host:1234", metadata={"printer"}) **Getting metadata back from the name server** The ``lookup`` (:meth:`Pyro4.naming.NameServer.lookup`) and ``list`` (:meth:`Pyro4.naming.NameServer.list`) methods of the name server have an optional ``return_metadata`` argument. By default it is False, and you just get back the registered URI (lookup) or a dictionary with the registered names and their URI as values (list). If you set it to True however, you'll get back tuples instead: (uri, set-of-metadata-tags):: ns.lookup("printer.secondfloor", return_metadata=True) # returns: (, {'printer'}) ns.list(return_metadata=True) # returns something like: # {'printer.secondfloor': ('PYRO:printer1@host:1234', {'printer'}), # 'Pyro.NameServer': ('PYRO:Pyro.NameServer@localhost:9090', {'class:Pyro4.naming.NameServer'})} # (as you can see the name server itself has also been registered with a metadata tag) **Querying on metadata (Yellow-page lookup)** You can ask the name server to list all objects having some set of metadata tags. The ``list`` (:meth:`Pyro4.naming.NameServer.list`) method of the name server has two optional arguments to allow you do do this: ``metadata_all`` and ``metadata_any``. #. ``metadata_all``: give all objects having *all* of the given metadata tags:: ns.list(metadata_all={"printer"}) # returns: {'printer.secondfloor': 'PYRO:printer1@host:1234'} ns.list(metadata_all={"printer", "communication"}) # returns: {} (there is no object that's both a printer and a communication device) #. ``metadata_any``: give all objects having *one* (or more) of the given metadata tags:: ns.list(metadata_any={"storage", "printer", "communication"}) # returns: {'printer.secondfloor': 'PYRO:printer1@host:1234'} **Querying on metadata via ``PYROMETA`` uri (Yellow-page lookup in uri)** As a convenience, similar to the ``PYRONAME`` uri protocol, you can use the ``PYROMETA`` uri protocol to let Pyro do the lookup for you. It only supports ``metadata_all`` lookup, but it allows you to conveniently get a proxy like this:: Pyro4.Proxy("PYROMETA:resource.printer,performance.fast") this will connect to a (randomly chosen) object with both the ``resource.printer`` and ``performance.fast`` metadata tags. Also see :ref:`nameserver-pyrometa`. You can find some code that uses the metadata API in the :file:`ns-metadata` example. Note that the ``nsc`` tool (:ref:`nameserver-nsc`) also allows you to manipulate the metadata in the name server from the command line. .. index:: Name Server API Other methods in the Name Server API ==================================== The name server has a few other methods that might be useful at times. For instance, you can ask it for a list of all registered objects. Because the name server itself is a regular Pyro object, you can access its methods through a regular Pyro proxy, and refer to the description of the exposed class to see what methods are available: :class:`Pyro4.naming.NameServer`. Pyro4-4.82/docs/source/pyrolite.rst000066400000000000000000000066301416147301300173220ustar00rootroot00000000000000.. index:: Pyrolite, Java, .NET, C# ******************************************* Pyrolite - client library for Java and .NET ******************************************* This library allows your Java or .NET program to interface very easily with the Python world. It uses the Pyro protocol to call methods on remote objects. It also supports convenient access to a Pyro Flame server including the remote interactive console. Pyrolite is a tiny library that implements a part of the client side Pyro library, hence its name 'lite'. Pyrolite has no additional dependencies. So if you don't need Pyro's full feature set, and don't require your Java/.NET code to host Pyro objects itself, Pyrolite may be a good choice to connect java or .NET and python. Pyrolite also contains a feature complete implementation of Python's :mod:`pickle` protocol (with fairly intelligent mapping of datatypes between Python and Java/.NET), and a small part of Pyro's client network protocol and proxy logic. It can use the Serpent serialization format as well. *Use the 4.x version of the library with Pyro4.* *Getting the .NET version:* The .NET version is available using the nuget package manager, package name is ``Razorvine.Pyrolite`` (and ``Razorvine.Serpent``, which is a dependency). `Package info `_. *Getting the Java version:* The Java library can be obtained from `Maven `_, groupid ``net.razorvine`` artifactid ``pyrolite``. Source is on Github: https://github.com/irmen/Pyrolite/tree/pyro4-legacy in the 'pyro4-legacy' branch. Readme: https://github.com/irmen/Pyrolite/blob/pyro4-legacy/README.md Small code example in Java: .. code-block:: java import net.razorvine.pyro.*; NameServerProxy ns = NameServerProxy.locateNS(null); PyroProxy remoteobject = new PyroProxy(ns.lookup("Your.Pyro.Object")); Object result = remoteobject.call("pythonmethod", 42, "hello", new int[]{1,2,3}); String message = (String)result; // cast to the type that 'pythonmethod' returns System.out.println("result message="+message); remoteobject.close(); ns.close(); You can also read `a more elaborate example `_. That writeup is an elaboration of the Pyro simple example greeting.py appearing in the introduction chapter, but with a Java (rather than Python) client. The same example in C#: .. code-block:: csharp using Razorvine.Pyro; using( NameServerProxy ns = NameServerProxy.locateNS(null) ) { using( PyroProxy something = new PyroProxy(ns.lookup("Your.Pyro.Object")) ) { object result = something.call("pythonmethod", 42, "hello", new int[]{1,2,3}); string message = (string)result; // cast to the type that 'pythonmethod' returns Console.WriteLine("result message="+message); } } You can also use Pyro Flame rather conveniently because of some wrapper classes: .. code-block:: java Config.SERIALIZER = Config.SerializerType.pickle; // flame requires the pickle serializer PyroProxy flame = new PyroProxy(hostname, port, "Pyro.Flame"); FlameModule r_module = (FlameModule) flame.call("module", "socket"); System.out.println("hostname=" + r_module.call("gethostname")); FlameRemoteConsole console = (FlameRemoteConsole) flame.call("console"); console.interact(); console.close(); Pyro4-4.82/docs/source/security.rst000066400000000000000000000213571416147301300173250ustar00rootroot00000000000000.. index:: security .. _security: ******** Security ******** .. warning:: Do not publish any Pyro objects to remote machines unless you've read and understood everything that is discussed in this chapter. This is also true when publishing Pyro objects with different credentials to other processes on the same machine. Why? In short: using Pyro has several security risks. Pyro has a few countermeasures to deal with them. Understanding the risks, the countermeasures, and their limits, is very important to avoid creating systems that are very easy to compromise by malicious entities. .. index:: double: security; pickle double: security; cloudpickle double: security; dill Pickle, cloudpickle and dill as serialization formats (optional) ================================================================ When configured to do so, Pyro is able to use the :py:mod:`pickle`, :py:mod:`cloudpickle` or :py:mod:`dill` modules to serialize objects and then sends them over the network. It is well known that using these serializers for this purpose is a security risk. The main problem is that allowing a program to deserialize this type of serialized data can cause arbitrary code execution and this may wreck or compromise your system. Because of this the default serializer is serpent, which doesn't have this security problem. Some other means to enhance security are discussed below. .. caution:: Pyro5 won't support insecure serializers such as these. If you want your code to be more easily ported to Pyro5 later, there's another reason to avoid using them. .. index:: double: security; network interfaces Network interface binding ========================= By default Pyro binds every server on localhost, to avoid exposing things on a public network or over the internet by mistake. If you want to expose your Pyro objects to anything other than localhost, you have to explicitly tell Pyro the network interface address it should use. This means it is a conscious effort to expose Pyro objects to other machines. It is possible to tell Pyro the interface address via an environment variable or global config item (``HOST``). In some situations - or if you're paranoid - it is advisable to override this setting in your server program by setting the config item from within your own code, instead of depending on an externally configured setting. .. index:: double: security; different user id Running Pyro servers with different credentials/user id ======================================================= The following is not a Pyro specific problem, but is important nonetheless: If you want to run your Pyro server as a different user id or with different credentials as regular users, *be very careful* what kind of Pyro objects you expose like this! Treat this situation as if you're exposing your server on the internet (even when it's only running on localhost). Keep in mind that it is still possible that a random user on the same machine connects to the local server. You may need additional security measures to prevent random users from calling your Pyro objects. .. index:: SSL, TLS double: security; encryption Secure communication via SSL/TLS ================================ Pyro itself doesn't encrypt the data it sends over the network. This means if you use the default configuration, you must never transfer sensitive data on untrusted networks (especially user data, passwords, and such) because eavesdropping is possible. You can run Pyro over a secure network (VPN, ssl/ssh tunnel) where the encryption is taken care of externally. It is also possible however to enable SSL/TLS in Pyro itself, so that all communication is secured via this industry standard that provides encryption, authentication, and anti-tampering (message integrity). **Using SSL/TLS** Enable it by setting the ``SSL`` config item to True, and configure the other SSL config items as required. You'll need to specify the cert files to use, private keys, and passwords if any. By default, the SSL mode only has a cert on the server (which is similar to visiting a https url in your browser). This means your *clients* can be sure that they are connecting to the expected server, but the *server* has no way to know what clients are connecting. You can solve this by using a HMAC key (see :ref:`hmackey`), but if you're already using SSL, a better way is to do custom certificate verification. You can do this in your client (checks the server's cert) but you can also tell your clients to use certs as well and check these in your server. This makes it 2-way-SSL or mutual authentication. For more details see here :ref:`cert_verification`. The SSL config items are in :ref:`config-items`. For example code on how to set up a 2-way-SSL Pyro client and server, with cert verification, see the ``ssl`` example. .. index:: double: security; object traversal double: security; dotted names Dotted names (object traversal) =============================== Using dotted names on Pyro proxies (such as ``proxy.aaa.bbb.ccc()``) is not possible in Pyro, because it is a security vulnerability (for similar reasons as described here https://legacy.python.org/news/security/PSF-2005-001/ ). .. index:: double: security; environment variables Environment variables overriding config items ============================================= Almost all config items can be overwritten by an environment variable. If you can't trust the environment in which your script is running, it may be a good idea to reset the config items to their default builtin values, without using any environment variables. See :doc:`config` for the proper way to do this. .. index:: double: security; HMAC signature Preventing arbitrary connections ================================ .. _hmackey: ...by using a HMAC signature via a shared private key ----------------------------------------------------- You can use a `HMAC signature `_ on every network transfer to prevent malicious requests. The idea is to only have legit clients connect to your Pyro server. Using the HMAC signature ensures that only clients with the correct secret key can create valid requests, and that it is impossible to modify valid requests (even though the network data is not encrypted). The hashing algorithm that is used in the HMAC is SHA-1. .. sidebar:: consider alternatives For industry standard encryption and connection verification, consider using SSL/TLS instead. You need to create and configure a secure shared key yourself. The key is a byte string and must be cryptographically secure (there are various methods to create such a key). Your server needs to set this key and every client that wants to connect to it also needs to set it. You can set the shared key via the ``_pyroHmacKey`` property on a proxy or a daemon:: daemon._pyroHmacKey = b"secretkey" proxy._pyroHmacKey = b"secretkey" .. warning:: It is hard to keep a shared secret key actually secret! People might read the source code of your software and extract the key from it. Pyro itself provides no facilities to help you with this, sorry. The Diffie-Hellman Key Exchange algorithm is one example of a secure solution to this problem. There's the ``diffie-hellman`` example that shows the basics, but DO NOT use it directly as being "the secure way to do this" -- it's only demo code. .. index:: certificate verification, 2-way-SSL .. _cert_verification: ...by using 2-way-SSL and certificate verificiation --------------------------------------------------- When using SSL, you should also do some custom certificate verification, such as checking the serial number and commonName. This way your code is not only certain that the communication is encrypted, but also that it is talking to the intended party and nobody else (middleman). The server hostname and cert expiration dates *are* checked automatically, but other attributes you have to verify yourself. This is fairly easy to do: you can use :ref:`conn_handshake` for this. You can then get the peer certificate using :py:meth:`Pyro4.socketutil.SocketConnection.getpeercert`. If you configure a client cert as well as a server cert, you can/should also do verification of client certificates in your server. This is a good way to be absolutely certain that you only allow clients that you know and trust, because you can check the required unique certificate attributes. Having certs on both client and server is called 2-way-SSL or mutual authentication. It's a bit too involved to fully describe here but it not much harder than the basic SSL configuration described earlier. You just have to make sure you supply a client certificate and that the server requires a client certificate (and verifies some properties of it). The ``ssl`` example shows how to do all this. Pyro4-4.82/docs/source/servercode.rst000066400000000000000000001176771416147301300176320ustar00rootroot00000000000000.. index:: server code ***************************** Servers: hosting Pyro objects ***************************** This chapter explains how you write code that publishes objects to be remotely accessible. These objects are then called *Pyro objects* and the program that provides them, is often called a *server* program. (The program that calls the objects is usually called the *client*. Both roles can be mixed in a single program.) Make sure you are familiar with Pyro's :ref:`keyconcepts` before reading on. .. seealso:: :doc:`config` for several config items that you can use to tweak various server side aspects. .. index:: single: decorators single: @Pyro4.expose single: @Pyro4.oneway single: REQUIRE_EXPOSE double: decorator; expose double: decorator; oneway .. _decorating-pyro-class: Creating a Pyro class and exposing its methods and properties ============================================================= Exposing classes, methods and properties is done using the ``@Pyro4.expose`` decorator. It lets you mark the following items to be available for remote access: - methods (including classmethod and staticmethod). You cannot expose a 'private' method, i.e. name starting with underscore. You *can* expose a 'dunder' method with double underscore for example ``__len__``. There is a short list of dunder methods that will never be remoted though (because they are essential to let the Pyro proxy function correctly). Make sure you put the ``@expose`` decorator after other decorators on the method, if any. - properties (these will be available as remote attributes on the proxy) It's not possible to expose a 'private' property (name starting with underscore). You can't expose attributes directly. It is required to provide a @property for them and decorate that with ``@expose``, if you want to provide a remotely accessible attribute. - classes as a whole (exposing a class has the effect of exposing every nonprivate method and property of the class automatically) Anything that isn't decorated with ``@expose`` is not remotely accessible. .. important:: **Private methods and attributes**: In the spirit of being secure by default, Pyro doesn't allow remote access to anything of your class unless explicitly told to do so. It will never allow remote access to 'private' methods and attributes (where 'private' means that their name starts with a single or double underscore). There's a special exception for the regular 'dunder' names with double underscores such as ``__len__`` though. Here's a piece of example code that shows how a partially exposed Pyro class may look like:: import Pyro4 class PyroService(object): value = 42 # not exposed def __dunder__(self): # exposed pass def _private(self): # not exposed pass def __private(self): # not exposed pass @Pyro4.expose def get_value(self): # exposed return self.value @Pyro4.expose @property def attr(self): # exposed as 'proxy.attr' remote attribute return self.value @Pyro4.expose @attr.setter def attr(self, value): # exposed as 'proxy.attr' writable self.value = value .. note:: Prior to Pyro version 4.46, the default behavior was different: Pyro exposed everything, no special action was needed in your server side code to make it available to remote calls. Probably the easiest way to make old code that was written for this model to fit the new default behavior is to add a single ``@Pyro4.expose`` decorator on all of your Pyro classes. Better (safer) is to only add it to the methods and properties of the classes that are accessed remotely. If you cannot (or don't want to) change your code to be compatible with the new behavior, you can set the ``REQUIRE_EXPOSE`` config item back to ``False`` (it is now ``True`` by default). This will restore the old behavior. Notice that it has been possible for a long time already for older code to utilize the ``@expose`` decorator and the current, safer, behavior by having ``REQUIRE_EXPOSE`` set to ``True``. That choice has now simply become the default. Before upgrading to Pyro 4.46 or newer you can try setting it to ``True`` yourself and then adding ``@expose`` decorators to your Pyro classes or methods as required. Once everything works as it should you can then effortlessly upgrade Pyro itself. .. index:: oneway decorator **Specifying one-way methods using the @Pyro4.oneway decorator:** You decide on the class of your Pyro object on the server, what methods are to be called as one-way. You use the ``@Pyro4.oneway`` decorator on these methods to mark them for Pyro. When the client proxy connects to the server it gets told automatically what methods are one-way, you don't have to do anything on the client yourself. Any calls your client code makes on the proxy object to methods that are marked with ``@Pyro4.oneway`` on the server, will happen as one-way calls:: import Pyro4 @Pyro4.expose class PyroService(object): def normal_method(self, args): result = do_long_calculation(args) return result @Pyro4.oneway def oneway_method(self, args): result = do_long_calculation(args) # no return value, cannot return anything to the client See :ref:`oneway-calls-client` for the documentation about how client code handles this. See the :file:`oneway` example for some code that demonstrates the use of oneway methods. Exposing classes and methods without changing existing source code ================================================================== In the case where you cannot or don't want to change existing source code, it's not possible to use the ``@expose`` decorator to tell Pyro what methods should be exposed. This can happen if you're dealing with third-party library classes or perhaps a generic module that you don't want to 'taint' with a Pyro dependency because it's used elsewhere too. There are a few possibilities to deal with this: **Don't use @expose at all** (not recommended) You can disable the requirement for adding ``@expose`` to classes/methods by setting ``REQUIRE_EXPOSE`` back to False. This is a global setting however and will affect all your Pyro classes in the server, so be careful. **Use adapter classes** The preferred solution is to not use the classes from the third party library directly, but create an adapter class yourself with the appropriate ``@expose`` set on it or on its methods. Register this adapter class instead. Then use the class from the library from within your own adapter class. This way you have full control over what exactly is exposed, and what parameter and return value types travel over the wire. **Create exposed classes by using ``@expose`` as a function** Creating adapter classes is good but if you're looking for the most convenient solution we can do better. You can still use ``@expose`` to make a class a proper Pyro class with exposed methods, *without having to change the source code* due to adding @expose decorators, and without having to create extra classes yourself. Remember that Python decorators are just functions that return another function (or class)? This means you can also call them as a regular function yourself, which allows you to use classes from third party libraries like this:: from awesome_thirdparty_library import SomeClassFromLibrary import Pyro4 # expose the class from the library using @expose as wrapper function: ExposedClass = Pyro4.expose(SomeClassFromLibrary) daemon.register(ExposedClass) # register the exposed class rather than the library class itself There are a few caveats when using this: #. You can only expose the class and all its methods as a whole, you can't cherrypick methods that should be exposed #. You have no control over what data is returned from the methods. It may still be required to deal with serialization issues for instance when a method of the class returns an object whose type is again a class from the library. See the :file:`thirdpartylib` example for a little server that deals with such a third party library. .. index:: publishing objects .. _publish-objects: Pyro Daemon: publishing Pyro objects ==================================== To publish a regular Python object and turn it into a Pyro object, you have to tell Pyro about it. After that, your code has to tell Pyro to start listening for incoming requests and to process them. Both are handled by the *Pyro daemon*. In its most basic form, you create one or more classes that you want to publish as Pyro objects, you create a daemon, register the class(es) with the daemon, and then enter the daemon's request loop:: import Pyro4 @Pyro4.expose class MyPyroThing(object): # ... methods that can be called go here... pass daemon = Pyro4.Daemon() uri = daemon.register(MyPyroThing) print(uri) daemon.requestLoop() Once a client connects, Pyro will create an instance of the class and use that single object to handle the remote method calls during one client proxy session. The object is removed once the client disconnects. Another client will cause another instance to be created for its session. You can control more precisely when, how, and for how long Pyro will create an instance of your Pyro class. See :ref:`server-instancemode` below for more details. Anyway, when you run the code printed above, the uri will be printed and the server sits waiting for requests. The uri that is being printed looks a bit like this: ``PYRO:obj_dcf713ac20ce4fb2a6e72acaeba57dfd@localhost:51850`` Client programs use these uris to access the specific Pyro objects. .. note:: From the address in the uri that was printed you can see that Pyro by default binds its daemons on localhost. This means you cannot reach them from another machine on the network (a security measure). If you want to be able to talk to the daemon from other machines, you have to explicitly provide a hostname to bind on. This is done by giving a ``host`` argument to the daemon, see the paragraphs below for more details on this. .. index:: private methods .. note:: **Private methods:** Pyro considers any method or attribute whose name starts with at least one underscore ('_'), private. These cannot be accessed remotely. An exception is made for the 'dunder' methods with double underscores, such as ``__len__``. Pyro follows Python itself here and allows you to access these as normal methods, rather than treating them as private. .. note:: You can publish any regular Python object as a Pyro object. However since Pyro adds a few Pyro-specific attributes to the object, you can't use: * types that don't allow custom attributes, such as the builtin types (``str`` and ``int`` for instance) * types with ``__slots__`` (a possible way around this is to add Pyro's custom attributes to your ``__slots__``, but that isn't very nice) .. note:: Most of the the time a Daemon will keep running. However it's still possible to nicely free its resources when the request loop terminates by simply using it as a context manager in a ``with`` statement, like so:: with Pyro4.Daemon() as daemon: daemon.register(...) daemon.requestLoop() .. index:: publishing objects oneliner, serveSimple .. _server-servesimple: Oneliner Pyro object publishing: serveSimple() ---------------------------------------------- Ok not really a one-liner, but one statement: use :py:meth:`serveSimple` to publish a dict of objects/classes and start Pyro's request loop. The code above could also be written as:: import Pyro4 @Pyro4.expose class MyPyroThing(object): pass obj = MyPyroThing() Pyro4.Daemon.serveSimple( { MyPyroThing: None, # register the class obj: None # register one specific instance }, ns=False) You can perform some limited customization: .. py:staticmethod:: Daemon.serveSimple(objects [host=None, port=0, daemon=None, ns=True, verbose=True]) Very basic method to fire up a daemon that hosts a bunch of objects. The objects will be registered automatically in the name server if you specify this. API reference: :py:func:`Pyro4.core.Daemon.serveSimple` :param objects: mapping of objects/classes to names, these are the Pyro objects that will be hosted by the daemon, using the names you provide as values in the mapping. Normally you'll provide a name yourself but in certain situations it may be useful to set it to ``None``. Read below for the exact behavior there. :type objects: dict :param host: optional hostname where the daemon should be accessible on. Necessary if you want to access the daemon from other machines. :type host: str or None :param port: optional port number where the daemon should be accessible on :type port: int :param daemon: optional existing daemon to use, that you created yourself. If you don't specify this, the method will create a new daemon object by itself. :type daemon: Pyro4.core.Daemon :param ns: optional, if True (the default), the objects will also be registered in the name server (located using :py:meth:`Pyro4.locateNS`) for you. If this parameters is False, your objects will only be hosted in the daemon and are not published in a name server. Read below about the exact behavior of the object names you provide in the ``objects`` dictionary. :type ns: bool :param verbose: optional, if True (the default), print out a bit of info on the objects that are registered :type verbose: bool :returns: nothing, it starts the daemon request loop and doesn't return until that stops. If you set ``ns=True`` your objects will appear in the name server as well (this is the default setting). Usually this means you provide a logical name for every object in the ``objects`` dictionary. If you don't (= set it to ``None``), the object will still be available in the daemon (by a generated name) but will *not* be registered in the name server (this is a bit strange, but hey, maybe you don't want all the objects to be visible in the name server). When not using a name server at all (``ns=False``), the names you provide are used as the object names in the daemon itself. If you set the name to ``None`` in this case, your object will get an automatically generated internal name, otherwise your own name will be used. .. important:: - The names you provide for each object have to be unique (or ``None``). For obvious reasons you can't register multiple objects with the same names. - if you use ``None`` for the name, you have to use the ``verbose`` setting as well, otherwise you won't know the name that Pyro generated for you. That would make your object more or less unreachable. The uri that is used to register your objects in the name server with, is of course generated by the daemon. So if you need to influence that, for instance because of NAT/firewall issues, it is the daemon's configuration you should be looking at. If you don't provide a daemon yourself, :py:meth:`serveSimple` will create a new one for you using the default configuration or with a few custom parameters you can provide in the call, as described above. If you don't specify the ``host`` and ``port`` parameters, it will simple create a Daemon using the default settings. If you *do* specify ``host`` and/or ``port``, it will use these as parameters for creating the Daemon (see next paragraph). If you need to further tweak the behavior of the daemon, you have to create one yourself first, with the desired configuration. Then provide it to this function using the ``daemon`` parameter. Your daemon will then be used instead of a new one:: custom_daemon = Pyro4.Daemon(host="example", nathost="example") # some additional custom configuration Pyro4.Daemon.serveSimple( { MyPyroThing: None }, daemon = custom_daemon) .. index:: double: Pyro daemon; creating a daemon Creating a Daemon ----------------- Pyro's daemon is ``Pyro4.Daemon`` (shortcut to :class:`Pyro4.core.Daemon`). It has a few optional arguments when you create it: .. function:: Daemon([host=None, port=0, unixsocket=None, nathost=None, natport=None, interface=DaemonObject, connected_socket=None]) Create a new Pyro daemon. :param host: the hostname or IP address to bind the server on. Default is ``None`` which means it uses the configured default (which is localhost). It is necessary to set this argument to a visible hostname or ip address, if you want to access the daemon from other machines. :type host: str or None :param port: port to bind the server on. Defaults to 0, which means to pick a random port. :type port: int :param unixsocket: the name of a Unix domain socket to use instead of a TCP/IP socket. Default is ``None`` (don't use). :type unixsocket: str or None :param nathost: hostname to use in published addresses (useful when running behind a NAT firewall/router). Default is ``None`` which means to just use the normal host. For more details about NAT, see :ref:`nat-router`. :type host: str or None :param natport: port to use in published addresses (useful when running behind a NAT firewall/router). If you use 0 here, Pyro will replace the NAT-port by the internal port number to facilitate one-to-one NAT port mappings. :type port: int :param interface: optional alternative daemon object implementation (that provides the Pyro API of the daemon itself) :type interface: Pyro4.core.DaemonObject :param connected_socket: optional existing socket connection to use instead of creating a new server socket :type interface: socket .. index:: double: Pyro daemon; registering objects/classes Registering objects/classes --------------------------- Every object you want to publish as a Pyro object needs to be registered with the daemon. You can let Pyro choose a unique object id for you, or provide a more readable one yourself. .. method:: Daemon.register(obj_or_class [, objectId=None, force=False]) Registers an object with the daemon to turn it into a Pyro object. :param obj_or_class: the singleton instance or class to register (class is the preferred way) :param objectId: optional custom object id (must be unique). Default is to let Pyro create one for you. :type objectId: str or None :param force: optional flag to force registration, normally Pyro checks if an object had already been registered. If you set this to True, the previous registration (if present) will be silently overwritten. :type force: bool :returns: an uri for the object :rtype: :class:`Pyro4.core.URI` It is important to do something with the uri that is returned: it is the key to access the Pyro object. You can save it somewhere, or perhaps print it to the screen. The point is, your client programs need it to be able to access your object (they need to create a proxy with it). Maybe the easiest thing is to store it in the Pyro name server. That way it is almost trivial for clients to obtain the proper uri and connect to your object. See :doc:`nameserver` for more information (:ref:`nameserver-registering`), but it boils down to getting a name server proxy and using its ``register`` method:: uri = daemon.register(some_object) ns = Pyro4.locateNS() ns.register("example.objectname", uri) .. note:: If you ever need to create a new uri for an object, you can use :py:meth:`Pyro4.core.Daemon.uriFor`. The reason this method exists on the daemon is because an uri contains location information and the daemon is the one that knows about this. Intermission: Example 1: server and client not using name server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A little code example that shows the very basics of creating a daemon and publishing a Pyro object with it. Server code:: import Pyro4 @Pyro4.expose class Thing(object): def method(self, arg): return arg*2 # ------ normal code ------ daemon = Pyro4.Daemon() uri = daemon.register(Thing) print("uri=",uri) daemon.requestLoop() # ------ alternatively, using serveSimple ----- Pyro4.Daemon.serveSimple( { Thing: None }, ns=False, verbose=True) Client code example to connect to this object:: import Pyro4 # use the URI that the server printed: uri = "PYRO:obj_b2459c80671b4d76ac78839ea2b0fb1f@localhost:49383" thing = Pyro4.Proxy(uri) print(thing.method(42)) # prints 84 With correct additional parameters --described elsewhere in this chapter-- you can control on which port the daemon is listening, on what network interface (ip address/hostname), what the object id is, etc. Intermission: Example 2: server and client, with name server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A little code example that shows the very basics of creating a daemon and publishing a Pyro object with it, this time using the name server for easier object lookup. Server code:: import Pyro4 @Pyro4.expose class Thing(object): def method(self, arg): return arg*2 # ------ normal code ------ daemon = Pyro4.Daemon(host="yourhostname") ns = Pyro4.locateNS() uri = daemon.register(Thing) ns.register("mythingy", uri) daemon.requestLoop() # ------ alternatively, using serveSimple ----- Pyro4.Daemon.serveSimple( { Thing: "mythingy" }, ns=True, verbose=True, host="yourhostname") Client code example to connect to this object:: import Pyro4 thing = Pyro4.Proxy("PYRONAME:mythingy") print(thing.method(42)) # prints 84 .. index:: double: Pyro daemon; unregistering objects Unregistering objects --------------------- When you no longer want to publish an object, you need to unregister it from the daemon: .. method:: Daemon.unregister(objectOrId) :param objectOrId: the object to unregister :type objectOrId: object itself or its id string .. index:: request loop Running the request loop ------------------------ Once you've registered your Pyro object you'll need to run the daemon's request loop to make Pyro wait for incoming requests. .. method:: Daemon.requestLoop([loopCondition]) :param loopCondition: optional callable returning a boolean, if it returns False the request loop will be aborted and the call returns This is Pyro's event loop and it will take over your program until it returns (it might never.) If this is not what you want, you can control it a tiny bit with the ``loopCondition``, or read the next paragraph. .. index:: double: event loop; integrate Pyro's requestLoop Integrating Pyro in your own event loop --------------------------------------- If you want to use a Pyro daemon in your own program that already has an event loop (aka main loop), you can't simply call ``requestLoop`` because that will block your program. A daemon provides a few tools to let you integrate it into your own event loop: * :py:attr:`Pyro4.core.Daemon.sockets` - list of all socket objects used by the daemon, to inject in your own event loop * :py:meth:`Pyro4.core.Daemon.events` - method to call from your own event loop when Pyro needs to process requests. Argument is a list of sockets that triggered. For more details and example code, see the :file:`eventloop` and :file:`gui_eventloop` examples. They show how to use Pyro including a name server, in your own event loop, and also possible ways to use Pyro from within a GUI program with its own event loop. .. index:: Combining Daemons Combining Daemon request loops ------------------------------ In certain situations you will be dealing with more than one daemon at the same time. For instance, when you want to run your own Daemon together with an 'embedded' Name Server Daemon, or perhaps just another daemon with different settings. Usually you run the daemon's :meth:`Pyro4.core.Daemon.requestLoop` method to handle incoming requests. But when you have more than one daemon to deal with, you have to run the loops of all of them in parallel somehow. There are a few ways to do this: 1. multithreading: run each daemon inside its own thread 2. multiplexing event loop: write a multiplexing event loop and call back into the appropriate daemon when one of its connections send a request. You can do this using :mod:`selectors` or :mod:`select` and you can even integrate other (non-Pyro) file-like selectables into such a loop. Also see the paragraph above. 3. use :meth:`Pyro4.core.Daemon.combine` to combine several daemons into one, so that you only have to call the requestLoop of that "master daemon". Basically Pyro will run an integrated multiplexed event loop for you. You can combine normal Daemon objects, the NameServerDaemon and also the name server's BroadcastServer. Again, have a look at the :file:`eventloop` example to see how this can be done. (Note: this will only work with the ``multiplex`` server type, not with the ``thread`` type) .. index:: double: Pyro daemon; shutdown double: Pyro daemon; cleaning up Cleaning up ----------- To clean up the daemon itself (release its resources) either use the daemon object as a context manager in a ``with`` statement, or manually call :py:meth:`Pyro4.core.Daemon.close`. Of course, once the daemon is running, you first need a clean way to stop the request loop before you can even begin to clean things up. You can use force and hit ctrl-C or ctrl-\ or ctrl-Break to abort the request loop, but this usually doesn't allow your program to clean up neatly as well. It is therefore also possible to leave the loop cleanly from within your code (without using :py:meth:`sys.exit` or similar). You'll have to provide a ``loopCondition`` that you set to ``False`` in your code when you want the daemon to stop the loop. You could use some form of semi-global variable for this. (But if you're using the threaded server type, you have to also set ``COMMTIMEOUT`` because otherwise the daemon simply keeps blocking inside one of the worker threads). Another possibility is calling :py:meth:`Pyro4.core.Daemon.shutdown` on the running daemon object. This will also break out of the request loop and allows your code to neatly clean up after itself, and will also work on the threaded server type without any other requirements. If you are using your own event loop mechanism you have to use something else, depending on your own loop. .. index:: single: @Pyro4.behavior instance modes; instance_mode instance modes; instance_creator .. _server-instancemode: Controlling Instance modes and Instance creation ================================================ While it is possible to register a single singleton *object* with the daemon, it is actually preferred that you register a *class* instead. When doing that, it is Pyro itself that creates an instance (object) when it needs it. This allows for more control over when and for how long Pyro creates objects. Controlling the instance mode and creation is done by decorating your class with ``Pyro4.behavior`` and setting its ``instance_mode`` or/and ``instance_creator`` parameters. It can only be used on a class definition, because these behavioral settings only make sense at that level. By default, Pyro will create an instance of your class per *session* (=proxy connection) Here is an example of registering a class that will have one new instance for *every single method call* instead:: import Pyro4 @Pyro4.behavior(instance_mode="percall") class MyPyroThing(object): @Pyro4.expose def method(self): return "something" daemon = Pyro4.Daemon() uri = daemon.register(MyPyroThing) print(uri) daemon.requestLoop() There are three possible choices for the ``instance_mode`` parameter: - ``session``: (the default) a new instance is created for every new proxy connection, and is reused for all the calls during that particular proxy session. Other proxy sessions will deal with a different instance. - ``single``: a single instance will be created and used for all method calls, regardless what proxy connection we're dealing with. This is the same as creating and registering a single object yourself (the old style of registering code with the deaemon). Be aware that the methods on this object can be called from separate threads concurrently. - ``percall``: a new instance is created for every single method call, and discarded afterwards. **Instance creation** .. sidebar:: Instance creation is lazy When you register a class in this way, be aware that Pyro only creates an actual instance of it when it is first needed. If nobody connects to the deamon requesting the services of this class, no instance is ever created. Normally Pyro will simply use a default parameterless constructor call to create the instance. If you need special initialization or the class's init method requires parameters, you have to specify an ``instance_creator`` callable as well. Pyro will then use that to create an instance of your class. It will call it with the class to create an instance of as the single parameter. See the :file:`instancemode` example to learn about various ways to use this. See the :file:`usersession` example to learn how you could use it to build user-bound resource access without concurrency problems. .. index:: automatic proxying Autoproxying ============ Pyro will automatically take care of any Pyro objects that you pass around through remote method calls. It will replace them by a proxy automatically, so the receiving side can call methods on it and be sure to talk to the remote object instead of a local copy. There is no need to create a proxy object manually. All you have to do is to register the new object with the appropriate daemon:: def some_pyro_method(self): thing=SomethingNew() self._pyroDaemon.register(thing) return thing # just return it, no need to return a proxy This feature can be enabled or disabled by a config item, see :doc:`config`. (it is on by default). If it is off, a copy of the object itself is returned, and the client won't be able to interact with the actual new Pyro object in the server. There is a :file:`autoproxy` example that shows the use of this feature, and several other examples also make use of it. Note that when using the marshal serializer, this feature doesn't work. You have to use one of the other serializers to use autoproxying. Also, it doesn't work correctly when you are using old-style classes (but they are from Python 2.2 and earlier, you should not be using these anyway). .. index:: concurrency model, server types, SERVERTYPE .. _object_concurrency: Server types and Concurrency model ================================== Pyro supports multiple server types (the way the Daemon listens for requests). Select the desired type by setting the ``SERVERTYPE`` config item. It depends very much on what you are doing in your Pyro objects what server type is most suitable. For instance, if your Pyro object does a lot of I/O, it may benefit from the parallelism provided by the thread pool server. However if it is doing a lot of CPU intensive calculations, the multiplexed server may be more appropriate. If in doubt, go with the default setting. .. index:: double: server type; threaded 1. threaded server (servertype ``"thread"``, this is the default) This server uses a dynamically adjusted thread pool to handle incoming proxy connections. If the max size of the thread pool is too small for the number of proxy connections, new proxy connections will fail with an exception. The size of the pool is configurable via some config items: - ``THREADPOOL_SIZE`` this is the maximum number of threads that Pyro will use - ``THREADPOOL_SIZE_MIN`` this is the minimum number of threads that must remain standby Every proxy on a client that connects to the daemon will be assigned to a thread to handle the remote method calls. This way multiple calls can potentially be processed concurrently. *This means your Pyro object may have to be made thread-safe*! If you registered the pyro object's class with instance mode ``single``, that single instance will be called concurrently from different threads. If you used instance mode ``session`` or ``percall``, the instance will not be called from different threads because a new one is made per connection or even per call. But in every case, if you access a shared resource from your Pyro object, you may need to take thread locking measures such as using Queues. .. index:: double: server type; multiplex 2. multiplexed server (servertype ``"multiplex"``) This server uses a connection multiplexer to process all remote method calls sequentially. No threads are used in this server. It uses the best supported selector available on your platform (kqueue, poll, select). It means only one method call is running at a time, so if it takes a while to complete, all other calls are waiting for their turn (even when they are from different proxies). The instance mode used for registering your class, won't change the way the concurrent access to the instance is done: in all cases, there is only one call active at all times. Your objects will never be called concurrently from different threads, because there are no threads. It does still affect when and how often Pyro creates an instance of your class. .. note:: If the ``ONEWAY_THREADED`` config item is enabled (it is by default), *oneway* method calls will be executed in a separate worker thread, regardless of the server type you're using. .. index:: double: server type; what to choose? *When to choose which server type?* With the threadpool server at least you have a chance to achieve concurrency, and you don't have to worry much about blocking I/O in your remote calls. The usual trouble with using threads in Python still applies though: Python threads don't run concurrently unless they release the :abbr:`GIL (Global Interpreter Lock)`. If they don't, you will still hang your server process. For instance if a particular piece of your code doesn't release the :abbr:`GIL (Global Interpreter Lock)` during a longer computation, the other threads will remain asleep waiting to acquire the :abbr:`GIL (Global Interpreter Lock)`. One of these threads may be the Pyro server loop and then your whole Pyro server will become unresponsive. Doing I/O usually means the :abbr:`GIL (Global Interpreter Lock)` is released. Some C extension modules also release it when doing their work. So, depending on your situation, not all hope is lost. With the multiplexed server you don't have threading problems: everything runs in a single main thread. This means your requests are processed sequentially, but it's easier to make the Pyro server unresponsive. Any operation that uses blocking I/O or a long-running computation will block all remote calls until it has completed. .. index:: double: server; serialization Serialization ============= Pyro will serialize the objects that you pass to the remote methods, so they can be sent across a network connection. Depending on the serializer that is being used for your Pyro server, there will be some limitations on what objects you can use, and what serialization format is required of the clients that connect to your server. You specify one or more serializers that are accepted in the daemon/server by setting the ``SERIALIZERS_ACCEPTED`` config item. This is a set of serializer names that are allowed to be used with your server. It defaults to the set of 'safe' serializers. A client that successfully talks to your server will get responses using the same serializer as the one used to send requests to the server. If your server also uses Pyro client code/proxies, you might also need to select the serializer for these by setting the ``SERIALIZER`` config item. See the :doc:`/config` chapter for details about the config items. See :ref:`object-serialization` for more details about serialization, the new config items, and how to deal with existing code that relies on pickle. .. note:: Since Pyro 4.20 the default serializer is "``serpent``". It used to be "``pickle``" in older versions. The default set of accepted serializers in the server is the set of 'safe' serializers, so "``pickle``" and "``dill``" are not among the default. Other features ============== .. index:: attributes added to Pyro objects Attributes added to Pyro objects -------------------------------- The following attributes will be added to your object if you register it as a Pyro object: * ``_pyroId`` - the unique id of this object (a ``str``) * ``_pyroDaemon`` - a reference to the :py:class:`Pyro4.core.Daemon` object that contains this object Even though they start with an underscore (and are private, in a way), you can use them as you so desire. As long as you don't modify them! The daemon reference for instance is useful to register newly created objects with, to avoid the need of storing a global daemon object somewhere. These attributes will be removed again once you unregister the object. .. index:: network adapter binding, IP address, localhost, 127.0.0.1 Network adapter binding and localhost ------------------------------------- All Pyro daemons bind on localhost by default. This is because of security reasons. This means only processes on the same machine have access to your Pyro objects. If you want to make them available for remote machines, you'll have to tell Pyro on what network interface address it must bind the daemon. This also extends to the built in servers such as the name server. .. warning:: Read chapter :doc:`security` before exposing Pyro objects to remote machines! There are a few ways to tell Pyro what network address it needs to use. You can set a global config item ``HOST``, or pass a ``host`` parameter to the constructor of a Daemon, or use a command line argument if you're dealing with the name server. For more details, refer to the chapters in this manual about the relevant Pyro components. Pyro provides a couple of utility functions to help you with finding the appropriate IP address to bind your servers on if you want to make them publicly accessible: * :py:func:`Pyro4.socketutil.getIpAddress` * :py:func:`Pyro4.socketutil.getInterfaceAddress` Cleaning up / disconnecting stale client connections ---------------------------------------------------- A client proxy will keep a connection open even if it is rarely used. It's good practice for the clients to take this in consideration and release the proxy. But the server can't enforce this, some clients may keep a connection open for a long time. Unfortunately it's hard to tell when a client connection has become stale (unused). Pyro's default behavior is to accept this fact and not kill the connection. This does mean however that many stale client connections will eventually block the server's resources, for instance all workers threads in the threadpool server. There's a simple possible solution to this, which is to specify a communication timeout on your server. For more information about this, read :ref:`tipstricks_release_proxy`. .. index:: Daemon API Daemon Pyro interface --------------------- A rather interesting aspect of Pyro's Daemon is that it (partly) is a Pyro object itself. This means it exposes a couple of remote methods that you can also invoke yourself if you want. The object exposed is :class:`Pyro4.core.DaemonObject` (as you can see it is a bit limited still). You access this object by creating a proxy for the ``"Pyro.Daemon"`` object. That is a reserved object name. You can use it directly but it is preferable to use the constant ``Pyro4.constants.DAEMON_NAME``. An example follows that accesses the daemon object from a running name server:: >>> import Pyro4 >>> daemon=Pyro4.Proxy("PYRO:"+Pyro4.constants.DAEMON_NAME+"@localhost:9090") >>> daemon.ping() >>> daemon.registered() ['Pyro.NameServer', 'Pyro.Daemon'] Pyro4-4.82/docs/source/tipstricks.rst000066400000000000000000001350551416147301300176560ustar00rootroot00000000000000.. index:: Tips & trics .. _tipstricks: ************* Tips & Tricks ************* .. index:: Best practices Best practices ============== .. index:: circular topology Avoid using insecure features. ------------------------------ Avoid using the ``pickle`` (and ``dill``, and ``cloudpickle``) serializers, they will make your solution insecure. Avoid using Flame (it requires pickle, but has severe security implications by itself). .. note:: These features are not available in Pyro5 as well, so if you want your code to be easily portable to Pyro5 later, there's another reason to not use them. Make as little as possible remotely accessible. ----------------------------------------------- Avoid sticking a ``@expose`` on the whole class, and instead mark only those methods exposed that you really want to be remotely accessible. Alternatively, make sure your exposed Pyro server class only consists of methods that are okay to be accessed remotely. Avoid circular communication topologies. ---------------------------------------- When you can have a circular communication pattern in your system (A-->B-->C-->A) this can cause some problems: * when reusing a proxy it causes a deadlock because the proxy is already being used for an active remote call. See the :file:`deadlock` example. * with the multiplex servertype, the server itself may also block for all other remote calls because the handling of the first is not yet completed. Avoid circularity, or use *oneway* method calls on at least one of the links in the chain. Another possible way out of a lock situation is to set ``COMMTIMEOUT`` so that after a certain period in a locking situation the caller aborts with a TimeoutError, effectively breaking the deadlock. .. index:: releasing a proxy .. _tipstricks_release_proxy: 'After X simultaneous proxy connections, Pyro seems to freeze!' Fix: Release your proxies when you can. ------------------------------------------------------------------------------------------------------- A connected proxy that is unused takes up resources on the server. In the case of the threadpool server type, it locks up a single thread. If you have too many connected proxies at the same time, the server may run out of threads and won't be able to accept new connections. You can use the ``THREADPOOL_SIZE`` config item to increase the maximum number of threads that Pyro will use. Or use the multiplex server instead, which doesn't have this limitation. Another option is to set ``COMMTIMEOUT`` to a certain value *on your server*, which will free up unused connections after the given time. But your client code may now crash with a TimeoutError or ConnectionClosedError when it tries to use a proxy that worked earlier. You can use Pyro's autoreconnect feature to work around this but it makes the code more complex. It is however advised to close (release) proxies that your program no longer needs, to free resources both in the client and in the server. Don't worry about reconnecting, Pyro does that automatically for you once the proxy is used again. You can use explicit ``_pyroRelease`` calls or use the proxy from within a context manager. It's not a good idea to release it after every single remote method call though, because then the cost of reconnecting the socket can be bad for performance. .. index:: binary blob seealso: binary blob; binary data transfer Avoid large binary blobs over the wire. --------------------------------------- Pyro is not designed to efficiently transfer large amounts of binary data over the network. Try to find another protocol that better suits this requirement. Read :ref:`binarytransfer` for some more details about this. How to deal with Numpy data (large *or* small) is explained here :ref:`numpy`. Note that Pyro has a 2 gigabyte message size limitation at this time. .. index:: object graphs Minimize object graphs that travel over the wire. ------------------------------------------------- Pyro will serialize the whole object graph you're passing, even when only a tiny fraction of it is used on the receiving end. Be aware of this: it may be necessary to define special lightweight objects for your Pyro interfaces that hold the data you need, rather than passing a huge object structure. It's good design practice as well to have an "external API" that is different from your internal code, and tuned for minimal communication overhead or complexity. Consider using basic data types instead of custom classes. ---------------------------------------------------------- Because Pyro serializes the objects you're passing, it needs to know how to serialize custom types. While you can teach Pyro about these (see :ref:`customizing-serialization`) it may sometimes be easier to just use a builtin datatype instead. For instance if you have a custom class whose state essentially is a set of numbers, consider then that it may be easier to just transfer a ``set`` or a ``list`` of those numbers rather than an instance of your custom class. It depends on your class and data of course, and whether the receiving code expects just the list of numbers or really needs an instance of your custom class. .. index:: Logging .. _logging: Logging ======= If you configure it (see :ref:`config-items`) Pyro will write a bit of debug information, errors, and notifications to a log file. It uses Python's standard :py:mod:`logging` module for this (See https://docs.python.org/2/library/logging.html ). Once enabled, your own program code could use Pyro's logging setup as well. But if you want to configure your own logging, make sure you do that before any Pyro imports. Then Pyro will skip its own autoconfig. A little example to enable logging by setting the required environment variables from the shell:: $ export PYRO_LOGFILE=pyro.log $ export PYRO_LOGLEVEL=DEBUG $ python my_pyro_program.py Another way is by modifying ``os.environ`` from within your code itself, *before* any import of Pyro4 is done:: import os os.environ["PYRO_LOGFILE"] = "pyro.log" os.environ["PYRO_LOGLEVEL"] = "DEBUG" import Pyro4 # do stuff... Finally, it is possible to initialize the logging by means of the standard Python ``logging`` module only, but then you still have to tell Pyro4 what log level it should use (or it won't log anything):: import logging logging.basicConfig() # or your own sophisticated setup logging.getLogger("Pyro4").setLevel(logging.DEBUG) logging.getLogger("Pyro4.core").setLevel(logging.DEBUG) # ... set level of other logger names as desired ... import Pyro4 # do stuff... The various logger names are similar to the module that uses the logger, so for instance logging done by code in ``Pyro4.core`` will use a logger category name of ``Pyro4.core``. Look at the top of the source code of the various modules from Pyro to see what the exact names are. .. index:: multiple NICs, network interfaces Multiple network interfaces =========================== This is a difficult subject but here are a few short notes about it. *At this time, Pyro doesn't support running on multiple network interfaces at the same time*. You can bind a deamon on INADDR_ANY (0.0.0.0) though, including the name server. But weird things happen with the URIs of objects published through these servers, because they will point to 0.0.0.0 and your clients won't be able to connect to the actual objects. The name server however contains a little trick. The broadcast responder can also be bound on 0.0.0.0 and it will in fact try to determine the correct ip address of the interface that a client needs to use to contact the name server on. So while you cannot run Pyro daemons on 0.0.0.0 (to respond to requests from all possible interfaces), sometimes it is possible to run only the name server on 0.0.0.0. The success ratio of all this depends heavily on your network setup. .. index:: same Python version Same major Python version required when using pickle, cloudpickle, dill or marshal ================================================================================== When Pyro is configured to use pickle, cloudpickle, dill or marshal as its serialization format, it is required to have the same *major* Python versions on your clients and your servers. Otherwise the different parties cannot decipher each others serialized data. This means you cannot let Python 2.x talk to Python 3.x with Pyro when using these serializers. However it should be fine to have Python 3.5 talk to Python 3.6 for instance. It may still be required to specify the pickle or dill protocol version though, because that needs to be the same on both ends as well. For instance, Python 3.4 introduced version 4 of the pickle protocol and as such won't be able to talk to Python 3.3 which is stuck on version 3 pickle protocol. You'll have to tell the Python 3.4 side to step down to protocol 3. There is a config item for that. The same will apply for dill protocol versions. If you are using cloudpickle, you can just set the pickle protocol version (as pickle is used under the hood). The implementation independent serialization protocols serpent and json don't have these limitations. .. index:: wire protocol version .. _wireprotocol: Wire protocol version ===================== Here is a little tip to find out what wire protocol version a given Pyro server is using. This could be useful if you are getting ``ProtocolError: invalid data or unsupported protocol version`` or something like that. It also works with Pyro 3.x. **Server** This is a way to figure out the protocol version number a given Pyro server is using: by reading the first 6 bytes from the server socket connection. The Pyro daemon will respond with a 4-byte string "``PYRO``" followed by a 2-byte number that is the protocol version used:: $ nc | od -N 6 -t x1c 0000000 50 59 52 4f 00 05 P Y R O \0 005 This one is talking protocol version ``00 05`` (5). This low number means it is a Pyro 3.x server. When you try it on a Pyro 4 server:: $ nc | od -N 6 -t x1c 0000000 50 59 52 4f 00 2c P Y R O \0 , This one is talking protocol version ``00 2c`` (44). For Pyro4 the protocol version started at 40 for the first release and is now at 46 for the current release at the time of writing. **Client** To find out the protocol version that your client code is using, you can use this:: $ python -c "import Pyro4.constants as c; print(c.PROTOCOL_VERSION)" .. index:: asynchronous, futures .. _future-functions: Asynchronous ('future') normal function calls ============================================= Pyro provides an asynchronous proxy to call remote methods asynchronously, see :ref:`async-calls`. For normal Python code, Python provides a similar mechanism in the form of the :py:class:`Pyro4.futures.Future` class (also available as ``Pyro4.Future``). With a syntax that is slightly different from normal method calls, it provides the same asynchronous function calls as the asynchronous proxy has. Note that Python itself has a similar thing in the standard library since version 3.2, see http://docs.python.org/3/library/concurrent.futures.html#future-objects . However Pyro's Future object is available on older Python versions too. It works slightly differently and perhaps a little bit easier as well. You create a ``Future`` object for a callable that you want to execute in the background, and receive its results somewhere in the future:: def add(x,y): return x+y futurecall = Pyro4.Future(add) result = futurecall(4,5) # do some other stuff... then access the value summation = result.value Actually calling the `Future` object returns control immediately and results in a :py:class:`Pyro4.futures.FutureResult` object. This is the exact same class as with the asynchrnous proxy. The most important attributes are ``value``, ``ready`` and the ``wait`` method. See :ref:`async-calls` for more details. You can also chain multiple calls, so that the whole call chain is executed sequentially in the background. You can do this directly on the ``Future`` object, with the :py:meth:`Pyro4.futures.Future.then` method. It has the same signature as the ``then`` method from the ``FutureResult`` class:: futurecall = Pyro4.Future(something) \ .then(somethingelse, 44) \ .then(lastthing, optionalargument="something") There's also a :py:meth:`Pyro4.futures.Future.iferror` method that allows you to register a callback to be invoked when an exception occurs. This method also exists on the ``FutureResult`` class. See the :file:`futures` example for more details and example code. You can delay the execution of the future for a number of seconds via the :py:meth:`Pyro4.futures.Future.delay` method, and you can cancel it altogether via the :py:meth:`Pyro4.futures.Future.cancel` method (which only works if the future hasn't been evaluated yet). .. note:: Async proxies are no longer available in Pyro5, so if you want your code to be easily portable to Pyro5 later, it may be better to not use them. .. index:: DNS DNS setup ========= Pyro depends on a working DNS configuration, at least for your local hostname (i.e. 'pinging' your local hostname should work). If your local hostname doesn't resolve to an IP address, you'll have to fix this. This can usually be done by adding an entry to the hosts file. For OpenSUSE, you can also use Yast to fix it (go to Network Settings, enable "Assign hostname to loopback IP"). If Pyro detects a problem with the dns setup it will log a WARNING in the logfile (if logging is enabled), something like: ``weird DNS setup: your-computer-hostname resolves to localhost (127.x.x.x)`` .. index:: NAT, router, firewall .. _nat-router: Pyro behind a NAT router/firewall ================================= You can run Pyro behind a NAT router/firewall. Assume the external hostname is 'pyro.server.com' and the external port is 5555. Also assume the internal host is 'server1.lan' and the internal port is 9999. You'll need to have a NAT rule that maps pyro.server.com:5555 to server1.lan:9999. You'll need to start your Pyro daemon, where you specify the ``nathost`` and ``natport`` arguments, so that Pyro knows it needs to 'publish' URIs containing that *external* location instead of just using the internal addresses:: # running on server1.lan d = Pyro4.Daemon(port=9999, nathost="pyro.server.com", natport=5555) uri = d.register(Something, "thing") print(uri) # "PYRO:thing@pyro.server.com:5555" As you see, the URI now contains the external address. :py:meth:`Pyro4.core.Daemon.uriFor` by default returns URIs with a NAT address in it (if ``nathost`` and ``natport`` were used). You can override this by setting ``nat=False``:: # d = Pyro4.Daemon(...) print(d.uriFor("thing")) # "PYRO:thing@pyro.server.com:5555" print(d.uriFor("thing", nat=False)) # "PYRO:thing@localhost:36124" uri2 = d.uriFor(uri.object, nat=False) # get non-natted uri The Name server can also be started behind a NAT: it has a couple of command line options that allow you to specify a nathost and natport for it. See :ref:`nameserver-nameserver`. .. note:: The broadcast responder always returns the internal address, never the external NAT address. Also, the name server itself won't translate any URIs that are registered with it. So if you want it to publish URIs with 'external' locations in them, you have to tell the Daemon that registers these URIs to use the correct nathost and natport as well. .. note:: In some situations the NAT simply is configured to pass through any port one-to-one to another host behind the NAT router/firewall. Pyro facilitates this by allowing you to set the natport to 0, in which case Pyro will replace it by the internal port number. .. index:: failed to locate the nameserver, connection refused 'Failed to locate the nameserver' or 'Connection refused' error, what now? ========================================================================== Usually when you get an error like "failed to locate the name server" or "connection refused" it is because there is a configuration problem in your network setup, such as a firewall blocking certain network connections. Sometimes it can be because you configured Pyro wrong. A checklist to follow to diagnose your issue can be as follows: - is the name server on a network interface that is visible on the network? If it's on localhost, then it's definitely not! (check the URI) - is the Pyro object's daemon on a network interface that is visible on the network? If it's on localhost, then it's definitely not! (check the URI) - with what URI is the Pyro object registered in the Name server? See previous item. - can you ping the server from your client machine? - can you telnet to the given host+port from your client machine? - dealing with IPV4 versus IPV6: do both client and server use the same protocol? - is the server's ip address as shown one of an externally reachable network interface? - do you have your server behind a NAT router? See :ref:`nat-router`. - do you have a firewall or packetfilter running that prevents the connection? - do you have the same Pyro versions on both server and client? - what does the pyro logfiles tell you (enable it via the config items on both the server and the client, including the name server. See :ref:`logging`. - (if not using the default:) do you have a compatible serializer configuration? - (if not using the default:) do you have a symmetric hmac key configuration? - can you obtain a few bytes from the wire using netcat, see :ref:`wireprotocol`. .. index:: binary data transfer, file transfer .. _binarytransfer: Binary data transfer / file transfer ==================================== .. sidebar:: ...if you do want to use Pyro for this... At the end of this paragraph, a few alternative approaches of reasonably efficient binary data transfer are presented, where (almost) all of the code still uses just Pyro's high level abstractions. Pyro is not meant to transfer large amounts of binary data (images, sound files, video clips): the protocol is not designed nor optimized for these kinds of data. The occasional transmission of such data is fine (:doc:`flame` even provides a convenience method for that, if you like: :meth:`Pyro4.utils.flame.Flame.sendfile`) but if you're dealing with a lot of them or with big files, it is usually better to use something else to do the actual data transfer (file share+file copy, ftp, http, scp, rsync). Also, Pyro has a 2 gigabyte message size limitation at this time (if your Python implementation and system memory even allow the process to reach this size). You can avoid this problem if you use the remote iterator feature (return chunks via an iterator or generator function and consume them on demand in your client). .. note:: Serpent and binary data: If you do transfer binary data using the serpent serializer, you have to be aware of the following. The wire protocol is text based so serpent has to encode any binary data. It uses base-64 to do that. This means on the receiving side, instead of the raw bytes, you get a little dictionary like this instead: ``{'data': 'aXJtZW4gZGUgam9uZw==', 'encoding': 'base64'}`` Your client code needs to be aware of this and to get the original binary data back, it has to base-64 decode the data element by itself. This is perhaps done the easiest by using the ``serpent.tobytes`` helper function from the ``serpent`` library, which will convert the result to actual bytes if needed (and leave it untouched if it is already in bytes form) The following table is an indication of the relative speeds when dealing with large amounts of binary data. It lists the results of the :file:`hugetransfer` example, using python 3.5, over a 1000 Mbps LAN connection: ========== ========== ============= ================ ==================== serializer str mb/sec bytes mb/sec bytearray mb/sec bytearray w/iterator ========== ========== ============= ================ ==================== pickle 77.8 79.6 69.9 35.0 marshal 71.0 73.0 73.0 37.8 serpent 25.0 14.1 13.5 13.5 json 31.5 not supported not supported not supported ========== ========== ============= ================ ==================== The json serializer only works with strings, it can't serialize binary data at all. The serpent serializer can, but read the note above about why it's quite inefficent there. Marshal and pickle are relatively efficient, speed-wise. But beware, when using ``pickle``, there's quite a difference in dealing with various types: **pickle datatype differences** ``str`` *Python 2.x:* efficient; directly encoded as a byte sequence, because that's what it is. *Python 3.x:* inefficient; encoded in UTF-8 on the wire, because it is a unicode string. ``bytes`` *Python 2.x:* same as ``str`` (Python 2.7) *Python 3.x:* efficient; directly encoded as a byte sequence. ``bytearray`` Inefficient; encoded as UTF-8 on the wire (pickle does this in both Python 2.x and 3.x) ``array("B")`` (array of unsigned ints of size 1) *Python 2.x:* very inefficient; every element is encoded as a separate token+value. *Python 3.x:* efficient; uses machine type encoding on the wire (a byte sequence). ``numpy arrays`` usually cannot be transferred directly, see :ref:`numpy`. **Alternative: avoid most of the serialization overhead by (ab)using annotations** Pyro allows you to add custom annotation chunks to the request and response messages (see :ref:`msg_annotations`). Because these are binary chunks they will not be passed through the serializer at all. There is a 64Kb total annotation size limit on messages though, so you have to split up larger files. The ``filetransfer`` example contains fully working example code to see this in action. It combines this with the remote iterator capability of Pyro to easily get all chunks of the file. It has to split up the file in small chunks but is still quite a bit faster than transmitting bytes through regular response values. Also it is using only regular Pyro high level logic and no low level network or socket code. **Alternative: integrating raw socket transfer in a Pyro server** It is possible to get data transfer speeds that are close to the limit of your network adapter by doing the actual data transfer via low-level socket code and everything else via Pyro. This keeps the amount of low-level code to a minimum. Have a look at the ``filetransfer`` example again, to see a possible way of doing this. It creates a special Daemon subclass that uses Pyro for everything as usual, but for actual file transfer it sets up a dedicated temporary socket connection over which the file data is transmitted. .. index:: MSG_WAITALL MSG_WAITALL socket option ========================= Pyro will use the ``MSG_WAITALL`` socket option to receive large messages, if it decides that the feature is available and working correctly. This avoids having to use a slower function that needs a loop to get all data. On most systems that define the ``socket.MSG_WAITALL`` symbol, it works fine, except on Windows: even though the option is there, it doesn't work reliably. Pyro thus won't use it by default on Windows, and will use it by default on other systems. You should set the ``USE_MSG_WAITALL`` config item to False yourself, if you find that your system has an unreliable implementation of this socket option. Please let me know what system (os/python version) it is so we could teach Pyro to select the correct option automatically in a new version. .. index:: IPv6 IPV6 support ============ Pyro4 supports IPv6 since version 4.18. You can use IPv6 addresses in the same places where you would normally have used IPv4 addresses. There's one exception: the address notation in a Pyro URI. For a numeric IPv6 address in a Pyro URI, you have to enclose it in brackets. For example: ``PYRO:objectname@[::1]:3456`` points at a Pyro object located on the IPv6 "::1" address (localhost). When Pyro displays a numeric IPv6 location from an URI it will also use the bracket notation. This bracket notation is only used in Pyro URIs, everywhere else you just type the IPv6 address without brackets. To tell Pyro to prefer using IPv6 you can use the ``PREFER_IP_VERSION`` config item. It is set to 4 by default, for backward compatibility reasons. This means that unless you change it to 6 (or 0), Pyro will be using IPv4 addressing. There is a new method to see what IP addressing is used: :py:meth:`Pyro4.socketutil.getIpVersion`, and a few other methods in :py:mod:`Pyro4.socketutil` gained a new optional argument to tell it if it needs to deal with an ipv6 address rather than ipv4, but these are rarely used in client code. .. index:: Numpy, numpy.ndarray .. _numpy: Pyro and Numpy ============== Pyro doesn't support Numpy out of the box. You'll see certain errors occur when trying to use numpy objects (ndarrays, etcetera) with Pyro:: TypeError: array([1, 2, 3]) is not JSON serializable or TypeError: don't know how to serialize class or TypeError: don't know how to serialize class These errors are caused by Numpy datatypes not being serializable by serpent or json serializers. There are several reasons these datatypes are not supported out of the box: #. numpy is a third party library and there are many, many others. It is not Pyro's responsibility to understand all of them. #. numpy is often used in scenarios with large amounts of data. Sending these large arrays over the wire through Pyro is often not the best solution. It is not useful to provide transparent support for numpy types when you'll be running into trouble often such as slow calls and large network overhead. #. Pyrolite (:doc:`pyrolite`) would have to get numpy support as well and that is a lot of work (because every numpy type would require a mapping to the appropriate Java or .NET type) If you understand this but still want to use numpy with Pyro, and pass numpy objects over the wire, you can do it! Choose one of the following options: #. Don't use Numpy datatypes as arguments or return values. Convert them to standard Python datatypes before using them in Pyro. So instead of just ``na = numpy.array(...); return na;``, use this instead: ``return na.tolist()``. Or perhaps even ``return array.array('i', na)`` (serpent understands ``array.array`` just fine). Note that the elements of a numpy array usually are of a special numpy datatype as well (such as ``numpy.int32``). If you don't convert these individually as well, you will still get serialization errors. That is why something like ``list(na)`` doesn't work: it seems to return a regular python list but the elements are still numpy datatypes. You have to use the full conversions as mentioned earlier. Note that you'll have to do a bit more work to deal with multi-dimensional arrays: you have to convert the shape of the array separately. #. If possible don't return the whole array. Redesign your API so that you might perhaps only return a single element from it, or a few, if that is all the client really needs. #. Tell Pyro to use :py:mod:`pickle`, :py:mod:`cloudpickle` or :py:mod:`dill` as serializer. These serializers *can* deal with numpy datatypes out of the box. However they have security implications. See :doc:`security`. (If you choose to use them anyway, also be aware that you must tell your name server about it as well, see :ref:`nameserver-pickle`) .. index:: double: HTTP gateway server; command line .. _http-gateway: Pyro via HTTP and JSON ====================== .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. Pyro provides a HTTP gateway server that translates HTTP requests into Pyro calls. It responds with JSON messages. This allows clients (including web browsers) to use a simple http interface to call Pyro objects. Pyro's JSON serialization format is used so the gateway simply passes the JSON response messages back to the caller. It also provides a simple web page that shows how stuff works. *Starting the gateway:* You can launch the HTTP gateway server via the command line tool. This will create a web server using Python's :py:mod:`wsgiref` server module. Because the gateway is written as a wsgi app, you can also stick it into a wsgi server of your own choice. Import ``pyro_app`` from ``Pyro4.utils.httpgateway`` to do that (that's the app you need to use). synopsys: :command:`python -m Pyro4.utils.httpgateway [options]` (or simply: :command:`pyro4-httpgateway [options]`) A short explanation of the available options can be printed with the help option: .. program:: Pyro4.utils.httpgateway .. option:: -h, --help Print a short help message and exit. Most other options should be self explanatory; you can set the listening host and portname etc. An important option is the exposed names regex option: this controls what objects are accessible from the http gateway interface. It defaults to something that won't just expose every internal object in your system. If you want to toy a bit with the examples provided in the gateway's web page, you'll have to change the option to something like: ``r'Pyro\.|test\.'`` so that those objects are exposed. This regex is the same as used when listing objects from the name server, so you can use the ``nsc`` tool to check it (with the listmatching command). *Setting Hmac keys for use by the gateway:* The ``-k`` and/or ``-g`` command line options to set the optional Hmac keys are deprecated since Pyro 4.72 because setting a hmac key like this is a security issue. You should set these keys with the PYRO_HMAC_KEY and PYRO_HTTPGATEWAY_KEY environment variables instead, before starting the gateway. *Using the gateway:* You request the url ``http://localhost:8080/pyro/<>/<>`` to invoke a method on the object with the given name (yes, every call goes through a naming server lookup). Parameters are passed via a regular query string parameter list (in case of a GET request) or via form post parameters (in case of a POST request). The response is a JSON document. In case of an exception, a JSON encoded exception object is returned. You can easily call this from your web page scripts using ``XMLHttpRequest`` or something like JQuery's ``$.ajax()``. Have a look at the page source of the gateway's web page to see how this could be done. Note that you have to comply with the browser's same-origin policy: if you want to allow your own scripts to access the gateway, you'll have to make sure they are loaded from the same website. The http gateway server is *stateless* at the moment. This means every call you do will end be processed by a new Pyro proxy in the gateway server. This is not impacting your client code though, because every call that it does is also just a stateless http call. It only impacts performance: doing large amounts of calls through the http gateway will perform much slower as the same calls processed by a native Pyro proxy (which you can instruct to operate in batch mode as well). However because Pyro is quite efficient, a call through the gateway is still processed in just a few milliseconds, naming lookup and json serialization all included. Special http request headers: - ``X-Pyro-Options``: add this header to the request to set certain pyro options for the call. Possible values (comma-separated): - ``oneway``: force the Pyro call to be a oneway call and return immediately. The gateway server still returns a 200 OK http response as usual, but the response data is empty. This option is to override the semantics for non-oneway method calls if you so desire. - ``X-Pyro-Gateway-Key``: add this header to the request to set the http gateway key. You can also set it on the request with a ``$key=....`` querystring parameter. Special Http response headers: - ``X-Pyro-Correlation-Id``: contains the correlation id Guid that was used for this request/response. Http response status codes: - 200 OK: all went well, response is the Pyro response message in JSON serialized format - 403 Forbidden: you're trying to access an object that is not exposed by configuration - 404 Not Found: you're requesting a non existing object - 500 Internal server error: something went wrong during request processing, response is serialized exception object (if available) Look at the :file:`http` example for working code how you could set this up. .. index:: current_context, correlation_id .. _current_context: Client information on the current_context, correlation id ========================================================= .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. Pyro provides a *thread-local* object with some information about the current Pyro method call, such as the client that's performing the call. It is available as :py:data:`Pyro4.current_context` (shortcut to :py:data:`Pyro4.core.current_context`). When accessed in a Pyro server it contains various attributes: .. py:attribute:: Pyro4.current_context.client (:py:class:`Pyro4.socketutil.SocketConnection`) this is the socket connection with the client that's doing the request. You can check the source to see what this is all about, but perhaps the single most useful attribute exposed here is ``sock``, which is the socket connection. So the client's IP address can for instance be obtained via :code:`Pyro4.current_context.client.sock.getpeername()[0]` . However, since for oneway calls the socket connection will likely be closed already, this is not 100% reliable. Therefore Pyro stores the result of the ``getpeername`` call in a separate attribute on the context: ``client_sock_addr`` (see below) .. py:attribute:: Pyro4.current_context.client_sock_addr (*tuple*) the socket address of the client doing the call. It is a tuple of the client host address and the port. .. py:attribute:: Pyro4.current_context.seq (*int*) request sequence number .. py:attribute:: Pyro4.current_context.msg_flags (*int*) message flags, see :py:class:`Pyro4.message.Message` .. py:attribute:: Pyro4.current_context.serializer_id (*int*) numerical id of the serializer used for this communication, see :py:class:`Pyro4.message.Message` . .. py:attribute:: Pyro4.current_context.annotations (*dict*) message annotations, key is a 4-letter string and the value is a byte sequence. Used to send and receive annotations with Pyro requests. See :ref:`msg_annotations` for more information about that. .. py:attribute:: Pyro4.current_context.response_annotations (*dict*) message annotations, key is a 4-letter string and the value is a byte sequence. Used in client code, the annotations returned by a Pyro server are available here. See :ref:`msg_annotations` for more information about that. .. py:attribute:: Pyro4.current_context.correlation_id (:py:class:`uuid.UUID`, optional) correlation id of the current request / response. If you set this (in your client code) before calling a method on a Pyro proxy, Pyro will transfer the correlation id to the server context. If the server on their behalf invokes another Pyro method, the same correlation id will be passed along. This way it is possible to relate all remote method calls that originate from a single call. To make this work you'll have to set this to a new :py:class:`uuid.UUID` in your client code right before you call a Pyro method. Note that it is required that the correlation id is of type :py:class:`uuid.UUID`. Note that the HTTP gateway (see :ref:`http-gateway`) also creates a correlation id for every request, and will return it via the ``X-Pyro-Correlation-Id`` HTTP-header in the response. It will also accept this header optionally on a request in which case it will use the value from the header rather than generating a new id. For an example of how this information can be retrieved, and how to set the ``correlation_id``, see the :file:`callcontext` example. See the :file:`usersession` example to learn how you could use it to build user-bound resource access without concurrency problems. .. index:: resource-tracking .. _resource_tracking: Automatically freeing resources when client connection gets closed ================================================================== .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. A client can call remote methods that allocate stuff in the server. Normally the client is responsible to call other methods once the resources should be freed. However if the client forgets this or the connection to the server is forcefully closed before the client can free the resources, the resources in the server will usually not be freed anymore. You may be able to solve this in your server code yourself (perhaps using some form of keepalive/timeout mechanism) but Pyro 4.63 and newer provides a built-in mechanism that can help: resource tracking on the client connection. Your server will register the resources when they are allocated, thereby making them tracked resources on the client connection. These tracked resources will be automatically freed by Pyro if the client connection is closed. For this to work, the resource object should have a ``close`` method (Pyro will call this). If needed, you can also override :py:meth:`Pyro4.core.Daemon.clientDisconnect` and do the cleanup yourself with the ``tracked_resources`` on the connection object. Resource tracking and untracking is done in your server class on the ``Pyro4.current_context`` object: .. py:method:: Pyro4.current_context.track_resource(resource) Let Pyro track the resource on the current client connection. .. py:method:: Pyro4.current_context.untrack_resource(resource) Untrack a previously tracked resource, useful if you have freed it normally. See the ``resourcetracking`` example for working code utilizing this. .. note:: The order in which the resources are freed is arbitrary. Also, if the resource can be garbage collected normally by Python, it is removed from the tracked resources. So the ``close`` method should not be the only way to properly free such resources (maybe you need a ``__del__`` as well). .. index:: annotations .. _msg_annotations: Message annotations =================== .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. Pyro's wire protocol allows for a very flexible messaging format by means of *annotations*. Annotations are extra information chunks that are added to the pyro messages traveling over the network. Pyro internally uses a couple of chunks to exchange extra data between a proxy and a daemon: correlation ids (annotation ``CORR``) and hmac signatures (annotation ``HMAC``). These chunk types are reserved and you should not touch them. All other annotation types are free to use in your own code (and will be ignored by Pyro itself). There's no limit on the number of annotations you can add to a message, but each individual annotation cannot be larger than 64 Kb. .. sidebar:: reserved annotation chunks The following annotation chunks are used by Pyro internally and should not be touched or used: ``CORR``, ``HMAC``, ``STRM`` and ``BLBI``. An annotation is a low level datastructure (to optimize the generation of network messages): a chunk identifier string of exactly 4 characters (such as "CODE"), and its value, a byte sequence. If you want to put specific data structures into an annotation chunk value, you have to encode them to a byte sequence yourself (of course, you could utilize a Pyro serializer for this). When processing a custom annotation, you have to decode it yourself as well. Communicating annotations with Pyro is done via a normal dictionary of chunk id -> data bytes. Pyro will take care of encoding this dictionary into the wire message and extracting it out of a response message. *Custom user annotations:* You can add your own annotations to messages. For server code, you do this by setting the ``response_annotations`` property of the :py:data:`Pyro4.current_context` in your Pyro object, right before returning the regular response value. Pyro will add the annotations dict to the response message. In client code, you can set the ``annotations`` property of the :py:data:`Pyro4.current_context` object right before the proxy method call. Pyro will then add that annotations dict to the request message. The older method to to this (before Pyro 4.56) was to create a subclass of ``Proxy`` or ``Daemon`` and override the methods :py:meth:`Pyro4.core.Proxy._pyroAnnotations` or :py:meth:`Pyro4.core.Daemon.annotations` respectively. These methods should return the custom annotations dict that should be added to request/response messages. This is still possible to not break older code. *Reacting on annotations:* In your server code, in the Daemon, you can use the :py:data:`Pyro4.current_context` to access the ``annotations`` of the last message that was received. In your client code, you can do that as well, but you should look at the ``response_annotations`` of this context object instead. If you're using large annotation chunks, it is advised to clear these fields after use. See :ref:`current_context`. The older method to do this (before Pyro 4.56) for client code was to create a proxy subclass and override the method :py:meth:`Pyro4.core.Proxy._pyroResponseAnnotations`. Pyro calls this method with the dictionary of any annotations received in a response message from the daemon, and the message type identifier of the response message. This still works to not break older code. For an example of how you can work with custom message annotations, see the :py:mod:`callcontext` example. .. index:: handshake .. _conn_handshake: Connection handshake ==================== .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. When a proxy is first connecting to a Pyro daemon, it exchanges a few messages to set up and validate the connection. This is called the connection *handshake*. Part of it is the daemon returning the object's metadata (see :ref:`metadata`). You can hook into this mechanism and influence the data that is initially exchanged during the connection setup, and you can act on this data. You can disallow the connection based on this, for example. You can set your own data on the proxy attribute :py:attr:`Pyro4.core.Proxy._pyroHandshake`. You can set any serializable object. Pyro will send this as the handshake message to the daemon when the proxy tries to connect. In the daemon, override the method :py:meth:`Pyro4.core.Daemon.validateHandshake` to customize/validate the connection setup. This method receives the data from the proxy and you can either raise an exception if you don't want to allow the connection, or return a result value if you are okay with the new connection. The result value again can be any serializable object. This result value will be received back in the Proxy where you can act on it if you subclass the proxy and override :py:meth:`Pyro4.core.Proxy._pyroValidateHandshake`. For an example of how you can work with connections handshake validation, see the :py:mod:`handshake` example. It implements a (bad!) security mechanism that requires the client to supply a "secret" password to be able to connect to the daemon. .. index:: dispatcher, gateway Efficient dispatchers or gateways that don't de/reserialize messages ==================================================================== .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. Imagine you're designing a setup where a Pyro call is essentially dispatched or forwarded to another server. The dispatcher (sometimes also called gateway) does nothing else than deciding who the message is for, and then forwarding the Pyro call to the actual object that performs the operation. This can be built easily with Pyro by 'intercepting' the call in a dispatcher object, and performing the remote method call *again* on the actual server object. There's nothing wrong with this except for perhaps two things: #. Pyro will deserialize and reserialize the remote method call parameters on every hop, this can be quite inefficient if you're dealing with many calls or large argument data structures. #. The dispatcher object is now dependent on the method call argument data types, because Pyro has to be able to de/reserialize them. This often means the dispatcher also needs to have access to the same source code files that define the argument data types, that the client and server use. As long as the dispatcher itself *doesn't have to know what is even in the actual message*, Pyro provides a way to avoid both issues mentioned above: use the :py:class:`Pyro4.core.SerializedBlob`. If you use that as the (single) argument to a remote method call, Pyro will not deserialize the message payload *until you ask for it* by calling the ``deserialized()`` method on it. Which is something you only do in the actual server object, and not in the dispatcher. Because the message is then never de/reserialized in the dispatcher code, you avoid the serializer overhead, and also don't have to include the source code for the serialized types in the dispatcher. It just deals with a blob of serialized bytes. An example that shows how this mechanism can be used, can be found as ``blob-dispatch`` in the examples folder. .. index:: socketpair, user provided sockets Hooking onto existing connected sockets such as from socketpair() ================================================================= For communication between threads or sub-processes, there is ``socket.socketpair()``. It creates spair of connected sockets that you can share between the threads or processes. Since Pyro 4.70 it is possible to tell Pyro to use a user-created socket like that, instead of creating new sockets itself, which means you can use Pyro to talk between threads or sub-processes over an efficient and isolated channel. You do this by creating a socket (or a pair) and providing it as the ``connected_socket`` parameter to the ``Daemon`` and ``Proxy`` classes. For the Daemon, don't pass any other arguments because they won't be used anyway. For the Proxy, set only the first parameter (``uri``) to just the *name* of the object in the daemon you want to connect to. So don't use a PYRO or PYRONAME prefix for the uri in this case. Closing the proxy or the daemon will *not* close the underlying user-supplied socket so you can use it again for another proxy (to access a different object). You created the socket(s) yourself, and you also have to close the socket(s) yourself. Also because the socketpair is internal to the process that created it, it's safe to use the pickle serializer on this connection. This can improve communication performance even further. See the ``socketpair`` example for two example programs (one using threads, the other using fork to create a child process). Pyro4-4.82/docs/source/tutorials.rst000066400000000000000000001111051416147301300174730ustar00rootroot00000000000000.. include:: .. index:: tutorial ******** Tutorial ******** This tutorial will explain a couple of basic Pyro concepts, a little bit about the name server, and you'll learn to write a simple Pyro application. You'll do this by writing a warehouse system and a stock market simulator, that demonstrate some key Pyro techniques. Warm-up ======= Before proceeding, you should install Pyro if you haven't done so. For instructions about that, see :doc:`install`. In this tutorial, you will use Pyro's default configuration settings, so once Pyro is installed, you're all set! All you need is a text editor and a couple of console windows. During the tutorial, you are supposed to run everything on a single machine. This avoids initial networking complexity. .. note:: For security reasons, Pyro runs stuff on localhost by default. If you want to access things from different machines, you'll have to tell Pyro to do that explicitly. At the end is a small section :ref:`not-localhost` that tells you how you can run the various components on different machines. .. note:: The code of the two tutorial 'projects' is included in the Pyro source archive. Just installing Pyro won't provide this. If you don't want to type all the code, you should extract the Pyro source archive (:file:`Pyro4-X.Y.tar.gz`) somewhere. You will then have an :file:`examples` directory that contains a truckload of examples, including the two tutorial projects we will be creating later in this tutorial, :file:`warehouse` and :file:`stockquotes`. (There is more in there as well: the :file:`tests` directory contains the test suite with all the unittests for Pyro's code base.) .. index:: double: tutorial; concepts and tools Pyro concepts and tools ======================= Pyro enables code to call methods on objects even if that object is running on a remote machine:: +----------+ +----------+ | server A | | server B | | | < network > | | | Python | | Python | | OBJECT ----------foo.invoke()--------> OBJECT | | | | foo | +----------+ +----------+ Pyro is mainly used as a library in your code but it also has several supporting command line tools [#commandline]_. We won't explain every one of them here as you will only need the "name server" for this tutorial. .. [#commandline] Actually there are no scripts or command files included with Pyro right now. The :ref:`command-line` are invoked by starting their package directly using the :kbd:`-m` argument of the Python interpreter. .. _keyconcepts: Key concepts ^^^^^^^^^^^^ Here are a couple of key concepts you encounter when using Pyro: Proxy A proxy is a substitute object for "the real thing". It intercepts the method calls you would normally do on an object as if it was the actual object. Pyro then performs some magic to transfer the call to the computer that contains the *real* object, where the actual method call is done, and the results are returned to the caller. This means the calling code doesn't have to know if it's dealing with a normal or a remote object, because the code is identical. The class implementing Pyro proxies is ``Pyro4.Proxy`` (shortcut for :class:`Pyro4.core.Proxy`) :abbr:`URI (Unique resource identifier)` This is what Pyro uses to identify every object. (similar to what a web page URL is to point to the different documents on the web). Its string form is like this: "PYRO:" + object name + "@" + server name + port number. There are a few other forms it can take as well. You can write the protocol in lowercase too if you want ("pyro:") but it will automatically be converted to uppercase internally. The class implementing Pyro uris is ``Pyro4.URI`` (shortcut for :class:`Pyro4.core.URI`) Pyro object This is a normal Python object but it is registered with Pyro so that you can access it remotely. Pyro objects are written just as any other object but the fact that Pyro knows something about them makes them special, in the way that you can call methods on them from other programs. A class can also be a Pyro object, but then you will also have to tell Pyro about how it should create actual objects from that class when handling remote calls. Pyro daemon (server) This is the part of Pyro that listens for remote method calls, dispatches them to the appropriate actual objects, and returns the results to the caller. All Pyro objects are registered in one or more daemons. Pyro name server The name server is a utility that provides a phone book for Pyro applications: you use it to look up a "number" by a "name". The name in Pyro's case is the logical name of a remote object. The number is the exact location where Pyro can contact the object. Usually there is just *one* name server running in your network. Serialization This is the process of transforming objects into streams of bytes that can be transported over the network. The receiver deserializes them back into actual objects. Pyro needs to do this with all the data that is passed as arguments to remote method calls, and their response data. Not all objects can be serialized, so it is possible that passing a certain object to Pyro won't work even though a normal method call would accept it just fine. Configuration Pyro can be configured in a lot of ways. Using environment variables (they're prefixed with ``PYRO_``) or by setting config items in your code. See the configuration chapter for more details. The default configuration should be ok for most situations though, so you many never have to touch any of these options at all! Starting a name server ^^^^^^^^^^^^^^^^^^^^^^ While the use of the Pyro name server is optional, we will use it in this tutorial. It also shows a few basic Pyro concepts, so let us begin by explaining a little about it. Open a console window and execute the following command to start a name server: :command:`python -m Pyro4.naming` (or simply: :command:`pyro4-ns`) The name server will start and it prints something like:: Not starting broadcast server for localhost. NS running on localhost:9090 (127.0.0.1) URI = PYRO:Pyro.NameServer@localhost:9090 .. sidebar:: Localhost By default, Pyro uses *localhost* to run stuff on, so you can't by mistake expose your system to the outside world. You'll need to tell Pyro explicitly to use something else than *localhost*. But it is fine for the tutorial, so we leave it as it is. The name server has started and is listening on *localhost port 9090*. It also printed an :abbr:`URI (unique resource identifier)`. Remember that this is what Pyro uses to identify every object. The name server can be stopped with a :kbd:`control-c`, or on Windows, with :kbd:`ctrl-break`. But let it run in the background for the rest of this tutorial. Interacting with the name server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There's another command line tool that let you interact with the name server: "nsc" (name server control tool). You can use it, amongst other things, to see what all known registered objects in the naming server are. Let's do that right now. Type: :command:`python -m Pyro4.nsc list` (or simply: :command:`pyro4-nsc list`) and it will print something like this:: --------START LIST Pyro.NameServer --> PYRO:Pyro.NameServer@localhost:9090 --------END LIST The only object that is currently registered, is the name server itself! (Yes, the name server is a Pyro object itself. Pyro and the "nsc" tool are using Pyro to talk to it). .. note:: As you can see, the name ``Pyro.NameServer`` is registered to point to the URI that we saw earlier. This is mainly for completeness sake, and is not often used, because there are different ways to get to talk to the name server (see below). .. sidebar:: The NameServer object The name server itself is a normal Pyro object which means the 'nsc' tool, and any other code that talks to it, is just using normal Pyro methods. The only "trickery" that makes it a bit different from other Pyro servers is perhaps the broadcast responder, and the two command line tools to interact with it (``Pyro4.naming`` and ``Pyro4.nsc``) This is cool, but there's a little detail left unexplained: *How did the nsc tool know where the name server was?* Pyro has a couple of tactics to locate a name server. The nsc tool uses them too: Pyro uses a network broadcast to see if there's a name server available somewhere (the name server contains a broadcast responder that will respond "Yeah hi I'm here"). So in many cases you won't have to configure anything to be able to discover the name server. If nobody answers though, Pyro tries the configured default or custom location. If still nobody answers it prints a sad message and exits. However if it found the name server, it is then possible to talk to it and get the location of any other registered object. This means that you won't have to hard code any object locations in your code, and that the code is capable of dynamically discovering everything at runtime. *But enough of that.* We need to start looking at how to actually write some code ourselves that uses Pyro! .. index:: double: tutorial; warehouse example Building a Warehouse ==================== .. hint:: All code of this part of the tutorial can be found in the :file:`examples/warehouse` directory. You'll build a simple warehouse that stores items, and that everyone can visit. Visitors can store items and retrieve other items from the warehouse (if they've been stored there). In this tutorial you'll first write a normal Python program that more or less implements the complete warehouse system, but in vanilla Python code. After that you'll add Pyro support to it, to make it a distributed warehouse system, where you can visit the central warehouse from many different computers. phase 1: a simple prototype ^^^^^^^^^^^^^^^^^^^^^^^^^^^ To start with, write the vanilla Python code for the warehouse and its visitors. This prototype is fully working but everything is running in a single process. It contains no Pyro code at all, but shows what the system is going to look like later on. The ``Warehouse`` object simply stores an array of items which we can query, and allows for a person to take an item or to store an item. Here is the code (:file:`warehouse.py`):: from __future__ import print_function class Warehouse(object): def __init__(self): self.contents = ["chair", "bike", "flashlight", "laptop", "couch"] def list_contents(self): return self.contents def take(self, name, item): self.contents.remove(item) print("{0} took the {1}.".format(name, item)) def store(self, name, item): self.contents.append(item) print("{0} stored the {1}.".format(name, item)) Then there is a ``Person`` that can visit the warehouse. The person has a name and deposit and retrieve actions on a particular warehouse. Here is the code (:file:`person.py`):: from __future__ import print_function import sys if sys.version_info < (3, 0): input = raw_input class Person(object): def __init__(self, name): self.name = name def visit(self, warehouse): print("This is {0}.".format(self.name)) self.deposit(warehouse) self.retrieve(warehouse) print("Thank you, come again!") def deposit(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type a thing you want to store (or empty): ").strip() if item: warehouse.store(self.name, item) def retrieve(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type something you want to take (or empty): ").strip() if item: warehouse.take(self.name, item) Finally you need a small script that actually runs the code. It creates the warehouse and two visitors, and makes the visitors perform their actions in the warehouse. Here is the code (:file:`visit.py`):: # This is the code that runs this example. from warehouse import Warehouse from person import Person warehouse = Warehouse() janet = Person("Janet") henry = Person("Henry") janet.visit(warehouse) henry.visit(warehouse) Run this simple program. It will output something like this:: $ python visit.py This is Janet. The warehouse contains: ['chair', 'bike', 'flashlight', 'laptop', 'couch'] Type a thing you want to store (or empty): television # typed in Janet stored the television. The warehouse contains: ['chair', 'bike', 'flashlight', 'laptop', 'couch', 'television'] Type something you want to take (or empty): couch # <-- typed in Janet took the couch. Thank you, come again! This is Henry. The warehouse contains: ['chair', 'bike', 'flashlight', 'laptop', 'television'] Type a thing you want to store (or empty): bricks # <-- typed in Henry stored the bricks. The warehouse contains: ['chair', 'bike', 'flashlight', 'laptop', 'television', 'bricks'] Type something you want to take (or empty): bike # <-- typed in Henry took the bike. Thank you, come again! phase 2: first Pyro version ^^^^^^^^^^^^^^^^^^^^^^^^^^^ That wasn't very exciting but you now have working code for the basics of the warehouse system. Now you'll use Pyro to turn the warehouse into a standalone component, that people from other computers can visit. You'll need to add a couple of lines to the :file:`warehouse.py` file so that it will start a Pyro server for the warehouse object. You can do this by registering your Pyro class with a 'Pyro daemon', the server that listens for and processes incoming remote method calls. One way to do that is like this (you can ignore the details about this for now):: Pyro4.Daemon.serveSimple( { Warehouse: "example.warehouse" }, ns = False) Next, we have to tell Pyro what parts of the class should be remotely accessible, and what parts aren't supposed to be accessible. This has to do with security. We'll be adding a ``@Pyro4.expose`` decorator on the Warehouse class definition to tell Pyro it is allowed to access the class remotely. You can ignore the ``@Pyro4.behavior`` line we also added for now (but it is required to properly have a persistent warehouse inventory). Finally we add a little ``main`` function so it will be started correctly, which should make the code now look like this (:file:`warehouse.py`):: from __future__ import print_function import Pyro4 @Pyro4.expose @Pyro4.behavior(instance_mode="single") class Warehouse(object): def __init__(self): self.contents = ["chair", "bike", "flashlight", "laptop", "couch"] def list_contents(self): return self.contents def take(self, name, item): self.contents.remove(item) print("{0} took the {1}.".format(name, item)) def store(self, name, item): self.contents.append(item) print("{0} stored the {1}.".format(name, item)) def main(): Pyro4.Daemon.serveSimple( { Warehouse: "example.warehouse" }, ns = False) if __name__=="__main__": main() Start the warehouse in a new console window, it will print something like this:: $ python warehouse.py Object <__main__.Warehouse object at 0x025F4FF0>: uri = PYRO:example.warehouse@localhost:51279 Pyro daemon running. It will become clear what you need to do with this output in a second. You now need to slightly change the :file:`visit.py` script that runs the thing. Instead of creating a warehouse directly and letting the persons visit that, it is going to use Pyro to connect to the stand alone warehouse object that you started above. It needs to know the location of the warehouse object before it can connect to it. This is the **uri** that is printed by the warehouse program above (``PYRO:example.warehouse@localhost:51279``). You'll need to ask the user to enter that uri string into the program, and use Pyro to create a `proxy` to the remote object:: uri = input("Enter the uri of the warehouse: ").strip() warehouse = Pyro4.Proxy(uri) That is all you need to change. Pyro will transparently forward the calls you make on the warehouse object to the remote object, and return the results to your code. So the code will now look like this (:file:`visit.py`):: # This is the code that visits the warehouse. import sys import Pyro4 from person import Person if sys.version_info<(3,0): input = raw_input uri = input("Enter the uri of the warehouse: ").strip() warehouse = Pyro4.Proxy(uri) janet = Person("Janet") henry = Person("Henry") janet.visit(warehouse) henry.visit(warehouse) Notice that the code of ``Warehouse`` and ``Person`` classes didn't change *at all*. Run the program. It will output something like this:: $ python visit.py Enter the uri of the warehouse: PYRO:example.warehouse@localhost:51279 # copied from warehouse output This is Janet. The warehouse contains: ['chair', 'bike', 'flashlight', 'laptop', 'couch'] Type a thing you want to store (or empty): television # typed in The warehouse contains: ['chair', 'bike', 'flashlight', 'laptop', 'couch', 'television'] Type something you want to take (or empty): couch # <-- typed in Thank you, come again! This is Henry. The warehouse contains: ['chair', 'bike', 'flashlight', 'laptop', 'television'] Type a thing you want to store (or empty): bricks # <-- typed in The warehouse contains: ['chair', 'bike', 'flashlight', 'laptop', 'television', 'bricks'] Type something you want to take (or empty): bike # <-- typed in Thank you, come again! And notice that in the other console window, where the warehouse server is running, the following is printed:: Janet stored the television. Janet took the couch. Henry stored the bricks. Henry took the bike. phase 3: final Pyro version ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The code from the previous phase works fine and could be considered to be the final program, but is a bit cumbersome because you need to copy-paste the warehouse URI all the time to be able to use it. You will simplify it a bit in this phase by using the Pyro name server. Also, you will use the Pyro excepthook to print a nicer exception message if anything goes wrong (by taking something from the warehouse that is not present! Try that now with the code from phase 2. You will get a ``ValueError: list.remove(x): x not in list`` but with a not so useful stack trace). .. Note:: Once again you can leave code of the ``Warehouse`` and ``Person`` classes **unchanged**. As you can see, Pyro is not getting in your way at all here. You can often use it with only adding a couple of lines to your existing code. Okay, stop the warehouse program from phase 2 if it is still running, and check if the name server that you started in `Starting a name server`_ is still running in its own console window. In :file:`warehouse.py` locate the statement ``Pyro4.Daemon.serveSimple(...`` and change the ``ns = False`` argument to ``ns = True``. This tells Pyro to use a name server to register the objects in. (The ``Pyro4.Daemon.serveSimple`` is a very easy way to start a Pyro server but it provides very little control. Look here :ref:`server-servesimple` for some more details, and you will learn about another way of starting a server in `Building a Stock market simulator`_). In :file:`visit.py` remove the input statement that asks for the warehouse uri, and change the way the warehouse proxy is created. Because you are now using a name server you can ask Pyro to locate the warehouse object automatically:: warehouse = Pyro4.Proxy("PYRONAME:example.warehouse") Finally, install the ``Pyro4.util.excepthook`` as excepthook. You'll soon see what this does to the exceptions and stack traces your program produces when something goes wrong with a Pyro object. So the code should look something like this (:file:`visit.py`):: # This is the code that visits the warehouse. import sys import Pyro4 import Pyro4.util from person import Person sys.excepthook = Pyro4.util.excepthook warehouse = Pyro4.Proxy("PYRONAME:example.warehouse") janet = Person("Janet") henry = Person("Henry") janet.visit(warehouse) henry.visit(warehouse) Start the warehouse program again in a separate console window. It will print something like this:: $ python warehouse.py Object <__main__.Warehouse object at 0x02496050>: uri = PYRO:obj_426e82eea7534fb5bc78df0b5c0b6a04@localhost:51294 name = example.warehouse Pyro daemon running. As you can see the uri is different this time, it now contains some random id code instead of a name. However it also printed an object name. This is the name that is now used in the name server for your warehouse object. Check this with the 'nsc' tool: :command:`python -m Pyro4.nsc list` (or simply: :command:`pyro4-nsc list`), which will print something like:: --------START LIST Pyro.NameServer --> PYRO:Pyro.NameServer@localhost:9090 example.warehouse --> PYRO:obj_426e82eea7534fb5bc78df0b5c0b6a04@localhost:51294 --------END LIST This means you can now refer to that warehouse object using the name ``example.warehouse`` and Pyro will locate the correct object for you automatically. This is what you changed in the :file:`visit.py` code so run that now to see that it indeed works! **Remote exception:** You also installed Pyro's custom excepthook so try that out. Run the :file:`visit.py` script and try to take something from the warehouse that is not present (for instance, batteries):: Type something you want to take (or empty): batteries Traceback (most recent call last): File "visit.py", line 12, in janet.visit(warehouse) File "d:\PROJECTS\Pyro4\examples\warehouse\phase3\person.py", line 14, in visit self.retrieve(warehouse) File "d:\PROJECTS\Pyro4\examples\warehouse\phase3\person.py", line 25, in retrieve warehouse.take(self.name, item) File "d:\PROJECTS\Pyro4\src\Pyro4\core.py", line 161, in __call__ return self.__send(self.__name, args, kwargs) File "d:\PROJECTS\Pyro4\src\Pyro4\core.py", line 314, in _pyroInvoke raise data ValueError: list.remove(x): x not in list +--- This exception occured remotely (Pyro) - Remote traceback: | Traceback (most recent call last): | File "d:\PROJECTS\Pyro4\src\Pyro4\core.py", line 824, in handleRequest | data=method(*vargs, **kwargs) # this is the actual method call to the Pyro object | File "warehouse.py", line 14, in take | self.contents.remove(item) | ValueError: list.remove(x): x not in list +--- End of remote traceback What you can see now is that you not only get the usual exception traceback, *but also the exception that occurred in the remote warehouse object on the server* (the "remote traceback"). This can greatly help locating problems! As you can see it contains the source code lines from the warehouse code that is running in the server, as opposed to the normal local traceback that only shows the remote method call taking place inside Pyro. .. index:: double: tutorial; stock market example Building a Stock market simulator ================================= .. hint:: All of the code of this part of the tutorial can be found in the :file:`examples/stockquotes` directory. .. sidebar:: simplified example The tutorial here is a simplified version of a stock quote simulation example. There's a more elaborate example available called :file:`examples/stockquotes-old` (it used to be this example in older versions of the documentation) We'll build a simple stock quote system. The idea is that we have multiple stock markets producing stock symbol quotes. There are viewers that aggregate and filter all stock quotes from the markets and display those from the companies we are interested in. ============= ====== ======= ====== Stockmarket 1 |rarr| Viewer Stockmarket 2 |rarr| |rarr| Viewer Stockmarket 3 |rarr| Viewer ... ... ============= ====== ======= ====== phase 1: simple prototype ^^^^^^^^^^^^^^^^^^^^^^^^^ Again, like the previous application (the warehouse), you first create a working version of the system by only using normal Python code. This simple prototype will be functional but everything will be running in a single process. It contains no Pyro code at all, but shows what the system is going to look like later on. First create a file :file:`stockmarket.py` that will simulate a stock market that is producing stock quotes for registered companies. For simplicity we will use a generator function that produces individual random stock quotes. The code is as follows:: # stockmarket.py import random import time class StockMarket(object): def __init__(self, marketname, symbols): self.name = marketname self.symbols = symbols def quotes(self): while True: symbol = random.choice(self.symbols) yield symbol, round(random.uniform(5, 150), 2) time.sleep(random.random()/2.0) For the actual viewer application we create a new file :file:`viewer.py` that iterates over the symbols produced by various stock markets. It prints the symbols from the companies we're interested in:: # viewer.py from __future__ import print_function from stockmarket import StockMarket class Viewer(object): def __init__(self): self.markets = set() self.symbols = set() def start(self): print("Shown quotes:", self.symbols) quote_sources = { market.name: market.quotes() for market in self.markets } while True: for market, quote_source in quote_sources.items(): quote = next(quote_source) # get a new stock quote from the source symbol, value = quote if symbol in self.symbols: print("{0}.{1}: {2}".format(market, symbol, value)) def main(): nasdaq = StockMarket("NASDAQ", ["AAPL", "CSCO", "MSFT", "GOOG"]) newyork = StockMarket("NYSE", ["IBM", "HPQ", "BP"]) viewer = Viewer() viewer.markets = {nasdaq, newyork} viewer.symbols = {"IBM", "AAPL", "MSFT"} viewer.start() if __name__ == "__main__": main() If you run this file :file:`viewer.py` it will print a stream of stock symbol quote updates that are being generated by the two stock markets (but only the few symbols that the viewer wants to see):: $ python viewer.py Shown quotes: {'MSFT', 'IBM', 'AAPL'} NYSE.IBM: 19.59 NASDAQ.MSFT: 25.06 NYSE.IBM: 89.54 NYSE.IBM: 44.08 NASDAQ.MSFT: 9.73 NYSE.IBM: 80.57 .... phase 2: Pyro version ^^^^^^^^^^^^^^^^^^^^^ Now you use Pyro to make the various components fully distributed. Pyro is used to make them talk to each other. The actual code for each component class hasn't really changed since phase 1, it is just the plumbing that you need to write to glue them together. Pyro is making this a matter of just a few lines of code that is Pyro-specific, the rest of the code is needed anyway to start up and configure the system. To be able to see the final result, the code is listed once more with comments on what changed with respect to the version in phase 1. stockmarket ----------- The :file:`stockmarket.py` is changed slightly. You have to add the ``@Pyro4.expose`` decorator on the methods (or class) that must be accessible remotely. Also, to access the ``name`` and ``symbols`` attributes of the class you have to turn them into real Python properties. Finally there is now a bit of startup logic to create some stock markets and make them available as Pyro objects. Notice that we gave each market their own defined name, this will be used in the viewer application later. For sake of example we are not using the ``serveSimple`` method here to publish our objects via Pyro. Rather, the daemon and name server are accessed by our own code. Notice that to ensure tidy cleanup of connectoin resources, they are both used as context managers in a ``with`` statement. Also notice that we can leave the generator function in the stockmarket class as-is; since version 4.49 Pyro is able to turn it into a remote generator without your client program ever noticing. The complete code for the Pyro version of :file:`stockmarket.py` is as follows:: # stockmarket.py from __future__ import print_function import random import time import Pyro4 @Pyro4.expose class StockMarket(object): def __init__(self, marketname, symbols): self._name = marketname self._symbols = symbols def quotes(self): while True: symbol = random.choice(self.symbols) yield symbol, round(random.uniform(5, 150), 2) time.sleep(random.random()/2.0) @property def name(self): return self._name @property def symbols(self): return self._symbols if __name__ == "__main__": nasdaq = StockMarket("NASDAQ", ["AAPL", "CSCO", "MSFT", "GOOG"]) newyork = StockMarket("NYSE", ["IBM", "HPQ", "BP"]) # for example purposes we will access the daemon and name server ourselves and not use serveSimple with Pyro4.Daemon() as daemon: nasdaq_uri = daemon.register(nasdaq) newyork_uri = daemon.register(newyork) with Pyro4.locateNS() as ns: ns.register("example.stockmarket.nasdaq", nasdaq_uri) ns.register("example.stockmarket.newyork", newyork_uri) print("Stockmarkets available.") daemon.requestLoop() viewer ------ You don't need to change the actual code in the ``Viewer``, other than how to tell it what stock market objects it should use. Rather than hard coding the fixed set of stockmarket names, it is more flexible to utilize Pyro's name server and ask that to return all stock markets it knows about. The ``Viewer`` class itself remains unchanged:: # viewer.py from __future__ import print_function import Pyro4 class Viewer(object): def __init__(self): self.markets = set() self.symbols = set() def start(self): print("Shown quotes:", self.symbols) quote_sources = { market.name: market.quotes() for market in self.markets } while True: for market, quote_source in quote_sources.items(): quote = next(quote_source) # get a new stock quote from the source symbol, value = quote if symbol in self.symbols: print("{0}.{1}: {2}".format(market, symbol, value)) def find_stockmarkets(): # You can hardcode the stockmarket names for nasdaq and newyork, but it # is more flexible if we just look for every available stockmarket. markets = [] with Pyro4.locateNS() as ns: for market, market_uri in ns.list(prefix="example.stockmarket.").items(): print("found market", market) markets.append(Pyro4.Proxy(market_uri)) if not markets: raise ValueError("no markets found! (have you started the stock markets first?)") return markets def main(): viewer = Viewer() viewer.markets = find_stockmarkets() viewer.symbols = {"IBM", "AAPL", "MSFT"} viewer.start() if __name__ == "__main__": main() running the program ------------------- To run the final stock quote system you need to do the following: - open a new console window and start the Pyro name server (:command:`python -m Pyro4.naming`, or simply: :command:`pyro4-ns`). - open another console window and start the stock market server - open another console window and start the viewer The stock market program doesn't print much by itself but it sends stock quotes to the viewer, which prints them:: $ python viewer.py found market example.stockmarket.newyork found market example.stockmarket.nasdaq Shown quotes: {'AAPL', 'IBM', 'MSFT'} NASDAQ.AAPL: 82.58 NYSE.IBM: 85.22 NYSE.IBM: 124.68 NASDAQ.AAPL: 88.55 NYSE.IBM: 40.97 NASDAQ.MSFT: 38.83 ... If you're interested to see what the name server now contains, type :command:`python -m Pyro4.nsc list` (or simply: :command:`pyro4-nsc list`):: $ pyro4-nsc list --------START LIST Pyro.NameServer --> PYRO:Pyro.NameServer@localhost:9090 metadata: ['class:Pyro4.naming.NameServer'] example.stockmarket.nasdaq --> PYRO:obj_3896de2eb38b4bed9d12ba91703539a4@localhost:51479 example.stockmarket.newyork --> PYRO:obj_1ab1a322e5c14f9e984a0065cd080f56@localhost:51479 --------END LIST .. index:: double: tutorial; running on different machines .. _not-localhost: phase 3: running it on different machines ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Before presenting the changes in phase 3, let's introduce some additional notions when working with Pyro. It's important for you to understand that, for security reasons, Pyro runs stuff on localhost by default. If you want to access things from different machines, you'll have to tell Pyro to do that explicitly. Here we show you how you can do this: Let's assume that you want to start the *name server* in such a way that it is accessible from other machines. To do that, type in the console one of two options (with an appropriate -n argument): $ python -m Pyro4.naming -n your_hostname # i.e. your_hostname = "192.168.1.99" or simply: $ pyro4-ns -n your_hostname If you want to implement this concept on the *warehouse server*, you'll have to modify :file:`warehouse.py`. Then, right before the ``serveSimple`` call, you have to tell it to bind the daemon on your hostname instead of localhost. One way to do this is by setting the ``HOST`` config item:: Pyro4.config.HOST = "your_hostname_here" Pyro4.Daemon.serveSimple(...) Optionally, you can choose to leave the code alone, and instead set the ``PYRO_HOST`` environment variable before starting the warehouse server. Another choice is to pass the required host (and perhaps even port) arguments to ``serveSimple``:: Pyro4.Daemon.serveSimple( { Warehouse: "example.warehouse" }, host = 'your_hostname_here', ns = True) Remember that if you want more details, refer to the chapters in this manual about the relevant Pyro components. Now, back on the new version of the *stock market server*, notice that this example already creates a daemon object instead of using the :py:meth:`serveSimple` call. You'll have to modify :file:`stockmarket.py` because that is the one creating a daemon. But you'll only have to add the proper ``host``and ``port`` arguments to the construction of the Daemon, to set it to your machine name instead of the default of localhost. Let's see the few minor changes that are required in the code: ... HOST_IP = "192.168.1.99" HOST_PORT = 9092 ... with Pyro4.Daemon(host=HOST_IP, port=HOST_PORT) as daemon: ... Of course, you could also change the ``HOST`` config item (either in the code itself, or by setting the ``PYRO_HOST`` environment variable before launching). Other means of creating connections =================================== In both tutorials above we used the Name Server for easy object lookup. The use of the name server is optional, see :ref:`name-server` for details. There are various other options for connecting your client code to your Pyro objects, have a look at the client code details: :ref:`object-discovery` and the server code details: :ref:`publish-objects`. Ok, what's next? ================ *Congratulations!* You completed the Pyro tutorials in which you built a simple warehouse storage system, and a stock market simulation system consisting of various independent components that talk to each other using Pyro. The Pyro distribution archive contains a truckload of example programs with short descriptions that you could study to see how to use the various features that Pyro has to offer. Or just browse the manual for more detailed information. Happy remote object programming! Pyro4-4.82/examples/000077500000000000000000000000001416147301300143025ustar00rootroot00000000000000Pyro4-4.82/examples/async/000077500000000000000000000000001416147301300154175ustar00rootroot00000000000000Pyro4-4.82/examples/async/Readme.txt000066400000000000000000000016101416147301300173530ustar00rootroot00000000000000This is an example that shows the asynchronous method call support. The server has a single method that has an artificial delay of three seconds before it returns the result of the computation. The client shows how you might use Pyro's asynchronous feature to run the remote method call in the background and deal with the results later (when they are available). client_batch.py shows how to do asynchronous batched calls. Notice how this is different from oneway batched calls because we will get results this time (just somewhere in the future). Oneway calls never return a result. client_callchain.py shows the 'call chain' feature, where you can chain one or more asynchronous function calls to be performed as soon as the asynchronous call result became available. You can chain normal functions but also more pyro calls. The result of the previous call is passed to the next call as argument. Pyro4-4.82/examples/async/client.py000066400000000000000000000043631416147301300172550ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input uri = input("enter async server object uri: ").strip() proxy = Pyro4.Proxy(uri) print("* normal call: (notice the delay)") print("result=", proxy.divide(100, 5)) print("\n* async call:") proxy._pyroAsync() asyncresult = proxy.divide(100, 5) # returns immediately print("result value available?", asyncresult.ready) # prints False because the server is still 'busy' print("client can do other stuff here.") print("getting result value...(will block until available)") print("resultvalue=", asyncresult.value) # blocks until the result is available print("\n* async call, with normal call inbetween:") normalproxy = Pyro4.Proxy(uri) asyncresult = proxy.divide(100, 5) # returns immediately print("client does normal call: ", normalproxy.multiply(5, 20)) print("client does normal call: ", normalproxy.multiply(5, 30)) print("getting result value of async call...(will block until available)") print("resultvalue=", asyncresult.value) # blocks until the result is available print("\n* async call with exception:") asyncresult = proxy.divide(100, 0) # will trigger a zero division error, 100//0 print("getting result value...") try: value = asyncresult.value print("Weird, this shouldn't succeed!?... resultvalue=", value) except ZeroDivisionError as x: print("got exception (expected):", repr(x)) print("\n* async call with timeout:") asyncresult = proxy.divide(100, 5) print("checking if ready within 2 seconds...") ready = asyncresult.wait(2) # wait for ready within 2 seconds but the server takes 3 print("status after waiting=", ready) # should print False print("checking again if ready within 5 seconds...(should be ok now)") ready = asyncresult.wait(timeout=5) # wait 5 seconds now (but server will be done within 1 more second) print("status after waiting=", ready) print("available=", asyncresult.ready) print("resultvalue=", asyncresult.value) print("\n* a few async calls at the same time:") results = [ proxy.divide(100, 7), proxy.divide(100, 6), proxy.divide(100, 5), proxy.divide(100, 4), proxy.divide(100, 3), ] print("getting values...") for result in results: print("result=", result.value) print("\ndone.") Pyro4-4.82/examples/async/client_batch.py000066400000000000000000000051001416147301300204040ustar00rootroot00000000000000from __future__ import print_function import sys import time import Pyro4 if sys.version_info < (3, 0): input = raw_input def asyncFunction(values): results = [value + 1 for value in values] print(">>> async batch function called, returning:", results) return results uri = input("enter async server object uri: ").strip() proxy = Pyro4.Proxy(uri) print("\n* batch async call:") batch = Pyro4.batch(proxy) batch.divide(100, 5) batch.divide(99, 9) batch.divide(555, 2) print("getting results...") asyncresults = batch(asynchronous=True) # returns immediately print("result value available?", asyncresults.ready) # prints False because the server is still 'busy' print("client can do other stuff here.") time.sleep(2) print("such as sleeping ;-)") time.sleep(2) print("sleeping some more, batch takes a while") time.sleep(2) print("getting result values...(will block until available)") results = asyncresults.value # blocks until the result is available print("resultvalues=", list(results)) print("\n* batch async call with chained function:") batch = Pyro4.batch(proxy) batch.divide(100, 5) batch.divide(99, 9) batch.divide(555, 2) asyncresults = batch(asynchronous=True) # returns immediately asyncresults.then(asyncFunction) \ .then(asyncFunction) \ .then(asyncFunction) print("getting result values...(will block until available)") print("final value=", asyncresults.value) print("\n* batch async call with exception:") batch = Pyro4.batch(proxy) batch.divide(1, 1) # first call is ok batch.divide(100, 0) # second call will trigger a zero division error, 100//0 asyncresults = batch(asynchronous=True) # returns immediately print("getting result values...") try: value = asyncresults.value print("Weird, this shouldn't succeed!?... resultvalues=", list(value)) except ZeroDivisionError as x: print("got exception (expected):", repr(x)) print("\n* batch async call with timeout:") batch = Pyro4.batch(proxy) batch.divide(100, 5) batch.divide(99, 9) batch.divide(555, 2) asyncresults = batch(asynchronous=True) # returns immediately print("checking if ready within 2 seconds...") ready = asyncresults.wait(2) # wait for ready within 2 seconds but the server takes 3 print("status after wait=", ready) # should print False print("checking again if ready within 10 seconds...(should be ok now)") ready = asyncresults.wait(timeout=10) # wait 10 seconds now (but server will be done within ~8 more seconds) print("status after wait=", ready) print("available=", asyncresults.ready) results = asyncresults.value print("resultvalues=", list(results)) print("\ndone.") Pyro4-4.82/examples/async/client_callchain.py000066400000000000000000000070241416147301300212500ustar00rootroot00000000000000from __future__ import print_function import sys import time import Pyro4 if sys.version_info < (3, 0): input = raw_input def asyncFunction(value, increase=1): print(">>> async function called with value={0} increase={1}".format(value, increase)) return value + increase def asyncWithMoreArgs(a, b, extra=None): print(">>> async func called with some arguments: a={0}, b={1}, extra={2}".format(a, b, extra)) return a + b uri = input("enter async server object uri: ").strip() proxy = Pyro4.Proxy(uri) print("\n* async call with error:") proxy._pyroAsync() def resulthandler(result): print("RESULT: ", result) def errorhandler(error): print("ERRORHANDLER: ", error) asyncresult = proxy.divide(100, 0).iferror(errorhandler).then(resulthandler) time.sleep(4) print("^^^^ above an error message should be printed out...") print("\n* async call with error, but without errorhandler:") asyncresult = proxy.divide(100, 0).then(resulthandler) time.sleep(4) print("^^^^ above, no error message should be printed out...") try: _ = asyncresult.value print("SHOULD RAISE ERROR INSTEAD!!") except ZeroDivisionError as x: print("expected error occurred when trying to obtain the result:", x) print("\n* async call with call chain:") asyncresult = proxy.divide(100, 5) # set a chain of callables to be invoked once the value is available asyncresult.then(asyncFunction) \ .then(asyncFunction) \ .then(asyncFunction) print("sleeping 4 seconds") time.sleep(4) # the call chain will be invoked during this sleep period print("back from sleep") # you can still access the final asyncresult.value. It should be 100/5+3=23 print("final value=", asyncresult.value) assert asyncresult.value == 23 print("\n* async call with call chain that is set 'too late':") asyncresult = proxy.divide(100, 5) # set a chain of callables to be invoked once the value is available # but we set it 'too late' (when the result is already available) time.sleep(4) # result will appear in 3 seconds asyncresult.then(asyncFunction) \ .then(asyncFunction) \ .then(asyncFunction) # let's see what the result value is, should be 100/5+3=23 print("final value=", asyncresult.value) assert asyncresult.value == 23 print("\n* async call with call chain, where calls have extra arguments:") asyncresult = proxy.multiply(5, 6) # set a chain of callables to be invoked once the value is available # the callable will be invoked like so: function(asyncvalue, normalarg, kwarg=..., kwarg=...) # (the value from the previous call is passed as the first argument to the next call) asyncresult.then(asyncWithMoreArgs, 1) \ .then(asyncWithMoreArgs, 2, extra=42) \ .then(asyncWithMoreArgs, 3, extra="last one") print("sleeping 1 second") time.sleep(1) # the call chain will be invoked during this sleep period print("back from sleep") # you can still access the final asyncresult.value. It should be 5*6+1+2+3=36 print("final value=", asyncresult.value) assert asyncresult.value == 36 print("\n* async call with call chain, where calls are new non-async pyro calls:") normalproxy = Pyro4.Proxy(uri) asyncresult = proxy.divide(100, 5) # set a chain of callables to be invoked once the value is available # the callable will be invoked like so: function(asyncvalue, kwarg=..., kwarg=...) asyncresult.then(normalproxy.add, increase=1) \ .then(normalproxy.add, increase=2) \ .then(normalproxy.add, increase=3) print("getting result value (will block until available)") print("final value=", asyncresult.value) assert asyncresult.value == 26 # 100/5+1+2+3=26 print("\ndone.") Pyro4-4.82/examples/async/server.py000066400000000000000000000010651416147301300173010ustar00rootroot00000000000000from __future__ import print_function import time import Pyro4 @Pyro4.expose class Thingy(object): def divide(self, a, b): print("dividing {0} by {1} after a slight delay".format(a, b)) time.sleep(3) return a // b def multiply(self, a, b): print("multiply {0} by {1}, no delay".format(a, b)) return a * b def add(self, value, increase): print("adding {1} to {0}, no delay".format(value, increase)) return value + increase Pyro4.Daemon.serveSimple({ Thingy: "example.async" }, ns=False) Pyro4-4.82/examples/attributes/000077500000000000000000000000001416147301300164705ustar00rootroot00000000000000Pyro4-4.82/examples/attributes/Readme.txt000066400000000000000000000000771416147301300204320ustar00rootroot00000000000000This is an example that shows direct remote attribute access. Pyro4-4.82/examples/attributes/client.py000066400000000000000000000061611416147301300203240ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input uri = input("enter attribute server object uri: ").strip() with Pyro4.Proxy(uri) as p: # Direct remote attribute access. print("\nDirect attribute access:") print("p.prop_value=", p.prop_value) print("adding 500 to p.prop_value") p.prop_value += 500 print("p.prop_value=", p.prop_value) print("actual remote value: ", p.getValue(), " (via p.getValue() remote method call)") if p.prop_value != p.getValue(): # they differ!? (should not happen) print("Remote value is different! The p.prop_value attribute must be a local one (not remote), this should not happen! (because metadata is enabled here)") print() # dunder names print("calling p.__dunder__()....: ", p.__dunder__()) with Pyro4.Proxy(uri) as p: # unexposed attributes print("\nAccessing unexposed attributes:") try: print("accessing p.sub...") _ = p.sub raise RuntimeError("this should not be possible!") # because p.sub is not an exposed property except AttributeError as x: print("ok, got expected error:", x) try: print("accessing p.value...") _ = p.value raise RuntimeError("this should not be possible!") # because p.value is not an exposed property except AttributeError as x: print("ok, got expected error:", x) try: print("accessing p._value...") _ = p._value raise RuntimeError("this should not be possible!") # because p._value is private except AttributeError as x: print("ok, got expected error:", x) try: print("accessing p.__value...") _ = p.__value raise RuntimeError("this should not be possible!") # because p.__value is private except AttributeError as x: print("ok, got expected error:", x) with Pyro4.Proxy(uri) as p: # Dotted name traversal is not supported by Pyro because that is a security vulnerability. # What will happen instead is that the first part of the name will be evaluated and returned, # and that the rest of the expression will be evaluated on the local object instead of # directly on the remote one. print("\nTrying dotted name traversal:") value = p.prop_sub print("value gotten from p.prop_sub=", value) print("\nTrying to update the dictionary directly on the remote object...") p.prop_sub.update({"test": "nope"}) # this will only update the local copy! new_value = p.prop_sub print("value gotten from p.prop_sub=", new_value, " (should be unchanged!)") assert new_value == value, "update should not have been done remotely" try: print("\nTrying longer dotted name: p.prop_sub.foobar.attribute") _ = p.prop_sub.foobar.attribute raise RuntimeError("this should not be possible!") except Exception as x: remote_tb = getattr(x, "_pyroTraceback", None) if remote_tb: raise RuntimeError("We got a remote traceback but this should have been a local one only") print("ok, got expected error (local only):", x) Pyro4-4.82/examples/attributes/server.py000066400000000000000000000012531416147301300203510ustar00rootroot00000000000000from __future__ import print_function import Pyro4 something = "Something" @Pyro4.expose class Thingy(object): def __init__(self): self.sub = {"name": "value"} self.value = 42 self._value = 123 self.__value = 999 def __dunder__(self): return "yep" def __len__(self): return 200 def getValue(self): return self.value @property def prop_value(self): return self.value @prop_value.setter def prop_value(self, value): self.value = value @property def prop_sub(self): return self.sub Pyro4.Daemon.serveSimple({ Thingy: "example.attributes" }, ns=False) Pyro4-4.82/examples/autoproxy/000077500000000000000000000000001416147301300163545ustar00rootroot00000000000000Pyro4-4.82/examples/autoproxy/Readme.txt000066400000000000000000000013031416147301300203070ustar00rootroot00000000000000This is an example that shows the autoproxy feature. Pyro will automatically return a Proxy instead of the object itself, if you are passing a Pyro object over a remote call. This means you can easily create new objects in a server and return them from remote calls, without the need to manually wrap them in a proxy. This behavior is enabled by default. It is different from older Pyro releases, so there is a config item AUTOPROXY that you can set to False if you want the old behaviour. You can try it with this example too, set the environment variable PYRO_AUTOPROXY to false and restart the server to see what the effect is. Note that when using the marshal serializer, this feature will not work. Pyro4-4.82/examples/autoproxy/client.py000066400000000000000000000010051416147301300202000ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input uri = input("enter factory server object uri: ").strip() factory = Pyro4.Proxy(uri) # create several things. print("Creating things.") thing1 = factory.createSomething(1) thing2 = factory.createSomething(2) thing3 = factory.createSomething(3) # interact with them on the server. print("Speaking stuff.") thing1.speak("I am the first") thing2.speak("I am second") thing3.speak("I am last then...") Pyro4-4.82/examples/autoproxy/server.py000066400000000000000000000012301416147301300202300ustar00rootroot00000000000000from __future__ import print_function import Pyro4 from thingy import Thingy @Pyro4.expose class Factory(object): def createSomething(self, number): # create a new item thing = Thingy(number) # connect it to the Pyro daemon to make it a Pyro object self._pyroDaemon.register(thing) # Return it. Pyro's autoproxy feature turns it into a proxy automatically. # If that feature is disabled, the object itself (a copy) is returned, # and the client won't be able to interact with the actual Pyro object here. return thing Pyro4.Daemon.serveSimple({ Factory: "example.autoproxy" }, ns=False) Pyro4-4.82/examples/autoproxy/thingy.py000066400000000000000000000003221416147301300202250ustar00rootroot00000000000000import Pyro4 @Pyro4.expose class Thingy(object): def __init__(self, number): self.number = number def speak(self, message): print("Thingy {0} says: {1}".format(self.number, message)) Pyro4-4.82/examples/autoreconnect/000077500000000000000000000000001416147301300171535ustar00rootroot00000000000000Pyro4-4.82/examples/autoreconnect/Readme.txt000066400000000000000000000036021416147301300211120ustar00rootroot00000000000000This is an example that shows the auto reconnect feature, from a client's perspective. Start the server and the client. You can stop the server while it's running. The client will report that the connection is lost, and that it is trying to rebind. Start the server again. You'll see that the client continues. There are 2 examples: - reconnect using NS (clientNS/serverNS) - reconnect using PYRO (client/server) NOTES: 1- Your server has to be prepared for this feature. It must not rely on any transient internal state to function correctly, because that state is lost when your server is restarted. You could make the state persistent on disk and read it back in at restart. 2- By default Pyro starts its daemons on a random port. If you want to support autoreconnection, you will need to restart your daemon on the port it used before. Easiest is to pick a fixed port. 3- If you rely on PYRO-uri's: then your server MUST register the objects with their old objectId to the daemon. Otherwise the client will try to access an unknown object Id. (this is not needed when you are ONLY using PYRONAME-uris, where a new lookup to the name server is performed instead) 4- If the NS loses its registrations, you're out of luck. Clients that rely on name server based reconnects will fail. 5- The client is reponsible for detecting a network problem itself. It must also explicitly call the reconnect method on the object. 6- Why isn't this automagic? Because you need to have control about it when a network problem occurs. Furthermore, only you can decide if your system needs this feature, and if it can support it (see points above). 7- Read the source files for info on what is going on. 8- Also see the 'disconnects' example for another swing at dealing with client timeouts/disconnects, and how a special proxy class can make it easier to deal with for the clients. Pyro4-4.82/examples/autoreconnect/client.py000066400000000000000000000013141416147301300210020ustar00rootroot00000000000000from __future__ import print_function import time import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input print("Autoreconnect using PYRO uri.") # We create a proxy with a PYRO uri. # Reconnect logic depends on the server now. # (it needs to restart the object with the same id) uri = input("Enter the uri that the server printed:").strip() obj = Pyro4.core.Proxy(uri) while True: print("call...") try: obj.method(42) print("Sleeping 1 second") time.sleep(1) except Pyro4.errors.ConnectionClosedError: # or possibly CommunicationError print("Connection lost. REBINDING...") print("(restart the server now)") obj._pyroReconnect() Pyro4-4.82/examples/autoreconnect/clientNS.py000066400000000000000000000011561416147301300212470ustar00rootroot00000000000000from __future__ import print_function import time import Pyro4 print("Autoreconnect using Name Server.") # We create a proxy with a PYRONAME uri. # That allows Pyro to look up the object again in the NS when # it needs to reconnect later. obj = Pyro4.core.Proxy("PYRONAME:example.autoreconnect") while True: print("call...") try: obj.method(42) print("Sleeping 1 second") time.sleep(1) except Pyro4.errors.ConnectionClosedError: # or possibly CommunicationError print("Connection lost. REBINDING...") print("(restart the server now)") obj._pyroReconnect() Pyro4-4.82/examples/autoreconnect/server.py000066400000000000000000000020601416147301300210310ustar00rootroot00000000000000from __future__ import print_function import time import Pyro4 print("Autoreconnect using PYRO uri.") @Pyro4.expose class TestClass(object): def method(self, arg): print("Method called with %s" % arg) print("You can now try to stop this server with ctrl-C/ctrl-Break") time.sleep(1) # We are responsible to (re)connect objects with the same object Id, # so that the client can reuse its PYRO-uri directly to reconnect. # There are a few options, such as depending on the Name server to # maintain a name registration for our object (see the serverNS for this). # Or we could store our objects in our own persistent database. # But for this example we will just use a pre-generated id (fixed name). # The other thing is that your Daemon must re-bind on the same port. # By default Pyro will select a random port so we specify a fixed port. with Pyro4.core.Daemon(port=7777) as daemon: uri = daemon.register(TestClass, objectId="example.autoreconnect_fixed_objectid") print("Server started, uri: %s" % uri) daemon.requestLoop() Pyro4-4.82/examples/autoreconnect/serverNS.py000066400000000000000000000036531416147301300213030ustar00rootroot00000000000000from __future__ import print_function import time import Pyro4.naming print("Autoreconnect using Name Server.") @Pyro4.expose class TestClass(object): def method(self, arg): print("Method called with %s" % arg) print("You can now try to stop this server with ctrl-C/ctrl-Break") time.sleep(1) # If we reconnect the object, it has to have the same objectId as before. # for this example, we rely on the Name Server registration to get our old id back. # If we KNOW 100% that PYRONAME-uris are the only thing used to access our # object, we could skip all this and just register as usual. # That works because the proxy, when reconnecting, will do a new nameserver lookup # and receive the new object uri back. This REQUIRES: # - clients will never connect using a PYRO-uri # - client proxy._pyroBind() is never called # BUT for sake of example, and because we really cannot guarantee the above, # here we go for the safe route and reuse our previous object id. ns = Pyro4.naming.locateNS() try: existing = ns.lookup("example.autoreconnect") print("Object still exists in Name Server with id: %s" % existing.object) print("Previous daemon socket port: %d" % existing.port) # start the daemon on the previous port daemon = Pyro4.core.Daemon(port=existing.port) # register the object in the daemon with the old objectId daemon.register(TestClass, objectId=existing.object) except Pyro4.errors.NamingError: print("There was no previous registration in the name server.") # just start a new daemon on a random port daemon = Pyro4.core.Daemon() # register the object in the daemon and let it get a new objectId # also need to register in name server because it's not there yet. uri = daemon.register(TestClass) ns.register("example.autoreconnect", uri) print("Server started.") daemon.requestLoop() # note: we are not removing the name server registration when terminating! Pyro4-4.82/examples/autoretry/000077500000000000000000000000001416147301300163405ustar00rootroot00000000000000Pyro4-4.82/examples/autoretry/Readme.txt000066400000000000000000000010201416147301300202670ustar00rootroot00000000000000This is an example that shows the auto retry feature (in the client). server.py -- the server you need to run for this example client.py -- client that uses auto retry settings The client disables and enables auto retries to show what happens. It shows an exception when auto retries disabled and server side closed the connection, and then use auto retries to avoid the exception raising to user code. Suggest to run the server with timeout warning output: PYRO_LOGLEVEL=WARNING PYRO_LOGFILE={stderr} python server.py Pyro4-4.82/examples/autoretry/client.py000066400000000000000000000017641416147301300202000ustar00rootroot00000000000000from __future__ import print_function from time import sleep import sys import Pyro4 import Pyro4.errors # client side will not have timeout Pyro4.config.COMMTIMEOUT = 0 # Not using auto-retry feature Pyro4.config.MAX_RETRIES = 0 obj = Pyro4.core.Proxy("PYRONAME:example.autoretry") print("Calling remote function 1st time (create connection)") obj.add(1, 1) print("Calling remote function 2nd time (not timed out yet)") obj.add(2, 2) print("Sleeping 1 second...") sleep(1) print("Calling remote function 3rd time (will raise an exception)") try: obj.add(3, 3) except Exception as e: print("Got exception %r as expected." % repr(e)) print("Now, let's enable the auto retry") obj._pyroRelease() obj._pyroMaxRetries = 2 print("Calling remote function 1st time (create connection)") obj.add(1, 1) print("Calling remote function 2nd time (not timed out yet)") obj.add(2, 2) print("Sleeping 1 second...") sleep(1) print("Calling remote function 3rd time (will not raise any exceptions)") obj.add(3, 3) Pyro4-4.82/examples/autoretry/server.py000066400000000000000000000004671416147301300202270ustar00rootroot00000000000000import Pyro4 @Pyro4.expose class CalcServer(object): def add(self, num1, num2): print("calling add: %d, %d" % (num1, num2)) return num1 + num2 Pyro4.config.COMMTIMEOUT = 0.5 # the server should time out easily now Pyro4.core.Daemon.serveSimple({ CalcServer: "example.autoretry" }) Pyro4-4.82/examples/banks/000077500000000000000000000000001416147301300154005ustar00rootroot00000000000000Pyro4-4.82/examples/banks/Readme.txt000066400000000000000000000006421416147301300173400ustar00rootroot00000000000000This is a simple electronic banking example. There are two banks:- Rabobank and ABN (don't ask - I'm from Holland) Their services are started with BankServer.py. The client runs some transactions on both banks (if found), like:- -creating accounts -deleting accounts -deposit money -withdraw money -inquire balance The ABN bank will not allow the client to overdraw and have a negative balance, the Rabobank will. Pyro4-4.82/examples/banks/banks.py000066400000000000000000000043161416147301300170540ustar00rootroot00000000000000import Pyro4 # Unrestricted account. class Account(object): def __init__(self): self._balance = 0.0 def withdraw(self, amount): self._balance -= amount def deposit(self, amount): self._balance += amount def balance(self): return self._balance # Restricted withdrawal account. class RestrictedAccount(Account): def withdraw(self, amount): if amount <= self._balance: self._balance -= amount else: raise ValueError('insufficent balance') # Abstract bank. @Pyro4.expose @Pyro4.behavior(instance_mode="single") class Bank(object): def __init__(self): self.accounts = {} def name(self): pass # must override this! def createAccount(self, name): pass # must override this! def deleteAccount(self, name): try: del self.accounts[name] except KeyError: raise KeyError('unknown account') def deposit(self, name, amount): try: return self.accounts[name].deposit(amount) except KeyError: raise KeyError('unknown account') def withdraw(self, name, amount): try: return self.accounts[name].withdraw(amount) except KeyError: raise KeyError('unknown account') def balance(self, name): try: return self.accounts[name].balance() except KeyError: raise KeyError('unknown account') def allAccounts(self): accs = {} for name in self.accounts.keys(): accs[name] = self.accounts[name].balance() return accs # Special bank: Rabobank. It has unrestricted accounts. @Pyro4.expose class Rabobank(Bank): def name(self): return 'Rabobank' def createAccount(self, name): if name in self.accounts: raise ValueError('Account already exists') self.accounts[name] = Account() # Special bank: ABN. It has restricted accounts. @Pyro4.expose class ABN(Bank): def name(self): return 'ABN bank' def createAccount(self, name): if name in self.accounts: raise ValueError('Account already exists') self.accounts[name] = RestrictedAccount() Pyro4-4.82/examples/banks/client.py000066400000000000000000000047471416147301300172440ustar00rootroot00000000000000# # Bank client. # # The client searches the two banks and performs a set of operations. # (the banks are searched simply by listing a namespace prefix path) # from __future__ import print_function import Pyro4.naming # A bank client. class client(object): def __init__(self, name): self.name = name def doBusiness(self, bank): print("\n*** %s is doing business with %s:" % (self.name, bank.name())) print("Creating account") try: bank.createAccount(self.name) except ValueError as x: print("Failed: %s" % x) print("Removing account and trying again") bank.deleteAccount(self.name) bank.createAccount(self.name) print("Deposit money") bank.deposit(self.name, 200.00) print("Deposit money") bank.deposit(self.name, 500.75) print("Balance=%.2f" % bank.balance(self.name)) print("Withdraw money") bank.withdraw(self.name, 400.00) print("Withdraw money (overdraw)") try: bank.withdraw(self.name, 400.00) except ValueError as x: print("Failed: %s" % x) print("End balance=%.2f" % bank.balance(self.name)) print("Withdraw money from non-existing account") try: bank.withdraw('GOD', 2222.22) print("!!! Succeeded?!? That is an error") except KeyError as x: print("Failed as expected: %s" % x) print("Deleting non-existing account") try: bank.deleteAccount('GOD') print("!!! Succeeded?!? That is an error") except KeyError as x: print("Failed as expected: %s" % x) ns = Pyro4.naming.locateNS() # list the available banks by looking in the NS for the given prefix path banknames = [name for name in ns.list(prefix="example.banks.")] if not banknames: raise RuntimeError('There are no banks to do business with!') banks = [] # list of banks (proxies) print() for name in banknames: print("Contacting bank: %s" % name) uri = ns.lookup(name) banks.append(Pyro4.core.Proxy(uri)) # Different clients that do business with all banks irmen = client('Irmen') suzy = client('Suzy') for bank in banks: irmen.doBusiness(bank) suzy.doBusiness(bank) # List all accounts print() for bank in banks: print("The accounts in the %s:" % bank.name()) accounts = bank.allAccounts() for name in accounts.keys(): print(" %s : %.2f" % (name, accounts[name])) Pyro4-4.82/examples/banks/server.py000066400000000000000000000010731416147301300172610ustar00rootroot00000000000000# # The banks server # from __future__ import print_function import Pyro4.core import Pyro4.naming import banks with Pyro4.core.Daemon() as daemon: with Pyro4.naming.locateNS() as ns: uri = daemon.register(banks.Rabobank) ns.register("example.banks.rabobank", uri) uri = daemon.register(banks.ABN) ns.register("example.banks.abn", uri) print("available banks:") print(list(ns.list(prefix="example.banks.").keys())) # enter the service loop. print("Banks are ready for customers.") daemon.requestLoop() Pyro4-4.82/examples/batchedcalls/000077500000000000000000000000001416147301300167135ustar00rootroot00000000000000Pyro4-4.82/examples/batchedcalls/Readme.txt000066400000000000000000000007471416147301300206610ustar00rootroot00000000000000This is an example that shows the batched calls feature. The example does a lot of method calls on the same proxy object. It shows the time it takes to do them individually. Afterwards, it does them again but this time using the batched calls feature. It prints the time taken and this should be much faster. It also shows what happens when one of the calls in the batch generates an error. (the batch is aborted and the error is raised locally once the result generator gets to it). Pyro4-4.82/examples/batchedcalls/client.py000066400000000000000000000111611416147301300205430ustar00rootroot00000000000000from __future__ import print_function import sys import time from Pyro4.util import getPyroTraceback import Pyro4 if sys.version_info < (3, 0): input = raw_input NUMBER_OF_LOOPS = 20000 uri = input("enter server object uri: ").strip() p = Pyro4.Proxy(uri) # First, we do a loop of N normal remote calls on the proxy # We time the loop and validate the computation result print("Normal remote calls...") begin = time.time() total = 0 p.printmessage("beginning normal calls") for i in range(NUMBER_OF_LOOPS): total += p.multiply(7, 6) total += p.add(10, 20) p.printmessage("end of normal calls") assert total == (NUMBER_OF_LOOPS * (7 * 6 + 10 + 20)) # check duration = time.time() - begin print("that took {0:.2f} seconds ({1:.0f} calls/sec)".format(duration, NUMBER_OF_LOOPS * 2.0 / duration)) duration_normal = duration # Now we do the same loop of N remote calls but this time we use # the batched calls proxy. It collects all calls and processes them # in a single batch. For many subsequent calls on the same proxy this # is much faster than doing all calls individually. # (but it has a few limitations and requires changes to your code) print("\nBatched remote calls...") begin = time.time() batch = Pyro4.batch(p) # get a batched call proxy for 'p' batch.printmessage("beginning batch #1") for i in range(NUMBER_OF_LOOPS): batch.multiply(7, 6) # queue a call, note that it returns 'None' immediately batch.add(10, 20) # queue a call, note that it returns 'None' immediately batch.printmessage("end of batch #1") print("processing the results...") total = 0 result = batch() # execute the batch of remote calls, it returns a generator that produces all results in sequence for r in result: total += r duration = time.time() - begin assert total == (NUMBER_OF_LOOPS * (7 * 6 + 10 + 20)) # check print("total time taken {0:.2f} seconds ({1:.0f} calls/sec)".format(duration, NUMBER_OF_LOOPS * 2.0 / duration // 100 * 100)) print("batched calls were {0:.1f} times faster than normal remote calls".format(duration_normal / duration)) # Now we do another loop of batched calls, but this time oneway (no results). print("\nOneway batched remote calls...") begin = time.time() batch = Pyro4.batch(p) # get a batched call proxy for 'p' batch.printmessage("beginning batch #2") for i in range(NUMBER_OF_LOOPS): batch.multiply(7, 6) # queue a call, note that it returns 'None' immediately batch.add(10, 20) # queue a call, note that it returns 'None' immediately batch.delay(2) # queue a delay of 2 seconds (but we won't notice) batch.printmessage("end of batch #2") print("executing batch, there will be no result values. Check server to see printed messages...") result = batch(oneway=True) # execute the batch of remote calls, oneway, will return None assert result is None duration = time.time() - begin print("total time taken {0:.2f} seconds ({1:.0f} calls/sec)".format(duration, NUMBER_OF_LOOPS * 2.0 / duration // 100 * 100)) print("oneway batched calls were {0:.1f} times faster than normal remote calls".format(duration_normal / duration)) # Batches can be executed asynchronous as well print("\nBatched remote calls, asynchronous...") batch = Pyro4.batch(p) # get a batched call proxy for 'p' batch.printmessage("beginning batch #3") batch.multiply(7, 6) # queue a call, note that it returns 'None' immediately batch.add(10, 20) # queue a call, note that it returns 'None' immediately batch.delay(2) # queue a delay, but this doesn't matter with asynchronous proxy batch.printmessage("end of batch #3") print("executing the batch... (should return immediately because async)") asyncresult = batch(asynchronous=True) # execute the batch, asynchronously (return immediately) print("processing the results...(should wait until async results become available)") results = list(asyncresult.value) print("results=", results) # Show what happens when one of the methods in a batch generates an error. # (the batch is aborted and the error is raised locally again). # Btw, you can re-use a batch proxy once you've called it and processed the results. print("\nBatch with an error. Dividing a number by decreasing divisors...") for d in range(3, -3, -1): # divide by 3,2,1,0,-1,-2,-3... but 0 will be a problem ;-) batch.divide(100, d) print("getting results...") divisor = 3 try: for result in batch(): print("100//%d = %d" % (divisor, result)) divisor -= 1 # this will raise the proper zerodivision exception once we're about # to process the batch result from the divide by 0 call. except ZeroDivisionError: print("A divide by zero error occurred during the batch! (expected)") print("".join(getPyroTraceback())) Pyro4-4.82/examples/batchedcalls/server.py000066400000000000000000000007501416147301300205750ustar00rootroot00000000000000import time import Pyro4 @Pyro4.expose class Thingy(object): def multiply(self, a, b): return a * b def add(self, a, b): return a + b def divide(self, a, b): return a // b def error(self): return 1 // 0 def delay(self, seconds): time.sleep(seconds) return seconds def printmessage(self, message): print(message) return 0 Pyro4.Daemon.serveSimple({ Thingy: "example.batched" }, ns=False) Pyro4-4.82/examples/benchmark/000077500000000000000000000000001416147301300162345ustar00rootroot00000000000000Pyro4-4.82/examples/benchmark/Readme.txt000066400000000000000000000015441416147301300201760ustar00rootroot00000000000000This test is to find out the average time it takes for a remote PYRO method call. Also it is a kind of stress test because lots of calls are made in a very short time. The oneway method call test is very fast if you run the client and server on different machines. If they're running on the same machine, the speedup is less noticable. There is also the 'connections' benchmark which tests the speed at which Pyro can make new proxy connections. It tests the raw connect speed (by releasing and rebinding existing proxies) and also the speed at which new proxies can be created that perform a single remote method call. Different serializers --------------------- Note that Pyro4's performance is very much affected by two things: 1) the network latency and bandwith 2) the characteristics of your data (small messages or large) 2) the serializer that is used. Pyro4-4.82/examples/benchmark/bench.py000066400000000000000000000013701416147301300176660ustar00rootroot00000000000000import Pyro4 @Pyro4.expose class bench(object): def length(self, string): return len(string) def timestwo(self, value): return value * 2 def bigreply(self): return 'BIG REPLY' * 500 def bigarg(self, arg): return len(arg) def manyargs(self, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15): return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15 def noreply(self, arg): pass def varargs(self, *args): return len(args) def keywords(self, **args): return args def echo(self, *args): return args @Pyro4.oneway def oneway(self, *args): # oneway doesn't return anything pass Pyro4-4.82/examples/benchmark/client.py000066400000000000000000000031211416147301300200610ustar00rootroot00000000000000from __future__ import print_function import sys import time import Pyro4 import bench Pyro4.config.SERIALIZER = "marshal" if sys.version_info < (3, 0): input = raw_input uri = input("Uri of benchmark server? ").strip() obj = Pyro4.core.Proxy(uri) obj._pyroBind() assert "oneway" in obj._pyroOneway # make sure this method is indeed marked as @oneway funcs = [ lambda: obj.length('Irmen de Jong'), lambda: obj.timestwo(21), lambda: obj.bigreply(), lambda: obj.manyargs(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15), lambda: obj.noreply([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]), lambda: obj.noreply(None), lambda: obj.varargs('een', 2, (3,), [4]), lambda: obj.keywords(arg1='zork'), lambda: obj.echo('een', 2, (3,), [4]), lambda: obj.echo({"aap": 42, "noot": 99, "mies": 987654}), lambda: obj.bigarg('Argument' * 50), lambda: obj.oneway('stringetje', 432423434, 9.8765432) ] print('-------- BENCHMARK REMOTE OBJECT ---------') begin = time.time() iters = 1000 print("warmup...") for _ in range(iters): funcs[0]() for i, f in enumerate(funcs, start=1): print("call #%d, %d times... " % (i, iters), end="") before = time.time() for _ in range(iters): f() print("%.3f" % (time.time() - before)) duration = time.time() - begin print('total time %.3f seconds' % duration) amount = len(funcs) * iters print('total method calls: %d' % amount) avg_pyro_msec = 1000.0 * duration / amount print('avg. time per method call: %.3f msec (%d/sec) (serializer: %s)' % (avg_pyro_msec, amount / duration, Pyro4.config.SERIALIZER)) Pyro4-4.82/examples/benchmark/connections.py000066400000000000000000000024401416147301300211300ustar00rootroot00000000000000from __future__ import print_function import time import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input uri = input("Uri of benchmark server? ").strip() print("Timing raw connect speed (no method call)...") p = Pyro4.core.Proxy(uri) p.oneway() ITERATIONS = 2000 begin = time.time() for loop in range(ITERATIONS): if loop % 500 == 0: print(loop) p._pyroRelease() p._pyroBind() duration = time.time() - begin print("%d connections in %.3f sec = %.0f conn/sec" % (ITERATIONS, duration, ITERATIONS / duration)) del p print("Timing proxy creation+connect+methodcall speed...") ITERATIONS = 2000 begin = time.time() for loop in range(ITERATIONS): if loop % 500 == 0: print(loop) with Pyro4.core.Proxy(uri) as p: p.oneway() duration = time.time() - begin print("%d new proxy calls in %.3f sec = %.0f calls/sec" % (ITERATIONS, duration, ITERATIONS / duration)) print("Timing proxy methodcall speed...") p = Pyro4.core.Proxy(uri) p.oneway() ITERATIONS = 10000 begin = time.time() for loop in range(ITERATIONS): if loop % 1000 == 0: print(loop) p.oneway() duration = time.time() - begin print("%d calls in %.3f sec = %.0f calls/sec" % (ITERATIONS, duration, ITERATIONS / duration)) print("Serializer used:", Pyro4.config.SERIALIZER) Pyro4-4.82/examples/benchmark/server.py000066400000000000000000000003631416147301300201160ustar00rootroot00000000000000from __future__ import print_function import Pyro4 import Pyro4.socketutil import bench Pyro4.Daemon.serveSimple({ bench.bench: "example.benchmark" }, daemon=Pyro4.Daemon(host=Pyro4.socketutil.getIpAddress("")), ns=False) Pyro4-4.82/examples/blob-dispatch/000077500000000000000000000000001416147301300170155ustar00rootroot00000000000000Pyro4-4.82/examples/blob-dispatch/Readme.txt000066400000000000000000000012621416147301300207540ustar00rootroot00000000000000This shows how you can pass through serialized arguments unchanged via Pyro4.core.SerializedBlob. The idea is that you tell Pyro to NOT serialize/deserialize particular message contents, because you'll be doing that yourself once it reaches the destination. This avoids a lot of serializer overhead (which is quite expensive). This way it is possible to make efficient dispatchers/proxies/routing services for Pyro, where only the actual receiving server at the end, deserializes the package once. Run this example by: - make sure a Pyro name server is running. - start a dispatcher from the dispatcher directory - start one or more listeners from listeners/main.py - start the client. Pyro4-4.82/examples/blob-dispatch/client/000077500000000000000000000000001416147301300202735ustar00rootroot00000000000000Pyro4-4.82/examples/blob-dispatch/client/client.py000066400000000000000000000014701416147301300221250ustar00rootroot00000000000000from __future__ import print_function import sys import datetime import Pyro4.util import Pyro4.core import Pyro4 from customdata import CustomData if sys.version_info < (3, 0): input = raw_input sys.excepthook = Pyro4.util.excepthook # teach Serpent how to serialize our data class Pyro4.util.SerializerBase.register_class_to_dict(CustomData, CustomData.to_dict) with Pyro4.Proxy("PYRONAME:example.blobdispatcher") as dispatcher: while True: topic = input("Enter topic to send data on (just enter to quit) ").strip() if not topic: break # create our custom data object and send it through the dispatcher data = CustomData(42, "hello world", datetime.datetime.now()) dispatcher.process_blob(Pyro4.core.SerializedBlob(topic, data)) print("processed") Pyro4-4.82/examples/blob-dispatch/client/customdata.py000066400000000000000000000012701416147301300230110ustar00rootroot00000000000000# note: this module is shared with the listeners so they can understand the data passed in class CustomData(object): serialized_classname = "blobdispatch.CustomData" def __init__(self, a, b, c): self.a = a self.b = b self.c = c def to_dict(self): """for (serpent) serialization""" return { "__class__": self.serialized_classname, "a": self.a, "b": self.b, "c": self.c } @classmethod def from_dict(cls, classname, d): """for (serpent) deserialization""" assert classname == cls.serialized_classname obj = cls(d["a"], d["b"], d["c"]) return obj Pyro4-4.82/examples/blob-dispatch/dispatcher/000077500000000000000000000000001416147301300211435ustar00rootroot00000000000000Pyro4-4.82/examples/blob-dispatch/dispatcher/dispatcher.py000066400000000000000000000015001416147301300236370ustar00rootroot00000000000000from __future__ import print_function from collections import defaultdict import Pyro4 # note: the dispatcher doesn't know anything about the CustomData class from the customdata module! @Pyro4.behavior(instance_mode="single") class Dispatcher(object): def __init__(self): self.listeners = defaultdict(list) @Pyro4.expose def register(self, topic, listener): self.listeners[topic].append(listener) print("New listener for topic {} registered: {}".format(topic, listener._pyroUri)) @Pyro4.expose def process_blob(self, blob): print("Dispatching blob with name:", blob.info) listeners = self.listeners.get(blob.info, []) for listener in listeners: listener.process_blob(blob) Pyro4.Daemon.serveSimple({ Dispatcher: "example.blobdispatcher" }) Pyro4-4.82/examples/blob-dispatch/listeners/000077500000000000000000000000001416147301300210255ustar00rootroot00000000000000Pyro4-4.82/examples/blob-dispatch/listeners/customdata.py000066400000000000000000000012661416147301300235500ustar00rootroot00000000000000# note: this module is shared with the client so we can understand the data they pass in class CustomData(object): serialized_classname = "blobdispatch.CustomData" def __init__(self, a, b, c): self.a = a self.b = b self.c = c def to_dict(self): """for (serpent) serialization""" return { "__class__": self.serialized_classname, "a": self.a, "b": self.b, "c": self.c } @classmethod def from_dict(cls, classname, d): """for (serpent) deserialization""" assert classname == cls.serialized_classname obj = cls(d["a"], d["b"], d["c"]) return obj Pyro4-4.82/examples/blob-dispatch/listeners/listener.py000066400000000000000000000014031416147301300232220ustar00rootroot00000000000000import Pyro4 import Pyro4.util from customdata import CustomData # teach Serpent how to deserialize our custom data class Pyro4.util.SerializerBase.register_dict_to_class(CustomData.serialized_classname, CustomData.from_dict) class Listener(object): def __init__(self, topic): self.topic = topic def register_with_dispatcher(self): with Pyro4.Proxy("PYRONAME:example.blobdispatcher") as dispatcher: dispatcher.register(self.topic, self) @Pyro4.expose def process_blob(self, blob): assert blob.info == self.topic customdata = blob.deserialized() print("Received custom data (type={}):".format(type(customdata))) print(" a={}, b={}, c={}".format(customdata.a, customdata.b, customdata.c)) Pyro4-4.82/examples/blob-dispatch/listeners/main.py000066400000000000000000000007411416147301300223250ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 from listener import Listener if len(sys.argv) != 2: print("Give topic as argument.") else: topic = sys.argv[1].strip() if not topic: raise ValueError("Must give topic name.") listener = Listener(topic) daemon = Pyro4.Daemon() daemon.register(listener) listener.register_with_dispatcher() print("Listener for topic {} waiting for data.".format(topic)) daemon.requestLoop() Pyro4-4.82/examples/callback/000077500000000000000000000000001416147301300160365ustar00rootroot00000000000000Pyro4-4.82/examples/callback/Readme.txt000066400000000000000000000046011416147301300177750ustar00rootroot00000000000000These examples shows how you can let a server call back to the client. There are 2 examples. 1) first example: server.py + client.py The client creates some worker objects on the server. It provides them with a callback object that lives in the client. When a worker is done with its task, it will invoke a method on the callback object. That means that this time, the client gets a call from the server that notifies it that a worker has completed its job. (Note: the client uses oneway calls to start up the workers, this ensures that they are running in the background) For all of this to work, the client needs to create a daemon as well: it needs to be able to receive (callback) calls after all. So it creates a daemon, a callback receiver, and starts it all up just like a server would do. The client counts the number of 'work completed' callbacks it receives. To remain in the daemon loop, the client provides a special loop condition that is true while the counter is less than the number of workers. Notice that the client sets PYRO_COMMTIMEOUT. That is needed because otherwise it will block in the default requestloop, and it will never evaluate the loopcondition. By setting a timeout we force it to periodically break from the blocking wait and check the loop condition. We could also have used the 'select' servertype instead of setting a PYRO_COMMTIMEOUT, because that one already breaks periodically. (PYRO_POLLTIMEOUT). 2) second example: server2.py + client2.py This example shows how to use the @Pyro4.callback decorator to flag a method to be a callback method. This makes Pyro log any exceptions that occur in this method also on the side where the method is running. Otherwise it would just silently pass the exception back to the side that was calling the callback method, and there is no way to see it occur on the callback side itself. It only logs a warning with the error and the traceback though. It doesn't actually print it to the screen, or raise the exception again. So you have to enable logging to see it appear. Also note that this example makes use of Pyro's AutoProxy feature. Sending pyro objects 'over the wire' will automatically convert them into proxies so that the other side will talk to the actual object, instead of a local copy. So the client just sends a callback object to the server, and the server can just return a worker object, as if it was a normal method call. Pyro4-4.82/examples/callback/client.py000066400000000000000000000020721416147301300176670ustar00rootroot00000000000000import random import Pyro4 # We need to set either a socket communication timeout, # or use the select based server. Otherwise the daemon requestLoop # will block indefinitely and is never able to evaluate the loopCondition. Pyro4.config.COMMTIMEOUT = 0.5 NUM_WORKERS = 5 class CallbackHandler(object): workdone = 0 @Pyro4.expose def done(self, number): print("callback: worker %d reports work is done!" % number) CallbackHandler.workdone += 1 with Pyro4.core.Daemon() as daemon: # register our callback handler callback = CallbackHandler() daemon.register(callback) # contact the server and put it to work print("creating a bunch of workers") with Pyro4.core.Proxy("PYRONAME:example.callback") as server: for _ in range(NUM_WORKERS): worker = server.addworker(callback) # provide our callback handler! worker.work(random.randint(1, 5)) print("waiting for all work complete...") daemon.requestLoop(loopCondition=lambda: CallbackHandler.workdone < NUM_WORKERS) print("done!") Pyro4-4.82/examples/callback/client2.py000066400000000000000000000023031416147301300177460ustar00rootroot00000000000000from __future__ import print_function import logging import sys import Pyro4 # initialize the logger so you can see what is happening with the callback exception message: logging.basicConfig(stream=sys.stderr, format="[%(asctime)s,%(name)s,%(levelname)s] %(message)s") log = logging.getLogger("Pyro4") log.setLevel(logging.WARNING) class CallbackHandler(object): def crash(self): a = 1 b = 0 return a // b @Pyro4.expose def call1(self): print("\n\ncallback 1 received from server!") print("going to crash - you won't see the exception here, only on the server") return self.crash() @Pyro4.expose @Pyro4.callback def call2(self): print("\n\ncallback 2 received from server!") print("going to crash - but you will see the exception here too") return self.crash() daemon = Pyro4.core.Daemon() callback = CallbackHandler() daemon.register(callback) with Pyro4.core.Proxy("PYRONAME:example.callback2") as server: server.doCallback(callback) # this is a oneway call, so we can continue right away print("waiting for callbacks to arrive...") print("(ctrl-c/break the program once it's done)") daemon.requestLoop() Pyro4-4.82/examples/callback/server.py000066400000000000000000000016671416147301300177300ustar00rootroot00000000000000import time import Pyro4 class Worker(object): def __init__(self, number, callback): self.number = number self.callback = callback print("Worker %d created" % self.number) @Pyro4.expose @Pyro4.oneway def work(self, amount): print("Worker %d busy..." % self.number) time.sleep(amount) print("Worker %d done. Informing callback client." % self.number) self._pyroDaemon.unregister(self) self.callback.done(self.number) # invoke the callback object class CallbackServer(object): def __init__(self): self.number = 0 @Pyro4.expose def addworker(self, callback): self.number += 1 print("server: adding worker %d" % self.number) worker = Worker(self.number, callback) self._pyroDaemon.register(worker) # make it a Pyro object return worker Pyro4.Daemon.serveSimple({ CallbackServer: "example.callback" }) Pyro4-4.82/examples/callback/server2.py000066400000000000000000000012621416147301300200010ustar00rootroot00000000000000import Pyro4 class CallbackServer(object): @Pyro4.expose @Pyro4.oneway def doCallback(self, callback): print("\n\nserver: doing callback 1 to client") try: callback.call1() except: print("got an exception from the callback:") print("".join(Pyro4.util.getPyroTraceback())) print("\n\nserver: doing callback 2 to client") try: callback.call2() except: print("got an exception from the callback:") print("".join(Pyro4.util.getPyroTraceback())) print("server: callbacks done.\n") Pyro4.Daemon.serveSimple({ CallbackServer: "example.callback2" }) Pyro4-4.82/examples/callcontext/000077500000000000000000000000001416147301300166225ustar00rootroot00000000000000Pyro4-4.82/examples/callcontext/Readme.txt000066400000000000000000000006471416147301300205670ustar00rootroot00000000000000This example shows the use of several advanced Pyro constructs: - overriding proxy and daemon to customize their behavior - setting the hmac key - using the call context in the server to obtain information about the client - setting and printing correlation ids - using custom message annotations (both old style with proxy/daemon method override, and new style using the call context which is possible since Pyro 4.56) Pyro4-4.82/examples/callcontext/client.py000066400000000000000000000057331416147301300204620ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 import uuid # example: set a single correlation id on the context that should be passed along Pyro4.current_context.correlation_id = uuid.uuid4() print("correlation id set to:", Pyro4.current_context.correlation_id) if sys.version_info < (3, 0): input = raw_input # custom proxy needed to get to annotation data, before Pyro 4.56 class CustomAnnotationProxy(Pyro4.Proxy): def __init__(self, uri): super(CustomAnnotationProxy, self).__init__(uri) self._pyroHmacKey = b"secr3t_k3y" # override the method that adds annotations and add our own def _pyroAnnotations(self): return {"XYZZ": b"Hello, I am a custom annotation from the proxy!"} def _pyroResponseAnnotations(self, annotations, msgtype): print(" Got response (msgtype=%d). Annotations:" % msgtype) for key in annotations: if key == "CORR": value = uuid.UUID(bytes=annotations[key]) elif key == "HMAC": value = "[...]" else: value = annotations[key] print(" {0} -> {1}".format(key, value)) uri = input("Enter the URI of the server object: ") print("\n------- (older) method to get annotations via callback on custom proxy... -----\n") with CustomAnnotationProxy(uri) as proxy: print("Sending a few messages using one proxy...") for i in range(4): msg = proxy.echo("hello-%d" % i) proxies = [CustomAnnotationProxy(uri) for _ in range(5)] for p in proxies: print("Sending one message from new proxy...") msg = p.echo("hello-%d" % id(p)) p._pyroRelease() with CustomAnnotationProxy(uri) as proxy: # oneway print("Sending a oneway message... (should only print a connection ok response)") proxy.oneway("hello-ONEWAY-1") print("Sending another oneway message... (should not print a response at all)") proxy.oneway("hello-ONEWAY-2") # asynchronous print("Asynchronous proxy message...") proxy._pyroAsync() result = proxy.echo("hello-ASYNC") _ = result.value print("\n------- get annotations via normal proxy and the call context... -----\n") input("press enter:") # the code below works as of Pyro 4.56. with Pyro4.Proxy(uri) as proxy: proxy._pyroHmacKey = b"secr3t_k3y" print("normal call") Pyro4.current_context.annotations = {"XYZZ": b"custom annotation from client via new way(1)"} result = proxy.echo("hi there - new method of annotation access in client") print("Annotations in response were: ", Pyro4.current_context.response_annotations) print("\noneway call") Pyro4.current_context.annotations = {"XYZZ": b"custom annotation from client via new way(2)"} proxy.oneway("hi there ONEWAY - new method of annotation access in client") print("Annotations in response were: ", Pyro4.current_context.response_annotations) print(" (should be empty because oneway!)") print("\nSee the console output on the server for more results.") Pyro4-4.82/examples/callcontext/server.py000066400000000000000000000025001416147301300204770ustar00rootroot00000000000000from __future__ import print_function import Pyro4 import threading @Pyro4.expose class EchoServer(object): def echo(self, message): ctx = Pyro4.current_context print("\nGot Message:", message) print(" thread: ", threading.current_thread().ident) print(" obj.pyroid: ", self._pyroId) print(" obj.daemon: ", self._pyroDaemon) print(" context.client: ", ctx.client) print(" context.client_sock_addr: ", ctx.client_sock_addr) print(" context.seq: ", ctx.seq) print(" context.msg_flags: ", ctx.msg_flags) print(" context.serializer_id: ", ctx.serializer_id) print(" context.correlation_id:", ctx.correlation_id) if "XYZZ" in ctx.annotations: print(" custom annotation 'XYZZ':", ctx.annotations["XYZZ"]) return message @Pyro4.oneway def oneway(self, message): return self.echo(message) class CustomDaemon(Pyro4.Daemon): def annotations(self): return {"DDAA": b"custom response annotation set by the daemon"} with CustomDaemon() as daemon: daemon._pyroHmacKey = b"secr3t_k3y" uri = daemon.register(EchoServer, "example.context") # provide a logical name ourselves print("Server is ready. You can use the following URI to connect:") print(uri) daemon.requestLoop() Pyro4-4.82/examples/chatbox/000077500000000000000000000000001416147301300157325ustar00rootroot00000000000000Pyro4-4.82/examples/chatbox/Readme.txt000066400000000000000000000016561416147301300177000ustar00rootroot00000000000000Chat box example. This chat box example is constructed as follows: A Chat Server (Pyro object) handles the login/logoff process and keeps track of all chat channels and clients that are subscribed to each channel. It implements the chatting and distributing of chat messages to all subscribers. It uses a oneway call for that to improve performance with a large number of subscribers, and to avoid blocking. The chat client runs the user input processing in the main thread. It runs another thread with the Pyro daemon that is listening for server chat messages, so that they can be printed while the main thread is still waiting for user input. Also note that this example makes use of Pyro's AutoProxy feature. Sending pyro objects 'over the wire' will automatically convert them into proxies so that the other side will talk to the actual object, instead of a local copy. So the client just sends the callback object to the server. Pyro4-4.82/examples/chatbox/client.py000066400000000000000000000044251416147301300175670ustar00rootroot00000000000000import sys import threading import Pyro4 if sys.version_info < (3, 0): input = raw_input # The daemon is running in its own thread, to be able to deal with server # callback messages while the main thread is processing user input. class Chatter(object): def __init__(self): self.chatbox = Pyro4.core.Proxy('PYRONAME:example.chatbox.server') self.abort = 0 @Pyro4.expose @Pyro4.oneway def message(self, nick, msg): if nick != self.nick: print('[{0}] {1}'.format(nick, msg)) def start(self): nicks = self.chatbox.getNicks() if nicks: print('The following people are on the server: %s' % (', '.join(nicks))) channels = sorted(self.chatbox.getChannels()) if channels: print('The following channels already exist: %s' % (', '.join(channels))) self.channel = input('Choose a channel or create a new one: ').strip() else: print('The server has no active channels.') self.channel = input('Name for new channel: ').strip() self.nick = input('Choose a nickname: ').strip() people = self.chatbox.join(self.channel, self.nick, self) print('Joined channel %s as %s' % (self.channel, self.nick)) print('People on this channel: %s' % (', '.join(people))) print('Ready for input! Type /quit to quit') try: try: while not self.abort: line = input('> ').strip() if line == '/quit': break if line: self.chatbox.publish(self.channel, self.nick, line) except EOFError: pass finally: self.chatbox.leave(self.channel, self.nick) self.abort = 1 self._pyroDaemon.shutdown() class DaemonThread(threading.Thread): def __init__(self, chatter): threading.Thread.__init__(self) self.chatter = chatter self.setDaemon(True) def run(self): with Pyro4.core.Daemon() as daemon: daemon.register(self.chatter) daemon.requestLoop(lambda: not self.chatter.abort) chatter = Chatter() daemonthread = DaemonThread(chatter) daemonthread.start() chatter.start() print('Exit.') Pyro4-4.82/examples/chatbox/server.py000066400000000000000000000047341416147301300176220ustar00rootroot00000000000000import Pyro4 # Chat box administration server. # Handles logins, logouts, channels and nicknames, and the chatting. @Pyro4.expose @Pyro4.behavior(instance_mode="single") class ChatBox(object): def __init__(self): self.channels = {} # registered channels { channel --> (nick, client callback) list } self.nicks = [] # all registered nicks on this server def getChannels(self): return list(self.channels.keys()) def getNicks(self): return self.nicks def join(self, channel, nick, callback): if not channel or not nick: raise ValueError("invalid channel or nick name") if nick in self.nicks: raise ValueError('this nick is already in use') if channel not in self.channels: print('CREATING NEW CHANNEL %s' % channel) self.channels[channel] = [] self.channels[channel].append((nick, callback)) self.nicks.append(nick) print("%s JOINED %s" % (nick, channel)) self.publish(channel, 'SERVER', '** ' + nick + ' joined **') return [nick for (nick, c) in self.channels[channel]] # return all nicks in this channel def leave(self, channel, nick): if channel not in self.channels: print('IGNORED UNKNOWN CHANNEL %s' % channel) return for (n, c) in self.channels[channel]: if n == nick: self.channels[channel].remove((n, c)) break self.publish(channel, 'SERVER', '** ' + nick + ' left **') if len(self.channels[channel]) < 1: del self.channels[channel] print('REMOVED CHANNEL %s' % channel) self.nicks.remove(nick) print("%s LEFT %s" % (nick, channel)) def publish(self, channel, nick, msg): if channel not in self.channels: print('IGNORED UNKNOWN CHANNEL %s' % channel) return for (n, c) in self.channels[channel][:]: # use a copy of the list try: c.message(nick, msg) # oneway call except Pyro4.errors.ConnectionClosedError: # connection dropped, remove the listener if it's still there # check for existence because other thread may have killed it already if (n, c) in self.channels[channel]: self.channels[channel].remove((n, c)) print('Removed dead listener %s %s' % (n, c)) Pyro4.Daemon.serveSimple({ ChatBox: "example.chatbox.server" }) Pyro4-4.82/examples/circular/000077500000000000000000000000001416147301300161065ustar00rootroot00000000000000Pyro4-4.82/examples/circular/Readme.txt000066400000000000000000000011221416147301300200400ustar00rootroot00000000000000Create a chain of objects calling each other: client --> A --> B ^ | | v | `----- C I.e. C calls A again. A detects that the message went full circle and returns the result (a 'trace' of the route) to the client. (the detection checks if the name of the current server is already in the current trace of the route, i.e., if it arrives for a second time on the same server, it concludes that we're done). First start the three servers (servA,B,C) and then run the client. You need to have a nameserver running. Pyro4-4.82/examples/circular/chain.py000066400000000000000000000015571416147301300175520ustar00rootroot00000000000000from __future__ import print_function import Pyro4 # a Chain member. Passes messages to the next link, # until the message went full-circle: then it exits. class Chain(object): def __init__(self, name, next_node): self.name = name self.nextName = next_node self.next = None @Pyro4.expose def process(self, message): if self.next is None: self.next = Pyro4.core.Proxy("PYRONAME:example.chain." + self.nextName) if self.name in message: print("Back at %s; we completed the circle!" % self.name) return ["complete at " + self.name] else: print("I'm %s, passing to %s" % (self.name, self.nextName)) message.append(self.name) result = self.next.process(message) result.insert(0, "passed on from " + self.name) return result Pyro4-4.82/examples/circular/client.py000066400000000000000000000002301416147301300177310ustar00rootroot00000000000000from __future__ import print_function import Pyro4.core obj = Pyro4.core.Proxy("PYRONAME:example.chain.A") print("Result=%s" % obj.process(["hello"])) Pyro4-4.82/examples/circular/servA.py000066400000000000000000000007051416147301300175420ustar00rootroot00000000000000from __future__ import print_function import Pyro4.core import Pyro4.naming import chain this_node = "A" next_node = "B" servername = "example.chain." + this_node with Pyro4.core.Daemon() as daemon: obj = chain.Chain(this_node, next_node) uri = daemon.register(obj) with Pyro4.naming.locateNS() as ns: ns.register(servername, uri) # enter the service loop. print("Server started %s" % this_node) daemon.requestLoop() Pyro4-4.82/examples/circular/servB.py000066400000000000000000000007051416147301300175430ustar00rootroot00000000000000from __future__ import print_function import Pyro4.core import Pyro4.naming import chain this_node = "B" next_node = "C" servername = "example.chain." + this_node with Pyro4.core.Daemon() as daemon: obj = chain.Chain(this_node, next_node) uri = daemon.register(obj) with Pyro4.naming.locateNS() as ns: ns.register(servername, uri) # enter the service loop. print("Server started %s" % this_node) daemon.requestLoop() Pyro4-4.82/examples/circular/servC.py000066400000000000000000000007051416147301300175440ustar00rootroot00000000000000from __future__ import print_function import Pyro4.core import Pyro4.naming import chain this_node = "C" next_node = "A" servername = "example.chain." + this_node with Pyro4.core.Daemon() as daemon: obj = chain.Chain(this_node, next_node) uri = daemon.register(obj) with Pyro4.naming.locateNS() as ns: ns.register(servername, uri) # enter the service loop. print("Server started %s" % this_node) daemon.requestLoop() Pyro4-4.82/examples/deadlock/000077500000000000000000000000001416147301300160505ustar00rootroot00000000000000Pyro4-4.82/examples/deadlock/Readme.txt000066400000000000000000000013151416147301300200060ustar00rootroot00000000000000This example shows some code that triggers a Pyro conversation deadlock. The client and server engage in a 'conversation' where they will deadlock because a single proxy is used for both the initial server method call, and client callback. The client callback method calls the server again. But it will fail, because the proxy it is using is still engaged in the original method call to the server and is locked (waiting for a response). A simple solution is to never reuse proxies in callbacks, and instead create new ones and use those in the callback functions. Another solution is to set COMMTIMEOUT such that after a certain time the client will abort with a timeout error, effectively breaking the deadlock. Pyro4-4.82/examples/deadlock/bouncer.py000066400000000000000000000021121416147301300200530ustar00rootroot00000000000000from __future__ import print_function import threading import Pyro4 # A message bouncer. Passes messages back to the callback # object, until a certain limit is reached. # In this example however, it will never reach the limit, # because the client will deadlock first. @Pyro4.expose class Bouncer(object): def __init__(self, name): self.name = name self.count = 0 self.callbackMutex = threading.Lock() def register(self, callback): self.callback = callback def process(self, message): print("in process", self.name) if len(message) >= 3: print("Back in", self.name, ", message is large enough... stopping!") return ["complete at " + self.name + ":" + str(self.count)] print("I'm", self.name, ", bouncing back...") message.append(self.name) with self.callbackMutex: result = self.callback.process(message) self.count += 1 result.insert(0, "passed on from " + self.name + ":" + str(self.count)) print("returned from callback") return result Pyro4-4.82/examples/deadlock/client.py000066400000000000000000000026151416147301300177040ustar00rootroot00000000000000from __future__ import print_function import threading import Pyro4 import bouncer abort = False def PyroLoop(daemon): daemon.requestLoop() def main(): global abort daemon = Pyro4.Daemon() server = Pyro4.Proxy("PYRONAME:example.deadlock") bounceObj = bouncer.Bouncer("Client") daemon.register(bounceObj) # callback object # register callback on the server server.register(bounceObj) # Now register server as 'callback' on the bounce object in this client # note: we're using the same proxy here as the main program! # This is the main cause of the deadlock, because this proxy will already # be engaged in a call when the callback object here wants to use it as well. # One solution could be to use a new proxy from inside the callback object, like this: # server2 = server.__copy__() # bounceObj.register(server2) bounceObj.register(server) # create a thread that handles callback requests thread = threading.Thread(target=PyroLoop, args=(daemon,)) thread.setDaemon(True) thread.start() print("This bounce example will deadlock!") print("Read the source or Readme.txt for more info why this is the case!") print("Calling server...") result = server.process(["hello"]) print("Result=", result) # <--- you will never see this, it will deadlock in the previous call if __name__ == '__main__': main() Pyro4-4.82/examples/deadlock/server.py000066400000000000000000000007511416147301300177330ustar00rootroot00000000000000from __future__ import print_function import Pyro4 import bouncer # you could set a comm timeout to avoid the deadlock situation...: # Pyro4.config.COMMTIMEOUT = 2 with Pyro4.Daemon() as daemon: uri = daemon.register(bouncer.Bouncer("Server")) Pyro4.locateNS().register("example.deadlock", uri) print("This bounce example will deadlock!") print("Read the source or Readme.txt for more info why this is the case!") print("Bouncer started.") daemon.requestLoop() Pyro4-4.82/examples/diffie-hellman/000077500000000000000000000000001416147301300171465ustar00rootroot00000000000000Pyro4-4.82/examples/diffie-hellman/Readme.txt000066400000000000000000000033461416147301300211120ustar00rootroot00000000000000Diffie-Hellman key exchange. Pyro supports a HMAC message digest key mechanism. You'll have to set the same key in both your server (on the daemon) and your client (on the proxy). It can be problematic to distribute such a shared private key among your client and server code, as you may not want to hardcode it (especially in the client!) There's are secure algorithms to tackle the "key exchange" problem, and this example shows one of them: the Diffie-Hellman key exchange. It's based on calculating stuff with large prime exponenents and modulos, but in the end, both the client and server agree on a shared secret key that: a) has never publicly been sent over the wire, b) is not hardcoded anywhere. This shared secret key is then used as Pyro HMAC key to authenticate the messages. A few IMPORTANT notes: - in this particular example there is NO ENCRYPTION done whatsoever. Encryption is a different topic! If you want, you can enable SSL/TLS in Pyro as well to provide this. However, if you use 2-way-ssl, this makes the use of the HMAC key somewhat obsolete, because mutual verification of the SSL certificates essentially then does the same thing. See the SSL example for more details. - this example shows an approach on a safe way to agree on a shared secret key. It then uses this for Pyro's HMAC key but that's just for the sake of example. - it's a rather silly example because in Pyro, the HMAC key is a per-daemon setting. ALL calls to objects on that daemon will use the same HMAC key. Re-connecting a new client to this example server will start a new key-exchange and reset the HMAC key to something else. So, only a single client can talk to this server at any time. That's not something you want in a real life situation! Pyro4-4.82/examples/diffie-hellman/client.py000066400000000000000000000014221416147301300207750ustar00rootroot00000000000000import Pyro4 import Pyro4.errors from diffiehellman import DiffieHellman dh = DiffieHellman(group=14) with Pyro4.locateNS() as ns: uri = ns.lookup("example.dh.secretstuff") print(uri) p = Pyro4.Proxy(uri) try: p.process("hey") raise RuntimeError("this should not be reached") except Pyro4.errors.PyroError as x: print("Error occured (expected!):", x) with Pyro4.Proxy("PYRONAME:example.dh.keyexchange") as keyex: print("exchange public keys...") other_key = keyex.exchange_key(dh.public_key) print("got server public key, creating shared secret key...") dh.make_shared_secret_and_key(other_key) print("setting key on proxy.") p._pyroHmacKey = dh.key print("Calling proxy again...") result = p.process("hey") print("Got reply:", result) Pyro4-4.82/examples/diffie-hellman/diffiehellman.py000066400000000000000000000217301416147301300223120ustar00rootroot00000000000000import os import sys import hashlib if sys.version_info < (3, 2): raise RuntimeError("You need python 3.2+ for this") class DiffieHellman(object): def __init__(self, num_bits=544, group=17): if num_bits % 8 != 0: raise ValueError("private key size should be multiple of 8 bits") self.num_bits = num_bits self.prime = self.get_prime(group) self.generator = 2 self.private_key = self.make_private_key() self.public_key = self.make_public_key() self.shared_secret = None self.__key = None def __str__(self): return """""".format(self.num_bits, self.public_key, self.private_key, self.shared_secret) def get_prime(self, group): """ Return a huge prime number. Groups according to http://www.rfc-base.org/txt/rfc-3526.txt Group 17 is secure enough for an AES256 key if used with a private key (exponent) of >=540 bits """ if group == 5: return 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF elif group == 14: return 0x0FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF elif group == 15: return 0x0FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF elif group == 16: return 0x0FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF elif group == 17: return 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DCC4024FFFFFFFFFFFFFFFF elif group == 18: return 0x0FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF raise ValueError("invalidprimenumber group, use one of 5, 14, 15, 16, 17, 18.") def make_private_key(self): """make a private key from data returned by the secure random number generator.""" random = os.urandom(self.num_bits//8) return int.from_bytes(random, "big") def make_public_key(self): """Generate a public key X with g^x mod p.""" return pow(self.generator, self.private_key, self.prime) def check_public_key(self, public_key): if 2 < public_key < self.prime - 1: if pow(public_key, (self.prime - 1)//2, self.prime) == 1: return True return False def make_shared_secret_and_key(self, other_key): """Compute shared secret (g^x mod p)^y mod p, which is actually: other_key ^ y mod p""" if not self.check_public_key(other_key): raise ValueError("other key is not a valid public key") self.shared_secret = pow(other_key, self.private_key, self.prime) self.__key = hashlib.sha256(self.shared_secret.to_bytes(self.shared_secret.bit_length()//8+1, 'big')).digest() @property def key(self): if self.__key is not None: return self.__key else: raise ValueError("shared key has not been calculated!") if __name__ == "__main__": a = DiffieHellman(group=5) b = DiffieHellman(group=5) a.make_shared_secret_and_key(b.public_key) b.make_shared_secret_and_key(a.public_key) print(a) print(b) print("Same key?", a.key == b.key) Pyro4-4.82/examples/diffie-hellman/server.py000066400000000000000000000030351416147301300210270ustar00rootroot00000000000000import Pyro4 import Pyro4.errors from diffiehellman import DiffieHellman Pyro4.config.SERVERTYPE = "multiplex" @Pyro4.expose class SecretStuff(object): def process(self, message): if not self._pyroDaemon._pyroHmacKey: raise Pyro4.errors.PyroError("no hmac key has been set, can't call this method!") print("Got the message:", message) return "message was received ok" ns = Pyro4.locateNS() daemon = Pyro4.Daemon() daemon._pyroHmacKey = b"will be set to shared secret key by KeyExchange" uri = daemon.register(SecretStuff) ns.register("example.dh.secretstuff", uri) @Pyro4.behavior(instance_mode="session") class KeyExchange(object): def __init__(self): print("New KeyExchange, initializing Diffie-Hellman") self.dh = DiffieHellman(group=14) @Pyro4.expose def exchange_key(self, other_public_key): print("received a public key, calculating shared secret...") self.dh.make_shared_secret_and_key(other_public_key) print("setting new shared secret key.") global daemon daemon._pyroHmacKey = self.dh.key return self.dh.public_key # The key exchange service can't be part of the same daemon as the # other service because it must not have a Hmac key set on the daemon. # So we create another daemon without hmac key and combine it. key_daemon = Pyro4.Daemon() uri = key_daemon.register(KeyExchange) ns.register("example.dh.keyexchange", uri) ns._pyroRelease() key_daemon.combine(daemon) print("Starting server loop...") key_daemon.requestLoop() Pyro4-4.82/examples/disconnects/000077500000000000000000000000001416147301300166165ustar00rootroot00000000000000Pyro4-4.82/examples/disconnects/Readme.txt000066400000000000000000000021171416147301300205550ustar00rootroot00000000000000Example code that shows a possible way to deal with client disconnects in the server. It sets the COMMTIMEOUT config item on the server side. This will make the connections timeout after the given time if no more data is received. That connection will then be terminated. The problem with this is, that a client that is still connected but simply takes too long between remote method calls, will encounter a ConnectionClosedError. But you can use Pyro's auto-reconnect feature to deal with this. The client.py code creates a special Proxy class that you use instead of Pyro's default, which will automatically do this for you on every method call. Alternatively you can do it explicitly in your own code like the 'autoreconnect' client example does. A drawback of the code shown is that it is not very efficient; it now requires two remote messages for every method invocation on the proxy. Note that the custom proxy class shown in the client uses some advanced features of the Pyro API: - overrides internal method that handles method calls - creates and receives custom wire protocol messages Pyro4-4.82/examples/disconnects/client.py000066400000000000000000000046321416147301300204530ustar00rootroot00000000000000from __future__ import print_function import sys import warnings import Pyro4 import Pyro4.message warnings.filterwarnings("ignore") if sys.version_info < (3, 0): input = raw_input print("You can run this client on a different computer so you can disable the network connection (by yanking out the lan cable or whatever).") print("Alternatively, wait for a timeout on the server, which will then close its connection.") uri = input("Uri of server? ").strip() class AutoReconnectingProxy(Pyro4.core.Proxy): """ A Pyro proxy that automatically recovers from a server disconnect. It does this by intercepting every method call and then it first 'pings' the server to see if it still has a working connection. If not, it reconnects the proxy and retries the method call. Drawback is that every method call now uses two remote messages (a ping, and the actual method call). This uses some advanced features of the Pyro API. """ def _pyroInvoke(self, *args, **kwargs): # We override the method that does the actual remote calls: _pyroInvoke. # If there's still a connection, try a ping to see if it is still alive. # If it isn't alive, reconnect it. If there's no connection, simply call # the original method (it will reconnect automatically). if self._pyroConnection: try: print(" ") Pyro4.message.Message.ping(self._pyroConnection, hmac_key=None) # utility method on the Message class print(" ") except Pyro4.errors.ConnectionClosedError: print(" ") self._pyroReconnect() print(" ") return super(AutoReconnectingProxy, self)._pyroInvoke(*args, **kwargs) with AutoReconnectingProxy(uri) as obj: result = obj.echo("12345") print("result =", result) print("\nClient proxy connection is still open. Disable the network now (or wait until the connection timeout on the server expires) and see what the server does.") print("Once you see on the server that it got a timeout or a disconnect, enable the network again.") input("Press enter to continue:") print("\nDoing a new call on the same proxy:") result = obj.echo("12345") print("result =", result) Pyro4-4.82/examples/disconnects/server.py000066400000000000000000000007231416147301300205000ustar00rootroot00000000000000from __future__ import print_function import logging import Pyro4 logging.basicConfig(level=logging.DEBUG) logging.getLogger("Pyro4").setLevel(logging.DEBUG) Pyro4.config.COMMTIMEOUT = 5.0 Pyro4.config.POLLTIMEOUT = 5.0 # only used for multiplexing server class TestDisconnect(object): @Pyro4.expose def echo(self, arg): print("echo: ", arg) return arg Pyro4.Daemon.serveSimple({ TestDisconnect: "example.disconnect" }, ns=False) Pyro4-4.82/examples/distributed-computing/000077500000000000000000000000001416147301300206275ustar00rootroot00000000000000Pyro4-4.82/examples/distributed-computing/Readme.txt000066400000000000000000000024711416147301300225710ustar00rootroot00000000000000A simple distributed computing example with "pull" model. There is a single central work dispatcher/gatherer that is contacted by every worker you create. The worker asks the dispatcher for a chunk of work data and returns the results when it is done. The worker in this example finds the prime factorials for the numbers that it gets as 'work' from the dispatcher, and returns the list of factorials as 'result' to the dispatcher. The client program generates a list of random numbers and sends each number as a single work item to the dispatcher. It collects the results and prints them to the screen once everything is complete. *** Starting up *** - We're using a Name Server, so start one. - start the dispatcher (dispatcher.py) - start one or more workers (worker.py). For best results, start one of these on every machine/CPU in your network :-) - finally, give the system a task to solve: start the client.py program. Note: The dispatcher is pretty braindead. It only has a single work and result queue. Running multiple clients will probably break the system. Improvements are left as an exercise. Note: because the workitem is a custom class that is passed over the network, we use a custom class deserializer to be able to get it back from Pyro. (otherwise we would have to fall back to using pickle as serializer) Pyro4-4.82/examples/distributed-computing/client.py000066400000000000000000000041731416147301300224640ustar00rootroot00000000000000import random import sys import Pyro4 from Pyro4.util import SerializerBase from workitem import Workitem # For 'workitem.Workitem' we register a deserialization hook to be able to get these back from Pyro SerializerBase.register_dict_to_class("workitem.Workitem", Workitem.from_dict) NUMBER_OF_ITEMS = 40 def main(): print("\nThis program will calculate Prime Factorials of a bunch of random numbers.") print("The more workers you will start (on different cpus/cores/machines),") print("the faster you will get the complete list of results!\n") with Pyro4.core.Proxy("PYRONAME:example.distributed.dispatcher") as dispatcher: placework(dispatcher) numbers = collectresults(dispatcher) printresults(numbers) def placework(dispatcher): print("placing work items into dispatcher queue.") for i in range(NUMBER_OF_ITEMS): if sys.version_info < (3, 0): # python 2.x range arguments needs to be within C int range number = random.randint(3211, 12000) * random.randint(3211, 11000) else: # python 3.x allows arbitrary size range number = random.randint(3211, 4999999) * random.randint(3211, 999999) item = Workitem(i + 1, number) dispatcher.putWork(item) def collectresults(dispatcher): print("getting results from dispatcher queue.") numbers = {} while len(numbers) < NUMBER_OF_ITEMS: try: item = dispatcher.getResult() except ValueError: print("Not all results available yet (got %d out of %d). Work queue size: %d" % (len(numbers), NUMBER_OF_ITEMS, dispatcher.workQueueSize())) else: print("Got result: %s (from %s)" % (item, item.processedBy)) numbers[item.data] = item.result if dispatcher.resultQueueSize() > 0: print("there's still stuff in the dispatcher result queue, that is odd...") return numbers def printresults(numbers): print("\nComputed Prime Factorials follow:") for (number, factorials) in numbers.items(): print("%d --> %s" % (number, factorials)) if __name__ == "__main__": main() Pyro4-4.82/examples/distributed-computing/dispatcher.py000066400000000000000000000024531416147301300233330ustar00rootroot00000000000000from __future__ import print_function try: import queue except ImportError: import Queue as queue import Pyro4 from Pyro4.util import SerializerBase from workitem import Workitem # For 'workitem.Workitem' we register a deserialization hook to be able to get these back from Pyro SerializerBase.register_dict_to_class("workitem.Workitem", Workitem.from_dict) @Pyro4.expose @Pyro4.behavior(instance_mode="single") class DispatcherQueue(object): def __init__(self): self.workqueue = queue.Queue() self.resultqueue = queue.Queue() def putWork(self, item): self.workqueue.put(item) def getWork(self, timeout=5): try: return self.workqueue.get(block=True, timeout=timeout) except queue.Empty: raise ValueError("no items in queue") def putResult(self, item): self.resultqueue.put(item) def getResult(self, timeout=5): try: return self.resultqueue.get(block=True, timeout=timeout) except queue.Empty: raise ValueError("no result available") def workQueueSize(self): return self.workqueue.qsize() def resultQueueSize(self): return self.resultqueue.qsize() # main program Pyro4.Daemon.serveSimple({ DispatcherQueue: "example.distributed.dispatcher" }) Pyro4-4.82/examples/distributed-computing/worker.py000066400000000000000000000031441416147301300225140ustar00rootroot00000000000000from __future__ import print_function import os import socket import sys from math import sqrt import Pyro4 from Pyro4.util import SerializerBase from workitem import Workitem # For 'workitem.Workitem' we register a deserialization hook to be able to get these back from Pyro SerializerBase.register_dict_to_class("workitem.Workitem", Workitem.from_dict) if sys.version_info < (3, 0): range = xrange # make sure to use the memory efficient range generator WORKERNAME = "Worker_%d@%s" % (os.getpid(), socket.gethostname()) def factorize(n): """simple algorithm to find the prime factorials of the given number n""" def isPrime(n): return not any(x for x in range(2, int(sqrt(n)) + 1) if n % x == 0) primes = [] candidates = range(2, n + 1) candidate = 2 while not primes and candidate in candidates: if n % candidate == 0 and isPrime(candidate): primes = primes + [candidate] + factorize(n // candidate) candidate += 1 return primes def process(item): print("factorizing %s -->" % item.data) sys.stdout.flush() item.result = factorize(int(item.data)) print(item.result) item.processedBy = WORKERNAME def main(): dispatcher = Pyro4.core.Proxy("PYRONAME:example.distributed.dispatcher") print("This is worker %s" % WORKERNAME) print("getting work from dispatcher.") while True: try: item = dispatcher.getWork() except ValueError: print("no work available yet.") else: process(item) dispatcher.putResult(item) if __name__ == "__main__": main() Pyro4-4.82/examples/distributed-computing/workitem.py000066400000000000000000000011321416147301300230370ustar00rootroot00000000000000class Workitem(object): def __init__(self, itemId, data): print("Created workitem %s" % itemId) self.itemId = itemId self.data = data self.result = None self.processedBy = None def __str__(self): return "" % str(self.itemId) @staticmethod def from_dict(classname, d): """this method is used to deserialize a workitem from Pyro""" assert classname == "workitem.Workitem" w = Workitem(d["itemId"], d["data"]) w.result = d["result"] w.processedBy = d["processedBy"] return w Pyro4-4.82/examples/distributed-computing2/000077500000000000000000000000001416147301300207115ustar00rootroot00000000000000Pyro4-4.82/examples/distributed-computing2/Readme.txt000066400000000000000000000024671416147301300226600ustar00rootroot00000000000000A simple distributed computing example with "push" model. There are a handful of word-counter instances. Each is able to count the word frequencies in the lines of text given to it. The counters are registered in the name server using a common name prefix, which is used by the dispatcher to get a list of all available counters. The client reads the text (Alice in Wonderland by Lewis Carroll, downloaded from Project Gutenberg) and hands it off to the counters to determine the word frequencies. To demonstrate the massive speedup you can potentially get, it first uses just a single counter to process the full text. Then it does it again but now uses the dispatcher counter instead - which distributes chunks of the text across all available counters in parallel. Make sure a name server is running before starting this example. NOTE: ----- This particular example is not a "real" distributed calculation because it uses *threads* to process multiple Pyro calls concurrently. Because of Python's GIL, threads will NOT run in parallel unless they wait for a signal or are doing I/O. This is why this example has an artificial timer delay to make the compute calls not cpu-bound thereby enabling actual parallel execution. For "true" distributed parallel calculations, have a look at the other distributed-computing examples. Pyro4-4.82/examples/distributed-computing2/alice.zip000066400000000000000000001604711416147301300225230ustar00rootroot00000000000000PKFc~L¡^ alice.txtYn35wpEEm> .]^ WP$k]{TGFiwȯ}m>$Z}m[GЬS]TZ͓ 3~j[jTm'Jԇӓ`G(L- mdu {qkW:=T-5Q~?ۖ30_$k.{Oи qOyNUz_NO5:a:zO?:=VGUxU]6-4rЭv^owuwU˫B-Z<_/՗o_ߨ|n-<Npw3QENY]_շͷ7=Y,w{_]}?Lcjդ5AR{lɫI 8;S `b טˋPZi\Z'Y|qR$ɰcmR1h\$ xU FWt̍HYV"u􍚙q'hĴA&ec⡭XD HpCx}сs#~r$:,ӉJѴF DhLu]( h<0}i>_/!@\2"bt4,T%EB&[C/ S|k\zȈGQ=@!gڪLd/"KrZ񨘂nCfZ^g,}˴1pzR5Pps= Ks+Q/&u4ɿg H@<b caq3i띞gyS<*.W5`1.\/LE j!'l{/S`l*)ڂcJaBz j`dH)p,+~2Lx$+5\ s`LO!QaLW":ֳqƙzWPjI-0i1GvHc4e+C='Ww_zf> ?mI0' (vpAͫaJ# сtLLV;͡*v,/=I ,a,''?ٿlP2EDs izuzt+j$8y{ DDD,i7x~$,PS+idG>],(J Oܐb3N!n/ B֍dQrLBK -&Oyqh?1fgf"_!\8(ck(8i|J%"CNo7ΔWJBԱt WdtPS36#Gq^MLbӐNNRϹHW xtYvQa˜xW8ԇc5cVnw :Wc\TcT-}Ǘ%Cd58z3$menjf*:ޱVw+C~ҥVsn͖S^1WTo -ЎUeNo~_N пZv&z "ф8vJj& :H!RBs %Yac`|"]h|-[0ŰÃ΁P!&#J|t;V< b^ʌ#|ʹ r1P̐c6%F0alM5n]`nolr:IG2oD|J:W@Y nf~>GΖz֑G3ÂHْP=_V|M-AxH6  iLS{F.4d V6iYs?LpJ2UD*cWt842 x@ jGΉx݄p2d/=6û,; _ \@JN$<ݚflB =xZB9%<7LPU~ ڎ^~̛WkGaezOJ˗VQ VyNN @KF%†bT/i/P):[IWhGGu)C(w+AWJ` rJC'jC/M-BMB'x6ϲQ7>r*ET6|`Mg^YFlZO# HKiM*LJܵxqWπG=+yf%]~Ddwr}K'Q(N["zHXvT=;gӟ}p {-}@Zy'~0[*"Mq3ZDPni]Ң[S__+cb T:RAwx[a|,U{eAˎ;uVYGX(M}LQC}KR7g fڢfxFpow85s@%9%t$e GfI 8ݦ4`k5]FVc*?g-QK;@qR<QG 墕`, s4h^|wQ䉓`G<N PQ>P˛R]Caޮ&@"*`tLq+Ы$&CT8P)#몫##p:7{X7^+:)vkmKCHiCo#!H yEÜfQ֌|Q \: 0v^q",4\E%#t\WQ%S^[w#yL_AX|Tv$0Q0| W* o ^N(uJ yj`ZM-  p7yQS' c, "DC5\C]kKi„IlRäTFw$K.݆(42kg_$cѦ<1]nNLBr;Ajq5V(2m&cwOiٷj\muSωԯiļ}WުUEeRGB  {MT4KׇlDzy!D84#@|>`ҟqҫSJuacW!KFX%xD-|9ra{Mzv Dl"49q[.E%\Rs+=3 ~dyWnVZ0xPū#nSRjw\Ft>kݷMD%\lR 2gsG߼H9da!d9EHLd";τ`W[TJ&zBBEެj<6Pj^!V<ʎY2:>&r{?Vt !i[Blĸb U ț= (<]Q7 ,d^z~HİKIg?#0zT ۏ橩).MNjQ_"Š{ra['B3vytfc8HI/L닀t2_3_0BK 1Y&yitY|BtYdD7G-|yo_7y _x~_ ?{U”g׫vPr<) :4j/͡>hM~*ӊ)e9/uey=lO&$lMh /=ᡴ )Ākߙ*x xޜ{[ QA g(@rzŎN#G1 hY&'G5N1/-ޒ{I@s_\ #4"er-F@Ч/MOoֈpDW; UIOP@VWΊzOtFbѣ \ÄVmtDs5Ye2]S 8܉[vEҾ'j|-/x/*eQGIC E'ŧJ)Tm4pIK?R50+#}0x_٫T6+WiI%3<燹}&${.T1+).*:D7DB|OF 6eEW[pG7׬4|~Jefk@X W%(m܅$NAI/GirR K 8 :MΝNYcy&z=1Ө\>X{8Opui3zMIƧ$&:/5e 6P^o2&|`bC 2P-S@ݛC6Z~um1W@tEŢʖ!Mݯ9(# !% kgkCIOi UûJy#r1}<l=DGmK|2=węsk -Vw!6uUf-NYzn9EcP@]h5>Qa,cojZs,T,p1Z80iO4Xww[NtdgkyĹ2oyרйܛ^3lc%hl-o*ģf0(8: N~5U̽)ebs[G y@䖣c~]׺njS%jh!Pxww觫#6|/o254MR3/vtq0r2NU Ak*[WQTG1RUi C`J<x}n~$SȿXn0{Ծ6}GxЃv*ٵRl箅ӑ\ՀY5Ɲh%+k,h3,'B lT1T*8*nWᎈ# L3jV2OfY6=&%?\0Rw$S.KuYö7ldn*.^Ji#IUQs-M(uD rvPJ@ZtkWچGxi]O>"tDkL[]4v /(ۙ)W֒OQC#wq͒/&?]'ZDnV#.˸[  Խ0B]Q d枣(^/I"koL,2N[F$7U<^Zљؙ`)3n*!M10%)Md }Fwp$:i\`|w1=Y(ewHŵdEtvبJ$`/ XѽֳJ ,򆂏Pᇔr?ȩXxCCJ4UEK7C%j=@,v:|=b7.K Ghڶ$&+:K~\J+ guNڵ ұ?U"=\BvO"k]r52R?XWHܷ?jnR֔)H`:}ޡ}HqBc_f_0uVÉUR"ؑ3. 奣~zaki>Ɩi^Ǟ Ktq}V \SW %$O>62mv56ֳDLw"G!$[8-Ax\zsiX»f%<9LB.'DH=T@*8W5KD$fM8,!K!Upᜰk4eF|=6Kdj5L?O4DԡT> -%_ ֞8xR r-V @6AXbjUFs֋LGzM 7<cO;e`F4K[首%FꆨD5,/l0v b)@CR;s 2䆱89f R3&[~X;<|7dȶZOQ e;.&4ZExΕ3rCrs#jѡ D)2r'RJ~JB@a ɨ~ұ]jf'dBuP&=a0]`8E']I΂RYfS/Kӛ rLe92jW;\i~5"`k#C4lBT')pa`5N]3kW3.4TFTнBmgˍ5BѬN.b#]cn2s%B$؎]"T$ -ͷ9Es5𕪾C]!$;=}iƨց|;Iͱ7$w!Pq!\@E,9;D!]䏂^lY9^Q'BAp)ϲNW j:^tTL?^܉dS3q'mK*yۜ"*GӷnMystk!ԡ&7>=Xmr $ Y&fP" <3ۡi!.LyX\ư*e7:J`uf?z]p{CJUzw3 +JScWiw{Vu SLꙬcdQj@I[#BɊVR}HL [/8 ":DL.I-O`}/ .żE(ܩ_J |Dx/\SH)[/TK؝g<%tQu\1\#0'ޕ=4}&_]I\aue劫l4'"`>8"hE @ 5{02nllNRLd͊;h%5NLQgwmhԍY:W4ł7/laӱNP%1֘t G;a|ЏsOвfl1qR츋 X#rb,-.VjWKc$n%MO`K4`1̱XoӎM`*+ěR*FEܠ̘Nb>-7u:דL;BT% m9V;dѻ1>H=MCZRڡ4KpE>]yᅦu$? ҟVͳc-Uxdi˝!@D WF }P]RW7?Cc̻ew yBMbUr\P:_p̬.M ͒ 4#{phvqjow77!X W,z$S)Mmǀ+dۙLN<&j 1- NeXݱmfΟLW̶1}n DFwBq/24컓F40UKi)Pd3Vxk~+-]]D)zIeO޽⛪\u9ft6G.&^隭Ab˲ǁァᄠVxޕX7Rv6io{K¤-jN>niA/T>WR/¨蔶Y$MzAΗb&3i iaK6JLPBO?mz:d[x13յcyCC;Gqz%]ҵ&چP#m Ib$HMG$AŮ8IIP5{Af;j7[-;yVu-5eXJjQl }^vYBK{a9 7ocJ67> khH_ * $LĴQ,qD4u6ZSԑk HyY ԏTUv?ObGi&ntM? ;iyk p ݻ埯iYameӀLw쫨QUU_ZLUSthF?)(9oJ*[| ,g- á0̪TNK"][: ~%]d{se; ~_ Z$4Y׬١M4R-,2Xh95ᛒJ`^Roԑy[heu"l+dd1~ xII9*הiףt"GG4mYS6~KJ43֘wy Jn5|[)=jynJ |jyaDžŽ+ȸW2F`%'0F; Q`m(;lW8^ׅxJ3SUNn\|X,+QTRYћvՕ)=*c*4$'dF+8UjUm`,q XjYlW( C-+$t.*ݠ!QB:0C{[t\tISIݸ4Qd8>5rC 0Auʑ!>(Q/02ɜPP(\6 QFnWy"lLw?\5sra"Ii)3@EEk-!Gߦ B<"<E}yw`> Ġ^K%"!Lg& m #hAƤ/bƯR4FzU#G3Z0R< z es !OKW2QǬ@:H_u)60r1ib/QCieΡ`%N)k"~AўDߴj9HN[P˴HАMs`I^붚Y=ܻ\%M+<ך, @yXl]\˒ѢJ,*IZi@!]\ä4v%+"t BϑͰUw)*7 }&Xs~.-V^fw+&<2hBhiRjYq-y$8_K,G O̊ЂnÆ,bRKI{Dcqb;y_ÜtHЬ*yfPY?/v< sn zc{(o} eW0րJܫ\(?HI<G. ElӈIr~ȁk:Onқr$9wWf XeGIuy?Ŗ- "E%EhOY#ci2;lO_T.%jٽސ y+4"&Ybq>[apX*~Q ުo0-`ҷ%%4C^97z*$;!z%<\2ƶa|-}I>1iDr'7UDzWf*BaZ#-xfð3+*4`t0뀆t}d԰3"I7]X+rE?8v/VuUPxRl\xx7\1\Rt|˥@lofy`rfQ4Uûl*7ُygwl't`28['SK+DtONE7P[PP-5ώssM'ΨMRS8D QSbi3Q.8ЍrJU[ `Պso^BZA},$Aͅ1*6Z֒2]um\Ř-"*184<ŴwV4YWnpФn QN@18JP2&ri}xiU  ,|'8@Xŋ= Bx>e/rucZv%2T~-8Iu36)U~,6kRfVPaҷ깎+>P€Hf?Ǩk5KYq#=D`hì"m@T3HۺոJ1RbRኣpqK C |Guzro7f&pXTk7I) %iyn6$̟0f"O{EPյ欷'GcAY !i)ٹ_%&x%q.rSP5^dnTTӷ8-M:t_ +cuS2U V-gIaQP"FwQSBepBxLTUvM`>%`<7$+Nwb|? WA`MN\klPnSɓ!2=-ؤ j,X&n<ƿ'e:AINٔх1+ƮxaAvb Φj lb #;5exp*SQ9c5G>Wd// BqH?({jF76U6KU#Yi fvȶ:)dcs 衸H9qX1jls`zƍ>Aiyr퀒"obm:27DDgHy)dqMX"ɹwh^SIT? {GY\`SCLܘv'VRMrj* Ald ԩC}mOamWm6rQuYPH)Կܤ* ɼb æT &|PszB"~Q1\b7*2?WUOv!_Fxrq P rU]_ _@zJOS@oI KN/7pZ.HSRO<gҕ|D{nUjă"+Baz(z·%" g3v\BV6?V0Dy ȦVA TK`l:#{")Jzir6株603HBTĂZ.SH*tvFQ ^9z'h_dW)MG(c'{|HԒ/YW CN9LkEb{Uiy߇wo!}e!tg'-/ gmR3(93wwukHt6SX0FYBjv6jvS] R= sfӕ ?H.^w| ?D_9Q1rԕ2XH< 'i{n& V4VwOQm;}n'_5f+ৄ#\6PJOA\"]12dTv_V(®E@.W"7|7l`=v'&1MTܔ5"!8;NT`mT7w9*pqt[N2fs>E$%̨ɿOV*m]Dkyk ӚչǠ"eʔQ&?w zAJ8pX*)!vBJ2·޲̹Z5ʋy)QI9ƶ"NsRX7\zq' u u2$˟GMnp?q]ec4q{y7?Xūɗw??eWG٤ث_Wy}XDot:e<{fH^~5}_e]ԴG1̐|gREWC!Tc ^pŀqzbK!ޟDB^v5KWWc@s4XTZkA0Nد+<- 5_$H${EG0 :>e{>^z5dW9|huZ+FB]xa*Y(Jivb^Ȏ2nvT& #5 %eQо9IO~0].VtRŃ2t+-^qRv59á?/7e2N c~.ØwA "9bc[DqO0*տ)g^xKil#awIMD"#xXGUx,`vˁlyK`iw>A.PvXy%^ r~+Qظ)ﴎk2m@/g<@^*PUlDdswu>Md/Էriz+%T-%$-aVGɭjb|}h= ;_t֞Q=Wyme7>2޳)hg=ѨD&q!$ VoƆF<= +@#DeT\2]ՃD4O$a'NʗP @:^`'nG<?3տڿM 嶛ي@ yP'K6ubT҄^@@mqyk)bnlU<&{M wPpӉjБȭۯAs5nmmij3?.RywPGP#7Ѹ~p7KT{|L$ ]V#=܅35<^^"0/e%{bQؙ.r9.; +*SF# sma\Cƙ"?tkF*+{)@"䚡B祜)"ǴpPCeHM xQ/\]ڌ,|c -.#[zLzv4-Zd=M2wp 'hyw;NΒ_>d"Z!g mT3` jPr~Ѝ}\giOQ(f%Olk'h.VLYC%ohN=s/pk.e,]Lhu-c3 38\wfWtmr*.7jT蠃3Zj>ɇc]+5yEU}.2gzםyJxjBHK 'Ợ:gZB/(1NA5G݆*c`To~ I@{?zW/d!歆PPR.o\F&L T3mQ2߅ ͠Ϩ1PR6O`Z` Pd$n»"Wl6LxW97mQ n]۬Ƭ@ f[M2'VH`3\Fh`)oLDS!7rRj0|.r@_7 Ŕ1 ΍f2 d*+{[VPN3kƃ!}LȔ+ + Q/k| bv}/#߱/ptqs2oy i+--79c%W!V9?L" (a.4uaFa+*UNIc~0](JH v~ׇ,UJ6iش جh;/p\ +; l-n+yP5Hi8-ҭMrZpJ*Oɺ΅=9QS7,~]Ѹ1L!n%˫,(=Mac79Kw7й|rfawIey4Qv%PM1 S:A%oQ3+гR)'s-UcɰơY*)7I]LYVF.Rd<2PTyws/IO9hWρD=rT8dq82 ԆB>!v5g]myV50}F`Jlɤ+g)xUOZJyo-+>;#â&N-B*g] #^g">`ݜ%;oFdr6agv~wes}G|V\Q'|X11 x<9EnPk 7ϣl_br6ʶW;Ŋ'KPJ(<3e$tzٽ|1Aou"<¦W pǀ!nC@Eӑ.%b,T^KZԺyܵl_R /|XL$HBL<9 Bs,ƶD﮲| _sG7alh[ =usnTN(XLp_ _؝{{Y.,NB \2K$6 I_?י0. NrCaV!js>I6  a1n,LmݎH(X YtVwKn_1bv`jE JdnʴVbnHHPW;QCm6F/X) XmZw'knYee" e\%k8o &!`cD_FE1/ Y\Wi U =8぀A&Y& 6Q`Ј'4ךewzG`iaإ)VU&-Ks mw'I1'/)-UA s[unGLI.>}7O"xJJ!OF׬V}4L)WOXʚv!ZlU8Q>zS{|SbXA)t+T T?a.su2s'Sڝ7ͼFͭnvg{d[pMw?ߝ/wP`e݄р\,?jUOݢ,񇗠.3=b+%[.hJ '֮WIQK;LWnifo2"hm7C[|P' ȩkYB*/ V/V̲dTѾLz\FsWsx(a#lqi"{K=;sv!5)8N͍z=͙ 848@z͒OL8?ވ8b@o\D$ q1sHU"gGr{kat="Q,ۀߊ i>nm/QkhP+H*d;KDlZY\->a/1yf[K! 2hMOű쁣TkZ6|H@^,k&vh߶St$[ 'A`㡎vMX5(yObpo,U">vj/F@Qa\wp\ (C[,NUݲ>W߲%CIgA~nꎴ 0G.hKcxQfJXyOF"fQX.A%*%Z)ueıuPq5zsv.T ѵd*`x ]~@bDN0E-BTH7+.gD19z>S>4rS%ݝ+7(&DJ?D'mbHB*BM$Ϊ46[)U>^(b8%m V~@HMꈰ̱>Wf^ `m0>Ym֑,$SkpX\vD%XRCb}($pKUS8]dK7jX:=ѵ&]H,4mJ$ ?Vp󞨹JycR2V򡕒;#j:+rP,r>`}!6UMip6FOt"Y$j2ˑd4־K_D[.+ٟ7i 5{BP/:p=/hYOUxO쮭Oi-=lw~vG@g?Әp&Hg[wALY%6c: e M< pvmWQ2> 75QU@4nB&tmE&Lp3wf(',0:"U(Po\;+^-D lvB1],m򞯷)@D9> drF[ߣ!m?K$pXrYcSr%j~Mo3xc E(HJ9Ar e~'tt$Mcg8NT?~BOT.*_--8̑R!4 YBr; w]oA:;z>XR %LTG5`'Ѩ{2U~!DWHd{%Y;8gz6ep[vbQ\W 8&^;c;B _=Z#2`AXa~ jguwtMtAy:d6!SSG}s4ghuLR yٲ_E3$Π w#woX&Q)M? Rz qzsHd|~R: |ќv_~2\9;|3%HR/o/2F&ES7[6hc 1V_ }0^oN T GuЖjTfp$HsR&:Þ)Dő6dgPE!¦Xfu~(.Hf ޯ* M8v` ὿'vqm9l2&dHp"xpڋCgHSB{PdxFlm﷞dkO#W^atDB&9r;R"4lɃ"sAsU*ma*^ aO"-ubI^I<2lڗ]I0rVʲ +4"wb q/q2(nڭ&R[x/*wHx62)1v*:"x*QqedȮ C*9ʜ0U2Ʉ pw`a:(rDj#n][E,b9ڽ_$|~6e vHd Mn"}:/6/_fѓy\UFJ26r#=FIweMoBI%$ZPjUl. eW²'Fr pVhmr 2n^. g5ioUZXx}GKz&}2YWNتqxӢe_ȱyaفZ^ TH`O'Ɓ9~.?_g<>󾥎&1WrJȓB)Dnu?C1iw%*͢.oz\͝;j"F\F0Do/"f\]iO}ɝ#ΦϢ\5^!2L}is(XCςIf08)aҺoL+糬,n;U(ןm@dq)mT{ؗ_zBLliG2@9Aſt=4 B1D1Zdƕ2 |jNDX%C{@,?f* e5qK"ʟx/')CW6,oƛ8߰FwH?:'t<l9edh.9+n3ݕf-$m{qgIM4m1&p W $:W]W= %؉O#{] 5H?LD8hjPtvRu+5}2坧Y*(RL>yCN/Toq= ¿ɒG!7sSwzs5XO0{3)<x]Ԗa;wGj9ww~3[}#\t%8A4X2[7jY܌M9"F=0YKJZ6SFR6Zfu&,f}v݁5*z?yD?z"ҺZ|{|m*skz'T$#!A+7w8H\0 B! :BNJQL{v[23!Q1YWUKI~29%K Ye@' #2s0̉!CMbRDўg1U?zB2^rf|'(J}1cH֣̅zeL5) 8`Y k{ v_Ȏ7诃|} vkqQB'oZOQTy^w)C$堹L40 pAǐ"٪U,1&+'N crUGd ONҿ-AK];Hv>waxXl.R>.b ޾pqIXD-0fgK𿸊abC L+Qt[}&魳{wP_"eSM(Wr7(E&2v&>D'd`~,̶s(8(!-n.sQJ#B3l[o+R+.|0k.a p@E)CU7'5U߆ &Js4 jRǂN,4,Pʥ u#Z|LzgN~8C(6B=$.,CvhARkC"A m rN=;ow*)!>%xAT Eq89kط?>ƵɤGZ}ƠԊcxCgz~`37E_rr^\ R)mR𶃼j3 ܖ!:'r3ELhkBߨ} G.D)lgWTCunfVD0xDm|0.Ye0(V$qEYF)ouemiGЖٖе)u2K#o<zlٳǠ Fh-"٘<.sĸ5SX D* mL9ˎ0¼Ϙk™EKmP{(]~\QbM9m("l/BdȆa%xmRuw0ţ'oUćNj"Xn i}mōymh۳p/W6V WU!&W%;Fb,K朑]G ^ @V!%n HNw>*FAA!$8l״ڈ`DFp3PD(>ם[&OzYG d ҸY$UiH0D@Mw'K,K2 V? ?$6c99%E8X/&u@abG竏4a5UqWD"nN~(ٷEd<|5:r?bwjlE̒~> (q'x 1a۩f* 6g7]b.t&mXZX8# te>Pˆ+ Ytv.R0eBS}jt MT/tv KJ{$fd'T?-Uعd6i>qК'KPJdp'FxCKO+;d/NZJ}mX& ϕH(7j~_]BB<= a D2.1atAcPB(G3~ WPZ4w&[/MiPz|hn.t_?.׫oJ?ޔqSz"C@p&^4CQ"&736HhRQGP([BaV|%%QӒ23K "#Jw'@^襄PK8,/,(a@cc&v|{['ĖEiDYX7Ś^x^d<9%0a*{eI4Tl!Vz #0]~ Rr}bv4d21nN+2 1"-l+ luQCzd9HT6t7W ZNl:m"t|J?Mwn:4֗&\ |Gl xVwA[[RD#(k7,%Ϙ:m~3WVWщ gOBJ@LbS $,yڠB#ͳvËzRyUPpW>QX5 ȒMy SI2m٥0hg2gs0a{s6t̄5Q| Rґ[nM-=& Xvq:P:N7 ˸'0vXX*WEl{YamѶ&?;B7Ӕ.[yVDTbE[U#3+c> #2&۲ݶ\TU $$)i1[E" l~xOsPe;Kisp Qvӱ)U0+7J)dt[{km.^GT{ Ԧ! ;C|A22f4f r&dℰJs^]8[E̊$~[8 "ų{o:UrwGQl-6H eP7oCjZ|*eQw@CqVCbuYהy ,=D^C kn4>HZs˓L&XGԖ z<TDJN=W`Ï ?r’D?kjCoTz- BTąv3is-K9hTozlć3:1Gm[%&ܸDS!k 1&Y-j?ݕs=alx\lf{ 犃!z4z_'T0,jE޿Gv DH5ХL'<Tn(A_ Mj2=1Z.M|[c E_+e-?LnM 0$ۆ1qᓦEPJyNfJ6%D)u  Z") 1vȃ& 3C~k4Iw9 -hgFloMggv{nG#)n&P '}nVsznՏ# jv̚]Dr1}߽y8!5 cW zE1A%=|B^yb(AGg }mkU{ľ؆ WnE8 i_@`J8(TPhi!C2֣;WHdhFbo2 hc\)}Pyj i XǴ 'hW[~`#S 찗 W9njA4*+~ݓl>헼;^S?q="cRYcJCj:,kb(ʛP7m,%[ 2*y%a*.>Ռ"7%){o0"i?I$Dv]੉RB*\] BEc4+]Ŧ[n.vrVּh㋁ 2Ӿ=za=l> o)_ (:~Y8 ]6q5RI2*N l:nT4pko jE8`R ;vItJM@jv1_ȴ0FŬ2h&LjQ F06SF„~r@S4Ph-1爙J6u *\LqiPU$.2OMqԔn]ݣ.?쏤GEHAL0lEH&eP6$Hnj\TvD-- DH )nBc7^s/@h܇ slۢ-ɕ$+|p}hacS+`Mƽarަ){O  ia`k>A/,p,w(9LUginJgf~4i[gc5ZAr[74i yJ(B`Omґz>?6*(!-;,ǓC5YQumBQ,-֪z|)ԇwET#loEhGr81*K}>9eM0^4!^['`cCz^ۓsq0&d齅p.4 ۬ShO0W6fak, G; NINw6nQ&iYhOICT/^?MIR|ɒځt9ɤ| oiAq ?܄(S8]&0|>)K '\ޝ}C+Iz!|Ĩ) C2 WeHLgtC s('$()6BP#FAcl͠'m`a&8Xht9•*#Ӧ*q tSt[%ºq.kZ,O@Աʍ4OٷMW`D nc#^;{Vha?* _Jϕ(&[j=ØN?o;\ ˈ{M-.Nj?di<Eb&-_է{TQe\3D4XP_dÚ5h%NT+WFjUn,_G3xӭG|7G 3qG=I2kQ(Zf#G>p*"s~m\V[GatÄ4  0\O*ԕHB.?DD=\4*kF8u4kQBkyXm(Wj>|d:})\Njtfy ,J_Af0®pwaq;m2yw݊LƵWozRHe3Y)%; t)v,4i8&Ei,3aYMط*h`JƱCƨ ǵӎC"G݅"O8Nj )-n5 `F;.F=eԔ@sӚ!0&՚R;Uyj67G,F&ho`|pz~YT\7Z8?ٵ]WJ-Yq iFg7;F Ln=j :`1ivrM/ &7tZ.:= EkcP>hWxmZ)O*%}$HT|6qyPgS13FXusʀ$T!L =ydb*<_y A$KW~AI9 J(DF:Px3 Ale@OhrО7 M@k梄uA} (;xvũPe7|&ƉnD2 > {'\xcXx\(eh )-`,鞂0'ظJ04v.xB(+ßEN M`Nfm%5xK!R; *420APkwd {AJNoSfy#-l0ߞ&l#S2)i ?6(Wk-WXYGEb/*ZL{e*8\1t::^|ipcn!+cn>ώ/FݼXx5QLIڗI+Cd#{9¦NUMf"+մS%_% UZxP0IȏivkۤikeU!\?.0aK`nT1P_!t%<`dĘemlje#e<L~dDwoyL=T>#p/ /]k=A? b݂g9jI2d19f# W6~L0ڦg:18_%!$dL15򜀁h*DJ)<`2Ƃ2)Mh~7/I Z9e91 풸bI2| ,X2hOIR=6͘w}mIp")ЇZg9.!ǧ/OQ=<_K˼s006\ٹMvxB[k@19mNl%q z`U4-HThc }># LR<#K:&2 HxR:9 êMb65 Lڛbk@J4B/R8D[dH/\SN6/⷇,ֺ&j7yG@^}U$`2a&v=L?J(7_'xi(‚VM͈hafM9d~5~0 &Fs(({E걲ίPӗi,ol&#zqį<M {w43*mh(bXNH/Lme5m)!G u$ 78gv;.KGG΁ hS lCב24 0oS#x;ق`Bù, ՄR0/x+*(^B xc RIĈ'a((Dggy*iԗjPXDVK:ZF=ZM7UثϞ>hhp3QYԻ Lx+lV_u]J/nM!"\,PNݱ@KC yHMS $hl/l|SA>~!_-":r-[<[=oةf:9{iԀvxdLU012Bp߫0门ذ bM7R}|gF6*HlG(~'03yP|ln1n3$SYDh3/ޙ23'|<T],o:gq|8pzq@<Z)dYTIj8jVBw϶ h0QƷߋwP'FO@4Uju8s9NT7L$pdȌ[lxͽd˥"Z*r-Cy 2WRΊm(Wb1C0-Z![}D{(SZE·|,g@:3(5DD)Xž%k9> 3SEqul<fa;><'Ղ.+sWOoPFXEJcȨj"^"Rlj 9 iQճN*f($,?$w xH#U]Rb)ô# }?mkXʛ} ' bGbyeb/~4F<ҰxnIfDF޸ル)YyԠ?IP!3>L$]G\q^\H4X97ϊ|{TA8ENƺ"tDDIҙ a3KUAP_Am6;uva{$M64v, .7/5NͼU;+Zj >K^D}λ| f0~S􀊖l] V=[Td [En&{WtPFL &|uD67A ۱]$БqM Pl3X=uf#| MU#I/|+-<ʅĞ_wNkϐtYԉvv@? 2 D!{9O4hS(8A .^d{.o{`vUQEe΋([8䇘gŭڦ |'i -C (=fP&o%&bkx 4EQ$7y57Hr_}9d$s,~ >FDE~bhb⫘ ৿|d@5V\r$:BE7TB\DO䭄L xgdʚ'o.HNqu4+:5QġJ>)8Iq)I3;v _R[z@qy3}:uנ7 _9꫑`SoמGzEs&#앜bE\GGAn<{e&+okGzz׹TwE4Q@dI) a&isύ(cHqĢ6YT%*ȉWo"޼/gރ/G7C,ZD{* w;D~OOQ7^Zɷa;M@ZᎏAkj#ûe\EW kp[/X^vL rXF-`h{6D4 $ndF  Ӆ#.#oQjbE!S!Q:LqvٞT7}F/M o-ֵVbRq@ 9Wsq>):m2s#^HDo_g}oB8{~-ޱwcxO,KۖB,7v#Kl\OhIQo"ϫp-!U o=ī# (*P[B1NDk.b#>^oõ}@o!?'[( ew|MfćXu`Bv p]PUX褱@e_>ʷj Bҋ]3:* )+X-jO=/zUx>)S_,(ٌ򹽃$ˡz0zbb6̸Rhɔ}Z'<iŮ #o~p{ϰGK\ض$l6@GBЉDd1u_E o @N^3*aF[` f_}Na(+ 8K:jt:Ol2om,lQ$zd/ ;GO+h׽+m.P&OOn@1o>[.cka^vQ'Hy|c=ccV( mk!Jgp=!qHl9Y;OZ;\2 g|ȆX'ټ9@Oڿ`A.Z0TG0+"ڠf0jku׏Zq8X,ۣ.~V-5yьB:+ ׉E&u䡒o[DWd@BYљÄr$%" ѴnfF][qICuj *-.<-ǸUI'~E7͋@_h>T`W뮯OΥSPr]Y$Arzˣre#`f(8|6[x "]$ ,I]ڝI aI `$>ԭ{Y+8WLΚ +e/#,5~XFOb]$8eqF͊ea od=8نcdۋ$]R%~Gԟ􈡱2L" e' #!aJfB" " zmݫQ6[X iAL1ዔ-!|eڽZSm:9*%JUa8A8lSᾼ Ϳ臓_gs}݌Ҵٶ¡e@',Hez֟%8*!u8;$?|3c7٭3%9;Ǔ Oc]IA0cvC4*d9.bo[-O~hTV tصAya*ָȲx~#a`Z"gX\9%I]/Ү2؊d8A="F(<m쁐7RhB8F4I$8PJ FN3"ԆwRJ̣:uVXIb׫Tj)4}gabwnV/RƦ%OEqP?ڗHoڎ"VoQ$+Lzx+ĘI u n-W2"(y;OAu^T=O9xT8.Sp3`3&떔p.M@ofGS;ZIu]\S;2ȳKXղ#5_?WbPPk1a>LD;2 S[t.NLy88^*E-\}QWRvڪt5ݘc?"bؚq;qx^!e&#qܕۛO2U\"{9*3F;eDRvԁEJ-N4c>] ^xU}U; p \[*\PՠLTϰ" bH[ṃE墔Le5~“*Ij:N$² a<i~;;DS^ޑ*:i_6&[ |N٪bRBEXXMPPxg)$b^.^MzC(VПрa7[JrDt#x t1C!(R3|b4iVI,*CGeg+u$kM'%CS#ME\k;|>MgGq_ YeغgoV:Q%G!"!k#&e$i{u T O)\XkC2'2Wx >.W̆Eo\HЌֽbկۨKˢBb#2l\CAT8bQE穆 C'_5(pDcR aLv (xzUڝ7i҈!GӠCg[1iUǭ.<%2:c|k@ !'4P vQnTϟ?|»v5df:|дIĴٗǰVeftxf<܁v7kbaTvX),cB݊QZM `{UiM. !UÚ}>g:_^o׿=qÇ?ιf'pjG&^: 13!}2GPV>&?%A廚 e3v -JxS+![a ~hVG)!$2xpVh4wko¾aV-T~.i["ޕv<@~c;wMqlEUIJ`-x&Y%@eY; .t?sv$}{~Wz3wh=u&ʏ+YKm߈,AtjR5^f\9BVLKcX[hf¡ޣxau9/PkB^ . L˕!UEK^Kb[ԅhPp+#85o灲/"w8y@Q0a7|a1gKO QAp.DŽq@ٓT6`w8vC߾i9 _T"؆ g<" 85q#vȧrP,@ ̝‹m7ي3/ $V \Cu3GCm"}^~RB[iUB]b(j'Z}4䂥muI,q%zK x$l$"cf_(rFK3'xa5ض' akVaQ b8"G, En7lKbt5/v)NFWjB0fQ2-2D9ckk_~ Ŋ :-VEقs[A7#8flx^r  $-oqWT /_U90/G˔d֎e)Wy0 W-l=WP^}'%MU@cy}^1vNYψՅ%l r>3҄ <~ ]r9  Egöj$o{^G}[>?*D8rY*-H>R}N* j6b#X7¾j^vS7Z)GFNdr";YHDMœ[ jEMDO\F5V=EzFOwQLw ~W^|L 0{5R+DGp4|0)ʮᡲ>X- D  U3kވϝUF1o4Zֻg~D_/gΰb\1a2}Pg҈\^Ts Giٔ|hnBj[%$ 5jq5)p(*Z%V[im(s|# \_jwl`&:'9ktJvG ~!>@_QT#[<!Gjk Q:i܁u! E23#g$zmFn%?ǬdXMCؠ^ S\RA|Ǡ; 5̥P-]'~aVH,RڔĦćqhH^)υF)+pjչ t1a!xJۿ*,=^uqF%YAt-LGe3*aID;2"m_PXfg_RdrmQ5Ч=Ak DF hNՏ,L@g p 5e๼aE UK7.;82B ऴFv2e.J"+GmYLJ8Qq`8F 2[umͯKru)Ԭ?(y'X+$xrJ!CK WV뀿wI.nAB"Aa-v(f؀TQ^_",XaTpYM-x~cwץW(i@`D0u"pٱLsԪ޲,ArZ `;)Ky B$ъA9f_.5:O&̚6 1FEar([@)j: BOhۓt!rsa:2f)מ, /.,,_/k&$\ w{= .%a7@d_eiR W"xt<ʝ6%lhAv M&4T>FӜ5+pRj+4ta5r Jd;) lXȧ\3iz 9q b4rK7$^(eG;uc3OOZ3/RRC9=!W.o+ff"&xh VK^F^sn{;٭k!rppqJF383Gl}$7S@U++\ۋA-+9PYHPW*ZZq5,8 }ud_lG`v(ʌ0d@nRJvIӡ;%K#ɝzs= /z"Z$!U!m*- |ӕvp[vG1SAި&-% 33$;^L6g " rˋA+ H *r> E^V=kLrt@V CCyU?oM3Ł= ?cgR)]ՏڟPaq CKfPySݨsiQ4&n;%Wpmuk ѿНglxf* Vh9 Ay&+3, /fqϴŢe12AH*{4B#7m N g[1-3,E\G+^W\n=`TSe@3E+oHJ\ -Vb]- 8(ԬFJΨL\F)vaIEjbQ/nK SeX- V?ۄ~f<񊍏5`}ld##b|U{B %t]O0e\q,TYx[ Y1"W8Cdʽ+rgoF/O QP tO2kAfֺ,v wh"0\ f5\AYT%'.ռ(q%9iBp (v!zŦ4 kd&˞gO"L}? g2D P]WcXѧƦ#* mqA4^Hp?!&f| e7"& 0bY=uDp0ZЅCgFuhG ua^ckJ3ס/e4bg^DAFoN:PDuz! d@6{G5Qvzy\ܛstTb2tžg:WϷ77I9e AzpE) 9QW핐G"Qn}uqkFЖ>BlcAy[[ѐi "St9M;M> oUKnمQuVJk"lV$|ja6ɾ*b#[]m2Y '%F;wH[2"t]t=ttcXK8!Q)^r[MݦZ)!=f&[8Ǡ#(S']/Tzf+L!:D{U19@jM5QSQghxdL_Ǘ.Ko!5Z? <ۍHhK3= j8)sU<%#̧e{w7%5W]K{[KdV% Wb)-7 ?,a[BcFU8" W M`wDݡ%_z@  ר‘YXHpODC(T=+[i#AopoXb:-ߏ?BzHpRpJDNGWRhcDk`M-ϖ`(D^oF7Жѕ2ng;zJ#Z䕫F.FYU|5Lo d!`br[3kq88>6EoY`G"D6,K'֑ydlA描H|K.TnA~aﰗuyK`RM>[ƈ"i=s$Wɫ։5I0PF @uh TjqSCQ!GK S~RFP|9E%;QRTDԷ@`+uI2 1a[J2bJ?EJ/fa%DLȚVAGs_~U=C_w3ڤ7 '8 Q&g'47k9s6;w Rwbivѝ6oa{DP+ gG;O0|ZU@:ɟ[wfA3IJZV`6L\{ռU\I~&3;_\dO7$;>O٧Oiv<$E ҥ< YvZ&M|سpbvcQXA)BC{$LM:ڑy2AV}6ނ#}h\ZKo .StZ[Ç 3 NB2\?ÊQP.6E}'\{!۔GOqW^8N uCk,L6?} xC,$CW>n| "Ǎ)  }3=Û ufOrkelu׏Фeēuoy\(Qt;VFI񩁈9n@2 zCUwY݀j:KraT -hlŸ͒]k/%OS2B5s넶O<*U9'}zϠ%vPTO׵7; @]hXse˙kEĔoH{";Zoڟ#`C<x!& Oi:spM!1sU+nᣦ[6]NJ] 4;6R T dJ=NImW_ 3J;_2&(%X&yZ;_Ʉ(sX8xz!zkl S{yuӶCzu8򸳉c1`Mz~^6(ɻT$'7{&ՌS?ӹk`ǺCuʥl\LK0'4S@"\xKlKqt60?{䰽|;b%` &֢r4#"lkbhY!(yAT5LE&H+FEOiku0iIŞȳ>t}Rz[#1Ef.tdY]liP4P7LaՔ.3ku~P7xo\|ӰO"gɒIFOꐑ*=IPIPbȧ6W+wO%Hy |=lɺ.sp0f(6=sItV)*DKpG8݌Uqm$sHcs&<}Sh]fw;MTa% 1U:TG+dujC1|yW|_.w+l . qЬ,_3&|T(,P*BT? :ݑ|^e ˽9l#OH+J]On4}JVeV?1%O+"rā#&;daXqj2B_/3V|6+@n/#yB  2"F'GJh퐯ݢWr\5u%`=W@9Hꖟ̿9/>zp52[8q=Rb +GȑCPwxUy)j46Qisg~QwѸRl[4sHIa_(3ah,78:6U~DUnޙ"kPYDTHWb.xCd8%SeVh{H r7>ީg* Ḋ|Z0cW3<; ^ cgxWZ6zԻ].o󇇇h_ob6OS{)JqzKh 'x(אE/HMkՕ؈5Y4vF]<_Rx4k<gM^LA@X+88^_ߥ: 0(o{nѶea:iu>__.IX"4>-KGk΄R4A}̅9/OaoyGԂh \Ãп-@q, ˙*}?)9\|S>Gӫ TcUް-H? ٗ3YdLk K HJB +m-3U#[zc5;A֨{/?vRqn_sKȯP{Ch:Tv]ZAp|~Õoyj /;/I.FX7|hDH8Ӹ\ʺD2yLZ gBO.deO{L>cG{Rvz}“,Cg w0vD9&y;0nc .Q>*Ѥrb; pkUK2DսӁc;U~s5'JQ\3,w~5f?dtĸ{O0 bn[G _ÏdAÄ>kPrsETwkp>ՃcIR%`h{y`?H]Cst6.3ng_Sxal!=/Di5t\ۢX^pgʕ4j[{셕 cz#1?$NR7bb[qM"˕DR ik_$lUEEyvVPs[% l%^.̔/n oI_>.GHHw$Ūy#՚u@$DZ!8OI҃ 774i yGҌjU_H#ĺ.Kw)~h[K zuZa)z;B{syQ$n#h:3XM\pey#*A#F}WA^L5>|#){Of gb Mp_ɮ脦'c䤉:3q"UhkhymlFVk')ȌPfh㑀l4Q/lTCe17C5ZTXZ[Hg-u1|΂YbÞnEDMFJw)餟>qm(U&Q*wCC<'_] FM'擇R{,Լ=8Ȧ-)m# ͳunq"<Q?K@j~l:L/+ hg*,#)ǭJNd.Xd+8>l)`Qe\UqN" 8 ]*KW|^>ca/M.;YLy%}_qJ?M b3 F "+V7'Ti?D14. i"kQA5. 6[i=/۰}ņgYzsX:cG]5ByAeNKY)JiU\&TvIq4lMi- D6Gxed:I%l}lGɲ,2`눛zKcNS7.š  RJZ%^uyaI]w2NȈJ݃/sUGK<8C߮(PfM0VݡxQ,^'n lXgs^γen U13Y>΍ ӿ?JgďBkJ0d ' !{5~3 R.cxCw=sxzRJ'LK *7Ia(F*Qkl擷Ӂg&޸N|nK4@~zptF0N[^W6+D\DXX7`y{zPt_ [|[JYm:!(<jNYҙ ? aS\KP. +hi){(~`<0lG(XZ/x7BcRmP@qaXmȽJǯ^#c%VsFǧ0^>p;ATlhq-xM;PpjH[O!CAoˠP^)`>3>xJ/n:2 8j܉Ƒ!J^.Z5W8 |'|v=䄅ҵ?'=Pi!aK~ e;Nn, E Cs\ܮ[T#Z\0))8 dPeCL܌.c׽ =ﮆÜ&!״ף%u/(=_/uH? 8G~) hԜ~֛M^SxnN։~ܴ)ҡӵ\y)7Z@y0p;9L[P뺪H`;x>?~ҒSIv<>ȉH[v2x<8>9`Mw;mr~r<ѣ|/qy =<=`9Sr $hr?nws.!"Sˏd-}֡p BHKZyhU3bėa*Ыj Їʥ*jb> FS'v~ 2;`طEAW_҆'AXزA~e@-ԃJȩ2rC>:ۄv$sk&l-o|%N|xtY,%E>Ho?iܪPU 6jV&w|3poIdHR\DpzjSr$\K!}w.&;\.?)='.l9pRy`O~2Vx&~'ώ'c/G3- Ot{$G2s8!d"aObg­ۘT2Ų DtF445E1#0O,őbLcC2CPޭ_)2z"YTB}9J>\Fbd}hkbFx`r""Dlp ɰ,lO>H0fF?׽2=Eeh4ݩ"-+yT3 Ap ȅܟPծ&<&?Zۗ{Jgd!p7D=^fs r2jȆ5liXn@z|}T] M~)n /Rz]TG ;|6D][v @'tЭ]e7'Ls$WznV7F}EW4v; dd+Sj0EN&AyLTAʹ&kͽ8{ 6KLfuꗛtǕN _rWśܤb&; ͡|%/5^HH0 %u?m ,zZ@,SecBU/1ƴ&E>v(^f_gPvH C^6^ՊcᏣ췿_ɇ ^Se`0 H!?Ġ#׆f'P|uq ٱ*8`/߼߼~}_o~=ʔ ʭ$(f-v22UOY(,*"MiLhj47W{FZD,*eu9+i3!i:)9Tll .6M/RAt$k.ȴPAbVw/C j!hL@x`KhHfߕL_bc]mpr~1IwΔuZSɰzs8-@WI{peL3mz6˒$ T7k@pGz xTzǬ[Ъ(p -k6^uS}$ʭe`y2jH7/82es°z(Z X4Iњ|,n<̔$k]vy:6;ʭ'&De4&vESHŶ?Mn\_.dX፯5~F i$Tb\uR c^b)dc gԶ;-Ua6',f a@ Y>w E|7>sϬ7@#:WS_SvĔ.~E%!A2kxr谞v:&hD9N7l{F ~T7X*ᾏD@=%LHP7 qgr X7| Ru@)LNH6~ 35s ׺:xЫb JmcfXs@mz(PVMO :Y%X>e0U1 $X%5Ub[DK4H\(qj|UkY~h[u0@lcf *bIQ ԕZ]u\5NT'mT@XI͋:ąg?$lO 4yxvF q:PK?Fc~L¡^ $ alice.txt l/D/ ('a','b','c'), ('d','e','f'), ('g','x','x')""" return zip_longest(*[iter(iterable)]*n, fillvalue=padvalue) @Pyro4.expose class Dispatcher(object): def count(self, lines): # use the name server's prefix lookup to get all registered wordcounters with Pyro4.locateNS() as ns: all_counters = ns.list(prefix="example.dc.wordcount.") counters = [Pyro4.Proxy(uri) for uri in all_counters.values()] for c in counters: c._pyroAsync() # set proxy in asynchronous mode roundrobin_counters = cycle(counters) # chop the text into chunks that can be distributed across the workers # uses asynchronous proxy so that we can hand off everything in parallel # counter is selected in a round-robin fashion from list of all available counters # (This is a brain dead way because it doesn't scale - all the asynchronous calls are hogging # the worker threads in the server. That's why we've increased that a lot at the start # of this file, just for the sake of this example!) async_results = [] for chunk in grouper(100, lines): counter = next(roundrobin_counters) result = counter.count(chunk) async_results.append(result) # gather the results print("Collecting %d results..." % len(async_results)) totals = Counter() for result in async_results: try: totals.update(result.value) except Pyro4.errors.CommunicationError as x: raise Pyro4.errors.PyroError("Something went wrong in the server when collecting the async responses: "+str(x)) for proxy in counters: proxy._pyroRelease() return totals if __name__ == "__main__": print("Spinning up 5 wordcounters, and 1 dispatcher.") Pyro4.config.SERVERTYPE = "thread" Pyro4.Daemon.serveSimple( { WordCounter(): "example.dc.wordcount.1", WordCounter(): "example.dc.wordcount.2", WordCounter(): "example.dc.wordcount.3", WordCounter(): "example.dc.wordcount.4", WordCounter(): "example.dc.wordcount.5", Dispatcher: "example.dc.dispatcher" }, verbose=False ) Pyro4-4.82/examples/distributed-computing3/000077500000000000000000000000001416147301300207125ustar00rootroot00000000000000Pyro4-4.82/examples/distributed-computing3/Readme.txt000066400000000000000000000013571416147301300226560ustar00rootroot00000000000000A simple distributed computing example where many client jobs are automatically distributed over a pool of workers. The load distribution is done by not connecting to a particular pyro object, or using a dispatcher service, but it is simply using the yellow-pages function (metadata lookup) to find one randomly chosen object that has the required metadata tag. It's pretty simple but is also a bit dumb; it doesn't know if the chosen worker is idle or busy with another client's request. Also it doesn't deal with a worker that crashed or is unreachable. Optimizing these things is an excercise left for the reader. *** Starting up *** - We're using a Name Server, so start one. - start one or more workers (the more the merrier) - run the client Pyro4-4.82/examples/distributed-computing3/client.py000066400000000000000000000010141416147301300225360ustar00rootroot00000000000000import random import Pyro4.errors for _ in range(100): # this submits 100 factorization requests to a random available pyro server that can factorize. # we do this in sequence but you can imagine that a whole pool of clients is submitting work in parallel. with Pyro4.Proxy("PYROMETA:example3.worker.factorizer") as w: n = number = random.randint(3211, 12000) * random.randint(4567, 21000) result = w.factorize(n) print("%s factorized %d: %s" % (w._pyroConnection.objectId, n, result)) Pyro4-4.82/examples/distributed-computing3/worker.py000066400000000000000000000026551416147301300226050ustar00rootroot00000000000000from __future__ import print_function import os import socket import sys from math import sqrt import Pyro4 import Pyro4.socketutil if sys.version_info < (3, 0): range = xrange # make sure to use the memory efficient range generator class Worker(object): @Pyro4.expose def factorize(self, n): print("factorize request received for", n) result = self._factorize(n) print(" -->", result) return result def _factorize(self, n): """simple algorithm to find the prime factorials of the given number n""" def isPrime(n): return not any(x for x in range(2, int(sqrt(n)) + 1) if n % x == 0) primes = [] candidates = range(2, n + 1) candidate = 2 while not primes and candidate in candidates: if n % candidate == 0 and isPrime(candidate): primes = primes + [candidate] + self._factorize(n // candidate) candidate += 1 return primes with Pyro4.Daemon(host=Pyro4.socketutil.getIpAddress(None)) as daemon: # create a unique name for this worker (otherwise it overwrites other workers in the name server) worker_name = "Worker_%d@%s" % (os.getpid(), socket.gethostname()) print("Starting up worker", worker_name) uri = daemon.register(Worker) with Pyro4.locateNS() as ns: ns.register(worker_name, uri, metadata={"example3.worker.factorizer"}) daemon.requestLoop() Pyro4-4.82/examples/distributed-mandelbrot/000077500000000000000000000000001416147301300207515ustar00rootroot00000000000000Pyro4-4.82/examples/distributed-mandelbrot/Readme.txt000066400000000000000000000033361416147301300227140ustar00rootroot00000000000000These examples are about calculating the Mandelbrot fractal set (z=z^2+c). NOTE: use the "launch_servers.sh" shell script to launch the name server and a reasonable number of Pyro mandelbrot server processes. First, a few notes: - The ascii animation runs at 100x40 resolution so make sure your console window is large enough. - The maximum iteration count is set to a quite high value to make the calculations more time consuming. If you want you can change both maxiter values in server.py down to something more reasonable such as 256. - try using Pypy instead of CPython to improve the speed dramatically The 'normal' code simply runs the calculation in a single Python process. It calculates every frame of the animation in one go and prints it to the screen. The 'client_asciizoom' program uses Pyro to offload the calculations to whatever mandelbrot server processes that are available. It discovers the available servers by using the metadata in the name server. To distribute the load evenly, it hands out the calculation of a single line in the frame to each server in a cyclic sequence. It uses Pyro batch calls to cluster these calls again to avoid having to do hundreds of remote calls per second, instead it will just call every server once per frame. The calls will return a bunch of resulting lines that are merged into the final animation frame, which is then printed to the screen. The graphics version is interesting too because it actually creates a nice picture! On my 8c/16t cpu the speedup of the distributed calculation of the graphical picture is massive. The normal single core version takes 22 seconds, while the distributed version only takes 2.6 seconds (and utilizes all cores of the cpu for nearly 100%). Pyro4-4.82/examples/distributed-mandelbrot/client_asciizoom.py000066400000000000000000000055411416147301300246630ustar00rootroot00000000000000# ascii animation of zooming a mandelbrot fractal, z=z^2+c from __future__ import print_function, division import os import time import threading import platform import Pyro4 class MandelZoomer(object): res_x = 100 res_y = 40 def __init__(self): self.num_lines_lock = threading.Lock() self.num_lines_ready = 0 self.all_lines_ready = threading.Event() self.result = [] with Pyro4.locateNS() as ns: mandels = ns.list(metadata_any={"class:mandelbrot_calc"}) mandels = list(mandels.items()) print("{0} mandelbrot calculation servers found.".format(len(mandels))) if not mandels: raise ValueError("launch at least one mandelbrot calculation server before starting this") time.sleep(2) self.mandels = [Pyro4.Proxy(uri) for _, uri in mandels] def batch_result(self, results): num_result_lines = 0 for linenr, line in results: self.result[linenr] = line num_result_lines += 1 with self.num_lines_lock: self.num_lines_ready += num_result_lines if self.num_lines_ready >= self.res_y: self.all_lines_ready.set() def screen(self, start, width): dr = width / self.res_x di = dr*(self.res_x/self.res_y) di *= 0.8 # aspect ratio correction self.num_lines_ready = 0 self.all_lines_ready.clear() self.result = ["?"] * self.res_y servers = [Pyro4.batch(proxy) for proxy in self.mandels] for i in range(self.res_y): server = servers[i % len(servers)] server.calc_line(start, self.res_x, i*di, dr, i) for batch in servers: batch(asynchronous=True).then(self.batch_result) self.all_lines_ready.wait(timeout=5) return "\n".join(self.result) def cls(self): if platform.platform().startswith("Windows"): os.system("cls") else: print(chr(27)+"[2J"+chr(27)+"[1;1H", end="") # ansi clear screen if __name__ == "__main__": start = -2.0-1.0j width = 3.0 duration = 30.0 wallclock_start = time.time() frames = 0 zoomer = MandelZoomer() zoomer.cls() print("This is a mandelbrot zoom animation running using Pyro, it will use all calculation server processes that are available.") while True: time_passed = time.time() - wallclock_start if time_passed >= duration: break actual_width = width * (1-time_passed/duration/1.1) actual_start = start + (0.06-0.002j)*time_passed frame = zoomer.screen(actual_start, actual_width) zoomer.cls() fps = frames/time_passed if time_passed > 0 else 0 print("%.1f FPS time=%.2f width=%.2f" % (fps, time_passed, actual_width)) print(frame) frames += 1 print("Final FPS: %.2f" % fps) Pyro4-4.82/examples/distributed-mandelbrot/client_graphics.py000066400000000000000000000036461416147301300244720ustar00rootroot00000000000000# mandelbrot fractal, z=z^2+c import time import tkinter from concurrent import futures from Pyro4 import Proxy, locateNS res_x = 1000 res_y = 800 class MandelWindow(object): def __init__(self): self.root = tkinter.Tk() self.root.title("Mandelbrot (Pyro multi CPU core version)") canvas = tkinter.Canvas(self.root, width=res_x, height=res_y, bg="#000000") canvas.pack() self.img = tkinter.PhotoImage(width=res_x, height=res_y) canvas.create_image((res_x/2, res_y/2), image=self.img, state="normal") with locateNS() as ns: mandels = ns.list(metadata_any={"class:mandelbrot_calc_color"}) mandels = list(mandels.items()) print("{0} mandelbrot calculation servers found.".format(len(mandels))) if not mandels: raise ValueError("launch at least one mandelbrot calculation server before starting this") self.mandels = [uri for _, uri in mandels] self.pool = futures.ThreadPoolExecutor(max_workers=len(self.mandels)) self.tasks = [] self.start_time = time.time() for line in range(res_y): self.tasks.append(self.calc_new_line(line)) self.root.after(100, self.draw_results) tkinter.mainloop() def draw_results(self): for task in futures.as_completed(self.tasks): y, pixeldata = task.result() self.img.put(pixeldata, (0, y)) self.root.update() duration = time.time() - self.start_time print("Calculation took: %.2f seconds" % duration) def calc_new_line(self, y): def line_task(server_uri, y): with Proxy(server_uri) as calcproxy: return calcproxy.calc_photoimage_line(y, res_x, res_y) uri = self.mandels[y % len(self.mandels)] # round robin server selection return self.pool.submit(line_task, uri, y) if __name__ == "__main__": window = MandelWindow() Pyro4-4.82/examples/distributed-mandelbrot/launch_servers.sh000077500000000000000000000004371416147301300243370ustar00rootroot00000000000000#!/usr/bin/env bash python -m Pyro4.naming & sleep 0.5 NUM_CPUS=$(python -c "import os; print(os.cpu_count())") echo "Launching ${NUM_CPUS} mandelbrot server processes..." for id in $(seq 1 ${NUM_CPUS}) do python server.py ${id} & done sleep 1 echo "" echo "Now start a client." Pyro4-4.82/examples/distributed-mandelbrot/normal.py000066400000000000000000000026321416147301300226160ustar00rootroot00000000000000# ascii animation of zooming a mandelbrot fractal, z=z^2+c from __future__ import print_function, division import os import time import platform from server import Mandelbrot res_x = 100 res_y = 40 def screen(start, width): mandel = Mandelbrot() dr = width / res_x di = dr*(res_x/res_y) di *= 0.8 # aspect ratio correction lines = mandel.calc_lines(start, res_x, dr, di, 0, res_y) return "\n".join(x[1] for x in lines) def cls(): if platform.platform().startswith("Windows"): os.system("cls") else: print(chr(27)+"[2J"+chr(27)+"[1;1H", end="") # ansi clear screen def zoom(): start = -2.0-1.0j width = 3.0 duration = 30.0 wallclock_start = time.time() frames = 0 cls() print("This is a mandelbrot zoom animation running without Pyro, in a single Python process.") time.sleep(2) while True: time_passed = time.time() - wallclock_start if time_passed >= duration: break actual_width = width * (1-time_passed/duration/1.1) actual_start = start + (0.06-0.002j)*time_passed frame = screen(actual_start, actual_width) cls() fps = frames/time_passed if time_passed > 0 else 0 print("%.1f FPS time=%.2f width=%.2f" % (fps, time_passed, actual_width)) print(frame) frames += 1 print("Final FPS: %.2f" % fps) if __name__ == "__main__": zoom() Pyro4-4.82/examples/distributed-mandelbrot/normal_graphics.py000066400000000000000000000022641416147301300244770ustar00rootroot00000000000000# mandelbrot fractal, z=z^2+c from __future__ import print_function, division import time try: import tkinter except ImportError: import Tkinter as tkinter from server import MandelbrotColorPixels res_x = 1000 res_y = 800 class MandelWindow(object): def __init__(self): self.root = tkinter.Tk() self.root.title("Mandelbrot (Single Core)") canvas = tkinter.Canvas(self.root, width=res_x, height=res_y, bg="#000000") canvas.pack() self.img = tkinter.PhotoImage(width=res_x, height=res_y) canvas.create_image((res_x/2, res_y/2), image=self.img, state="normal") self.mandel = MandelbrotColorPixels() self.start_time = time.time() self.root.after(1000, lambda: self.draw_line(0)) tkinter.mainloop() def draw_line(self, y): _, pixeldata = self.mandel.calc_photoimage_line(y, res_x, res_y) self.img.put(pixeldata, (0, y)) if y < res_y: self.root.after_idle(lambda: self.draw_line(y+1)) else: duration = time.time() - self.start_time print("Calculation took: %.2f seconds" % duration) if __name__ == "__main__": window = MandelWindow() Pyro4-4.82/examples/distributed-mandelbrot/server.py000066400000000000000000000053271416147301300226400ustar00rootroot00000000000000from __future__ import print_function, division import sys import Pyro4 @Pyro4.expose class Mandelbrot(object): maxiters = 500 def calc_line(self, start, res_x, ii, dr, line_nr): line = "" z = start + complex(0, ii) for r in range(res_x): z += complex(dr, 0) iters = self.iterations(z) line += " " if iters >= self.maxiters else chr(iters % 64 + 32) return line_nr, line def calc_lines(self, start, res_x, dr, di, start_line_nr, num_lines): lines = [] for i in range(num_lines): line = "" for r in range(res_x): z = start + complex(r*dr, i*di) iters = self.iterations(z) line += " " if iters >= self.maxiters else chr(iters % 64 + 32) lines.append((i+start_line_nr, line)) return lines def iterations(self, z): c = z for n in range(self.maxiters): if abs(z) > 2: return n z = z*z + c return self.maxiters @Pyro4.expose class MandelbrotColorPixels(object): maxiters = 500 def calc_photoimage_line(self, y, res_x, res_y): line = [] for x in range(res_x): rgb = self.mandel_iterate(x, y, res_x, res_y) line.append(rgb) # tailored response for easy drawing into a tkinter PhotoImage: return y, "{"+" ".join("#%02x%02x%02x" % rgb for rgb in line)+"}" def mandel_iterate(self, x, y, res_x, res_y): zr = (x/res_x - 0.5) * 1 - 0.3 zi = (y/res_y - 0.5) * 1 - 0.9 zi *= res_y/res_x # aspect correction z = complex(zr, zi) c = z iters = 0 for iters in range(self.maxiters+1): if abs(z) > 2: break z = z*z + c if iters >= self.maxiters: return 0, 0, 0 r = (iters+32) % 255 g = iters % 255 b = (iters+40) % 255 return int(r), int(g), int(b) if __name__ == "__main__": # spawn a Pyro daemon process # (can't use threads, because of the GIL) if len(sys.argv) != 2: raise SystemExit("give argument: server_id number") server_id = int(sys.argv[1]) with Pyro4.Daemon() as d: with Pyro4.locateNS() as ns: mandel_server = d.register(Mandelbrot) mandel_color_server = d.register(MandelbrotColorPixels) ns.register("mandelbrot_"+str(server_id), mandel_server, safe=True, metadata={"class:mandelbrot_calc"}) ns.register("mandelbrot_color_"+str(server_id), mandel_color_server, safe=True, metadata={"class:mandelbrot_calc_color"}) print("Mandelbrot calculation server #{} ready.".format(server_id)) d.requestLoop() Pyro4-4.82/examples/echoserver/000077500000000000000000000000001416147301300164475ustar00rootroot00000000000000Pyro4-4.82/examples/echoserver/Readme.txt000077500000000000000000000003541416147301300204120ustar00rootroot00000000000000Shows how you might use the built-in test echo server. So, this example only contains some client code. You are supposed to start the echo server with something like: $ python -m Pyro4.test.echoserver or: $ pyro4-test-echoserver Pyro4-4.82/examples/echoserver/client.py000066400000000000000000000016101416147301300202750ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 import Pyro4.util print("First start the built-in test echo server with something like:") print("$ python -m Pyro4.test.echoserver") print("Enter the server's uri that was printed:") if sys.version_info < (3, 0): uri = raw_input() else: uri = input() uri = uri.strip() echoserver = Pyro4.Proxy(uri) response = echoserver.echo("hello") print("\ngot back from the server: %s" % response) response = echoserver.echo([1, 2, 3, 4]) print("got back from the server: %s" % response) for element in echoserver.generator(): print("got element from remote iterator:", element) try: echoserver.error() except: print("\ncaught an exception (expected), traceback:") print("".join(Pyro4.util.getPyroTraceback())) print("\nshutting down the test echo server. (restart it if you want to run this again)") echoserver.shutdown() Pyro4-4.82/examples/eventloop/000077500000000000000000000000001416147301300163155ustar00rootroot00000000000000Pyro4-4.82/examples/eventloop/Readme.txt000066400000000000000000000013641416147301300202570ustar00rootroot00000000000000This example shows a possible use of a custom 'event loop'. That means that your own program takes care of the main event loop, and that it needs to detect when 'events' happen on the appropriate Pyro objects. This particular example uses select to wait for the set of objects (sockets, really) and calls the correct event handler. You can add your own application's sockets easily this way. See the 'server_threads.py' how this is done. Since Pyro 4.44 it is possible to easily merge/combine the event loops of different daemons. This way you don't have to write your own event loop multiplexer if you're only dealing with Pyro daemons. See the 'server_multiplexed.py' how this is done. (this only works for the multiplex server type, not for threaded). Pyro4-4.82/examples/eventloop/client.py000066400000000000000000000007201416147301300201440ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input with Pyro4.core.Proxy("PYRONAME:example.embedded.server") as proxy: print("5*11=%d" % proxy.multiply(5, 11)) print("'x'*10=%s" % proxy.multiply('x', 10)) input("press enter to do a loop of some more calls:") for i in range(1, 20): print("2*i=%d" % proxy.multiply(2, i)) print("'@'*i=%s" % proxy.multiply('@', i)) Pyro4-4.82/examples/eventloop/server_multiplexed.py000066400000000000000000000031361416147301300226140ustar00rootroot00000000000000from __future__ import print_function import socket import time import Pyro4.core import Pyro4.naming import Pyro4.socketutil Pyro4.config.SERVERTYPE = "multiplex" Pyro4.config.POLLTIMEOUT = 3 hostname = socket.gethostname() my_ip = Pyro4.socketutil.getIpAddress(None, workaround127=True) @Pyro4.expose class EmbeddedServer(object): def multiply(self, x, y): return x * y print("MULTIPLEXED server type. Initializing services...") print("Make sure that you don't have a name server running already!\n") # start a name server with broadcast server nameserverUri, nameserverDaemon, broadcastServer = Pyro4.naming.startNS(host=my_ip) assert broadcastServer is not None, "expect a broadcast server to be created" print("got a Nameserver, uri=%s" % nameserverUri) # create a Pyro daemon pyrodaemon = Pyro4.core.Daemon(host=hostname) serveruri = pyrodaemon.register(EmbeddedServer()) print("server uri=%s" % serveruri) # register it with the embedded nameserver nameserverDaemon.nameserver.register("example.embedded.server", serveruri) print("") # Because this server runs the different daemons using the "multiplex" server type, # we can use the built in support (since Pyro 4.44) to combine multiple daemon event loops. # We can then simply run the event loop of the 'master daemon'. It will dispatch correctly. pyrodaemon.combine(nameserverDaemon) pyrodaemon.combine(broadcastServer) def loopcondition(): print(time.asctime(), "Waiting for requests...") return True pyrodaemon.requestLoop(loopcondition) # clean up nameserverDaemon.close() broadcastServer.close() pyrodaemon.close() print("done") Pyro4-4.82/examples/eventloop/server_threads.py000066400000000000000000000056601416147301300217160ustar00rootroot00000000000000from __future__ import print_function import socket import select import time import Pyro4.core import Pyro4.naming import Pyro4.socketutil Pyro4.config.SERVERTYPE = "thread" hostname = socket.gethostname() my_ip = Pyro4.socketutil.getIpAddress(None, workaround127=True) @Pyro4.expose class EmbeddedServer(object): def multiply(self, x, y): return x * y print("THREADED server type. Initializing services...") print("Make sure that you don't have a name server running already!\n") # start a name server with broadcast server nameserverUri, nameserverDaemon, broadcastServer = Pyro4.naming.startNS(host=my_ip) assert broadcastServer is not None, "expect a broadcast server to be created" print("got a Nameserver, uri=%s" % nameserverUri) # create a Pyro daemon pyrodaemon = Pyro4.core.Daemon(host=hostname) serveruri = pyrodaemon.register(EmbeddedServer()) print("server uri=%s" % serveruri) # register it with the embedded nameserver nameserverDaemon.nameserver.register("example.embedded.server", serveruri) print("") # Below is our custom event loop. # Because this particular server runs the different daemons using the "tread" server type, # there is no built in way of combining the different event loops and server sockets. # We have to write our own multiplexing server event loop, and dispatch the requests # to the server that they belong to. # It is a bit silly to do it this way because the choice for a threaded server type # has already been made-- so you could just as well run the different daemons' request loops # each in their own thread and avoid writing this integrated event loop altogether. # But for the sake of example we write out our own loop: try: while True: print(time.asctime(), "Waiting for requests...") # create sets of the socket objects we will be waiting on # (a set provides fast lookup compared to a list) nameserverSockets = set(nameserverDaemon.sockets) pyroSockets = set(pyrodaemon.sockets) rs = [broadcastServer] # only the broadcast server is directly usable as a select() object rs.extend(nameserverSockets) rs.extend(pyroSockets) rs, _, _ = select.select(rs, [], [], 3) eventsForNameserver = [] eventsForDaemon = [] for s in rs: if s is broadcastServer: print("Broadcast server received a request") broadcastServer.processRequest() elif s in nameserverSockets: eventsForNameserver.append(s) elif s in pyroSockets: eventsForDaemon.append(s) if eventsForNameserver: print("Nameserver received a request") nameserverDaemon.events(eventsForNameserver) if eventsForDaemon: print("Daemon received a request") pyrodaemon.events(eventsForDaemon) except KeyboardInterrupt: pass nameserverDaemon.close() broadcastServer.close() pyrodaemon.close() print("done") Pyro4-4.82/examples/exceptions/000077500000000000000000000000001416147301300164635ustar00rootroot00000000000000Pyro4-4.82/examples/exceptions/Readme.txt000066400000000000000000000013671416147301300204300ustar00rootroot00000000000000This test is to show PYRO's remote exception capabilities. The remote object contains various member functions which raise various kinds of exceptions. The client will print those. Note the special handling of the Pyro exception. It is possible to extract and print the *remote* traceback. You can then see where in the code on the remote side the error occured! By installing Pyro's excepthook (Pyro4.util.excepthook) you can even see the remote traceback when you're not catching any exceptions. Also try to set PYRO_DETAILED_TRACEBACK to True (on the server) to get a very detailed traceback in your client. This can help debugging. Note: you can only use your own exception classes, when you are using the pickle serializer. This is not the default. Pyro4-4.82/examples/exceptions/client.py000066400000000000000000000026171416147301300203210ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 test = Pyro4.core.Proxy("PYRONAME:example.exceptions") print(test.div(2.0, 9.0)) try: print(2 // 0) except ZeroDivisionError as x: print("DIVIDE BY ZERO: %s" % x) try: print(test.div(2, 0)) except ZeroDivisionError as x: print("DIVIDE BY ZERO: %s" % x) try: result = test.error() print("%r, %s" % (result, result)) except ValueError as x: print("VALUERROR: %s" % x) try: result = test.error2() print("%r, %s" % (result, result)) except ValueError as x: print("VALUERROR: %s" % x) try: result = test.othererr() print("%r, %s" % (result, result)) except Exception as x: print("ANOTHER ERROR: %s" % x) try: result = test.unserializable() print("%r, %s" % (result, result)) except Exception as x: print("UNSERIALIZABLE ERROR: %s" % x) print("\n*** invoking server method that crashes, catching traceback ***") try: print(test.complexerror()) except Exception as x: print("CAUGHT ERROR >>> %s" % x) print("Printing Pyro traceback >>>>>>") print("".join(Pyro4.util.getPyroTraceback())) print("<<<<<<< end of Pyro traceback") print("\n*** installing pyro's excepthook") sys.excepthook = Pyro4.util.excepthook print("*** invoking server method that crashes, not catching anything ***") print(test.complexerror()) # due to the excepthook, the exception will show the pyro error Pyro4-4.82/examples/exceptions/excep.py000066400000000000000000000014521416147301300201430ustar00rootroot00000000000000import pickle import Pyro4 class UnserializableError(Exception): def __reduce__(self): raise pickle.PicklingError("make this nonpickleable") @Pyro4.expose class TestClass(object): def div(self, arg1, arg2): return arg1 / arg2 def error(self): raise ValueError('a valueerror! Great!') def error2(self): return ValueError('a valueerror! Great!') def othererr(self): raise RuntimeError('a runtime error!') def complexerror(self): x = Foo() x.crash() def unserializable(self): raise UnserializableError("this error can't be serialized") class Foo(object): def crash(self): self.crash2('going down...') def crash2(self, arg): # this statement will crash on purpose: x = arg // 2 Pyro4-4.82/examples/exceptions/server.py000066400000000000000000000002131416147301300203370ustar00rootroot00000000000000import Pyro4 import excep Pyro4.Daemon.serveSimple( { excep.TestClass: "example.exceptions" }, ns=True, verbose=True) Pyro4-4.82/examples/extended-pickle/000077500000000000000000000000001416147301300173475ustar00rootroot00000000000000Pyro4-4.82/examples/extended-pickle/Readme.txt000066400000000000000000000012221416147301300213020ustar00rootroot00000000000000Pyro 4.62 added support for the cloudpickle serializer. Support for dill, another "extended pickle" serializer, has been present since 4.42 The interesting thing about these "extende pickle" serializers is that they can serialize a lot more Python objects than regular pickle can. For instance, it is possible to actually serialize actual *functions*. This means it becomes trivial to let client-defined functions be executed on remote machines, simply by passing the actual function as an argument. Be aware of the severe security implications when using this though! (any client that can connect will be trivially able to execute any code in the server) Pyro4-4.82/examples/extended-pickle/client.py000066400000000000000000000016471416147301300212070ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input Pyro4.config.SERIALIZER = "cloudpickle" # can also use "dill" uri = input("Uri of extended-pickle example server? ").strip() class WorkerThing(object): def __init__(self, name): self.name = name def __call__(self, servername): return "this is the result of object %s on server %s" % (self.name, servername) def somework(servername): return "this is the result of function somework on server %s" % servername with Pyro4.core.Proxy(uri) as p: print("sending a callable object to the server:") print(" ", p.work(WorkerThing("peter"))) print("sending a function to the server:") print(" ", p.work(somework)) print("sending a lambda to the server:") print(" ", p.work(lambda servername: "this is the result of the lambda function on server %s" % servername)) Pyro4-4.82/examples/extended-pickle/server.py000066400000000000000000000007671416147301300212410ustar00rootroot00000000000000from __future__ import print_function import Pyro4 class Server(object): @Pyro4.expose def work(self, callable): print("RECEIVED WORK:", callable) result = callable("ExtendedPickle") # perform the work! print(" result:", result) return result Pyro4.config.SERIALIZERS_ACCEPTED.add("cloudpickle") Pyro4.config.SERIALIZERS_ACCEPTED.add("dill") Pyro4.Daemon.serveSimple( { Server: "example.extended-pickle" }, ns=False, verbose=True) Pyro4-4.82/examples/filetransfer/000077500000000000000000000000001416147301300167665ustar00rootroot00000000000000Pyro4-4.82/examples/filetransfer/Readme.txt000066400000000000000000000053051416147301300207270ustar00rootroot00000000000000Pyro isn't ideal for transfering large amounts of data or lots of binary data. In some situations it's okay such as sending the occasional PNG file of a forum profile portrait, but generally for intensive file transfer it's better to use one of the established protocols that are optimized for this (rsync, ftp, http, etcetera). That being said you could opt for a hybrid approach: use Pyro for regular remote calls and provide a second network interface for the large data transfers, that will avoid the Pyro protocol and serialization overhead and size limitations (2 Gb). This example does exactly that: it runs a Pyro server that also serves a raw socket interface over which the large binary data files are sent. They're prepared in the regular Pyro server code and identified via a guid. The client then obtains the binary data by first sending the guid and then receiving the data over the raw socket connection in a streaming manner. If the binary data is very large it is better to store it first as temporary files on the disk in the server, otherwise you risk running out of system memory which will crash your python process. The client code as given selects the file storage approach. It will then stream the data from the server, thereby avoiding the need to allocate a huge amount of memory. (If you need to process all of the data at once you end up collecting it together anyway, but you'll be able to do this yourself in the most efficient way suitable for your application) As the data transfer averages at the end will show, the raw socket transfer is much faster than transferring the data via regular Pyro calls, and it will use a lot less memory and CPU as well. The speed does depend a bit on the performance and fragmentation of your hard drive where the temporary files are created. Also if your OS supports the os.sendfile() function (usually on Linux, BSD and OSX, but not Windows) you'll benefit even more from optimized data transfer. Note: Performance of the download via iterator is almost identical to the normal transfer speed of regular python/pyro calls. It is still a lot slower than raw data transfer, but at least you avoid having to load all of the data in memory at once. Note: The annotation stream is somewhere in the middle of the pack. Here the annotation mechanism is (ab)used to transfer binary file data chunks, thereby almost completely avoiding the overhead of the serialization mechanism. Note: the only "security" on the raw socket interface is that you have to know the id of a data file that you want to obtain. It's not advised to use this example as-is in a production environment. For more benchmark numbers regarding large binary data transfer using Pyro, see the 'hugetransfer' example. Pyro4-4.82/examples/filetransfer/client.py000066400000000000000000000111271416147301300206200ustar00rootroot00000000000000from __future__ import print_function import time import threading import sys import socket import zlib import Pyro4 import serpent if sys.version_info < (3, 0): input = raw_input def regular_pyro(uri): blobsize = 10*1024*1024 num_blobs = 10 total_size = 0 start = time.time() name = threading.currentThread().name with Pyro4.core.Proxy(uri) as p: for _ in range(num_blobs): print("thread {0} getting a blob using regular Pyro call...".format(name)) data = p.get_with_pyro(blobsize) data = serpent.tobytes(data) # in case of serpent encoded bytes total_size += len(data) assert total_size == blobsize*num_blobs duration = time.time() - start print("thread {0} done, {1:.2f} Mb/sec.".format(name, total_size/1024.0/1024.0/duration)) def via_iterator(uri): blobsize = 10*1024*1024 num_blobs = 10 total_size = 0 start = time.time() name = threading.currentThread().name with Pyro4.core.Proxy(uri) as p: for _ in range(num_blobs): print("thread {0} getting a blob using remote iterators...".format(name)) for chunk in p.iterator(blobsize): chunk = serpent.tobytes(chunk) # in case of serpent encoded bytes total_size += len(chunk) assert total_size == blobsize*num_blobs duration = time.time() - start print("thread {0} done, {1:.2f} Mb/sec.".format(name, total_size/1024.0/1024.0/duration)) def via_annotation_stream(uri): name = threading.currentThread().name start = time.time() total_size = 0 print("thread {0} downloading via annotation stream...".format(name)) with Pyro4.core.Proxy(uri) as p: perform_checksum = False for progress, checksum in p.annotation_stream(perform_checksum): chunk = Pyro4.current_context.response_annotations["FDAT"] if perform_checksum and zlib.crc32(chunk) != checksum: raise ValueError("checksum error") total_size += len(chunk) assert progress == total_size Pyro4.current_context.response_annotations.clear() # clean them up once we're done with them duration = time.time() - start print("thread {0} done, {1:.2f} Mb/sec.".format(name, total_size/1024.0/1024.0/duration)) def raw_socket(uri): blobsize = 40*1024*1024 num_blobs = 10 total_size = 0 name = threading.currentThread().name with Pyro4.core.Proxy(uri) as p: print("thread {0} preparing {1} blobs of size {2} Mb".format(name, num_blobs, blobsize/1024.0/1024.0)) blobs = {} for _ in range(num_blobs): file_id, blob_address = p.prepare_file_blob(blobsize) blobs[file_id] = blob_address start = time.time() for file_id in blobs: print("thread {0} retrieving blob using raw socket...".format(name)) blob_address = blobs[file_id] sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(tuple(blob_address)) sock.sendall(file_id.encode()) size = 0 chunk = "dummy" while chunk: chunk = sock.recv(60000) size += len(chunk) sock.close() assert size == blobsize total_size += size duration = time.time() - start assert total_size == blobsize * num_blobs print("thread {0} done, {1:.2f} Mb/sec.".format(name, total_size/1024.0/1024.0/duration)) if __name__ == "__main__": uri = input("Uri of filetransfer server? ").strip() print("\n\n**** regular pyro calls ****\n") t1 = threading.Thread(target=regular_pyro, args=(uri, )) t2 = threading.Thread(target=regular_pyro, args=(uri, )) t1.start() t2.start() t1.join() t2.join() input("enter to continue:") print("\n\n**** transfer via iterators ****\n") t1 = threading.Thread(target=via_iterator, args=(uri, )) t2 = threading.Thread(target=via_iterator, args=(uri, )) t1.start() t2.start() t1.join() t2.join() input("enter to continue:") print("\n\n**** transfer via annotation stream ****\n") t1 = threading.Thread(target=via_annotation_stream, args=(uri, )) t2 = threading.Thread(target=via_annotation_stream, args=(uri, )) t1.start() t2.start() t1.join() t2.join() input("enter to continue:") print("\n\n**** raw socket transfers ****\n") t1 = threading.Thread(target=raw_socket, args=(uri, )) t2 = threading.Thread(target=raw_socket, args=(uri, )) t1.start() t2.start() t1.join() t2.join() input("enter to exit:") Pyro4-4.82/examples/filetransfer/server.py000066400000000000000000000122441416147301300206510ustar00rootroot00000000000000from __future__ import print_function import select import tempfile import uuid import io import os import threading import zlib import Pyro4 import Pyro4.core import Pyro4.socketutil datafiles = {} # temporary files datablobs = {} # in-memory @Pyro4.expose class FileServer(object): def get_with_pyro(self, size): print("sending %d bytes" % size) data = b"x" * size return data def iterator(self, size): chunksize = size//100 print("sending %d bytes via iterator, chunks of %d bytes" % (size, chunksize)) data = b"x" * size i = 0 while i < size: yield data[i:i+chunksize] i += chunksize def annotation_stream(self, with_checksum=False): # create a large temporary file f = tempfile.TemporaryFile() for _ in range(5000): f.write(b"1234567890!" * 1000) filesize = f.tell() f.seek(os.SEEK_SET, 0) # return the file data via annotation stream (remote iterator) print("transmitting file via annotations stream (%d bytes)..." % filesize) with f: annotation_size = 65000 # leave some room for Pyro's internal annotation chunks while True: chunk = f.read(annotation_size) if not chunk: break # store the file data chunk in the FDAT response annotation, # and return the current file position and checksum (if asked). Pyro4.current_context.response_annotations = {"FDAT": chunk} yield f.tell(), zlib.crc32(chunk) if with_checksum else 0 def prepare_file_blob(self, size): print("preparing file-based blob of size %d" % size) file_id = str(uuid.uuid4()) f = tempfile.TemporaryFile() chunk = b"x" * 100000 for _ in range(size//100000): f.write(chunk) f.write(b"x"*(size % 100000)) f.flush() f.seek(0, io.SEEK_SET) # os.fsync(f) datafiles[file_id] = f blobsock_info = self._pyroDaemon.blobsocket.getsockname() # return the port info for the blob socket as well return file_id, blobsock_info def prepare_memory_blob(self, size): print("preparing in-memory blob of size %d" % size) file_id = str(uuid.uuid4()) datablobs[file_id] = b"x" * size blobsock_info = self._pyroDaemon.blobsocket.getsockname() # return the port info for the blob socket as well return file_id, blobsock_info class FileServerDaemon(Pyro4.core.Daemon): def __init__(self, host=None, port=0): super(FileServerDaemon, self).__init__(host, port) host, _ = self.transportServer.sock.getsockname() self.blobsocket = Pyro4.socketutil.createSocket(bind=(host, 0), timeout=Pyro4.config.COMMTIMEOUT, nodelay=False) print("Blob socket available on:", self.blobsocket.getsockname()) def close(self): self.blobsocket.close() super(FileServerDaemon, self).close() def requestLoop(self, loopCondition=lambda: True): while loopCondition: rs = [self.blobsocket] rs.extend(self.sockets) rs, _, _ = select.select(rs, [], [], 3) daemon_events = [] for sock in rs: if sock in self.sockets: daemon_events.append(sock) elif sock is self.blobsocket: self.handle_blob_connect(sock) if daemon_events: self.events(daemon_events) def handle_blob_connect(self, sock): csock, caddr = sock.accept() thread = threading.Thread(target=self.blob_client, args=(csock,)) thread.daemon = True thread.start() def blob_client(self, csock): file_id = Pyro4.socketutil.receiveData(csock, 36).decode() print("{0} requesting file id {1}".format(csock.getpeername(), file_id)) is_file, data = self.find_blob_data(file_id) if is_file: if hasattr(os, "sendfile"): print("...from file using sendfile()") out_fn = csock.fileno() in_fn = data.fileno() sent = 1 offset = 0 while sent: sent = os.sendfile(out_fn, in_fn, offset, 512000) offset += sent else: print("...from file using plain old read(); your os doesn't have sendfile()") while True: chunk = data.read(512000) if not chunk: break csock.sendall(chunk) else: print("...from memory") csock.sendall(data) csock.close() def find_blob_data(self, file_id): if file_id in datablobs: return False, datablobs.pop(file_id) elif file_id in datafiles: return True, datafiles.pop(file_id) else: raise KeyError("no data for given id") with FileServerDaemon(host=Pyro4.socketutil.getIpAddress("")) as daemon: uri = daemon.register(FileServer, "example.filetransfer") print("Filetransfer server URI:", uri) daemon.requestLoop() Pyro4-4.82/examples/flame/000077500000000000000000000000001416147301300153665ustar00rootroot00000000000000Pyro4-4.82/examples/flame/Readme.txt000066400000000000000000000022621416147301300173260ustar00rootroot00000000000000Pyro Flame example. Flame = "foreign location automatic module exposer" Without actually writing any code on the server you can still write clients that access modules and other things on the server. You'll have to start a Pyro Flame server before running the client. Set the correct configuration (see below) and run the following command: python -m Pyro4.utils.flameserver or: pyro4-flameserver Security (explicitly enable Flame, pickle serializer ---------------------------------------------------- By default, Flame is switched off; the feature cannot be used. This is because it has severe security implications. If you want to use Flame, you have to explicitly enable it in the server's configuration (FLAME_ENABLED config item). Also, because a lot of custom classes are passed over the network, flame requires the pickle serializer (SERIALIZER config item). When launching the server via the above utility command, this is taken care of automatically. If you write your own server and client, remember to configure this correctly yourself. For this example, setting the environment variable: PYRO_FLAME_ENABLED=true before launching the flame server is enough to make it work. Pyro4-4.82/examples/flame/client.py000066400000000000000000000026631416147301300172250ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4.utils.flame if sys.version_info < (3, 0): input = raw_input Pyro4.config.SERIALIZER = "pickle" # flame requires pickle serializer print("Start a Pyro Flame server somewhere.") location = input("what is the location of the flame server, hostname:portnumber? ") print() # connect! flame = Pyro4.utils.flame.connect(location) # basic stuff socketmodule = flame.module("socket") osmodule = flame.module("os") print("remote host name=", socketmodule.gethostname()) print("remote server current directory=", osmodule.getcwd()) flame.execute("import math") root = flame.evaluate("math.sqrt(500)") print("calculated square root=", root) try: print("remote exceptions also work...", flame.evaluate("1//0")) except ZeroDivisionError: print("(caught ZeroDivisionError)") # print something to the remote server output flame.builtin("print")("Hello there, remote server stdout!") # upload a module source and call a function, on the server, in this new module modulesource = open("stuff.py").read() flame.sendmodule("flameexample.stuff", modulesource) result = flame.module("flameexample.stuff").doSomething("hello", 42) print("\nresult from uploaded module:", result) # remote console with flame.console() as console: print("\nStarting a remote console. Enter some commands to execute remotely. End the console as usual.") console.interact() print("Console session ended.") Pyro4-4.82/examples/flame/stuff.py000066400000000000000000000004771416147301300170770ustar00rootroot00000000000000# this module will be sent to the server as 'flameexample.stuff' from __future__ import print_function def doSomething(name, number): print("This text is printed from a module whose code was uploaded by the client:") print(" Hello, my name is {0} and my number is {1}.".format(name, number)) return 999 Pyro4-4.82/examples/futures/000077500000000000000000000000001416147301300157775ustar00rootroot00000000000000Pyro4-4.82/examples/futures/Readme.txt000066400000000000000000000004111416147301300177310ustar00rootroot00000000000000This is an example that shows the asynchronous function call support for normal Python functions. This is just a little extra that Pyro provides, that also works for normal Python code. It looks similar to the asynchronous proxy support from the `async` example. Pyro4-4.82/examples/futures/futures.py000066400000000000000000000015171416147301300200520ustar00rootroot00000000000000from __future__ import print_function import Pyro4 def myfunction(a, b, extra=None): print(">>> myfunction called with: a={0}, b={1}, extra={2}".format(a, b, extra)) return a + b print("\n* just a single future call:") future = Pyro4.Future(myfunction) result = future(5, 6) # we can do stuff here in the meantime... print("result value=", result.value) assert result.value == 11 print("\n* several calls chained:") future = Pyro4.Future(myfunction) future.then(myfunction, 10) future.then(myfunction, 20, extra="something") # the callables will be invoked like so: function(asyncvalue, normalarg, kwarg=..., kwarg=...) # (the value from the previous call is passed as the first argument to the next call) result = future(5, 6) # we can do stuff here in the meantime... print("result value=", result.value) assert result.value == 41 Pyro4-4.82/examples/gui_eventloop/000077500000000000000000000000001416147301300171615ustar00rootroot00000000000000Pyro4-4.82/examples/gui_eventloop/Readme.txt000066400000000000000000000021571416147301300211240ustar00rootroot00000000000000This example shows two ways of embedding Pyro's event loop in another application, in this case a GUI application (written using Tkinter). There's one application where a background thread is used for the Pyro daemon. This means you can't directly update the GUI from the Pyro objects (because GUI update calls need to be performed from the GUI mainloop thread). So the threaded gui server submits the gui update calls via a Queue to the actual gui thread. There is a nice thing however, the GUI won't freeze up if a Pyro method call takes a while to execute. The other application doesn't use any threads besides the normal GUI thread. It uses a Tkinter-callback to check Pyro's sockets at a fast interval rate to see if it should dispatch any events to the daemon. Not using threads means you can directly update the GUI from Pyro calls but it also means the GUI will freeze if a Pyro method call takes a while. You also can't use Pyro's requestloop anymore, as it will lock up the GUI while it waits for incoming calls. You'll need to check yourself, using select() on the Pyro socket(s) and dispatching to the daemon manually. Pyro4-4.82/examples/gui_eventloop/client.py000066400000000000000000000011031416147301300210040ustar00rootroot00000000000000from __future__ import print_function import sys import time import Pyro4 print("First make sure one of the gui servers is running.") print("Enter the object uri that was printed:") if sys.version_info < (3, 0): uri = raw_input() else: uri = input() uri = uri.strip() guiserver = Pyro4.Proxy(uri) guiserver.message("Hello there!") time.sleep(0.5) guiserver.message("How's it going?") time.sleep(2) for i in range(20): guiserver.message("Counting {0}".format(i)) guiserver.message("now calling the sleep method with 5 seconds") guiserver.sleep(5) print("done!") Pyro4-4.82/examples/gui_eventloop/gui_nothreads.py000066400000000000000000000126341416147301300223740ustar00rootroot00000000000000""" This example shows a Tkinter GUI application that uses event loop callbacks to integrate Pyro's event loop into the Tkinter GUI mainloop. No threads are used. The Pyro event callback is called every so often to check if there are Pyro events to handle, and handles them synchronously. """ import time import select import Pyro4 try: from tkinter import * import tkinter.simpledialog as simpledialog except ImportError: from Tkinter import * import tkSimpleDialog as simpledialog # Set the Pyro servertype to the multiplexing select-based server that doesn't # use a threadpool to service method calls. This way the method calls are # handled inside the main thread as well. Pyro4.config.SERVERTYPE = "multiplex" # The frequency with which the GUI loop calls the Pyro event handler. PYRO_EVENTLOOP_HZ = 50 class PyroGUI(object): """ The Tkinter GUI application that also listens for Pyro calls. """ def __init__(self): self.tk = Tk() self.tk.wm_title("Pyro in a Tkinter GUI eventloop - without threads") self.tk.wm_geometry("500x500") buttonframe = Frame(self.tk) button = Button(buttonframe, text="Messagebox", command=self.button_msgbox_clicked) button.pack(side=LEFT) button = Button(buttonframe, text="Add some text", command=self.button_text_clicked) button.pack(side=LEFT) button = Button(buttonframe, text="Clear all text", command=self.button_clear_clicked) button.pack(side=LEFT) quitbutton = Button(buttonframe, text="Quit", command=self.tk.quit) quitbutton.pack(side=RIGHT) frame = Frame(self.tk, padx=2, pady=2) buttonframe.pack(fill=X) rlabel = Label(frame, text="Pyro server messages:") rlabel.pack(fill=X) self.msg = Message(frame, anchor=NW, width=500, aspect=80, background="white", relief="sunken") self.msg.pack(fill=BOTH, expand=1) frame.pack(fill=BOTH) self.serveroutput = [] def install_pyro_event_callback(self, daemon): """ Add a callback to the tkinter event loop that is invoked every so often. The callback checks the Pyro sockets for activity and dispatches to the daemon's event process method if needed. """ def pyro_event(): while True: # for as long as the pyro socket triggers, dispatch events s, _, _ = select.select(daemon.sockets, [], [], 0.01) if s: daemon.events(s) else: # no more events, stop the loop, we'll get called again soon anyway break self.tk.after(1000 // PYRO_EVENTLOOP_HZ, pyro_event) self.tk.after(1000 // PYRO_EVENTLOOP_HZ, pyro_event) def mainloop(self): self.tk.mainloop() def button_msgbox_clicked(self): # this button event handler is here only to show that gui events are still processed normally number = simpledialog.askinteger("A normal popup", "Hi there enter a number", parent=self.tk) def button_clear_clicked(self): self.serveroutput = [] self.msg.config(text="") def button_text_clicked(self): # add some random text to the message list self.add_message("The quick brown fox jumps over the lazy dog!") def add_message(self, message): message = "[{0}] {1}".format(time.strftime("%X"), message) self.serveroutput.append(message) self.serveroutput = self.serveroutput[-27:] self.msg.config(text="\n".join(self.serveroutput)) @Pyro4.expose class MessagePrinter(object): """ The Pyro object that interfaces with the GUI application. """ def __init__(self, gui): self.gui = gui def message(self, messagetext): # Add the message to the screen. # Note that you can't do anything that requires gui interaction # (such as popping a dialog box asking for user input), # because the gui (tkinter) is busy processing this pyro call. # It can't do two things at the same time when embedded this way. # If you do something in this method call that takes a long time # to process, the GUI is frozen during that time (because no GUI update # events are handled while this callback is active). self.gui.add_message("from Pyro: " + messagetext) def sleep(self, duration): # Note that you can't perform blocking stuff at all because the method # call is running in the gui mainloop thread and will freeze the GUI. # Try it - you will see the first message but everything locks up until # the sleep returns and the method call ends self.gui.add_message("from Pyro: sleeping {0} seconds...".format(duration)) self.gui.tk.update() time.sleep(duration) self.gui.add_message("from Pyro: woke up!") def main(): gui = PyroGUI() # create a pyro daemon with object daemon = Pyro4.Daemon() obj = MessagePrinter(gui) uri = daemon.register(obj, "pyrogui.message") gui.add_message("Pyro server started. Not using threads.") gui.add_message("Use the command line client to send messages.") urimsg = "Pyro object uri = {0}".format(uri) gui.add_message(urimsg) print(urimsg) # add a Pyro event callback to the gui's mainloop gui.install_pyro_event_callback(daemon) # enter the mainloop gui.mainloop() if __name__ == "__main__": main() Pyro4-4.82/examples/gui_eventloop/gui_threads.py000066400000000000000000000140671416147301300220410ustar00rootroot00000000000000""" This example shows a Tkinter GUI application that uses a worker thread to run Pyro's event loop. Usually, the GUI toolkit requires that GUI operations are done from within the GUI thread. So, if Pyro interfaces with the GUI, it cannot do that directly because the method calls are done from a different thread. This means we need a layer between them, this example uses a Queue to submit GUI operations to Tkinter's main loop. For this example, the mainloop runs a callback function every so often to check for new work in that Queue and will process it if the Pyro worker thread has put something in it. """ import time import threading try: import queue except ImportError: import Queue as queue import Pyro4 try: from tkinter import * import tkinter.simpledialog as simpledialog except ImportError: from Tkinter import * import tkSimpleDialog as simpledialog # The frequency with which the GUI mainloop checks for work in the Pyro queue. PYRO_QUEUE_HZ = 50 class PyroGUI(object): """ The Tkinter GUI application that also listens for Pyro calls. """ def __init__(self): self.pyro_queue = queue.Queue() self.tk = Tk() self.tk.wm_title("Pyro in a Tkinter GUI eventloop - with threads") self.tk.wm_geometry("500x500") buttonframe = Frame(self.tk) button = Button(buttonframe, text="Messagebox", command=self.button_msgbox_clicked) button.pack(side=LEFT) button = Button(buttonframe, text="Add some text", command=self.button_text_clicked) button.pack(side=LEFT) button = Button(buttonframe, text="Clear all text", command=self.button_clear_clicked) button.pack(side=LEFT) quitbutton = Button(buttonframe, text="Quit", command=self.tk.quit) quitbutton.pack(side=RIGHT) frame = Frame(self.tk, padx=2, pady=2) buttonframe.pack(fill=X) rlabel = Label(frame, text="Pyro server messages:") rlabel.pack(fill=X) self.msg = Message(frame, anchor=NW, width=500, aspect=80, background="white", relief="sunken") self.msg.pack(fill=BOTH, expand=1) frame.pack(fill=BOTH) self.serveroutput = [] def install_pyro_queue_callback(self): """ Add a callback to the tkinter event loop that is invoked every so often. The callback checks the Pyro work queue for work and processes it. """ def check_pyro_queue(): try: while True: # get a work item from the queue (until it is empty) workitem = self.pyro_queue.get_nowait() # execute it in the gui's mainloop thread workitem["callable"](*workitem["vargs"], **workitem["kwargs"]) except queue.Empty: pass self.tk.after(1000 // PYRO_QUEUE_HZ, check_pyro_queue) self.tk.after(1000 // PYRO_QUEUE_HZ, check_pyro_queue) def mainloop(self): self.tk.mainloop() def button_msgbox_clicked(self): # this button event handler is here only to show that gui events are still processed normally number = simpledialog.askinteger("A normal popup", "Hi there enter a number", parent=self.tk) def button_clear_clicked(self): self.serveroutput = [] self.msg.config(text="") def button_text_clicked(self): # add some random text to the message list self.add_message("The quick brown fox jumps over the lazy dog!") def add_message(self, message): message = "[{0}] {1}".format(time.strftime("%X"), message) self.serveroutput.append(message) self.serveroutput = self.serveroutput[-27:] self.msg.config(text="\n".join(self.serveroutput)) @Pyro4.expose class MessagePrinter(object): """ The Pyro object that interfaces with the GUI application. It uses a Queue to transfer GUI update calls to Tkinter's mainloop. """ def __init__(self, gui): self.gui = gui def message(self, messagetext): # put a gui-update work item in the queue self.gui.pyro_queue.put({ "callable": self.gui.add_message, "vargs": ("from Pyro: " + messagetext,), "kwargs": {} }) def sleep(self, duration): # Note that you *can* perform blocking stuff now because the method # call is running in its own thread. It won't freeze the GUI anymore. # However you cannot do anything that requires GUI interaction because # that needs to go through the queue so the mainloop can pick that up. # (opening a dialog from this worker thread will still freeze the GUI) # But a simple sleep() call works fine and the GUI stays responsive. self.gui.pyro_queue.put({ "callable": self.gui.add_message, "vargs": ("from Pyro: sleeping {0} seconds...".format(duration),), "kwargs": {} }) time.sleep(duration) self.gui.pyro_queue.put({ "callable": self.gui.add_message, "vargs": ("from Pyro: woke up!",), "kwargs": {} }) class PyroDaemon(threading.Thread): def __init__(self, gui): threading.Thread.__init__(self) self.gui = gui self.started = threading.Event() def run(self): daemon = Pyro4.Daemon() obj = MessagePrinter(self.gui) self.uri = daemon.register(obj, "pyrogui.message2") self.started.set() daemon.requestLoop() def main(): gui = PyroGUI() # create a pyro daemon with object, running in its own worker thread pyro_thread = PyroDaemon(gui) pyro_thread.setDaemon(True) pyro_thread.start() pyro_thread.started.wait() gui.add_message("Pyro server started. Using Pyro worker thread.") gui.add_message("Use the command line client to send messages.") urimsg = "Pyro object uri = {0}".format(pyro_thread.uri) gui.add_message(urimsg) print(urimsg) # add a Pyro event callback to the gui's mainloop gui.install_pyro_queue_callback() # enter the mainloop gui.mainloop() if __name__ == "__main__": main() Pyro4-4.82/examples/handshake/000077500000000000000000000000001416147301300162305ustar00rootroot00000000000000Pyro4-4.82/examples/handshake/Readme.txt000066400000000000000000000012201416147301300201610ustar00rootroot00000000000000This example shows how you can customize the connection handshake mechanism. The proxy is overridden to send custom handshake data to the daemon, in this case, a "secret" string to gain access. The daemon is overridden to check the handshake string and only allow a client connection if it sends the correct "secret" string. (This is not the same as the hmac key you can set. That is a signature of every message to make sure it came unchanged from the client and wasn't altered mid-flight by some middle man. The initial handshake check done here in this example, is just a single initial check to allow a client to connect, or refuse it altogether.) Pyro4-4.82/examples/handshake/client.py000066400000000000000000000012641416147301300200630ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input class CustomHandshakeProxy(Pyro4.Proxy): def _pyroValidateHandshake(self, response): # this will get called if the connection is okay by the server print("Proxy received handshake response data: ", response) uri = input("Enter the URI of the server object: ") secret = input("Enter the secret code of the server (or make a mistake on purpose to see what happens): ") with CustomHandshakeProxy(uri) as proxy: proxy._pyroHandshake = secret print("connecting...") proxy._pyroBind() proxy.ping() print("Connection ok!") print("done.") Pyro4-4.82/examples/handshake/server.py000066400000000000000000000020451416147301300201110ustar00rootroot00000000000000from __future__ import print_function import Pyro4 import Pyro4.constants secret_code = "pancakes" class CustomDaemon(Pyro4.Daemon): def validateHandshake(self, conn, data): print("Daemon received handshake request from:", conn.sock.getpeername()) print("Handshake data:", data) # if needed, you can inspect Pyro4.current_context if data == secret_code: print("Secret code okay! Connection accepted.") # return some custom handshake data: return ["how", "are", "you", "doing"] else: print("Secret code wrong! Connection refused.") raise ValueError("wrong secret code, connection refused") def clientDisconnect(self, conn): print("Daemon client disconnects:", conn.sock.getpeername()) with CustomDaemon() as daemon: print("Server is ready. You can use the following URI to connect:") print(daemon.uriFor(Pyro4.constants.DAEMON_NAME)) print("When asked, enter the following secret code: ", secret_code) daemon.requestLoop() Pyro4-4.82/examples/http/000077500000000000000000000000001416147301300152615ustar00rootroot00000000000000Pyro4-4.82/examples/http/Readme.txt000066400000000000000000000016711416147301300172240ustar00rootroot00000000000000A few client programs that demonstrate the use of Pyro's http gateway. Make sure you first start the gateway, for instance: python -m Pyro4.utils.httpgateway -e 'Pyro.|test.' or: pyro4-httpgateway -e 'Pyro.|test.' The code assumes the gateway runs on the default location. The '-e' option tells it to expose all Pyro objects (such as the name server) and the test objects (such as the test echo server). For completeness, also start the test echoserver (pyro4-test-echoserver) Then run a client of your choosing: client.js: javascript client code, for node.js client.py: python (3.x) client code ... and try opening the url that the server printed in your web browser. Javascript client code that runs in a browser is problematic due to the same origin policy. The gateway's web page does contain some examples of this that you can run in your browser. Simply navigate to the url that is printed when you start the http gateway server. Pyro4-4.82/examples/http/client.js000066400000000000000000000050361416147301300171010ustar00rootroot00000000000000/** Client side javascript example that talks to Pyro's http gateway. You can run this with node.js. **/ var http = require('http'); function obtainCharset (headers) { // Find the charset, if specified. var charset; var contentType = headers['content-type'] || ''; var matches = contentType.match(/charset=([^;,\r\n]+)/i); if (matches && matches[1]) { charset = matches[1]; } return charset || 'utf-8'; } function pyro_call(object, method, callback) { http.get({ hostname: "localhost", port: 8080, path: "/pyro/"+object+"/"+method, headers: { // "X-Pyro-Options": "options,here" // "X-Pyro-Gateway-Key": "secretgatewaykey" // "X-Pyro-Correlation-Id": "03e5899c-1117-11e5-b1fa-001e8c7827a6" } }, function(res) { var charset = obtainCharset(res.headers); res.setEncoding(charset); buffer=''; res.on('data', function(d) { buffer += d.toString(); }); res.on('end', function() { if(res.statusCode==200) { // all was well, process the response data as json if(buffer) { var parsed = JSON.parse(buffer); callback(parsed); } else { callback(null); } } else { // 404, 500 or some other server error occurred console.error("Server returned error response:"); console.error(buffer); } }); }).on('error', function(e) { // connection error of some sort console.error("ERROR:", e.toString()); }); } /*--------- do some pyro calls: ----------*/ pyro_call("Pyro.NameServer", "list", function(response) { console.log("\nLIST--->"); console.log(JSON.stringify(response, null, 4)); }); pyro_call("Pyro.NameServer", "$meta", function(response) { console.log("\nMETA--->"); console.log(JSON.stringify(response, null, 4)); }); pyro_call("Pyro.NameServer", "lookup?name=Pyro.NameServer", function(response) { console.log("\nLOOKUP--->"); console.log(JSON.stringify(response, null, 4)); }); pyro_call("test.echoserver", "oneway_slow", function(response) { console.log("\nONEWAY_SLOW--->"); console.log(JSON.stringify(response, null, 4)); }); pyro_call("test.echoserver", "slow", function(response) { console.log("\nSLOW--->"); console.log(JSON.stringify(response, null, 4)); }); Pyro4-4.82/examples/http/client.py000066400000000000000000000042461416147301300171170ustar00rootroot00000000000000import sys assert sys.version_info > (3, 0), "requires python 3.x" # sorry, too lazy to port this to older versions import json import re import pprint from urllib.request import urlopen, Request from urllib.error import HTTPError def get_charset(req): charset = "utf-8" match = re.match(r".* charset=(.+)", req.getheader("Content-Type")) if match: charset = match.group(1) return charset def pyro_call(object_name, method, callback): request = Request("http://127.0.0.1:8080/pyro/{0}/{1}".format(object_name, method), # headers={"x-pyro-options": "oneway", "x-pyro-gateway-key": "secretgatewaykey"} ) with urlopen(request) as req: charset = get_charset(req) data = req.read().decode(charset) if data: callback(json.loads(data)) else: callback(None) def write_result(result): pprint.pprint(result, width=40) try: print("\nLIST--->") pyro_call("Pyro.NameServer", "list", write_result) except HTTPError as x: print("Error:", x) print("Error response data:", x.read()) try: print("\nMETA--->") pyro_call("Pyro.NameServer", "$meta", write_result) except HTTPError as x: print("Error:", x) print("Error response data:", x.read()) try: print("\nLOOKUP--->") pyro_call("Pyro.NameServer", "lookup?name=Pyro.NameServer", write_result) except HTTPError as x: print("Error:", x) print("Error response data:", x.read()) try: print("\nONEWAY_SLOW--->") pyro_call("test.echoserver", "oneway_slow", write_result) except HTTPError as x: print("Error:", x) print("Error response data:", x.read()) try: print("\nSLOW--->") pyro_call("test.echoserver", "slow", write_result) except HTTPError as x: print("Error:", x) print("Error response data:", x.read()) # Note that there is a nicer way to pass the parameters, you can probably # grab them from a function's vargs and/or kwargs and convert those to # a querystring using the appropriate library function. # Then you can call the method as usual and don't have to worry about adding the querystring # (or sticking it in a POST request if the params are too large)... Pyro4-4.82/examples/hugetransfer/000077500000000000000000000000001416147301300167775ustar00rootroot00000000000000Pyro4-4.82/examples/hugetransfer/Readme.txt000066400000000000000000000030331416147301300207340ustar00rootroot00000000000000This test transfers huge data structures to see how Pyro handles those. It sets a socket timeout as well to see how Pyro handles that. A couple of problems could be exposed by this test: - Some systems don't really seem to like non blocking sockets and large data transfers. For instance Mac OS X seems eager to cause EAGAIN errors when your data exceeds 'the devils number' number of bytes. Note that this problem only occurs when using specific socket code. Pyro contains a workaround. More info: http://old.nabble.com/The-Devil%27s-Number-td9169165.html http://www.cherrypy.org/ticket/598 - Other systems seemed to have problems receiving large chunks of data. Windows causes memory errors when the receive buffer is too large. Pyro's receive loop works with comfortable smaller data chunks, to avoid these kind of problems. Performance numbers with the various serializers on my local network: serializer | performance (string) | performance (bytes) -----------+--------------------------------------------- pickle | 33260 kb/sec | 33450 kb/sec marshal | 27900 kb/sec | 32300 kb/sec json | 23856 kb/sec | not supported serpent | 13358 kb/sec | 9066 kb/sec Performance of the download via iterator is almost identical to the normal transfer speed. Note: For a possible approach on transferring large amounts of binary data *efficiently*, see the 'filetransfer' example. It works with a raw socket connection and avoids the Pyro protocol and serialization overhead. Pyro4-4.82/examples/hugetransfer/client.py000066400000000000000000000042041416147301300206270ustar00rootroot00000000000000from __future__ import print_function import sys import time import warnings import Pyro4 import serpent warnings.filterwarnings("ignore") # Pyro4.config.COMMTIMEOUT=2 print("Enter the server's uri that was printed:") if sys.version_info < (3, 0): uri = raw_input() else: uri = input() uri = uri.strip() datasize = 5 * 1024 * 1024 # 5 mb def do_test(data): assert len(data) == datasize totalsize = 0 with Pyro4.core.Proxy(uri) as obj: obj._pyroBind() begin = time.time() for i in range(10): print("transferring %d bytes" % datasize) size = obj.transfer(data) assert size == datasize totalsize += datasize duration = time.time() - begin totalsize = float(totalsize) print("It took %.2f seconds to transfer %d mb." % (duration, totalsize / 1024 / 1024)) print("That is %.0f kb/sec. = %.1f mb/sec. (serializer: %s)" % (totalsize / 1024 / duration, totalsize / 1024 / 1024 / duration, Pyro4.config.SERIALIZER)) def do_test_chunks(): with Pyro4.core.Proxy(uri) as p: totalsize = 0 begin = time.time() for chunk in p.download_chunks(datasize*10): chunk = serpent.tobytes(chunk) # in case of serpent encoded bytes totalsize += len(chunk) print(".", end="", flush=True) assert totalsize == datasize*10 duration = time.time() - begin totalsize = float(totalsize) print("It took %.2f seconds to transfer %d mb." % (duration, totalsize / 1024 / 1024)) print("That is %.0f kb/sec. = %.1f mb/sec. (serializer: %s)" % (totalsize / 1024 / duration, totalsize / 1024 / 1024 / duration, Pyro4.config.SERIALIZER)) data = 'x' * datasize print("\n\n----test with string data----") do_test(data) print("\n\n----test with byte data----") data = b'x' * datasize do_test(data) data = bytearray(b'x' * datasize) print("\n\n----test with bytearray data----") do_test(data) print("\n\n----test download via iterator----") do_test_chunks() print("\n\n (tip: also see the 'filetransfer' example for more efficient ways to transfer large amoungs of binary data)")Pyro4-4.82/examples/hugetransfer/server.py000066400000000000000000000016611416147301300206630ustar00rootroot00000000000000from __future__ import print_function import Pyro4 import Pyro4.socketutil import serpent # Pyro4.config.COMMTIMEOUT=2 class Testclass(object): @Pyro4.expose def transfer(self, data): if Pyro4.config.SERIALIZER == "serpent" and type(data) is dict: data = serpent.tobytes(data) # in case of serpent encoded bytes print("received %d bytes" % len(data)) return len(data) @Pyro4.expose def download_chunks(self, size): print("client requests a 'streaming' download of %d bytes" % size) data = bytearray(size) i = 0 chunksize = 200000 print(" using chunks of size", chunksize) while i < size: yield data[i:i+chunksize] i += chunksize Pyro4.Daemon.serveSimple( { Testclass: "example.hugetransfer" }, host=Pyro4.socketutil.getIpAddress("localhost", workaround127=True), ns=False, verbose=True) Pyro4-4.82/examples/instancemode/000077500000000000000000000000001416147301300167535ustar00rootroot00000000000000Pyro4-4.82/examples/instancemode/Readme.txt000066400000000000000000000002611416147301300207100ustar00rootroot00000000000000This example shows the use of the instance_mode option when exposing a class. Please make sure a name server is running somewhere first, before starting the server and client. Pyro4-4.82/examples/instancemode/client.py000066400000000000000000000020201416147301300205750ustar00rootroot00000000000000from __future__ import print_function import Pyro4 print("Showing the different instancing modes.") print("The number printed, is the id of the instance that handled the call.") print("\n-----PERCALL (different number every time) -----") with Pyro4.Proxy("PYRONAME:instance.percall") as p: print(p.msg("hello1")) print(p.msg("hello2")) print(p.msg("hello3")) print("\n-----SESSION (same numbers within session) -----") with Pyro4.Proxy("PYRONAME:instance.session") as p: print(p.msg("hello1")) print(p.msg("hello1")) print(p.msg("hello1")) print(" ..new proxy..") with Pyro4.Proxy("PYRONAME:instance.session") as p: print(p.msg("hello2")) print(p.msg("hello2")) print(p.msg("hello2")) print("\n-----SINGLE (same number always) -----") with Pyro4.Proxy("PYRONAME:instance.single") as p: print(p.msg("hello1")) print(p.msg("hello1")) print(p.msg("hello1")) with Pyro4.Proxy("PYRONAME:instance.single") as p: print(p.msg("hello2")) print(p.msg("hello2")) print(p.msg("hello2")) Pyro4-4.82/examples/instancemode/server.py000066400000000000000000000023141416147301300206330ustar00rootroot00000000000000from __future__ import print_function import Pyro4 @Pyro4.behavior(instance_mode="single") class SingleInstance(object): @Pyro4.expose def msg(self, message): print("[%s] %s.msg: %s" % (id(self), self.__class__.__name__, message)) return id(self) @Pyro4.behavior(instance_mode="session", instance_creator=lambda clazz: clazz.create_instance()) class SessionInstance(object): @Pyro4.expose def msg(self, message): print("[%s] %s.msg: %s" % (id(self), self.__class__.__name__, message)) return id(self), self.correlation_id @classmethod def create_instance(cls): obj = cls() obj.correlation_id = Pyro4.current_context.correlation_id return obj @Pyro4.behavior(instance_mode="percall") class PercallInstance(object): @Pyro4.expose def msg(self, message): print("[%s] %s.msg: %s" % (id(self), self.__class__.__name__, message)) return id(self) if __name__ == "__main__": # please make sure a name server is running somewhere first. Pyro4.Daemon.serveSimple({ SingleInstance: "instance.single", SessionInstance: "instance.session", PercallInstance: "instance.percall" }, verbose=True) Pyro4-4.82/examples/itunes/000077500000000000000000000000001416147301300156115ustar00rootroot00000000000000Pyro4-4.82/examples/itunes/Readme.txt000066400000000000000000000005201416147301300175440ustar00rootroot00000000000000A simple iTunes remote controller tool. (only works on Mac OS X) Run 'itunescontroller' on the mac with the iTunes that you want to control. It only works on mac os because it uses the osascript utility to manipulate iTunes via a few applescript commands. Run the 'remote' on any other host. It does a few remote control operations. Pyro4-4.82/examples/itunes/itunescontroller.py000066400000000000000000000036461416147301300216070ustar00rootroot00000000000000from __future__ import print_function import subprocess import socket import Pyro4 # You can get a lot more info about scripting iTunes here: # http://dougscripts.com/itunes/ @Pyro4.expose class ITunes(object): def __init__(self): # start itunes subprocess.call(["osascript", "-e", "tell application \"iTunes\" to player state"]) def play(self): # continue play subprocess.call(["osascript", "-e", "tell application \"iTunes\" to play"]) def pause(self): # pause play subprocess.call(["osascript", "-e", "tell application \"iTunes\" to pause"]) def stop(self): # stop playing subprocess.call(["osascript", "-e", "tell application \"iTunes\" to stop"]) def next(self): # next song in list subprocess.call(["osascript", "-e", "tell application \"iTunes\" to next track"]) def previous(self): # previous song in list subprocess.call(["osascript", "-e", "tell application \"iTunes\" to previous track"]) def playlist(self, listname): # start playing a defined play list subprocess.call(["osascript", "-e", "tell application \"iTunes\" to play playlist \"{0}\"".format(listname)]) def currentsong(self): # return title and artist of current song return subprocess.check_output(["osascript", "-e", "tell application \"iTunes\"", "-e", "set thisTitle to name of current track", "-e", "set thisArtist to artist of current track", "-e", "set output to thisTitle & \" - \" & thisArtist", "-e", "end tell"]).strip() print("starting...") itunes = ITunes() daemon = Pyro4.Daemon(host=socket.gethostname(), port=39001) uri = daemon.register(itunes, "itunescontroller") print("iTunes controller started, uri =", uri) daemon.requestLoop() Pyro4-4.82/examples/itunes/remote.py000066400000000000000000000010121416147301300174500ustar00rootroot00000000000000from __future__ import print_function import sys import time import Pyro4 if sys.version_info < (3, 0): input = raw_input host = input("enter the hostname of the itunescontroller: ") itunes = Pyro4.Proxy("PYRO:itunescontroller@{0}:39001".format(host)) print("setting Playlist 'Music'...") itunes.playlist("Music") itunes.play() print("Current song:", itunes.currentsong()) time.sleep(6) print("next song...") itunes.next() print("Current song:", itunes.currentsong()) time.sleep(6) print("stop.") itunes.stop() Pyro4-4.82/examples/maxsize/000077500000000000000000000000001416147301300157625ustar00rootroot00000000000000Pyro4-4.82/examples/maxsize/Readme.txt000077500000000000000000000011721416147301300177240ustar00rootroot00000000000000Shows how the MAX_MESSAGE_SIZE config item works. The client sends a big message first without a limit, then with a limit set on the message size. The second attempt will fail with a protocol error. The client talks to the echo server so you'll have to start the echo server first in another window: $ python -m Pyro4.test.echoserver or: $ pyro4-test-echoserver You can try to set the PYRO_MAX_MESSAGE_SIZE environment variable to a small value (such as 2000) before starting the echo server, to see how it deals with receiving messages that are too large on the server. (Pyro will log an error and close the connection). Pyro4-4.82/examples/maxsize/client.py000066400000000000000000000016271416147301300176200ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 import Pyro4.errors huge_object = [42] * 10000 simple_object = {"message": "hello", "irrelevant": huge_object} print("First start the built-in test echo server with something like:") print("$ python -m Pyro4.test.echoserver") print("Enter the server's uri that was printed:") if sys.version_info < (3, 0): uri = raw_input() else: uri = input() uri = uri.strip() echoserver = Pyro4.Proxy(uri) Pyro4.config.MAX_MESSAGE_SIZE = 0 print("\nSending big data with no limit on message size...") response = echoserver.echo(simple_object) print("success.") try: Pyro4.config.MAX_MESSAGE_SIZE = 2500 print("\nSending big data with a limit on message size...") response = echoserver.echo(simple_object) print("Hmm, this should have raised an exception") except Pyro4.errors.MessageTooLargeError as x: print("EXCEPTION (expected):", x) Pyro4-4.82/examples/messagebus/000077500000000000000000000000001416147301300164405ustar00rootroot00000000000000Pyro4-4.82/examples/messagebus/Readme.txt000066400000000000000000000040021416147301300203720ustar00rootroot00000000000000Shows how to build a simple asynchronous pubsub message bus. (Note that it is NOT aiming to be a reliable high performance msgbus to compete with solustions such as zmq, rabbitmq, celery) It uses a few Pyro features to achieve this: - autoproxy (for subscribers) - instance_mode - auto reconnect Start the message bus server from this example's directory with: python -m messagebus.server [options] Use -h to get a help screen of available options. You can run multiple publishers at the same time, make sure you give a different location argument when you start each of them to see the results. You can also run multiple subscribers at the same time, the published messages will be delivered to each subscriber. There are two kinds of subscribers: - one that automatically consumes the messages as soon as they arrive on the bus, - one that has a manual message processing loop The messagebus is a bit simplistic if you use the in-memory storage: it only keeps messages and subscribers in memory. If the message bus server dies, everything is lost. If an error occurs when forwarding a message to subscribers, the message is immediately discarded. If it was a communication error, the subscriber is immediately removed from the topic as well. The in-memory storage is very fast though, so if you're after a very high message troughput, it may be the storage option for you. However you can also use the SqliteStorage which uses a database on disk to store topics, messages and subscriptions. If the message bus server dies, it will simply continue where it was. No messages will get lost, and it also remembers the subscribers. So simply restarting the message bus server is enough to get everything back on track without lost data. The sqlite storage is slower than the in-memory storage (and MUCH slower when running on Windows), so if you need a high message troughput, it may not be suitable. There's no queue mechanism, this is left as an excercise for the reader. (A queue is 1-to-1 communication whereas pubsub is 1-to-many) Pyro4-4.82/examples/messagebus/manytopics_publisher.py000066400000000000000000000012771416147301300232640ustar00rootroot00000000000000""" This is the producer for the 'many topics' messages example. """ from __future__ import print_function import time import random import sys import Pyro4 from Pyro4.util import excepthook from messagebus import PYRO_MSGBUS_NAME sys.excepthook = excepthook # add a bunch of topics to the bus bus = Pyro4.Proxy("PYRONAME:"+PYRO_MSGBUS_NAME) for letter in "abcdefghijklmnopqrstuvwxyz": bus.add_topic("msg.topic.%s" % letter) print("publishing messages on a random topic") seq = 1 while True: time.sleep(0.05) letter = random.choice("abcdefghijklmnopqrstuvwxyz") topic = "msg.topic.%s" % letter bus.send(topic, "message %d" % seq) seq += 1 print("", letter, seq, end="\r") Pyro4-4.82/examples/messagebus/manytopics_subscriber.py000066400000000000000000000033631416147301300234300ustar00rootroot00000000000000""" This is the subscriber for the 'many topics' messages example. For code with more explanations, see the regular 'weather' message example code. """ from __future__ import print_function import os import time import threading import Pyro4 from operator import itemgetter from messagebus.messagebus import Subscriber Pyro4.config.AUTOPROXY = True @Pyro4.expose class Subber(Subscriber): def init_counters(self, topics): self.message_counter = {} self.last_message = {} for t in topics: self.message_counter[t] = 0 self.last_message[t] = None def consume_message(self, topic, message): self.message_counter[topic] += 1 self.last_message[topic] = message def clear_screen(): os.system(['clear', 'cls'][os.name == 'nt']) subber = Subber() d = Pyro4.Daemon() d.register(subber) daemon_thread = threading.Thread(target=d.requestLoop) daemon_thread.daemon = True daemon_thread.start() # mass subscribe to all available topics topics = list(sorted(subber.bus.topics())) subber.init_counters(topics) for t in topics: subber.bus.subscribe(t, subber) # show a table of the active topics on the bus while True: clear_screen() print(time.ctime(), "-- active topics on the messagebus:") print("{:20} : {:5} {} {}".format("topic", "count", "last_recv", "last message data")) for topic, count in sorted(subber.message_counter.items(), key=itemgetter(1), reverse=True): msg = subber.last_message[topic] if msg: print("{:20} : {:5d} - {} {!r:.20}".format(topic, count, msg.created.time(), msg.data)) else: print("{:20} : {:5d}".format(topic, count)) print("(restart me to refresh the list of topics)") time.sleep(1) Pyro4-4.82/examples/messagebus/messagebus/000077500000000000000000000000001416147301300205765ustar00rootroot00000000000000Pyro4-4.82/examples/messagebus/messagebus/__init__.py000066400000000000000000000004041416147301300227050ustar00rootroot00000000000000""" Pyro MessageBus: a simple pub/sub message bus. Provides a way of communicating where the sender and receivers are fully decoupled. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ PYRO_MSGBUS_NAME = "Pyro.MessageBus" Pyro4-4.82/examples/messagebus/messagebus/messagebus.py000066400000000000000000000477471416147301300233310ustar00rootroot00000000000000""" Pyro MessageBus: a simple pub/sub message bus. Provides a way of communicating where the sender and receivers are fully decoupled. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import threading import uuid import datetime import time import logging import traceback import sys from collections import defaultdict from contextlib import closing try: import cPickle as pickle except ImportError: import pickle try: import sqlite3 except ImportError: sqlite3 = None try: import queue except ImportError: import Queue as queue import Pyro4 import Pyro4.errors from Pyro4.util import SerializerBase from . import PYRO_MSGBUS_NAME __all__ = ["MessageBus", "Message", "SerializerBase"] log = logging.getLogger("Pyro4.MessageBus") if sys.version_info >= (3, 0): basestring = str class Message(object): __slots__ = ("msgid", "created", "data") def __init__(self, msgid, created, data): self.msgid = msgid self.created = created self.data = data @staticmethod def to_dict(obj): return { "__class__": "Pyro4.utils.messagebus.message", "msgid": str(obj.msgid), "created": obj.created.isoformat(), "data": obj.data } @classmethod def from_dict(cls, classname, d): return cls(uuid.UUID(d["msgid"]), datetime.datetime.strptime(d["created"], "%Y-%m-%dT%H:%M:%S.%f"), d["data"]) # make sure Pyro knows how to serialize the custom Message class SerializerBase.register_class_to_dict(Message, Message.to_dict) SerializerBase.register_dict_to_class("Pyro4.utils.messagebus.message", Message.from_dict) @Pyro4.expose class Subscriber(object): def __init__(self, auto_consume=True, max_queue_size=5000): self.bus = Pyro4.Proxy("PYRONAME:"+PYRO_MSGBUS_NAME) self.received_messages = queue.Queue(maxsize=max_queue_size) if auto_consume: self.__bus_consumer_thread = threading.Thread(target=self.__bus_consume_message) self.__bus_consumer_thread.daemon = True self.__bus_consumer_thread.start() def incoming_message(self, topic, message): self.received_messages.put((topic, message)) def incoming_messages(self, topic, messages): # this is an optimization to receive multiple messages for the topic at the same time for msg in messages: self.incoming_message(topic, msg) def __bus_consume_message(self): # this runs in a thread, to pick up and process incoming messages while True: try: topic, message = self.received_messages.get(timeout=1) except queue.Empty: time.sleep(0.002) continue try: self.consume_message(topic, message) except Exception: print("Error while consuming message:", file=sys.stderr) traceback.print_exc(file=sys.stderr) def consume_message(self, topic, message): # this is called from a background thread raise NotImplementedError("subclass should implement this") class MemoryStorage(object): """ Storage implementation that just uses in-memory dicts. It is very fast. Stopping the message bus server will make it instantly forget about every topic and pending messages. """ def __init__(self): self.messages = {} # topic -> list of pending messages self.subscribers = {} # topic -> set of subscribers self.proxy_cache = {} self.total_msg_count = 0 def topics(self): return self.messages.keys() def create_topic(self, topic): if topic in self.messages: return self.messages[topic] = [] self.subscribers[topic] = set() def remove_topic(self, topic): if topic not in self.messages: return del self.messages[topic] for sub in self.subscribers.get(topic, set()): if hasattr(sub, "_pyroRelease"): sub._pyroRelease() if hasattr(sub, "_pyroUri"): try: proxy = self.proxy_cache[sub._pyroUri] proxy._pyroRelease() del self.proxy_cache[sub._pyroUri] except KeyError: pass del self.subscribers[topic] def add_message(self, topic, message): self.messages[topic].append(message) self.total_msg_count += 1 def add_subscriber(self, topic, subscriber): if hasattr(subscriber, "_pyroUri"): # if we already have a subscriber proxy for this uri, use that instead subscriber = self.proxy_cache.get(subscriber._pyroUri, subscriber) self.proxy_cache[subscriber._pyroUri] = subscriber self.subscribers[topic].add(subscriber) def remove_subscriber(self, topic, subscriber): if subscriber in self.subscribers[topic]: if hasattr(subscriber, "_pyroRelease"): subscriber._pyroRelease() if hasattr(subscriber, "_pyroUri"): try: proxy = self.proxy_cache[subscriber._pyroUri] proxy._pyroRelease() del self.proxy_cache[subscriber._pyroUri] except KeyError: pass self.subscribers[topic].discard(subscriber) def all_pending_messages(self): return {topic: list(msgs) for topic, msgs in self.messages.items()} def has_pending_messages(self, topic): return topic in self.messages and any(self.messages[topic]) def has_subscribers(self, topic): return topic in self.subscribers and any(self.subscribers[topic]) def all_subscribers(self): all_subs = {} for topic, subs in self.subscribers.items(): all_subs[topic] = set(subs) return all_subs def remove_messages(self, topics_messages): for topic in topics_messages: if topic in self.messages: msg_list = self.messages[topic] for message in topics_messages[topic]: try: msg_list.remove(message) except ValueError: pass def stats(self): subscribers = pending = 0 for subs in self.subscribers.values(): subscribers += len(subs) for msgs in self.messages.values(): pending += len(msgs) return len(self.messages), subscribers, pending, self.total_msg_count class SqliteStorage(object): """ Storage implementation that uses a sqlite database to store the messages and subscribers. It is a lot slower than the in-memory storage, but no data is lost if the messagebus dies. If you restart it, it will also reconnect to the subscribers and carry on from where it stopped. """ dbconnections = {} def __init__(self): conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") conn.execute(""" CREATE TABLE IF NOT EXISTS Topic ( id INTEGER PRIMARY KEY, topic NVARCHAR(500) UNIQUE NOT NULL ); """) conn.execute(""" CREATE TABLE IF NOT EXISTS Message( id CHAR(36) PRIMARY KEY, created DATETIME NOT NULL, topic INTEGER NOT NULL, msgdata BLOB NOT NULL, FOREIGN KEY(topic) REFERENCES Topic(id) ); """) conn.execute(""" CREATE TABLE IF NOT EXISTS Subscription( id INTEGER PRIMARY KEY, topic INTEGER NOT NULL, subscriber NVARCHAR(500) NOT NULL, FOREIGN KEY(topic) REFERENCES Topic(id) ); """) conn.commit() self.proxy_cache = {} self.total_msg_count = 0 def dbconn(self): # return the db-connection for the current thread thread = threading.current_thread() try: return self.dbconnections[thread] except KeyError: conn = sqlite3.connect("messages.sqlite", detect_types=sqlite3.PARSE_DECLTYPES) self.dbconnections[thread] = conn return conn def topics(self): conn = self.dbconn() with closing(conn.cursor()) as cursor: return [r[0] for r in cursor.execute("SELECT topic FROM Topic").fetchall()] def create_topic(self, topic): conn = self.dbconn() with closing(conn.cursor()) as cursor: if not cursor.execute("SELECT EXISTS(SELECT 1 FROM Topic WHERE topic=?)", [topic]).fetchone()[0]: cursor.execute("INSERT INTO Topic(topic) VALUES(?)", [topic]) conn.commit() def remove_topic(self, topic): conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") with closing(conn.cursor()) as cursor: topic_id = cursor.execute("SELECT id FROM Topic WHERE topic=?", [topic]).fetchone() if not topic_id: return else: topic_id = topic_id[0] sub_uris = [r[0] for r in cursor.execute("SELECT subscriber FROM Subscription WHERE topic=?", [topic_id]).fetchall()] cursor.execute("DELETE FROM Subscription WHERE topic=?", [topic_id]) cursor.execute("DELETE FROM Message WHERE topic=?", [topic_id]) cursor.execute("DELETE FROM Topic WHERE id=?", [topic_id]) conn.commit() for uri in sub_uris: try: proxy = self.proxy_cache[uri] proxy._pyroRelease() del self.proxy_cache[uri] except KeyError: pass def add_message(self, topic, message): msg_data = pickle.dumps(message.data, pickle.HIGHEST_PROTOCOL) if sys.version_info < (3, 0): blob_data = buffer(msg_data) else: blob_data = msg_data conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") with closing(conn.cursor()) as cursor: res = cursor.execute("SELECT id FROM Topic WHERE topic=?", [topic]).fetchone() if not res: raise KeyError(topic) topic_id = res[0] if cursor.execute("SELECT EXISTS(SELECT 1 FROM Subscription WHERE topic=?)", [topic_id]).fetchone()[0]: # there is at least one subscriber for this topic, insert the message (otherwise just discard it) cursor.execute("INSERT INTO Message(id, created, topic, msgdata) VALUES (?,?,?,?)", [str(message.msgid), message.created, topic_id, blob_data]) conn.commit() self.total_msg_count += 1 def add_subscriber(self, topic, subscriber): if not hasattr(subscriber, "_pyroUri"): raise ValueError("can only store subscribers that are a Pyro proxy") uri = subscriber._pyroUri.asString() conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") with closing(conn.cursor()) as cursor: topic_id = cursor.execute("SELECT id FROM Topic WHERE topic=?", [topic]).fetchone()[0] if not cursor.execute("SELECT EXISTS(SELECT 1 FROM Subscription WHERE topic=? AND subscriber=?)", [topic_id, uri]).fetchone()[0]: cursor.execute("INSERT INTO Subscription(topic, subscriber) VALUES (?,?)", [topic_id, uri]) self.proxy_cache[uri] = subscriber conn.commit() def remove_subscriber(self, topic, subscriber): conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") uri = subscriber._pyroUri.asString() with closing(conn.cursor()) as cursor: cursor.execute("DELETE FROM Subscription WHERE topic=(SELECT id FROM Topic WHERE topic=?) AND subscriber=?", [topic, uri]) conn.commit() try: proxy = self.proxy_cache[uri] proxy._pyroRelease() del self.proxy_cache[uri] except KeyError: pass def all_pending_messages(self): conn = self.dbconn() with closing(conn.cursor()) as cursor: msgs = cursor.execute("SELECT t.topic, m.id, m.created, m.msgdata FROM Message AS m, Topic as t WHERE m.topic=t.id").fetchall() result = defaultdict(list) for msg in msgs: if sys.version_info < (3, 0): blob_data = str(msg[3]) else: blob_data = msg[3] result[msg[0]].append(Message(uuid.UUID(msg[1]), datetime.datetime.strptime(msg[2], "%Y-%m-%d %H:%M:%S.%f"), pickle.loads(blob_data))) return result def has_pending_messages(self, topic): conn = self.dbconn() with closing(conn.cursor()) as cursor: return cursor.execute("SELECT EXISTS(SELECT 1 FROM Message WHERE topic=(SELECT id FROM Topic WHERE topic=?))", [topic]).fetchone()[0] def has_subscribers(self, topic): conn = self.dbconn() with closing(conn.cursor()) as cursor: return cursor.execute("SELECT EXISTS(SELECT 1 FROM Subscription WHERE topic=(SELECT id FROM Topic WHERE topic=?))", [topic]).fetchone()[0] def all_subscribers(self): conn = self.dbconn() with closing(conn.cursor()) as cursor: result = cursor.execute("SELECT s.id, t.topic, s.subscriber FROM Subscription AS s, Topic AS t WHERE t.id=s.topic").fetchall() subs = defaultdict(list) for sub_id, topic, uri in result: if uri in self.proxy_cache: proxy = self.proxy_cache[uri] subs[topic].append(proxy) else: try: proxy = Pyro4.Proxy(uri) except Exception: log.exception("Cannot create pyro proxy, sub_id=%d, uri=%s", sub_id, uri) cursor.execute("DELETE FROM Subscription WHERE id=?", [sub_id]) else: self.proxy_cache[uri] = proxy subs[topic].append(proxy) conn.commit() return subs def remove_messages(self, topics_messages): if not topics_messages: return all_guids = [[str(message.msgid)] for msglist in topics_messages.values() for message in msglist] conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") with closing(conn.cursor()) as cursor: cursor.executemany("DELETE FROM Message WHERE id = ?", all_guids) conn.commit() def stats(self): conn = self.dbconn() with closing(conn.cursor()) as cursor: topics = cursor.execute("SELECT COUNT(*) FROM Topic").fetchone()[0] subscribers = cursor.execute("SELECT COUNT(*) FROM Subscription").fetchone()[0] pending = cursor.execute("SELECT COUNT(*) FROM Message").fetchone()[0] return topics, subscribers, pending, self.total_msg_count def make_messagebus(clazz): if make_messagebus.storagetype == "sqlite": return clazz(storage=SqliteStorage()) elif make_messagebus.storagetype == "memory": return clazz(storage=MemoryStorage()) else: raise ValueError("invalid storagetype") @Pyro4.behavior(instance_mode="single", instance_creator=make_messagebus) @Pyro4.expose class MessageBus(object): def __init__(self, storage=None): if storage is None: storage = MemoryStorage() self.storage = storage # topic -> list of pending messages log.info("using storage: %s", self.storage.__class__.__name__) self.msg_lock = threading.Lock() self.msg_added = threading.Event() self.sender = threading.Thread(target=self.__sender, name="messagebus.sender") self.sender.daemon = True self.sender.start() log.info("started") def add_topic(self, topic): if not isinstance(topic, basestring): raise TypeError("topic must be str") with self.msg_lock: self.storage.create_topic(topic) def remove_topic(self, topic): try: if self.storage.has_pending_messages(topic) or self.storage.has_subscribers(topic): raise ValueError("topic still has pending messages and/or subscribers") except KeyError: pass else: with self.msg_lock: self.storage.remove_topic(topic) def topics(self): with self.msg_lock: return set(self.storage.topics()) def send(self, topic, message): message = Message(uuid.uuid4(), datetime.datetime.now(), message) with self.msg_lock: self.storage.add_message(topic, message) self.msg_added.set() # signal that a new message has arrived self.msg_added.clear() @Pyro4.oneway def send_no_ack(self, topic, message): self.send(topic, message) def subscribe(self, topic, subscriber): """Add a subscription to a topic.""" meth = getattr(subscriber, "incoming_message", None) if not meth or not callable(meth): raise TypeError("subscriber must have incoming_message() method") self.add_topic(topic) # make sure the topic exists with self.msg_lock: self.storage.add_subscriber(topic, subscriber) log.debug("subscribed: %s -> %s" % (topic, subscriber)) def unsubscribe(self, topic, subscriber): """Remove a subscription to a topic.""" with self.msg_lock: self.storage.remove_subscriber(topic, subscriber) log.debug("unsubscribed %s from topic %s" % (subscriber, topic)) def _unsubscribe_many(self, subscribers): if subscribers: topics = self.storage.topics() with self.msg_lock: for topic in topics: for subscriber in subscribers: self.storage.remove_subscriber(topic, subscriber) log.debug("unsubscribed from all topics: %s" % subscribers) def __sender(self): # this runs in a thread, to pick up and forward incoming messages prev_print_stats = 0 while True: self.msg_added.wait(timeout=2.01) if time.time() - prev_print_stats >= 10: prev_print_stats = time.time() self._print_stats() with self.msg_lock: msgs_per_topic = self.storage.all_pending_messages() subs_per_topic = self.storage.all_subscribers() subs_to_remove = set() for topic, messages in msgs_per_topic.items(): if topic not in subs_per_topic or not messages: continue for subscriber in subs_per_topic[topic]: if subscriber in subs_to_remove: # skipping because subscriber is scheduled for removal continue try: try: # send the batch of messages pending for this topic in one go subscriber.incoming_messages(topic, messages) except Pyro4.errors.MessageTooLargeError: # the batch doesn't fit in the configured max msg size, send them one by one instead for message in messages: subscriber.incoming_message(topic, message) except Exception as x: # can't deliver them, drop the subscription log.warning("error delivering message(s) for topic=%s, subscriber=%s, error=%r" % (topic, subscriber, x)) log.warning("removing subscription because of that error") subs_to_remove.add(subscriber) # remove processed messages if msgs_per_topic: with self.msg_lock: self.storage.remove_messages(msgs_per_topic) # remove broken subscribers self._unsubscribe_many(subs_to_remove) def _print_stats(self): topics, subscribers, pending, messages = self.storage.stats() timestamp = datetime.datetime.now() timestamp = timestamp.replace(microsecond=0) print("\r[%s] stats: %d topics, %d subs, %d pending, %d total " % (timestamp, topics, subscribers, pending, messages), end="") Pyro4-4.82/examples/messagebus/messagebus/server.py000066400000000000000000000030411416147301300224540ustar00rootroot00000000000000""" Pyro MessageBus: a simple pub/sub message bus. Provides a way of communicating where the sender and receivers are fully decoupled. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function from optparse import OptionParser from . import PYRO_MSGBUS_NAME from .messagebus import make_messagebus, MessageBus import Pyro4 Pyro4.config.COMMTIMEOUT = 20.0 Pyro4.config.POLLTIMEOUT = 10.0 Pyro4.config.MAX_MESSAGE_SIZE = 256*1024 # 256 kb Pyro4.config.MAX_RETRIES = 3 if __name__ == "__main__": parser = OptionParser() parser.add_option("-n", "--host", dest="host", default="localhost", help="hostname to bind server on") parser.add_option("-p", "--port", dest="port", type="int", default=0, help="port to bind server on (0=random)") parser.add_option("-u", "--unixsocket", help="Unix domain socket name to bind server on") parser.add_option("-s", "--storage", dest="storage", type="choice", choices=["sqlite", "memory"], default="sqlite", help="storage type (default=%default)") options, args = parser.parse_args() make_messagebus.storagetype = options.storage daemon = Pyro4.Daemon(host=options.host, port=options.port, unixsocket=options.unixsocket) uri = daemon.register(MessageBus) print("Pyro Message Bus.") print(" uri =", uri) ns = Pyro4.locateNS() ns.register(PYRO_MSGBUS_NAME, uri) print(" name =", PYRO_MSGBUS_NAME) print("Server running, storage is {}.".format(make_messagebus.storagetype)) daemon.requestLoop() Pyro4-4.82/examples/messagebus/publisher.py000066400000000000000000000016061416147301300210120ustar00rootroot00000000000000""" This is the publisher meant for the 'weather' messages example. """ from __future__ import print_function import time import random import sys import Pyro4 import Pyro4.errors from Pyro4.util import excepthook from messagebus import PYRO_MSGBUS_NAME sys.excepthook = excepthook if sys.version_info < (3, 0): input = raw_input location = input("Give city or country to use as location: ").strip() or 'Amsterdam' bus = Pyro4.Proxy("PYRONAME:"+PYRO_MSGBUS_NAME) bus.add_topic("weather-forecast") while True: time.sleep(0.01) forecast = (location, random.choice(["sunny", "cloudy", "storm", "rainy", "hail", "thunder", "calm", "mist", "cold", "hot"])) print("Forecast:", forecast) try: bus.send("weather-forecast", forecast) except Pyro4.errors.CommunicationError: print("connection to the messagebus is lost, reconnecting...") bus._pyroReconnect() Pyro4-4.82/examples/messagebus/subscriber.py000066400000000000000000000025061416147301300211600ustar00rootroot00000000000000""" This is a subscriber meant for the 'weather' messages example. It uses a callback to process incoming messages. """ from __future__ import print_function import sys import Pyro4 from messagebus.messagebus import Subscriber from Pyro4.util import excepthook sys.excepthook = excepthook if sys.version_info < (3, 0): input = raw_input Pyro4.config.AUTOPROXY = True @Pyro4.expose class Subber(Subscriber): def consume_message(self, topic, message): # This callback-method is called automatically when a message arrives on the bus. print("\nGOT MESSAGE:") print(" topic:", topic) print(" msgid:", message.msgid) print(" created:", message.created) print(" data:", message.data) hostname = input("hostname to bind on (empty=localhost): ").strip() or "localhost" # create a messagebus subscriber that uses automatic message retrieval (via a callback) subber = Subber() d = Pyro4.Daemon(host=hostname) d.register(subber) topics = subber.bus.topics() print("Topics on the bus: ", topics) print("Subscribing to weather-forecast.") subber.bus.subscribe("weather-forecast", subber) # note: we subscribe on the bus *after* registering the subber as a Pyro object # this results in Pyro automatically making a proxy for the subber print("Subscribed on weather-forecast") d.requestLoop() Pyro4-4.82/examples/messagebus/subscriber_manual_consume.py000066400000000000000000000046001416147301300242430ustar00rootroot00000000000000""" This is a subscriber meant for the 'weather' messages example. It uses a custom code loop to get and process messages. """ from __future__ import print_function import sys import threading import time import Pyro4 from messagebus.messagebus import Subscriber from Pyro4.util import excepthook sys.excepthook = excepthook if sys.version_info < (3, 0): input = raw_input Pyro4.config.AUTOPROXY = True @Pyro4.expose class Subber(Subscriber): def consume_message(self, topic, message): # In this case, this consume message method is called by our own code loop. print("\nPROCESSING MESSAGE:") print(" topic:", topic) print(" msgid:", message.msgid) print(" created:", message.created) print(" data:", message.data) def manual_message_loop(self): print("Entering manual message processing loop (5 messages).") processed = 0 while processed < 5: time.sleep(0.5) print("\nApprox. number of received messages:", self.received_messages.qsize()) topic, message = self.received_messages.get() # get a message from the queue (they are put there by the Pyro messagebus) self.consume_message(topic, message) processed += 1 print("\nEnd.") hostname = input("hostname to bind on (empty=localhost): ").strip() or "localhost" # create a messagebus subscriber that uses manual message retrieval (via explicit call) # because we're doing the message loop ourselves, the Pyro daemon has to run in a separate thread subber = Subber(auto_consume=False) d = Pyro4.Daemon(host=hostname) d.register(subber) daemon_thread = threading.Thread(target=d.requestLoop) daemon_thread.daemon = True daemon_thread.start() topics = subber.bus.topics() print("Topics on the bus: ", topics) print("Subscribing to weather-forecast.") subber.bus.subscribe("weather-forecast", subber) # note: we subscribe on the bus *after* registering the subber as a Pyro object # this results in Pyro automatically making a proxy for the subber print("Subscribed on weather-forecast") # run the manual message loop print("Entering message loop, you should see the msg count increasing.") subber.manual_message_loop() subber.bus.unsubscribe("weather-forecast", subber) print("Unsubscribed from the topic.") print("Entering message loop again, you should see the msg count decrease.") subber.manual_message_loop() Pyro4-4.82/examples/nameserverstress/000077500000000000000000000000001416147301300177155ustar00rootroot00000000000000Pyro4-4.82/examples/nameserverstress/Readme.txt000066400000000000000000000002451416147301300216540ustar00rootroot00000000000000This example contains a stress test for the Naming Server. It creates a bunch of threads that connect to the NS and create/delete registrations randomly, very fast. Pyro4-4.82/examples/nameserverstress/stress.py000066400000000000000000000042611416147301300216150ustar00rootroot00000000000000from __future__ import print_function import sys import random import time import threading import Pyro4.errors import Pyro4.naming def randomname(): def partname(): return str(random.random())[-2:] parts = ["stresstest"] for i in range(random.randint(1, 10)): parts.append(partname()) return ".".join(parts) class NamingTrasher(threading.Thread): def __init__(self, nsuri, number): threading.Thread.__init__(self) self.daemon = True self.number = number self.ns = Pyro4.core.Proxy(nsuri) self.mustStop = False def list(self): items = self.ns.list() def register(self): for i in range(4): try: self.ns.register(randomname(), 'PYRO:objname@host:555') except Pyro4.errors.NamingError: pass def remove(self): self.ns.remove(randomname()) def lookup(self): try: uri = self.ns.lookup(randomname()) except Pyro4.errors.NamingError: pass def listprefix(self): entries = self.ns.list(prefix="stresstest.51") def listregex(self): entries = self.ns.list(regex=r"stresstest\.??\.41.*") def run(self): print("Name Server trasher running.") while not self.mustStop: random.choice((self.list, self.register, self.remove, self.lookup, self.listregex, self.listprefix))() sys.stdout.write("%d " % self.number) sys.stdout.flush() time.sleep(0.001) print("Trasher exiting.") def main(): threads = [] ns = Pyro4.naming.locateNS() print("Removing previous stresstest registrations...") ns.remove(prefix="stresstest.") print("Done. Starting.") for i in range(5): nt = NamingTrasher(ns._pyroUri, i) nt.start() threads.append(nt) try: while True: time.sleep(1) except KeyboardInterrupt: pass print("Break-- waiting for threads to stop.") for nt in threads: nt.mustStop = True nt.join() count = ns.remove(prefix="stresstest.") print("cleaned up %d names." % count) if __name__ == '__main__': main() Pyro4-4.82/examples/nonameserver/000077500000000000000000000000001416147301300170065ustar00rootroot00000000000000Pyro4-4.82/examples/nonameserver/Readme.txt000066400000000000000000000004231416147301300207430ustar00rootroot00000000000000This example shows a way to use Pyro without a Name server. Look at the simplicity of the client. The only thing you need to figure out is how to get the correct URI in the client. This example just lets you enter it on the console. You can copy it from the server's output. Pyro4-4.82/examples/nonameserver/client.py000066400000000000000000000005541416147301300206420ustar00rootroot00000000000000# Client that doesn't use the Name Server. Uses URI directly. from __future__ import print_function import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input uri = input("Enter the URI of the quote object: ") with Pyro4.core.Proxy(uri) as quotegen: print("Getting some quotes...") print(quotegen.quote()) print(quotegen.quote()) Pyro4-4.82/examples/nonameserver/server.py000066400000000000000000000016031416147301300206660ustar00rootroot00000000000000# The server that doesn't use the Name Server. from __future__ import print_function import os import Pyro4 class QuoteGen(object): @Pyro4.expose def quote(self): try: quote = os.popen('fortune').read() if len(quote) > 0: return quote return "This system cannot provide you a good fortune, install 'fortune'" except: return "This system knows no witty quotes :-(" with Pyro4.core.Daemon() as daemon: quote1 = QuoteGen() quote2 = QuoteGen() uri1 = daemon.register(quote1) # let Pyro create a unique name for this one uri2 = daemon.register(quote2, "example.quotegen") # provide a logical name ourselves print("QuoteGen is ready, not using the Name Server.") print("You can use the following two URIs to connect to me:") print(uri1) print(uri2) daemon.requestLoop() Pyro4-4.82/examples/ns-metadata/000077500000000000000000000000001416147301300165005ustar00rootroot00000000000000Pyro4-4.82/examples/ns-metadata/Readme.txt000066400000000000000000000005031416147301300204340ustar00rootroot00000000000000This example shows the metadata capabilities of the name server (Since Pyro 4.40) and the use of the PYROMETA magic uri protocol (since Pyro 4.51) Before running the example.py, make sure the name server is running. You should choose an appropriate storage type for the name server (dbm storage doesn't support metadata). Pyro4-4.82/examples/ns-metadata/example.py000066400000000000000000000071411416147301300205100ustar00rootroot00000000000000import Pyro4 from Pyro4.naming import type_meta from resources import LaserPrinter, MatrixPrinter, PhotoPrinter, TapeStorage, DiskStorage, Telephone, Faxmachine # register various objects with some metadata describing their resource class ns = Pyro4.locateNS() d = Pyro4.Daemon() uri = d.register(LaserPrinter) ns.register("example.resource.laserprinter", uri, metadata=type_meta(LaserPrinter) | {"resource:printer", "performance:fast"}) uri = d.register(MatrixPrinter) ns.register("example.resource.matrixprinter", uri, metadata=type_meta(MatrixPrinter) | {"resource:printer", "performance:slow"}) uri = d.register(PhotoPrinter) ns.register("example.resource.photoprinter", uri, metadata=type_meta(PhotoPrinter) | {"resource:printer", "performance:slow"}) uri = d.register(TapeStorage) ns.register("example.resource.tapestorage", uri, metadata=type_meta(TapeStorage) | {"resource:storage", "performance:slow"}) uri = d.register(DiskStorage) ns.register("example.resource.diskstorage", uri, metadata=type_meta(DiskStorage) | {"resource:storage", "performance:fast"}) uri = d.register(Telephone) ns.register("example.resource.telephone", uri, metadata=type_meta(Telephone) | {"resource:communication"}) uri = d.register(Faxmachine) ns.register("example.resource.faxmachine", uri, metadata=type_meta(Faxmachine) | {"resource:communication"}) # check that the name server is actually capable of storing metadata uri, metadata = ns.lookup("example.resource.laserprinter", return_metadata=True) if not metadata: raise NameError("The name server doesn't support storing metadata. Check its storage type.") # list all registrations with their metadata entries = ns.list(return_metadata=True) for name in entries: uri, metadata = entries[name] print(name) print(" uri:", uri) print(" meta:", ", ".join(metadata)) print() # query for various metadata print("\nall storage:") devices = ns.list(metadata_all={"resource:storage"}) for name, uri in devices.items(): print(" {} -> {}".format(name, uri)) print("\nall FAST printers:") devices = ns.list(metadata_all={"resource:printer", "performance:fast"}) for name, uri in devices.items(): print(" {} -> {}".format(name, uri)) print("\nall storage OR communication devices :") devices = ns.list(metadata_any={"resource:storage", "resource:communication"}) for name, uri in devices.items(): print(" {} -> {}".format(name, uri)) # upgrade the photo printer uri, meta = ns.lookup("example.resource.photoprinter", return_metadata=True) meta = set(meta) meta.discard("performance:slow") meta.add("performance:fast") ns.set_metadata("example.resource.photoprinter", meta) print("\nall FAST printers (after photoprinter upgrade):") devices = ns.list(metadata_all={"resource:printer", "performance:fast"}) for name, uri in devices.items(): print(" {} -> {}".format(name, uri)) print("\nall resource types:") devices = ns.list(metadata_all={"class:resources.Resource"}) for name, uri in devices.items(): print(" {} -> {}".format(name, uri)) print("\n\nPYROMETA protocol for easy yellow-pages lookup:\n") nameserver = Pyro4.Proxy("PYROMETA:class:Pyro4.naming.NameServer") print("Proxy to look up 'any nameserver' via its class metadata:") print(" ", nameserver) nameserver._pyroBind() print("Proxy for 'any namesever' bound to candidate:") print(" ", nameserver._pyroUri) printer = Pyro4.Proxy("PYROMETA:resource:printer,performance:slow") print("Proxy for 'any slow printer':") print(" ", printer) print("(this example doesn't actually implement these objects so we leave it at that)") Pyro4-4.82/examples/ns-metadata/resources.py000066400000000000000000000006151416147301300210660ustar00rootroot00000000000000from Pyro4 import expose class Resource(object): pass @expose class LaserPrinter(Resource): pass @expose class MatrixPrinter(Resource): pass @expose class PhotoPrinter(Resource): pass @expose class TapeStorage(Resource): pass @expose class DiskStorage(Resource): pass @expose class Telephone(Resource): pass @expose class Faxmachine(Resource): pass Pyro4-4.82/examples/oneway/000077500000000000000000000000001416147301300156045ustar00rootroot00000000000000Pyro4-4.82/examples/oneway/Readme.txt000066400000000000000000000017231416147301300175450ustar00rootroot00000000000000This example shows the use of 'oneway' method calls. If you flag a method call 'oneway' (with the @Pyro4.oneway decorator), Pyro will not wait for a response from the remote object. This means that your client program can continue to work, while the remote object is still busy processing the method call. (Normal remote method calls are synchronous and will always block until the remote object is done with the method call). This example also shows the use of the ONEWAY_THREADED setting in the server. This setting is on by default. It means that oneway method calls are executed in their own separate thread, so the server remains responsive for additional calls from the same client even when the oneway call is still running. If you set this to False, the server will process all calls from the same proxy sequentially (and additional calls will have to wait). Note that a different proxy will still be able to execute calls regardless of the setting of ONEWAY_THREADED. Pyro4-4.82/examples/oneway/client.py000066400000000000000000000016311416147301300174350ustar00rootroot00000000000000from __future__ import print_function import time import Pyro4 with Pyro4.core.Proxy("PYRONAME:example.oneway") as serv: print("starting server using a oneway call") serv.oneway_start(6) print("doing some more oneway calls inbetween") serv.nothing() serv.nothing() serv.nothing() serv.nothing() time.sleep(2) print("\nNow contacting the server to see if it's done.") print("we are faster, so you should see a few attempts,") print("until the server is finished.") while True: print("server done?") if serv.ready(): print("yes!") break else: print("no, trying again") time.sleep(1) print("getting the result from the server: %s" % serv.result()) print("\nCalling oneway work method, server will continue working while we are done (check the server console output).") serv.oneway_work() Pyro4-4.82/examples/oneway/server.py000066400000000000000000000021401416147301300174610ustar00rootroot00000000000000from __future__ import print_function import time import Pyro4 # set the oneway behavior to run inside a new thread, otherwise the client stalls. # this is the default, but I've added it here just for clarification. Pyro4.config.ONEWAY_THREADED = True @Pyro4.expose class Server(object): def __init__(self): self.busy = False @Pyro4.oneway def oneway_start(self, duration): print("start request received. Starting work...") self.busy = True for i in range(duration): time.sleep(1) print(duration - i) print("work is done!") self.busy = False def ready(self): print("ready status requested (%r)" % (not self.busy)) return not self.busy def result(self): return "The result :)" def nothing(self): print("nothing got called, doing nothing") @Pyro4.oneway def oneway_work(self): for i in range(10): print("work work..", i+1) time.sleep(1) print("work's done!") # main program Pyro4.Daemon.serveSimple({ Server: "example.oneway" }) Pyro4-4.82/examples/proxysharing/000077500000000000000000000000001416147301300170375ustar00rootroot00000000000000Pyro4-4.82/examples/proxysharing/Readme.txt000066400000000000000000000006541416147301300210020ustar00rootroot00000000000000This example shows how Pyro deals with sharing proxies in different threads. Due to internal locking you can freely share proxies among threads. The lock makes sure that only a single thread is actually using the proxy's communication channel at all times. This can be convenient BUT it may not be the best way. The lock essentially prevents parallelism. If you want calls to go in parallel, give each thread their own proxy. Pyro4-4.82/examples/proxysharing/client.py000066400000000000000000000060401416147301300206670ustar00rootroot00000000000000from __future__ import print_function import sys import time import threading import Pyro4.naming import Pyro4.core if sys.version_info < (3, 0): current_thread = threading.currentThread else: current_thread = threading.current_thread stop = False def myThread(nsproxy, proxy): global stop name = current_thread().getName() try: while not stop: result = nsproxy.list(prefix="example.") result = proxy.method("the quick brown fox jumps over the lazy dog") except Exception as x: print("**** Exception in thread %s: {%s} %s" % (name, type(x), x)) nsproxy = Pyro4.naming.locateNS() proxy = Pyro4.core.Proxy("PYRONAME:example.proxysharing") # now create a handful of threads and give each of them the same two proxy objects threads = [] for i in range(5): thread = threading.Thread(target=myThread, args=(nsproxy, proxy)) # thread.setDaemon(True) thread.setDaemon(False) threads.append(thread) thread.start() print("Running a bunch of threads for 5 seconds.") print("They're hammering the name server and the test server using the same proxy.") print("You should not see any exceptions.") time.sleep(5) stop = True for thread in threads: thread.join() print("Done.") print("\nNow showing why proxy sharing might not be a good idea for parallelism.") print("Starting 10 threads with the same proxy that all call the work() method.") def myThread2(proxy): global stop while not stop: proxy.work() stop = False proxy.reset_work() threads = [] for i in range(10): thread = threading.Thread(target=myThread2, args=[proxy]) thread.setDaemon(False) threads.append(thread) thread.start() print("waiting 5 seconds") start = time.time() time.sleep(5) print("waiting until threads have stopped...") stop = True for thread in threads: thread.join() duration = int(time.time() - start) print("--> time until everything completed: %.2f" % duration) print("--> work done on the server: %d" % proxy.get_work_done()) print("you can see that the 10 threads are waiting for each other to complete,") print("and that not a lot of work has been done on the server.") print("\nDoing the same again but every thread now has its own proxy.") print("Starting 10 threads with different proxies that all call the work() method.") proxy.reset_work() stop = False threads = [] for i in range(10): proxy = Pyro4.core.Proxy(proxy._pyroUri) # create a new proxy thread = threading.Thread(target=myThread2, args=[proxy]) thread.setDaemon(False) threads.append(thread) thread.start() print("waiting 5 seconds") start = time.time() time.sleep(5) print("waiting until threads have stopped...") stop = True for thread in threads: thread.join() duration = int(time.time() - start) print("--> time until everything completed: %.2f" % duration) print("--> work done on the server: %d" % proxy.get_work_done()) print("you can see that this time the 10 threads didn't have to wait for each other,") print("and that they got a lot more work done because they really ran in parallel.") Pyro4-4.82/examples/proxysharing/server.py000066400000000000000000000010661416147301300207220ustar00rootroot00000000000000from __future__ import print_function import time import Pyro4 @Pyro4.expose @Pyro4.behavior(instance_mode="single") class RemoteObject(object): def __init__(self): self.amount = 0 def method(self, arg): return " ~~this is the remote result~~ " def work(self): print("work... %d" % self.amount) time.sleep(0.5) self.amount += 1 def reset_work(self): self.amount = 0 def get_work_done(self): return self.amount Pyro4.Daemon.serveSimple({ RemoteObject: "example.proxysharing" }) Pyro4-4.82/examples/resourcetracking/000077500000000000000000000000001416147301300176545ustar00rootroot00000000000000Pyro4-4.82/examples/resourcetracking/Readme.txt000066400000000000000000000007641416147301300216210ustar00rootroot00000000000000This example shows how you can utilize the resource tracking feature to properly close allocated resources when a client connection gets closed forcefully before the client can properly free the resources itself. The client allocates and frees some (fictional) resources. The server registers them as tracked resources for the current client connection. If you kill the client before it can cleanly free the resources, Pyro will free them for you as soon as the connection to the server is closed. Pyro4-4.82/examples/resourcetracking/client.py000066400000000000000000000013641416147301300215100ustar00rootroot00000000000000from __future__ import print_function import sys import random import Pyro4 if sys.version_info < (3, 0): input = raw_input uri = input("Enter the URI of the server object: ") with Pyro4.Proxy(uri) as proxy: print("currently allocated resources:", proxy.list()) name1 = hex(random.randint(0, 999999))[-4:] name2 = hex(random.randint(0, 999999))[-4:] print("allocating resource...", name1) proxy.allocate(name1) print("allocating resource...", name2) proxy.allocate(name2) input("\nhit Enter now to continue normally or ^C/break to abort the connection forcefully:") print("free resources normally...") proxy.free(name1) proxy.free(name2) print("allocated resources:", proxy.list()) print("done.") Pyro4-4.82/examples/resourcetracking/server.py000066400000000000000000000036611416147301300215420ustar00rootroot00000000000000from __future__ import print_function import Pyro4 import Pyro4.constants class CustomDaemon(Pyro4.Daemon): def clientDisconnect(self, conn): # If required, you *can* override this to do custom resource freeing. # But this is not needed if your resource objects have a proper 'close' method; # this method is called by Pyro itself once the client connection gets closed. # In this example this override is only used to print out some info. print("client disconnects:", conn.sock.getpeername()) print(" resources: ", [r.name for r in conn.tracked_resources]) class Resource(object): # a fictional resource that gets allocated and must be freed again later. def __init__(self, name, collection): self.name = name self.collection = collection def close(self): # Pyro will call this on a tracked resource once the client's connection gets closed! # (Unless the resource can be carbage collected normally by Python.) print("Resource: closing", self.name) self.collection.discard(self) @Pyro4.expose @Pyro4.behavior(instance_mode="single") class Service(object): def __init__(self): self.resources = set() # the allocated resources def allocate(self, name): resource = Resource(name, self.resources) self.resources.add(resource) Pyro4.current_context.track_resource(resource) print("service: allocated resource", name, " for client", Pyro4.current_context.client_sock_addr) def free(self, name): resources = {r for r in self.resources if r.name == name} self.resources -= resources for r in resources: r.close() Pyro4.current_context.untrack_resource(r) def list(self): return [r.name for r in self.resources] with CustomDaemon() as daemon: Pyro4.Daemon.serveSimple({ Service: "service" }, ns=False, daemon=daemon) Pyro4-4.82/examples/robots/000077500000000000000000000000001416147301300156125ustar00rootroot00000000000000Pyro4-4.82/examples/robots/Readme.txt000066400000000000000000000032371416147301300175550ustar00rootroot00000000000000This is an example that more or less presents an online multiplayer game. The game is a robot destruction derby. It is played on a grid. There are some obstructing walls on the grid that hurt when you collide into them. If you collide into another robot, the other robot takes damage. All robots start with a certain amount of health. If it reaches zero, the robot dies. The last man standing wins! Before starting the gameserver, you need to start a nameserver, if you want to connect remotely to the game server! If you don't have a nameserver running, you can still launch the gameserver but you won't be able to connect to it with the Pyro clients. You can click a button to add a couple of robots that are controlled by the server itself. But it is more interesting to actually connect remote robots to the server! Use client.py for that (provide a name and a robot type). The client supports a few robot types that have different behaviors. The robot behavior is controlled by the client! The server only handles game mechanics. In the game server, the Pyro calls are handled by a daemon thread. The GUI updates are done by Tkinter using after() calls. The most interesting parts of this example are perhaps these: - server uses identical code to work with local and remote robots (it did require a few minor tweaks to work around serialization requirements) - Pyro used together with an interactive GUI application (Tkinter) - game state handled by the server, influenced by the clients (robot behavior) - this example uses Pyro's AutoProxy feature. Registering observers and getting a robot object back is done via proxies automatically because those are Pyro objects. Pyro4-4.82/examples/robots/client.py000066400000000000000000000053751416147301300174540ustar00rootroot00000000000000import random import sys import robot import remote import Pyro4 class DrunkenGameObserver(remote.GameObserver): @Pyro4.oneway @Pyro4.expose def world_update(self, iteration, world, robotdata): # change directions randomly if random.random() > 0.8: if random.random() >= 0.5: dx, dy = random.randint(-1, 1), 0 else: dx, dy = 0, random.randint(-1, 1) if random.random() > 0.7: self.robot.emote("..Hic! *burp*") self.robot.change_direction((dx, dy)) class AngryGameObserver(remote.GameObserver): def __init__(self): super(AngryGameObserver, self).__init__() self.directions = [(1, 0), (0, 1), (-1, 0), (0, -1)] # clockwise motion self.directioncounter = 0 @Pyro4.oneway @Pyro4.expose def world_update(self, iteration, world, robotdata): # move in a loop yelling angry stuff if iteration % 50 == 0: self.robot.emote("I'll kill you all! GRR") if iteration % 10 == 0: self.directioncounter = (self.directioncounter + 1) % 4 self.robot.change_direction(self.directions[self.directioncounter]) class ScaredGameObserver(remote.GameObserver): def __init__(self): super(ScaredGameObserver, self).__init__() # run to a corner self.direction = random.choice([(-1, -1), (1, -1), (1, 1), (-1, 1)]) @Pyro4.oneway @Pyro4.expose def start(self): super(ScaredGameObserver, self).start() self.robot.change_direction(self.direction) @Pyro4.oneway @Pyro4.expose def world_update(self, iteration, world, robotdata): if iteration % 50 == 0: self.robot.emote("I'm scared!") observers = { "drunk": DrunkenGameObserver, "angry": AngryGameObserver, "scared": ScaredGameObserver, } # register the Robot class with Pyro's serializers: Pyro4.util.SerializerBase.register_class_to_dict(robot.Robot, robot.Robot.robot_to_dict) Pyro4.util.SerializerBase.register_dict_to_class("robot.Robot", robot.Robot.dict_to_robot) def main(args): if len(args) != 3: print("usage: client.py ") print(" type is one of: %s" % list(observers.keys())) return name = args[1] observertype = args[2] with Pyro4.Daemon() as daemon: observer = observers[observertype]() daemon.register(observer) gameserver = Pyro4.Proxy("PYRONAME:example.robotserver") robot = gameserver.register(name, observer) robot.emote("Hi there! I'm here to kick your ass") observer.robot = robot print("Pyro server registered on %s" % daemon.locationStr) daemon.requestLoop() if __name__ == "__main__": main(sys.argv) Pyro4-4.82/examples/robots/gameserver.py000066400000000000000000000237421416147301300203340ustar00rootroot00000000000000import random import time import threading import robot import remote try: from tkinter import * except ImportError: from Tkinter import * import Pyro4 import Pyro4.util class VisibleRobot(robot.Robot): """represents a robot that is visible on the screen.""" def __init__(self, name, position, direction, grid, color='red'): super(VisibleRobot, self).__init__(name, (grid.width, grid.height), position, direction) self.grid = grid x = self.x * grid.squaresize y = self.y * grid.squaresize self.tkid = grid.create_rectangle(x, y, x + grid.squaresize, y + grid.squaresize, fill=color, outline='black') self.text_tkid = None def popuptext(self, text, sticky=False): if self.text_tkid: self.grid.delete(self.text_tkid) self.text_tkid = self.grid.create_text(self.x * self.grid.squaresize, self.y * self.grid.squaresize, text=text, anchor=CENTER, fill='red') self.text_timer = time.time() if not sticky: self.grid.after(1000, self.__removetext, self.text_tkid) def delete_from_grid(self): self.grid.delete(self.tkid) if self.text_tkid: self.grid.delete(self.text_tkid) def __removetext(self, textid): self.grid.delete(textid) if textid == self.text_tkid: self.text_tkid = None def move(self, world=None): super(VisibleRobot, self).move(world) x = self.x * self.grid.squaresize y = self.y * self.grid.squaresize self.grid.coords(self.tkid, x, y, x + self.grid.squaresize, y + self.grid.squaresize) if self.text_tkid: # also move the popup text self.grid.coords(self.text_tkid, self.x * self.grid.squaresize, self.y * self.grid.squaresize) def died(self, killer, world): self.popuptext("ARGH I died") self.observer.death(killer=killer) self.grid.after(800, lambda: self.grid.delete(self.tkid)) def collision(self, other): self.popuptext("Bam!") other.popuptext("ouch") def emote(self, text): self.popuptext(text, False) class RobotGrid(Canvas): def __init__(self, parent, width, height, squaresize=20): self.squaresize = squaresize self.width = width self.height = height pixwidth = width * self.squaresize pixheight = height * self.squaresize Canvas.__init__(self, parent, width=pixwidth, height=pixheight, background='#e0e0e0') self.xview_moveto(0) self.yview_moveto(0) for x in range(width): self.create_line(x * self.squaresize, 0, x * self.squaresize, pixheight, fill='#d0d0d0') for y in range(height): self.create_line(0, y * self.squaresize, pixwidth, y * self.squaresize, fill='#d0d0d0') def draw_wall(self, wall, color='navy'): x = wall.x * self.squaresize y = wall.y * self.squaresize self.create_rectangle(x, y, x + self.squaresize, y + self.squaresize, fill=color, outline=color) class GameEngine(object): def __init__(self, gui, world): self.gui = gui self.grid = gui.grid self.world = world self.build_walls() self.gui.buttonhandler = self self.survivor = None self.open_for_signups = True self.iteration = 0 def button_clicked(self, button): if button == "add_bot" and self.open_for_signups: for i in range(5): name = "local_bot_%d" % self.gui.listbox.size() gameobserver = remote.LocalGameObserver(name) robot = self.signup_robot(name, gameobserver) gameobserver.robot = robot elif button == "start_round": self.open_for_signups = False if self.survivor: self.survivor.delete_from_grid() self.gui.enable_buttons(False) self.start_round() def start_round(self): self.gui.statuslabel.config(text="new round!") print("WORLD:") for line in self.world.dump(): print(line.tostring()) print("NUMBER OF ROBOTS: %d" % len(self.world.robots)) txtid = self.grid.create_text(20, 20, text="GO!", font=("Courier", 120, "bold"), anchor=NW, fill='purple') self.grid.after(1500, lambda: self.grid.delete(txtid)) self.grid.after(2000, self.update) self.grid.after(2000, self.notify_start) self.iteration = 0 def notify_start(self): for robot in self.world.robots: robot.observer.start() def notify_worldupdate(self): self.iteration += 1 for robot in self.world.robots: robot.observer.world_update(self.iteration, self.world, robot) def notify_winner(self, winner): winner.observer.victory() def update(self): for robot in self.world.robots: robot.move(self.world) self.notify_worldupdate() self.gui.statuslabel.config(text="survivors: %d" % len(self.world.robots)) if len(self.world.robots) < 1: print("[server] No results.") self.round_ends() elif len(self.world.robots) == 1: self.survivor = self.world.robots[0] self.world.remove(self.survivor) self.survivor.popuptext("I WIN! HURRAH!", True) print("[server] %s wins!" % self.survivor.name) self.gui.statuslabel.config(text="winner: %s" % self.survivor.name) self.notify_winner(self.survivor) self.round_ends() else: self.gui.tk.after(40, self.update) def round_ends(self): self.gui.listbox.delete(0, END) self.gui.enable_buttons(True) self.open_for_signups = True def build_walls(self): wall_offset = 4 wall_size = 10 for x in range(wall_size): wall = robot.Wall((x + wall_offset, wall_offset)) self.world.add_wall(wall) self.grid.draw_wall(wall) wall = robot.Wall((x + wall_offset, wall_size + wall_offset + 1)) self.world.add_wall(wall) self.grid.draw_wall(wall) wall = robot.Wall((wall_offset, x + wall_offset + 1)) self.world.add_wall(wall) self.grid.draw_wall(wall) wall = robot.Wall((wall_size + wall_offset + 2, x + wall_offset + 1)) self.world.add_wall(wall) self.grid.draw_wall(wall) def signup_robot(self, name, observer=None): if not self.open_for_signups: raise RuntimeError("signups are closed, try again later") for r in self.world.robots: if r.name == name: raise ValueError("that name is already taken") colorint = random.randint(0, 0xFFFFFF) color = '#%06x' % colorint inversecolor = 'black' self.gui.listbox.insert(END, name) self.gui.listbox.itemconfig(END, bg=color, fg=inversecolor) while True: x = random.randint(0, self.grid.width - 1) y = random.randint(int(self.grid.height * 0), self.grid.height - 1) if not self.world.collides(x, y): break r = VisibleRobot(name, (x, y), (0, 0), self.grid, color=color) self.world.add_robot(r) r.observer = observer r.popuptext(name) return remote.RemoteBot(r, self) def remove_robot(self, robot): robot.delete_from_grid() self.world.remove(robot) # listnames=list(self.gui.listbox.get(0,END)) # listnames.remove(robot.name) # self.gui.listbox.delete(0,END) # self.gui.listbox.insert(END,*listnames) class GUI(object): def __init__(self, width, height): self.tk = Tk() self.tk.wm_title("bot destruction derby") lframe = Frame(self.tk, borderwidth=3, relief="raised", padx=2, pady=2, background='#808080') self.grid = RobotGrid(lframe, width, height, squaresize=16) rframe = Frame(self.tk, padx=2, pady=2) rlabel = Label(rframe, text="Signups:") rlabel.pack(fill=X) self.listbox = Listbox(rframe, width=15, height=20, font=(None, 8)) self.listbox.pack() self.addrobotbutton = Button(rframe, text="Add 5 local bots", command=lambda: self.buttonhandler.button_clicked("add_bot")) self.addrobotbutton.pack() self.startbutton = Button(rframe, text="Start round!", command=lambda: self.buttonhandler.button_clicked("start_round")) self.startbutton.pack() self.statuslabel = Label(rframe, width=20) self.statuslabel.pack(side=BOTTOM) self.grid.pack() lframe.pack(side=LEFT) rframe.pack(side=RIGHT, fill=BOTH) self.buttonhandler = None def enable_buttons(self, enabled=True): if enabled: self.addrobotbutton.config(state=NORMAL) self.startbutton.config(state=NORMAL) else: self.addrobotbutton.config(state=DISABLED) self.startbutton.config(state=DISABLED) class PyroDaemonThread(threading.Thread): def __init__(self, engine): threading.Thread.__init__(self) self.pyroserver = remote.GameServer(engine) self.pyrodaemon = Pyro4.Daemon() self.ns = Pyro4.locateNS() self.setDaemon(True) def run(self): with self.pyrodaemon: with self.ns: uri = self.pyrodaemon.register(self.pyroserver) self.ns.register("example.robotserver", uri) print("Pyro server registered on %s" % self.pyrodaemon.locationStr) self.pyrodaemon.requestLoop() # register the Robot class with Pyro's serializers: Pyro4.util.SerializerBase.register_class_to_dict(VisibleRobot, robot.Robot.robot_to_dict) def main(): width = 25 height = 25 gui = GUI(width, height) world = robot.World(width, height) engine = GameEngine(gui, world) try: PyroDaemonThread(engine).start() except Pyro4.errors.NamingError: print("Can't find the Pyro Nameserver. Running without remote connections.") gui.tk.mainloop() if __name__ == "__main__": main() Pyro4-4.82/examples/robots/remote.py000066400000000000000000000035611416147301300174640ustar00rootroot00000000000000from __future__ import print_function import random import Pyro4 @Pyro4.expose class GameServer(object): def __init__(self, engine): self.engine = engine def register(self, name, observer): robot = self.engine.signup_robot(name, observer) self._pyroDaemon.register(robot) # make the robot a pyro object return robot @Pyro4.expose class RemoteBot(object): def __init__(self, robot, engine): self.robot = robot self.engine = engine def get_data(self): return self.robot def change_direction(self, direction): self.robot.dx, self.robot.dy = direction def emote(self, text): self.robot.emote(text) def terminate(self): self.engine.remove_robot(self.robot) @Pyro4.expose class LocalGameObserver(object): def __init__(self, name): self.name = name self.robot = None @Pyro4.oneway def world_update(self, iteration, world, robotdata): # change directions randomly if random.random() > 0.8: if random.random() >= 0.5: dx, dy = random.randint(-1, 1), 0 else: dx, dy = 0, random.randint(-1, 1) self.robot.change_direction((dx, dy)) def start(self): self.robot.emote("Here we go!") def victory(self): print("[%s] I WON!!!" % self.name) def death(self, killer): if killer: print("[%s] I DIED (%s did it)" % (self.name, killer.name)) else: print("[%s] I DIED" % self.name) @Pyro4.expose class GameObserver(object): def world_update(self, iteration, world, robotdata): pass def start(self): print("Battle starts!") def victory(self): print("I WON!!!") def death(self, killer): print("I DIED") if killer: print("%s KILLED ME :(" % killer.name) Pyro4-4.82/examples/robots/robot.py000066400000000000000000000077421416147301300173230ustar00rootroot00000000000000from __future__ import print_function import sys import array class Wall(object): """an obstructing static wall""" def __init__(self, position): self.x, self.y = position def __getstate__(self): return self.x, self.y def __setstate__(self, state): self.x, self.y = state class Robot(object): """represents a robot moving on a grid.""" def __init__(self, name, grid_dimensions, position, direction=(0, 0), strength=5): self.name = name self.x, self.y = position self.dx, self.dy = direction self.gridw, self.gridh = grid_dimensions self.strength = strength def __str__(self): return "ROBOT '%s'; pos(%d,%d); dir(%d,%d); strength %d" % (self.name, self.x, self.y, self.dx, self.dy, self.strength) @staticmethod def dict_to_robot(classname, data): assert classname == "robot.Robot" return Robot(data["name"], data["grid_dimensions"], data["position"], data["direction"], data["strength"]) @staticmethod def robot_to_dict(robot): return { "__class__": "robot.Robot", "name": robot.name, "grid_dimensions": (robot.gridw, robot.gridh), "position": (robot.x, robot.y), "direction": (robot.dx, robot.dy), "strength": robot.strength, } def move(self, world=None): # minmax to avoid moving off the sides x = min(self.gridw - 1, max(0, self.x + self.dx)) y = min(self.gridh - 1, max(0, self.y + self.dy)) if x == self.x and y == self.y: return if world and self.__process_collision(x, y, world): return self.x, self.y = x, y def __process_collision(self, newx, newy, world): other = world.collides(newx, newy) if not other: return False # we didn't hit anything self.dx, self.dy = 0, 0 # come to a standstill when we hit something if isinstance(other, Wall): self.strength -= 1 # hit wall, decrease our strength if self.strength <= 0: print("[server] %s killed himself!" % self.name) world.remove(self) self.died(None, world) else: other.strength -= 1 # hit other robot, decrease other robot's strength self.collision(other) if other.strength <= 0: world.remove(other) other.died(self, world) print("[server] %s killed %s!" % (self.name, other.name)) return True def killed(self, victim, world): """you can override this to react on kills""" pass def collision(self, other): """you can override this to react on collisions between bots""" pass def emote(self, text): """you can override this""" print("[server] %s says: '%s'" % (self.name, text)) class World(object): """the world the robots move in (Cartesian grid)""" def __init__(self, width, height): self.width = width self.height = height self.all = [] self.robots = [] def add_wall(self, wall): self.all.append(wall) def add_robot(self, bot): self.all.append(bot) self.robots.append(bot) def collides(self, x, y): for obj in self.all: if obj.x == x and obj.y == y: return obj return None def remove(self, obj): self.all.remove(obj) self.robots.remove(obj) def dump(self): line = ' ' * self.width if sys.version_info >= (3, 0): line = bytes(line, "ASCII") grid = [array.array('b', line) for y in range(self.height)] for obj in self.all: grid[obj.y][obj.x] = ord('R') if isinstance(obj, Robot) else ord('#') return grid def __getstate__(self): return self.width, self.height, self.all, self.robots def __setstate__(self, state): self.width, self.height, self.all, self.robots = state Pyro4-4.82/examples/ser_custom/000077500000000000000000000000001416147301300164655ustar00rootroot00000000000000Pyro4-4.82/examples/ser_custom/Readme.txt000077500000000000000000000007551416147301300204350ustar00rootroot00000000000000Shows the use of the serializer hooks to be able to transfer custom classes via Pyro (without using the pickle serializer). If you don't use the serializer hooks, the code will crash with a SerializeError: unsupported serialized class, but now, it will happily transfer your object using the custom serialization hooks. It is recommended to avoid using these hooks if possible, there's a security risk to create arbitrary objects from serialized data that is received from untrusted sources. Pyro4-4.82/examples/ser_custom/client.py000077500000000000000000000032601416147301300203210ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 from Pyro4.util import SerializerBase import mycustomclasses # use serpent Pyro4.config.SERIALIZER = "serpent" # register the special serialization hooks def thingy_class_to_dict(obj): print("{serializer hook, converting to dict: %s}" % obj) return { "__class__": "waheeee-custom-thingy", "number-attribute": obj.number } def thingy_dict_to_class(classname, d): print("{deserializer hook, converting to class: %s}" % d) return mycustomclasses.Thingy(d["number-attribute"]) def otherthingy_dict_to_class(classname, d): print("{deserializer hook, converting to class: %s}" % d) return mycustomclasses.OtherThingy(d["number"]) # for 'Thingy' we register both serialization and deserialization hooks SerializerBase.register_class_to_dict(mycustomclasses.Thingy, thingy_class_to_dict) SerializerBase.register_dict_to_class("waheeee-custom-thingy", thingy_dict_to_class) # for 'OtherThingy' we only register a deserialization hook (and for serialization depend on serpent's default behavior) SerializerBase.register_dict_to_class("mycustomclasses.OtherThingy", otherthingy_dict_to_class) # regular pyro stuff if sys.version_info < (3, 0): input = raw_input uri = input("Enter the URI of the server object: ") serv = Pyro4.core.Proxy(uri) print("\nTransferring thingy...") o = mycustomclasses.Thingy(42) response = serv.method(o) print("type of response object:", type(response)) print("response:", response) print("\nTransferring otherthingy...") o = mycustomclasses.OtherThingy(42) response = serv.othermethod(o) print("type of response object:", type(response)) print("response:", response) Pyro4-4.82/examples/ser_custom/mycustomclasses.py000077500000000000000000000006251416147301300223030ustar00rootroot00000000000000# defines custom classes class Thingy(object): def __init__(self, num): self.number = num def __str__(self): return "" class OtherThingy(object): def __init__(self, num): self.number = num def __str__(self): return "" Pyro4-4.82/examples/ser_custom/server.py000077500000000000000000000031651416147301300203550ustar00rootroot00000000000000from __future__ import print_function import Pyro4 from Pyro4.util import SerializerBase import mycustomclasses # use serpent Pyro4.config.SERIALIZER = "serpent" # register the special serialization hooks def thingy_class_to_dict(obj): print("{serializer hook, converting to dict: %s}" % obj) return { "__class__": "waheeee-custom-thingy", "number-attribute": obj.number } def thingy_dict_to_class(classname, d): print("{deserializer hook, converting to class: %s}" % d) return mycustomclasses.Thingy(d["number-attribute"]) def otherthingy_dict_to_class(classname, d): print("{deserializer hook, converting to class: %s}" % d) return mycustomclasses.OtherThingy(d["number"]) # for 'Thingy' we register both serialization and deserialization hooks SerializerBase.register_dict_to_class("waheeee-custom-thingy", thingy_dict_to_class) SerializerBase.register_class_to_dict(mycustomclasses.Thingy, thingy_class_to_dict) # for 'OtherThingy' we only register a deserialization hook (and for serialization depend on serpent's default behavior) SerializerBase.register_dict_to_class("mycustomclasses.OtherThingy", otherthingy_dict_to_class) # regular Pyro server stuff @Pyro4.expose class Server(object): def method(self, arg): print("\nmethod called, arg=", arg) response = mycustomclasses.Thingy(999) return response def othermethod(self, arg): print("\nothermethod called, arg=", arg) response = mycustomclasses.OtherThingy(999) return response Pyro4.core.Daemon.serveSimple( { Server: "example.customclasses" }, ns=False) Pyro4-4.82/examples/servertypes/000077500000000000000000000000001416147301300166755ustar00rootroot00000000000000Pyro4-4.82/examples/servertypes/Readme.txt000066400000000000000000000007101416147301300206310ustar00rootroot00000000000000Shows the different behaviors of Pyro's server types. First start the server, it will ask what type of server you want to run. The client will print some information about what's happening. Try it with different server types and see how that changes the behavior. You can also try to set ONEWAY_THREADED=False on the server side, to change the behavior of oneway calls. The client will print a message if it detects you have been fiddling with this ;-) Pyro4-4.82/examples/servertypes/client.py000066400000000000000000000062061416147301300205310ustar00rootroot00000000000000from __future__ import print_function import sys import time import threading import Pyro4 if sys.version_info < (3, 0): current_thread = threading.currentThread else: current_thread = threading.current_thread serv = Pyro4.core.Proxy("PYRONAME:example.servertypes") print("--------------------------------------------------------------") print(" This part is independent of the type of the server. ") print("--------------------------------------------------------------") print("Calling 5 times oneway method. Should return immediately.") serv.reset() begin = time.time() serv.onewaydelay() serv.onewaydelay() serv.onewaydelay() serv.onewaydelay() serv.onewaydelay() print("Done with the oneway calls.") completed = serv.getcount() print("Number of completed calls in the server: %d" % completed) print(" (this should be 0, because all 5 calls are still busy in the background)") if completed > 0: print(" !!! The oneway calls were not running in the background !!!") print(" ??? Are you sure ONEWAY_THREADED=True on the server ???") print() print("Calling normal delay 5 times. They will all be processed by the same server thread because we're using the same proxy.") r = serv.delay() print(" call processed by: %s" % r) r = serv.delay() print(" call processed by: %s" % r) r = serv.delay() print(" call processed by: %s" % r) r = serv.delay() print(" call processed by: %s" % r) r = serv.delay() print(" call processed by: %s" % r) time.sleep(2) print("Number of completed calls in the server: %d" % serv.getcount()) print(" (this should be 10, because by now the 5 oneway calls have completed as well)") serv.reset() print("\n--------------------------------------------------------------") print(" This part depends on the type of the server. ") print("--------------------------------------------------------------") print("Creating 5 threads that each call the server at the same time.") serverconfig = serv.getconfig() if serverconfig["SERVERTYPE"] == "thread": print("Servertype is thread. All calls will run in parallel.") print("The time this will take is 1 second (every thread takes 1 second in parallel).") print("You will see that the requests are handled by different server threads.") elif serverconfig["SERVERTYPE"] == "multiplex": print("Servertype is multiplex. The threads will need to get in line.") print("The time this will take is 5 seconds (every thread takes 1 second sequentially).") print("You will see that the requests are handled by a single server thread.") else: print("Unknown servertype") def func(uri): # This will run in a thread. Create a proxy just for this thread: with Pyro4.core.Proxy(uri) as p: processed = p.delay() print("[ thread %s called delay, processed by: %s ] " % (current_thread().getName(), processed)) serv._pyroBind() # simplify the uri threads = [] for i in range(5): t = threading.Thread(target=func, args=[serv._pyroUri]) t.setDaemon(True) threads.append(t) t.start() print("Waiting for threads to finish:") for t in threads: t.join() print("Done. Number of completed calls in the server: %d" % serv.getcount()) Pyro4-4.82/examples/servertypes/server.py000066400000000000000000000023701416147301300205570ustar00rootroot00000000000000from __future__ import print_function import time import sys import threading import Pyro4 if sys.version_info < (3, 0): input = raw_input current_thread = threading.currentThread else: current_thread = threading.current_thread @Pyro4.expose @Pyro4.behavior(instance_mode="single") class Server(object): def __init__(self): self.callcount = 0 def reset(self): self.callcount = 0 def getcount(self): return self.callcount # the number of completed calls def getconfig(self): return Pyro4.config.asDict() def delay(self): threadname = current_thread().getName() print("delay called in thread %s" % threadname) time.sleep(1) self.callcount += 1 return threadname @Pyro4.oneway def onewaydelay(self): threadname = current_thread().getName() print("onewaydelay called in thread %s" % threadname) time.sleep(1) self.callcount += 1 # main program Pyro4.config.SERVERTYPE = "undefined" servertype = input("Servertype threaded or multiplex (t/m)?") if servertype == "t": Pyro4.config.SERVERTYPE = "thread" else: Pyro4.config.SERVERTYPE = "multiplex" Pyro4.Daemon.serveSimple({ Server: "example.servertypes" }) Pyro4-4.82/examples/shoppingcart/000077500000000000000000000000001416147301300170035ustar00rootroot00000000000000Pyro4-4.82/examples/shoppingcart/Readme.txt000066400000000000000000000013101416147301300207340ustar00rootroot00000000000000A very simple example that shows the creation and manipulation of new objects in the server. It is a shop where the clients need to take a shopping cart (created in the shop server) and put items in it from the shop's inventory. After that they take it to the shop's counter to pay and get a receipt. Due to Pyro's autoproxy feature the shopping carts are automatically returned to the client as a proxy. The Shoppingcart objects remain in the shop server. The client code interacts with them (and with the shop) remotely. The shop returns a receipt (just a text list of purchased goods) at checkout time, and puts back the shopping cart (unregisters and deletes the object) when the client leaves the store. Pyro4-4.82/examples/shoppingcart/clients.py000066400000000000000000000037761416147301300210330ustar00rootroot00000000000000from __future__ import print_function import random import Pyro4 shop = Pyro4.Proxy("PYRONAME:example.shop") print("Simulating some customers.") harrysCart = shop.enter("Harry") sallysCart = shop.enter("Sally") shoplifterCart = shop.enter("shoplifter") # harry buys 4 things and sally 5, shoplifter takes 3 items # note that we put the item directly in the shopping cart. goods = list(shop.goods().keys()) for i in range(4): item = random.choice(goods) print("Harry buys %s" % item) harrysCart.purchase(item) for i in range(5): item = random.choice(goods) print("Sally buys %s" % item) sallysCart.purchase(item) for i in range(3): item = random.choice(goods) print("Shoplifter takes %s" % item) shoplifterCart.purchase(item) print("Customers currently in the shop: %s" % shop.customers()) # Go to the counter to pay and get a receipt. # The shopping cart is still 'inside the shop' (=on the server) # so it knows what is in there for every customer in the store. # Harry pays by just telling his name (and the shop looks up # harry's shoppingcart). # Sally just hands in her shopping cart directly. # The shoplifter tries to leave without paying. try: receipt = shop.payByName("Harry") except: print("ERROR: %s" % ("".join(Pyro4.util.getPyroTraceback()))) print("Harry payed. The cart now contains: %s (should be empty)" % harrysCart.getContents()) print("Harry got this receipt:") print(receipt) receipt = shop.payCart(sallysCart) print("Sally payed. The cart now contains: %s (should be empty)" % sallysCart.getContents()) print("Sally got this receipt:") print(receipt) print("Harry is leaving.") shop.leave("Harry") print("Sally is leaving.") shop.leave("Sally") print("Shoplifter is leaving. (should be impossible i.e. give an error)") try: shop.leave("shoplifter") except: print("".join(Pyro4.util.getPyroTraceback())) print("Harry is attempting to put stuff back in his cart again,") print("which should fail because the cart does no longer exist.") harrysCart.purchase("crap") Pyro4-4.82/examples/shoppingcart/shoppingcart.py000066400000000000000000000006641416147301300220640ustar00rootroot00000000000000from __future__ import print_function import Pyro4 @Pyro4.expose class ShoppingCart(object): def __init__(self): self.contents = [] print("(shoppingcart %d taken)" % id(self)) def purchase(self, item): self.contents.append(item) print("(%s put into shoppingcart %d)" % (item, id(self))) def empty(self): self.contents = [] def getContents(self): return self.contents Pyro4-4.82/examples/shoppingcart/shopserver.py000066400000000000000000000043611416147301300215610ustar00rootroot00000000000000from __future__ import print_function import time import Pyro4 from shoppingcart import ShoppingCart @Pyro4.expose @Pyro4.behavior(instance_mode="single") class Shop(object): inventory = { "paper": 1.25, "bread": 1.50, "meat": 5.99, "milk": 0.80, "fruit": 2.65, "chocolate": 3.99, "pasta": 0.50, "sauce": 1.20, "vegetables": 1.40, "cookies": 1.99, "pizza": 3.60, "shampoo": 2.22, "whiskey": 24.99 } customersInStore = {} def enter(self, name): print("Customer %s enters the store." % name) print("Customer takes a shopping cart.") # create a cart and return it as a pyro object to the client cart = ShoppingCart() self.customersInStore[name] = cart self._pyroDaemon.register(cart) # make cart a pyro object return cart def customers(self): return list(self.customersInStore.keys()) def goods(self): return self.inventory def payByName(self, name): print("Customer %s goes to the counter to pay." % name) cart = self.customersInStore[name] return self.payCart(cart, name) def payCart(self, cart, name=None): receipt = [] if name: receipt.append("Receipt for %s." % name) receipt.append("Receipt Date: " + time.asctime()) total = 0.0 for item in cart.getContents(): price = self.inventory[item] total += price receipt.append("%13s %.2f" % (item, price)) receipt.append("") receipt.append("%13s %.2f" % ("total:", total)) cart.empty() return "\n".join(receipt) def leave(self, name): print("Customer %s leaves." % name) cart = self.customersInStore[name] print(" their shopping cart contains: %s" % cart.getContents()) if cart.getContents(): print(" it is not empty, they are trying to shoplift!") raise Exception("attempt to steal a full cart prevented") # delete the cart and unregister it with pyro del self.customersInStore[name] self._pyroDaemon.unregister(cart) # main program Pyro4.Daemon.serveSimple({ Shop: "example.shop" }) Pyro4-4.82/examples/socketpair/000077500000000000000000000000001416147301300164465ustar00rootroot00000000000000Pyro4-4.82/examples/socketpair/pair-fork.py000066400000000000000000000032001416147301300207050ustar00rootroot00000000000000# this example forks() and thus won't work on Windows. from __future__ import print_function import os import signal import socket import Pyro4 # it's okay to use Pickle because we trust our own processes, and it is efficient Pyro4.config.SERIALIZER = "pickle" Pyro4.config.SERIALIZERS_ACCEPTED |= {"pickle"} # create our own socket pair (server-client sockets that are already connected) sock1, sock2 = socket.socketpair() pid = os.fork() if pid == 0: # we are the child process, we host the daemon. class Echo(object): @Pyro4.expose def echo(self, message): print("server got message: ", message) return "thank you" # create a daemon with some Pyro objectrunning on our custom server socket daemon = Pyro4.Daemon(connected_socket=sock1) daemon.register(Echo, "echo") print("Process PID={:d}: Pyro daemon running on {:s}\n".format(os.getpid(), daemon.locationStr)) daemon.requestLoop() else: # we are the parent process, we create a Pyro client proxy print("Process PID={:d}: Pyro client.\n".format(os.getpid())) # create a client running on the client socket with Pyro4.Proxy("echo", connected_socket=sock2) as p: reply = p.echo("hello!") print("client got reply:", reply) reply = p.echo("hello again!") print("client got reply:", reply) with Pyro4.Proxy("echo", connected_socket=sock2) as p: reply = p.echo("hello2!") print("client got reply:", reply) reply = p.echo("hello2 again!") print("client got reply:", reply) os.kill(pid, signal.SIGTERM) os.waitpid(pid, 0) print("\nThe end.") Pyro4-4.82/examples/socketpair/pair-thread.py000066400000000000000000000025371416147301300212270ustar00rootroot00000000000000# this example uses a background thread to run the daemon in, also works on Windows from __future__ import print_function import socket import threading import Pyro4 # it's okay to use Pickle because we trust our own processes, and it is efficient Pyro4.config.SERIALIZER = "pickle" Pyro4.config.SERIALIZERS_ACCEPTED |= {"pickle"} # create our own socket pair (server-client sockets that are already connected) sock1, sock2 = socket.socketpair() class Echo(object): @Pyro4.expose def echo(self, message): print("server got message: ", message) return "thank you" # create a daemon with some Pyro objectrunning on our custom server socket daemon = Pyro4.Daemon(connected_socket=sock1) daemon.register(Echo, "echo") print("(Pyro daemon running on", daemon.locationStr, ")\n") daemonthread = threading.Thread(target=daemon.requestLoop) daemonthread.daemon = True daemonthread.start() # create a client running on the client socket with Pyro4.Proxy("echo", connected_socket=sock2) as p: reply = p.echo("hello!") print("client got reply:", reply) reply = p.echo("hello again!") print("client got reply:", reply) with Pyro4.Proxy("echo", connected_socket=sock2) as p: reply = p.echo("hello2!") print("client got reply:", reply) reply = p.echo("hello2 again!") print("client got reply:", reply) print("\nThe end.") Pyro4-4.82/examples/socketpair/readme.txt000066400000000000000000000012401416147301300204410ustar00rootroot00000000000000Pyro 4.70 introduced the possibility to run a Pyro Daemon and Proxy over a user-supplied, already connected socket, such as those produced by the socket.socketpair() function. This makes it easy to communicate efficiently with a child process or background thread, using Pyro. The pair-fork.py program uses fork() to run a background process (Windows doesn't support this) The pair-thread.py program uses a background thread for the Pyro daemon (works on Windows too). Note that it is fine to use pickle as serializer here because all communication is done between threads or processes that we created ourselves and the socket is not accessible to the outside world. Pyro4-4.82/examples/ssl/000077500000000000000000000000001416147301300151035ustar00rootroot00000000000000Pyro4-4.82/examples/ssl/Readme.txt000066400000000000000000000015671416147301300170520ustar00rootroot00000000000000SSL example showing how to configure 2-way-SSL with custom certificate validation. What this means is that the server has a certificate, and the client as well. The server only accepts connections from clients that provide the proper certificate (and ofcourse, clients only connect to servers having a proper certificate). By using Pyro's handshake mechanism you can easily add custom certificate verification steps in both the client (proxy) and server (daemon). This is more or less required, because you should be checking if the certificate is indeed from the party you expected... This example uses the self-signed demo certs that come with Pyro, so in the code you'll also see that we configure the SSL_CACERTS so that SSL will accept the self-signed certificate as a valid cert. If the connection is successfully established, all communication is then encrypted and secure. Pyro4-4.82/examples/ssl/client.py000066400000000000000000000053511416147301300167370ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4.core import Pyro4.errors if sys.version_info < (3, 0): input = raw_input Pyro4.config.SSL = True Pyro4.config.SSL_CACERTS = "../../certs/server_cert.pem" # to make ssl accept the self-signed server cert Pyro4.config.SSL_CLIENTCERT = "../../certs/client_cert.pem" Pyro4.config.SSL_CLIENTKEY = "../../certs/client_key.pem" print("SSL enabled (2-way).") def verify_cert(cert): if not cert: raise Pyro4.errors.CommunicationError("cert missing") # note: hostname and expiry date validation is already successfully performed by the SSL layer itself # not_before = datetime.datetime.utcfromtimestamp(ssl.cert_time_to_seconds(cert["notBefore"])) # print("not before:", not_before) # not_after = datetime.datetime.utcfromtimestamp(ssl.cert_time_to_seconds(cert["notAfter"])) # print("not after:", not_after) # today = datetime.datetime.now() # if today > not_after or today < not_before: # raise Pyro4.errors.CommunicationError("cert not yet valid or expired") if cert["serialNumber"] != "D163AB82B8B74DE6": raise Pyro4.errors.CommunicationError("cert serial number incorrect") issuer = dict(p[0] for p in cert["issuer"]) subject = dict(p[0] for p in cert["subject"]) if issuer["organizationName"] != "Razorvine.net": # issuer is not often relevant I guess, but just to show that you have the data raise Pyro4.errors.CommunicationError("cert not issued by Razorvine.net") if subject["countryName"] != "NL": raise Pyro4.errors.CommunicationError("cert not for country NL") if subject["organizationName"] != "Razorvine.net": raise Pyro4.errors.CommunicationError("cert not for Razorvine.net") print("(SSL server cert is ok: serial={ser}, subject={subj})" .format(ser=cert["serialNumber"], subj=subject["organizationName"])) # to make Pyro verify the certificate on new connections, use the handshake mechanism: class CertCheckingProxy(Pyro4.core.Proxy): def _pyroValidateHandshake(self, response): cert = self._pyroConnection.getpeercert() verify_cert(cert) # Note: to automatically enforce certificate verification for all proxy objects you create, # you can also monkey-patch the method in the Proxy class itself. # Then you don't have to make sure that you're using CertCheckingProxy every time. # However some other Proxy subclass can (will) override this again! # # def certverifier(self, response): # cert = self._pyroConnection.getpeercert() # verify_cert(cert) # Pyro4.core.Proxy._pyroValidateHandshake = certverifier uri = input("Server uri: ").strip() with CertCheckingProxy(uri) as p: response = p.echo("client speaking") print("response:", response) Pyro4-4.82/examples/ssl/server.py000066400000000000000000000045341416147301300167710ustar00rootroot00000000000000from __future__ import print_function import Pyro4.core class Safe(object): @Pyro4.expose def echo(self, message): print("got message:", message) return "hi!" Pyro4.config.SSL = True Pyro4.config.SSL_REQUIRECLIENTCERT = True # enable 2-way ssl Pyro4.config.SSL_SERVERCERT = "../../certs/server_cert.pem" Pyro4.config.SSL_SERVERKEY = "../../certs/server_key.pem" Pyro4.config.SSL_CACERTS = "../../certs/client_cert.pem" # to make ssl accept the self-signed client cert print("SSL enabled (2-way).") class CertValidatingDaemon(Pyro4.core.Daemon): def validateHandshake(self, conn, data): cert = conn.getpeercert() if not cert: raise Pyro4.errors.CommunicationError("client cert missing") # note: hostname and expiry date validation is already successfully performed by the SSL layer itself # not_before = datetime.datetime.utcfromtimestamp(ssl.cert_time_to_seconds(cert["notBefore"])) # print("not before:", not_before) # not_after = datetime.datetime.utcfromtimestamp(ssl.cert_time_to_seconds(cert["notAfter"])) # print("not after:", not_after) # today = datetime.datetime.now() # if today > not_after or today < not_before: # raise Pyro4.errors.CommunicationError("cert not yet valid or expired") if cert["serialNumber"] != "9BFD9872D96F066C": raise Pyro4.errors.CommunicationError("cert serial number incorrect") issuer = dict(p[0] for p in cert["issuer"]) subject = dict(p[0] for p in cert["subject"]) if issuer["organizationName"] != "Razorvine.net": # issuer is not often relevant I guess, but just to show that you have the data raise Pyro4.errors.CommunicationError("cert not issued by Razorvine.net") if subject["countryName"] != "NL": raise Pyro4.errors.CommunicationError("cert not for country NL") if subject["organizationName"] != "Razorvine.net": raise Pyro4.errors.CommunicationError("cert not for Razorvine.net") print("(SSL client cert is ok: serial={ser}, subject={subj})" .format(ser=cert["serialNumber"], subj=subject["organizationName"])) return super(CertValidatingDaemon, self).validateHandshake(conn, data) d = CertValidatingDaemon() uri = d.register(Safe) print("server uri:", uri) d.requestLoop() Pyro4-4.82/examples/stockquotes-old/000077500000000000000000000000001416147301300174425ustar00rootroot00000000000000Pyro4-4.82/examples/stockquotes-old/Readme.txt000077500000000000000000000054251416147301300214110ustar00rootroot00000000000000This example is the OLD code from the Pyro tutorial where a simple stock quote system was built. It is based on callbacks instead of iterators. For the new version (which is a lot easier) look at the 'stockquotes' example. The idea is that we have multiple stock markets producing stock symbol quotes. There is an aggregator that combines the quotes from all stock markets. Finally there are multiple viewers that can register themselves by the aggregator and let it know what stock symbols they're interested in. The viewers will then receive near-real-time stock quote updates for the symbols they selected. (Everything is fictional, of course). Stockmarket ->-----\ /----> Viewer Stockmarket ->------> Aggregator ->-----> Viewer Stockmarket ->-----/ \----> Viewer The tutorial consists of 3 phases: phase 1: Simple prototype code where everything is running in a single process. Main.py creates all objects, connects them together, and contains a loop that drives the stockmarket quote generation. This code is fully operational but contains no Pyro code at all and shows what the system is going to look like later on. phase 2: Still no Pyro code, but the components are now more autonomous. They each have a main function that starts up the component and connects it to the other component(s). As the Stockmarket is the source of the data, it now contains a thread that produces stock quote changes. Main.py now only starts the various components and then sits to wait for an exit signal. While this phase still doesn't use Pyro at all, the structure of the code and the components are very close to what we want to achieve in the end where everything is fully distributed. phase 3: The components are now fully distributed and we used Pyro to make them talk to each other. There is no main.py anymore because you have to start every component by itself: (in separate console windows for instance) - start a Pyro name server (python -m Pyro4.naming). - start the stockmarket - start the aggregator - start one or more of the viewers. A lot of subjects are not addressed in this tutorial, such as what to do when one or more of the viewers quits (error handling and unregistration), what to do when a new stockmarket is opening when we have a system running already, what if a viewer is blocking the processing of the stock quote updates, etc. Note that phase 3 of this example makes use of Pyro's AutoProxy feature. Sending pyro objects 'over the wire' will automatically convert them into proxies so that the other side will talk to the actual object, instead of a local copy. This is how the aggregator makes itself known to the stockmarket and the viewer makes itself known to the aggregator. Pyro4-4.82/examples/stockquotes-old/phase1/000077500000000000000000000000001416147301300206235ustar00rootroot00000000000000Pyro4-4.82/examples/stockquotes-old/phase1/aggregator.py000066400000000000000000000010601416147301300233140ustar00rootroot00000000000000class Aggregator(object): def __init__(self): self.viewers = {} self.symbols = [] def add_symbols(self, symbols): self.symbols.extend(symbols) def available_symbols(self): return self.symbols def view(self, viewer, symbols): self.viewers[viewer] = symbols def quotes(self, market, stockquotes): for symbol, value in stockquotes.items(): for viewer, symbols in self.viewers.items(): if symbol in symbols: viewer.quote(market, symbol, value) Pyro4-4.82/examples/stockquotes-old/phase1/main.py000066400000000000000000000013051416147301300221200ustar00rootroot00000000000000from __future__ import print_function import time from stockmarket import StockMarket from aggregator import Aggregator from viewer import Viewer def main(): nasdaq = StockMarket("NASDAQ", ["AAPL", "CSCO", "MSFT", "GOOG"]) newyork = StockMarket("NYSE", ["IBM", "HPQ", "BP"]) agg = Aggregator() agg.add_symbols(nasdaq.symbols()) agg.add_symbols(newyork.symbols()) print("aggregated symbols:", agg.available_symbols()) nasdaq.listener(agg) newyork.listener(agg) view = Viewer() agg.view(view, ["IBM", "AAPL", "MSFT"]) print("") while True: nasdaq.generate() newyork.generate() time.sleep(0.5) if __name__ == "__main__": main() Pyro4-4.82/examples/stockquotes-old/phase1/stockmarket.py000066400000000000000000000013211416147301300235210ustar00rootroot00000000000000import random class StockMarket(object): def __init__(self, marketname, symbols): self.name = marketname self.symbolmeans = {} for symbol in symbols: self.symbolmeans[symbol] = random.uniform(20, 200) self.aggregators = [] def generate(self): quotes = {} for symbol, mean in self.symbolmeans.items(): if random.random() < 0.2: quotes[symbol] = round(random.normalvariate(mean, 20), 2) for aggregator in self.aggregators: aggregator.quotes(self.name, quotes) def listener(self, aggregator): self.aggregators.append(aggregator) def symbols(self): return self.symbolmeans.keys() Pyro4-4.82/examples/stockquotes-old/phase1/viewer.py000066400000000000000000000002461416147301300225000ustar00rootroot00000000000000from __future__ import print_function class Viewer(object): def quote(self, market, symbol, value): print("{0}.{1}: {2}".format(market, symbol, value)) Pyro4-4.82/examples/stockquotes-old/phase2/000077500000000000000000000000001416147301300206245ustar00rootroot00000000000000Pyro4-4.82/examples/stockquotes-old/phase2/aggregator.py000066400000000000000000000017721416147301300233270ustar00rootroot00000000000000from __future__ import print_function class Aggregator(object): def __init__(self): self.viewers = {} self.symbols = [] def add_symbols(self, symbols): self.symbols.extend(symbols) def available_symbols(self): return self.symbols def view(self, viewer, symbols): print("aggregator gets a new viewer, for symbols:", symbols) self.viewers[viewer] = symbols def quotes(self, market, stockquotes): for symbol, value in stockquotes.items(): for viewer, symbols in self.viewers.items(): if symbol in symbols: viewer.quote(market, symbol, value) def main(stockmarkets): aggregator = Aggregator() for market in stockmarkets: aggregator.add_symbols(market.symbols()) market.listener(aggregator) if not aggregator.available_symbols(): raise ValueError("no symbols found!") print("aggregated symbols:", aggregator.available_symbols()) return aggregator Pyro4-4.82/examples/stockquotes-old/phase2/main.py000066400000000000000000000005361416147301300221260ustar00rootroot00000000000000from __future__ import print_function import sys import stockmarket import aggregator import viewer if sys.version_info < (3, 0): input = raw_input def main(): markets = stockmarket.main() aggr = aggregator.main(markets) viewer.main(aggr) print("\nPress enter to quit.\n") input() if __name__ == "__main__": main() Pyro4-4.82/examples/stockquotes-old/phase2/stockmarket.py000066400000000000000000000025421416147301300235300ustar00rootroot00000000000000from __future__ import print_function import random import threading import time class StockMarket(object): def __init__(self, marketname, symbols): self.name = marketname self.symbolmeans = {} for symbol in symbols: self.symbolmeans[symbol] = random.uniform(20, 200) self.aggregators = [] def generate(self): quotes = {} for symbol, mean in self.symbolmeans.items(): if random.random() < 0.2: quotes[symbol] = round(random.normalvariate(mean, 20), 2) print("new quotes generated for", self.name) for aggregator in self.aggregators: aggregator.quotes(self.name, quotes) def listener(self, aggregator): print("market {0} adding new aggregator".format(self.name)) self.aggregators.append(aggregator) def symbols(self): return self.symbolmeans.keys() def run(self): def generate_symbols(): while True: time.sleep(random.random()) self.generate() thread = threading.Thread(target=generate_symbols) thread.setDaemon(True) thread.start() def main(): nasdaq = StockMarket("NASDAQ", ["AAPL", "CSCO", "MSFT", "GOOG"]) newyork = StockMarket("NYSE", ["IBM", "HPQ", "BP"]) nasdaq.run() newyork.run() return [nasdaq, newyork] Pyro4-4.82/examples/stockquotes-old/phase2/viewer.py000066400000000000000000000004331416147301300224770ustar00rootroot00000000000000from __future__ import print_function class Viewer(object): def quote(self, market, symbol, value): print("{0}.{1}: {2}".format(market, symbol, value)) def main(aggregator): viewer = Viewer() aggregator.view(viewer, ["IBM", "AAPL", "MSFT"]) return viewer Pyro4-4.82/examples/stockquotes-old/phase3/000077500000000000000000000000001416147301300206255ustar00rootroot00000000000000Pyro4-4.82/examples/stockquotes-old/phase3/aggregator.py000066400000000000000000000026301416147301300233220ustar00rootroot00000000000000from __future__ import print_function import Pyro4 @Pyro4.expose class Aggregator(object): def __init__(self): self.viewers = {} self.symbols = [] def add_symbols(self, symbols): self.symbols.extend(symbols) def available_symbols(self): return self.symbols def view(self, viewer, symbols): print("aggregator gets a new viewer, for symbols:", symbols) self.viewers[viewer] = symbols def quotes(self, market, stockquotes): for symbol, value in stockquotes.items(): for viewer, symbols in self.viewers.items(): if symbol in symbols: viewer.quote(market, symbol, value) def main(): aggregator = Aggregator() daemon = Pyro4.Daemon() agg_uri = daemon.register(aggregator) ns = Pyro4.locateNS() ns.register("example.stockquote-old.aggregator", agg_uri) for market, market_uri in ns.list(prefix="example.stockmarket-old.").items(): print("joining market", market) stockmarket = Pyro4.Proxy(market_uri) stockmarket.listener(aggregator) aggregator.add_symbols(stockmarket.symbols()) if not aggregator.available_symbols(): raise ValueError("no symbols found! (have you started the stock market first?)") print("Aggregator running. Symbols:", aggregator.available_symbols()) daemon.requestLoop() if __name__ == "__main__": main() Pyro4-4.82/examples/stockquotes-old/phase3/stockmarket.py000066400000000000000000000034441416147301300235330ustar00rootroot00000000000000from __future__ import print_function import random import threading import time import Pyro4 class StockMarket(object): def __init__(self, marketname, symbols): self.name = marketname self.symbolmeans = {} for symbol in symbols: self.symbolmeans[symbol] = random.uniform(20, 200) self.aggregators = [] def generate(self): quotes = {} for symbol, mean in self.symbolmeans.items(): if random.random() < 0.2: quotes[symbol] = round(random.normalvariate(mean, 20), 2) print("new quotes generated for", self.name) for aggregator in self.aggregators: aggregator.quotes(self.name, quotes) @Pyro4.expose def listener(self, aggregator): print("market {0} adding new aggregator".format(self.name)) self.aggregators.append(aggregator) @Pyro4.expose def symbols(self): return list(self.symbolmeans.keys()) def run(self): def generate_symbols(): while True: time.sleep(random.random()) self.generate() thread = threading.Thread(target=generate_symbols) thread.setDaemon(True) thread.start() def main(): nasdaq = StockMarket("NASDAQ", ["AAPL", "CSCO", "MSFT", "GOOG"]) newyork = StockMarket("NYSE", ["IBM", "HPQ", "BP"]) with Pyro4.Daemon() as daemon: nasdaq_uri = daemon.register(nasdaq) newyork_uri = daemon.register(newyork) with Pyro4.locateNS() as ns: ns.register("example.stockmarket-old.nasdaq", nasdaq_uri) ns.register("example.stockmarket-old.newyork", newyork_uri) nasdaq.run() newyork.run() print("Stockmarkets running.") daemon.requestLoop() if __name__ == "__main__": main() Pyro4-4.82/examples/stockquotes-old/phase3/viewer.py000066400000000000000000000014701416147301300225020ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input @Pyro4.expose class Viewer(object): def quote(self, market, symbol, value): print("{0}.{1}: {2}".format(market, symbol, value)) def main(): viewer = Viewer() with Pyro4.Daemon() as daemon: daemon.register(viewer) aggregator = Pyro4.Proxy("PYRONAME:example.stockquote-old.aggregator") print("Available stock symbols:", aggregator.available_symbols()) symbols = input("Enter symbols you want to view (comma separated):") symbols = [symbol.strip() for symbol in symbols.split(",")] aggregator.view(viewer, symbols) print("Viewer listening on symbols", symbols) daemon.requestLoop() if __name__ == "__main__": main() Pyro4-4.82/examples/stockquotes/000077500000000000000000000000001416147301300166665ustar00rootroot00000000000000Pyro4-4.82/examples/stockquotes/Readme.txt000066400000000000000000000057211416147301300206310ustar00rootroot00000000000000This example is the code from the Pyro tutorial where a simple stock quote system is built. It processes a stream of stock symbol quotes. The idea is that we have multiple stock markets producing stock symbol quotes. The viewer application aggregates all quotes from all the markets and filters them so you only see the ones you're interested in. Stockmarket ->-----\ Stockmarket ->------>------> Aggregator/Filter/Viewer Stockmarket ->-----/ There's a big simplification here in that the stockmarkets not really produce quote events themselves, instead they return quotes when asked for. But as this is an example, it is sufficient. A more elaborate example where there can be more viewers and the stockmarkets decide themselves when a new quote is available, can be found in the example 'stockquotes-old'. It uses callbacks instead of generators. The tutorial here consists of 3 phases: phase 1: Simple prototype code where everything is running in a single process. viewer.py is the main program that creates all objects, connects them together, and runs the main loop to display stock quotes. This code is fully operational but contains no Pyro code at all. It just shows what the system is going to look like later on. phase 2: The components are now distributed and we use Pyro to make them talk to each other. You have to start both component by itself (in separate console windows for instance): - start a Pyro name server (python -m Pyro4.naming). - start the stockmarket.py (it will create several different markets) - start the viewer.py to see the stream of quotes coming in. The code of the classes themselves is almost identical to phase1, including attribute access and the use of generator functions. The only thing we had to change is to create properties for the attributes that are accessed, and adding an expose decorator. Support for remote iterators/generators is available since Pyro 4.49. In the viewer we didn't hardcode the stock market names but instead we ask the name server for all available stock markets. phase 3: Similar to phase2, but now we make two small changes: a) we use the Pyro name server in such a way that it is accessible from other machines, and b) we run the stock market server in a way that the host is not "localhost" by default and can be accessed by different machines. To do this, create the daemon with the arguments 'host' and 'port' set (i.e. host=HOST_IP, port=HOST_PORT). Again, you have to start both component by itself (in separate console windows for instance): - start a Pyro name server like this: (python -m Pyro4.naming -n 192.168.1.99 -p 9091) or (pyro4-ns -n 192.168.1.99 -p 9091) - start the stockmarket.py (set HOST_IP and HOST_PORT accordingly. Also, make sure HOST_PORT is already open). - start the viewer.py in different remote machines to see the stream of quotes coming in on each window. Pyro4-4.82/examples/stockquotes/phase1/000077500000000000000000000000001416147301300200475ustar00rootroot00000000000000Pyro4-4.82/examples/stockquotes/phase1/stockmarket.py000066400000000000000000000005451416147301300227540ustar00rootroot00000000000000import random import time class StockMarket(object): def __init__(self, marketname, symbols): self.name = marketname self.symbols = symbols def quotes(self): while True: symbol = random.choice(self.symbols) yield symbol, round(random.uniform(5, 150), 2) time.sleep(random.random()/2.0) Pyro4-4.82/examples/stockquotes/phase1/viewer.py000066400000000000000000000017171416147301300217300ustar00rootroot00000000000000from __future__ import print_function from stockmarket import StockMarket class Viewer(object): def __init__(self): self.markets = set() self.symbols = set() def start(self): print("Shown quotes:", self.symbols) quote_sources = { market.name: market.quotes() for market in self.markets } while True: for market, quote_source in quote_sources.items(): quote = next(quote_source) # get a new stock quote from the source symbol, value = quote if symbol in self.symbols: print("{0}.{1}: {2}".format(market, symbol, value)) def main(): nasdaq = StockMarket("NASDAQ", ["AAPL", "CSCO", "MSFT", "GOOG"]) newyork = StockMarket("NYSE", ["IBM", "HPQ", "BP"]) viewer = Viewer() viewer.markets = {nasdaq, newyork} viewer.symbols = {"IBM", "AAPL", "MSFT"} viewer.start() if __name__ == "__main__": main() Pyro4-4.82/examples/stockquotes/phase2/000077500000000000000000000000001416147301300200505ustar00rootroot00000000000000Pyro4-4.82/examples/stockquotes/phase2/stockmarket.py000066400000000000000000000022311416147301300227470ustar00rootroot00000000000000from __future__ import print_function import random import time import Pyro4 @Pyro4.expose class StockMarket(object): def __init__(self, marketname, symbols): self._name = marketname self._symbols = symbols def quotes(self): while True: symbol = random.choice(self.symbols) yield symbol, round(random.uniform(5, 150), 2) time.sleep(random.random()/2.0) @property def name(self): return self._name @property def symbols(self): return self._symbols if __name__ == "__main__": nasdaq = StockMarket("NASDAQ", ["AAPL", "CSCO", "MSFT", "GOOG"]) newyork = StockMarket("NYSE", ["IBM", "HPQ", "BP"]) # for example purposes we will access the daemon and name server ourselves and not use serveSimple with Pyro4.Daemon() as daemon: nasdaq_uri = daemon.register(nasdaq) newyork_uri = daemon.register(newyork) with Pyro4.locateNS() as ns: ns.register("example.stockmarket.nasdaq", nasdaq_uri) ns.register("example.stockmarket.newyork", newyork_uri) print("Stockmarkets available.") daemon.requestLoop() Pyro4-4.82/examples/stockquotes/phase2/viewer.py000066400000000000000000000025171416147301300217300ustar00rootroot00000000000000from __future__ import print_function import Pyro4 class Viewer(object): def __init__(self): self.markets = set() self.symbols = set() def start(self): print("Shown quotes:", self.symbols) quote_sources = { market.name: market.quotes() for market in self.markets } while True: for market, quote_source in quote_sources.items(): quote = next(quote_source) # get a new stock quote from the source symbol, value = quote if symbol in self.symbols: print("{0}.{1}: {2}".format(market, symbol, value)) def find_stockmarkets(): # You can hardcode the stockmarket names for nasdaq and newyork, but it # is more flexible if we just look for every available stockmarket. markets = [] with Pyro4.locateNS() as ns: for market, market_uri in ns.list(prefix="example.stockmarket.").items(): print("found market", market) markets.append(Pyro4.Proxy(market_uri)) if not markets: raise ValueError("no markets found! (have you started the stock markets first?)") return markets def main(): viewer = Viewer() viewer.markets = find_stockmarkets() viewer.symbols = {"IBM", "AAPL", "MSFT"} viewer.start() if __name__ == "__main__": main() Pyro4-4.82/examples/stockquotes/phase3/000077500000000000000000000000001416147301300200515ustar00rootroot00000000000000Pyro4-4.82/examples/stockquotes/phase3/stockmarket.py000066400000000000000000000025021416147301300227510ustar00rootroot00000000000000from __future__ import print_function import random import time import Pyro4 HOST_IP = "127.0.0.1" # Set accordingly (i.e. "192.168.1.99") HOST_PORT = 9092 # Set accordingly (i.e. 9876) @Pyro4.expose class StockMarket(object): def __init__(self, marketname, symbols): self._name = marketname self._symbols = symbols def quotes(self): while True: symbol = random.choice(self.symbols) yield symbol, round(random.uniform(5, 150), 2) time.sleep(random.random()/2.0) @property def name(self): return self._name @property def symbols(self): return self._symbols if __name__ == "__main__": nasdaq = StockMarket("NASDAQ", ["AAPL", "CSCO", "MSFT", "GOOG"]) newyork = StockMarket("NYSE", ["IBM", "HPQ", "BP"]) # Add the proper "host" and "port" arguments for the construction # of the Daemon so it can be accessed remotely with Pyro4.Daemon(host=HOST_IP, port=HOST_PORT) as daemon: nasdaq_uri = daemon.register(nasdaq) newyork_uri = daemon.register(newyork) with Pyro4.locateNS() as ns: ns.register("example.stockmarket.nasdaq", nasdaq_uri) ns.register("example.stockmarket.newyork", newyork_uri) print("Stockmarkets available.") daemon.requestLoop() Pyro4-4.82/examples/stockquotes/phase3/viewer.py000066400000000000000000000025171416147301300217310ustar00rootroot00000000000000from __future__ import print_function import Pyro4 class Viewer(object): def __init__(self): self.markets = set() self.symbols = set() def start(self): print("Shown quotes:", self.symbols) quote_sources = { market.name: market.quotes() for market in self.markets } while True: for market, quote_source in quote_sources.items(): quote = next(quote_source) # get a new stock quote from the source symbol, value = quote if symbol in self.symbols: print("{0}.{1}: {2}".format(market, symbol, value)) def find_stockmarkets(): # You can hardcode the stockmarket names for nasdaq and newyork, but it # is more flexible if we just look for every available stockmarket. markets = [] with Pyro4.locateNS() as ns: for market, market_uri in ns.list(prefix="example.stockmarket.").items(): print("found market", market) markets.append(Pyro4.Proxy(market_uri)) if not markets: raise ValueError("no markets found! (have you started the stock markets first?)") return markets def main(): viewer = Viewer() viewer.markets = find_stockmarkets() viewer.symbols = {"IBM", "AAPL", "MSFT"} viewer.start() if __name__ == "__main__": main() Pyro4-4.82/examples/streaming/000077500000000000000000000000001416147301300162735ustar00rootroot00000000000000Pyro4-4.82/examples/streaming/Readme.txt000066400000000000000000000004661416147301300202370ustar00rootroot00000000000000Show the iterator item streaming support in Pyro 4.49 or newer. If enabled in the server (it is enabled by default), you can return an iterator or generator from a remote call. The client receives a real iterator as a result and can iterate over it to stream the elements one by one from the remote iterator. Pyro4-4.82/examples/streaming/client.py000066400000000000000000000010141416147301300201170ustar00rootroot00000000000000from __future__ import print_function, division import sys import Pyro4 import Pyro4.util if sys.version_info < (3, 0): input = raw_input sys.excepthook = Pyro4.util.excepthook uri = input("Enter streaming server uri: ").strip() with Pyro4.Proxy(uri) as p: print("\nnormal list:") print(p.list()) print("\nvia iterator:") print(list(p.iterator())) print("\nvia generator:") print(list(p.generator())) print("\nslow generator:") for number in p.slow_generator(): print(number) Pyro4-4.82/examples/streaming/server.py000066400000000000000000000015721416147301300201600ustar00rootroot00000000000000from __future__ import print_function, division import time import Pyro4 if Pyro4.config.ITER_STREAMING: print("Note: iter-streaming has been enabled in the Pyro config.") else: print("Note: iter-streaming has not been enabled in the Pyro config (PYRO_ITER_STREAMING).") @Pyro4.expose class Streamer(object): def list(self): return [1, 2, 3, 4, 5, 6, 7, 8, 9] def iterator(self): return iter([1, 2, 3, 4, 5, 6, 7, 8, 9]) def generator(self): i = 1 while i < 10: yield i i += 1 def slow_generator(self): i = 1 while i < 10: time.sleep(0.5) yield i i += 1 def fibonacci(self): a, b = 0, 1 while True: yield a a, b = b, a + b Pyro4.Daemon.serveSimple({ Streamer: "example.streamer" }, ns=False) Pyro4-4.82/examples/thirdpartylib/000077500000000000000000000000001416147301300171635ustar00rootroot00000000000000Pyro4-4.82/examples/thirdpartylib/Readme.txt000066400000000000000000000012361416147301300211230ustar00rootroot00000000000000This example shows two ways of dealing with a third party library whose source code you cannot or don't want to change, and still use its classes directly in Pyro. The first server uses the @expose decorator but applies it as a regular function to create a wrapped, eposed class from the library class. That wrapped class is then registered instead. There are a couple of caveats when using this approach, see the relevant paragraph in the server chapter in the documentation for details. The second server2 shows the approach that I personally prefer: creating explicit adapter classes that call out to the library. You then have full control over what is happening. Pyro4-4.82/examples/thirdpartylib/awesome_thirdparty_library.py000066400000000000000000000011241416147301300251710ustar00rootroot00000000000000# This is an AWESOME LIBRARY. # You can use its AWESOME CLASSES to do Great Things. # The author however DOESN'T allow you to CHANGE the source code and taint it with Pyro decorators! class WeirdReturnType(object): def __init__(self, value): self.value = value class AwesomeClass(object): def method(self, arg): print("Awesome object is called with: ", arg) return "awesome" def private(self): print("This should be a private method...") return "boo" def weird(self): print("Weird!") return WeirdReturnType("awesome") Pyro4-4.82/examples/thirdpartylib/client.py000066400000000000000000000012301416147301300210070ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 import Pyro4.errors if sys.version_info < (3, 0): input = raw_input uri = input("Enter the URI of the thirdparty library object: ").strip() with Pyro4.core.Proxy(uri) as remote: print(remote.method("how are you?")) try: print(remote.weird()) except Pyro4.errors.SerializeError: print("couldn't call weird() due to serialization error of the result value! (is ok)") try: print(remote.private()) # we can call this if full class is exposed... except AttributeError: print("couldn't call private(), it doesn't seem to be exposed! (is ok)") Pyro4-4.82/examples/thirdpartylib/server.py000066400000000000000000000007161416147301300210470ustar00rootroot00000000000000from __future__ import print_function from awesome_thirdparty_library import AwesomeClass import Pyro4 # expose the class from the library using @expose as wrapper function: ExposedClass = Pyro4.expose(AwesomeClass) with Pyro4.Daemon() as daemon: # register the wrapped class instead of the library class itself: uri = daemon.register(ExposedClass, "example.thirdpartylib") print("wrapped class registered, uri: ", uri) daemon.requestLoop() Pyro4-4.82/examples/thirdpartylib/server2.py000066400000000000000000000017111416147301300211250ustar00rootroot00000000000000from __future__ import print_function from awesome_thirdparty_library import AwesomeClass import Pyro4 # create adapter class that only exposes what should be accessible, # and calls into the library class from there: class AwesomeAdapterClass(AwesomeClass): @Pyro4.expose def method(self, arg): print("Adapter class is called...") return super(AwesomeAdapterClass, self).method(arg) @Pyro4.expose def weird(self): result = super(AwesomeAdapterClass, self).weird() # we have full control over what is returned and can turn the custom # result class into a normal string value that has no issues traveling over the wire return "weird " + result.value with Pyro4.Daemon() as daemon: # register the adapter class instead of the library class itself: uri = daemon.register(AwesomeAdapterClass, "example.thirdpartylib") print("adapter class registered, uri: ", uri) daemon.requestLoop() Pyro4-4.82/examples/timeout/000077500000000000000000000000001416147301300157705ustar00rootroot00000000000000Pyro4-4.82/examples/timeout/Readme.txt000066400000000000000000000005651416147301300177340ustar00rootroot00000000000000This is an example that shows the connection timeout handling (in the client). server.py -- the server you need to run for this example client.py -- client that uses timeout settings The client disables and enables timeouts to show what happens. It shows timeouts during long remote method calls, but also timeouts when trying to connect to a unresponsive server. Pyro4-4.82/examples/timeout/client.py000066400000000000000000000056271416147301300176320ustar00rootroot00000000000000from __future__ import print_function import time import sys import Pyro4 import Pyro4.errors # NOTE: the timer in IronPython seems to be wacky. # So we use wider margins for that, to check if the delays are ok. def approxEqual(x, y): return abs(x - y) < 0.2 # disable timeout globally Pyro4.config.COMMTIMEOUT = 0 obj = Pyro4.core.Proxy("PYRONAME:example.timeout") obj._pyroBind() print("No timeout is configured. Calling delay with 2 seconds.") start = time.time() result = obj.delay(2) assert result == "slept 2 seconds" duration = time.time() - start if sys.platform != "cli": assert approxEqual(duration, 2), "expected 2 seconds duration" else: assert 1.0 < duration < 3.0, "expected about 2 seconds duration" # override timeout for this object obj._pyroTimeout = 1 print("Timeout set to 1 seconds. Calling delay with 2 seconds.") start = time.time() try: result = obj.delay(2) print("!?should have raised TimeoutError!?") except Pyro4.errors.TimeoutError: print("TimeoutError! As expected!") duration = time.time() - start if sys.platform != "cli": assert approxEqual(duration, 1), "expected 1 seconds duration" else: assert 0.9 < duration < 1.9, "expected about 1 second duration" # set timeout globally Pyro4.config.COMMTIMEOUT = 1 obj = Pyro4.core.Proxy("PYRONAME:example.timeout") print("COMMTIMEOUT is set globally. Calling delay with 2 seconds.") start = time.time() try: result = obj.delay(2) print("!?should have raised TimeoutError!?") except Pyro4.errors.TimeoutError: print("TimeoutError! As expected!") duration = time.time() - start if sys.platform != "cli": assert approxEqual(duration, 1), "expected 1 seconds duration" else: assert 0.9 < duration < 1.9, "expected about 1 second duration" # override again for this object obj._pyroTimeout = None print("No timeout is configured. Calling delay with 3 seconds.") start = time.time() result = obj.delay(3) assert result == "slept 3 seconds" duration = time.time() - start if sys.platform != "cli": assert approxEqual(duration, 3), "expected 3 seconds duration" else: assert 2.5 < duration < 3.5, "expected about 3 second duration" print("Trying to connect to the frozen daemon.") obj = Pyro4.core.Proxy("PYRONAME:example.timeout.frozendaemon") obj._pyroTimeout = 1 print("Timeout set to 1 seconds. Trying to connect.") start = time.time() try: result = obj.delay(5) print("!?should have raised TimeoutError!?") except Pyro4.errors.TimeoutError: print("TimeoutError! As expected!") duration = time.time() - start if sys.platform != "cli": assert approxEqual(duration, 1), "expected 1 seconds duration" else: assert 0.9 < duration < 1.9, "expected about 1 second duration" print("Disabling timeout and trying to connect again. This may take forever now.") print("Feel free to abort with ctrl-c or ctrl-break.") obj._pyroTimeout = None obj.delay(1) Pyro4-4.82/examples/timeout/server.py000066400000000000000000000014341416147301300176520ustar00rootroot00000000000000from __future__ import print_function import time import Pyro4.naming import Pyro4.core @Pyro4.expose class TimeoutServer(object): def delay(self, amount): print("sleeping %d" % amount) time.sleep(amount) print("done.") return "slept %d seconds" % amount Pyro4.config.COMMTIMEOUT = 0 # the server won't be using timeouts ns = Pyro4.naming.locateNS() daemon = Pyro4.core.Daemon() daemon2 = Pyro4.core.Daemon() obj = TimeoutServer() obj2 = TimeoutServer() uri = daemon.register(obj) uri2 = daemon2.register(obj2) ns.register("example.timeout", uri) ns.register("example.timeout.frozendaemon", uri2) print("Server ready.") # Note that we're only starting one of the 2 daemons. # daemon2 is not started to simulate connection timeouts. daemon.requestLoop() Pyro4-4.82/examples/timezones/000077500000000000000000000000001416147301300163175ustar00rootroot00000000000000Pyro4-4.82/examples/timezones/Readme.txt000066400000000000000000000003351416147301300202560ustar00rootroot00000000000000This example exercises the support for pytz/dateutil timezones in the serializers. Pickle is able to serialize everything just fine. Not everything works for the other serializers, consider it a work in progress for now.Pyro4-4.82/examples/timezones/client.py000066400000000000000000000041341416147301300201510ustar00rootroot00000000000000from __future__ import print_function import Pyro4 import datetime import sys if sys.version_info < (3, 0): input = raw_input uri = input("What is the server uri? ").strip() fmt = '%Y-%m-%d %H:%M:%S %Z%z' print("local time without timezone: ", datetime.datetime.now().strftime(fmt)) def test(): with Pyro4.core.Proxy(uri) as serv: print("\nFIRST: no timezone") date1 = serv.echo(datetime.datetime.now()) print("{0}\n {1} ({2})".format(date1, repr(date1), type(date1))) if hasattr(date1, "tzinfo"): print(" tzinfo =", date1.tzinfo) else: print(" no tzinfo attribute") print("\nSECOND: PyTz timezones") date1 = serv.pytz() assert isinstance(date1.tzinfo, datetime.tzinfo) print("{0}\n {1} ({2})\n {3}".format(date1, date1.tzinfo, type(date1.tzinfo), date1.strftime(fmt))) date2 = serv.echo(date1) print("{0}\n {1} ({2})\n {3}".format(date2, date2.tzinfo, type(date2.tzinfo), date2.strftime(fmt))) assert date1 == date2 print("\nTHIRD: DateUtil timezones") date1 = serv.dateutil() assert isinstance(date1.tzinfo, datetime.tzinfo) print("{0}\n {1} ({2})\n {3}".format(date1, date1.tzinfo, type(date1.tzinfo), date1.strftime(fmt))) date2 = serv.echo(date1) print("{0}\n {1} ({2})\n {3}".format(date2, date2.tzinfo, type(date2.tzinfo), date2.strftime(fmt))) assert date1 == date2 # pickle. print("\n******* pickle *******") print("******* (expecting no errors) ******") Pyro4.config.SERIALIZER = "pickle" try: test() except: import traceback traceback.print_exc() # serpent. print("\n******* serpent *******") Pyro4.config.SERIALIZER = "serpent" try: test() except: import traceback traceback.print_exc() # json. print("\n******* json *******") Pyro4.config.SERIALIZER = "json" try: test() except: import traceback traceback.print_exc() # marshal. print("\n******* marshal *******") Pyro4.config.SERIALIZER = "marshal" try: test() except: import traceback traceback.print_exc() Pyro4-4.82/examples/timezones/server.py000066400000000000000000000014111416147301300201740ustar00rootroot00000000000000from __future__ import print_function import datetime import pytz import dateutil.tz import Pyro4 fmt = '%Y-%m-%d %H:%M:%S %Z%z' Pyro4.config.SERIALIZERS_ACCEPTED = {"pickle", "marshal", "json", "serpent"} @Pyro4.expose class Server(object): def echo(self, date): print("ECHO:") print(" [raw] ", repr(date)) if hasattr(date, "isoformat"): print(" [iso] ", date.isoformat()) return date def pytz(self): tz_nl = pytz.timezone("Europe/Amsterdam") return tz_nl.localize(datetime.datetime.now()) def dateutil(self): tz_nl = dateutil.tz.gettz("Europe/Amsterdam") return datetime.datetime.now(tz_nl) # main program Pyro4.Daemon.serveSimple({ Server: "example.timezones" }, ns=False) Pyro4-4.82/examples/unixdomainsock/000077500000000000000000000000001416147301300173355ustar00rootroot00000000000000Pyro4-4.82/examples/unixdomainsock/Readme.txt000066400000000000000000000005651416147301300213010ustar00rootroot00000000000000This is a very simple example that uses a Unix domain socket instead of a normal tcp/ip socket for server communications. The only difference is the parameter passed to the Daemon class. The client code is unaware of any special socket because you just feed it any Pyro URI. This time the URI will encode a Unix domain socket however, instead of a hostname+port number. Pyro4-4.82/examples/unixdomainsock/abstract_namespace_server.py000066400000000000000000000011631416147301300251150ustar00rootroot00000000000000# this only works on Linux # it uses the abstract namespace socket feature. from __future__ import print_function import Pyro4 @Pyro4.expose class Thingy(object): def message(self, arg): print("Message received:", arg) return "Roger!" with Pyro4.Daemon(unixsocket="\0example_unix.sock") as d: # notice the 0-byte at the start uri = d.register(Thingy, "example.unixsock") print("Server running, uri=", uri) string_uri = str(uri) print("Actually, the uri contains a 0-byte, make sure you copy the part between the quotes to the client:") print(repr(string_uri)) d.requestLoop() Pyro4-4.82/examples/unixdomainsock/client.py000066400000000000000000000005521416147301300211670ustar00rootroot00000000000000from __future__ import print_function import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input uri = input("enter the server uri: ").strip() if "\\x00" in uri: uri=uri.replace("\\x00", "\x00") print("(uri contains 0-byte)") with Pyro4.Proxy(uri) as p: response = p.message("Hello there!") print("Response was:", response) Pyro4-4.82/examples/unixdomainsock/server.py000066400000000000000000000006621416147301300212210ustar00rootroot00000000000000from __future__ import print_function import os import Pyro4 @Pyro4.expose class Thingy(object): def message(self, arg): print("Message received:", arg) return "Roger!" if os.path.exists("example_unix.sock"): os.remove("example_unix.sock") with Pyro4.Daemon(unixsocket="example_unix.sock") as d: uri = d.register(Thingy, "example.unixsock") print("Server running, uri=", uri) d.requestLoop() Pyro4-4.82/examples/usersession/000077500000000000000000000000001416147301300166645ustar00rootroot00000000000000Pyro4-4.82/examples/usersession/Readme.txt000066400000000000000000000013001416147301300206140ustar00rootroot00000000000000This example shows the use of a couple of advanced Pyro constructs to achieve a thread safe, per-user resource connection in the Pyro server. It utilizes: - instance_mode "session" (is the default since 4.47) - annotations to pass the 'user token' from client to server - current_context to access the annotations in the server code - a silly global key-value database to trigger concurrency issues There are probably other ways of achieving the same thing (for instance, using the client connection on the current_context instead of explicitly passing along the user token) but it's just an example to give some inspiration. Before starting the server make sure you have a Pyro name server running. Pyro4-4.82/examples/usersession/client.py000066400000000000000000000043041416147301300205150ustar00rootroot00000000000000from __future__ import print_function import threading import time import sys import Pyro4 if sys.version_info < (3, 0): input = raw_input def get_user_token(): return "user123" class CustomAnnotationProxy(Pyro4.Proxy): # override the method that adds annotations and add our own custom user token annotation def _pyroAnnotations(self): return {"USER": get_user_token().encode("utf-8")} class DbAccessor(threading.Thread): def __init__(self, uri): super(DbAccessor, self).__init__() self.proxy = CustomAnnotationProxy(uri) self.daemon = True def run(self): for i in range(3): try: self.proxy.store("number", 100+i) num = self.proxy.retrieve("number") print("[%s] num=%s" % (self.name, num)) except Exception: import traceback traceback.print_exc() print("\n***** Sequential access using multiple proxies on the Session-Bound Database... (no issues)") with CustomAnnotationProxy("PYRONAME:example.usersession.sessiondb") as p1,\ CustomAnnotationProxy("PYRONAME:example.usersession.sessiondb") as p2: p1.store("number", 42) p1.retrieve("number") p2.store("number", 43) p2.retrieve("number") print("\n***** Sequential access using multiple proxies on the Singleton Database... (no issues)") with CustomAnnotationProxy("PYRONAME:example.usersession.singletondb") as p1,\ CustomAnnotationProxy("PYRONAME:example.usersession.singletondb") as p2: p1.store("number", 42) p1.retrieve("number") p2.store("number", 43) p2.retrieve("number") print("\n***** Multiple concurrent proxies on the Session-Bound Database... (no issues)") input("enter to start: ") t1 = DbAccessor("PYRONAME:example.usersession.sessiondb") t2 = DbAccessor("PYRONAME:example.usersession.sessiondb") t1.start() t2.start() time.sleep(1) t1.join() t2.join() print("\n***** Multiple concurrent proxies on the Singleton Database... (threading problem)") input("enter to start: ") t1 = DbAccessor("PYRONAME:example.usersession.singletondb") t2 = DbAccessor("PYRONAME:example.usersession.singletondb") t1.start() t2.start() time.sleep(1) t1.join() t2.join() Pyro4-4.82/examples/usersession/database.py000066400000000000000000000052731416147301300210110ustar00rootroot00000000000000from __future__ import print_function import threading import time import random import traceback class DummyDatabase(object): """Key-value datastore""" def __init__(self): self.storage = {} self.allowed_users = ["user123", "admin"] def connect(self, user): return Connection(self, user) def __setitem__(self, key, value): time.sleep(random.random()/10) # artificial delay self.storage[key] = value def __getitem__(self, item): time.sleep(random.random()/10) # artificial delay return self.storage[item] class Connection(object): """ Connection to the key-value datastore with artificial limitation that only a single thread may use the connection at the same time """ def __init__(self, db, user=None): self.db = db self.user = user self.lock = threading.RLock() def store(self, key, value, user=None): user = user or self.user assert user in self.db.allowed_users, "access denied" if self.lock.acquire(blocking=False): print("DB: user %s stores: %s = %s" % (user, key, value)) self.db[key] = value self.lock.release() else: raise RuntimeError("ERROR: concurrent connection access (write) by multiple different threads") def retrieve(self, key, user=None): user = user or self.user assert user in self.db.allowed_users, "access denied" if self.lock.acquire(blocking=False): print("DB: user %s retrieve: %s" % (user, key)) value = self.db[key] self.lock.release() return value else: raise RuntimeError("ERROR: concurrent connection access (read) by multiple different threads") if __name__ == "__main__": # first single threaded access db = DummyDatabase() conn = db.connect("user123") for i in range(5): conn.store("amount", 100+i) conn.retrieve("amount") # now multiple threads, should crash class ClientThread(threading.Thread): def __init__(self, conn): super(ClientThread, self).__init__() self.conn = conn self.daemon = True def run(self): for i in range(5): try: self.conn.store("amount", 100+i) except Exception: traceback.print_exc() try: self.conn.retrieve("amount") except Exception: traceback.print_exc() client1 = ClientThread(conn) client2 = ClientThread(conn) client1.start() client2.start() time.sleep(0.1) client1.join() client2.join() Pyro4-4.82/examples/usersession/server.py000066400000000000000000000037621416147301300205540ustar00rootroot00000000000000from __future__ import print_function import Pyro4 from database import DummyDatabase Pyro4.config.SERVERTYPE = "thread" database = DummyDatabase() @Pyro4.behavior(instance_mode="single") @Pyro4.expose class SingletonDatabase(object): """ This pyro object will exhibit problems when used from multiple proxies at the same time because it will access the database connection concurrently from different threads """ def __init__(self): print("[%s] new instance and connection" % self.__class__.__name__) self.conn = database.connect(user=None) # user is per-call, not global def store(self, key, value): # get the user-token from the USER annotation ctx = Pyro4.current_context user = ctx.annotations["USER"].decode("utf-8") self.conn.store(key, value, user=user) def retrieve(self, key): # get the user-token from the USER annotation ctx = Pyro4.current_context user = ctx.annotations["USER"].decode("utf-8") return self.conn.retrieve(key, user=user) def ping(self): return "hi" @Pyro4.behavior(instance_mode="session") @Pyro4.expose class SessionboundDatabase(object): """ This pyro object will work fine when used from multiple proxies at the same time because you'll get a new instance for every new session (proxy connection) """ def __init__(self): # get the user-token from the USER annotation ctx = Pyro4.current_context user = ctx.annotations["USER"].decode("utf-8") self.connection = database.connect(user) print("[%s] new instance and connection for user: %s" % (self.__class__.__name__, user)) def store(self, key, value): self.connection.store(key, value) def retrieve(self, key): return self.connection.retrieve(key) def ping(self): return "hi" Pyro4.Daemon.serveSimple({ SingletonDatabase: "example.usersession.singletondb", SessionboundDatabase: "example.usersession.sessiondb" }) Pyro4-4.82/examples/warehouse/000077500000000000000000000000001416147301300163045ustar00rootroot00000000000000Pyro4-4.82/examples/warehouse/Readme.txt000077500000000000000000000026251416147301300202520ustar00rootroot00000000000000This example is the code from the Pyro tutorial where we build a simple warehouse that stores items. The idea is that there is one big warehouse that everyone can store items in, and retrieve other items from (if they're in the warehouse). The tutorial consists of 3 phases: phase 1: Simple prototype code where everything is running in a single process. visit.py creates the warehouse and two visitors. This code is fully operational but contains no Pyro code at all and shows what the system is going to look like later on. phase 2: Pyro is now used to make the warehouse a standalone component. You can still visit it of course. visit.py does need the URI of the warehouse however. (It is printed as soon as the warehouse is started) The code of the Warehouse and the Person classes is unchanged. phase 3: Phase 2 works fine but is a bit cumbersome because you need to copy-paste the warehouse URI to be able to visit it. Phase 3 simplifies things a bit by using the Pyro name server. Also, it uses the Pyro excepthook to print a nicer exception message if anything goes wrong. (Try taking something from the warehouse that is not present!) The code of the Warehouse and the Person classes is still unchanged. Note: to avoid having to deal with serialization issues, this example only passes primitive types (strings in this case) to the remote method calls. Pyro4-4.82/examples/warehouse/phase1/000077500000000000000000000000001416147301300174655ustar00rootroot00000000000000Pyro4-4.82/examples/warehouse/phase1/person.py000066400000000000000000000015221416147301300213450ustar00rootroot00000000000000from __future__ import print_function import sys if sys.version_info < (3, 0): input = raw_input class Person(object): def __init__(self, name): self.name = name def visit(self, warehouse): print("This is {0}.".format(self.name)) self.deposit(warehouse) self.retrieve(warehouse) print("Thank you, come again!") def deposit(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type a thing you want to store (or empty): ").strip() if item: warehouse.store(self.name, item) def retrieve(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type something you want to take (or empty): ").strip() if item: warehouse.take(self.name, item) Pyro4-4.82/examples/warehouse/phase1/visit.py000066400000000000000000000003341416147301300211750ustar00rootroot00000000000000# This is the code that runs this example. from warehouse import Warehouse from person import Person warehouse = Warehouse() janet = Person("Janet") henry = Person("Henry") janet.visit(warehouse) henry.visit(warehouse) Pyro4-4.82/examples/warehouse/phase1/warehouse.py000066400000000000000000000007261416147301300220460ustar00rootroot00000000000000from __future__ import print_function class Warehouse(object): def __init__(self): self.contents = ["chair", "bike", "flashlight", "laptop", "couch"] def list_contents(self): return self.contents def take(self, name, item): self.contents.remove(item) print("{0} took the {1}.".format(name, item)) def store(self, name, item): self.contents.append(item) print("{0} stored the {1}.".format(name, item)) Pyro4-4.82/examples/warehouse/phase2/000077500000000000000000000000001416147301300174665ustar00rootroot00000000000000Pyro4-4.82/examples/warehouse/phase2/person.py000066400000000000000000000015221416147301300213460ustar00rootroot00000000000000from __future__ import print_function import sys if sys.version_info < (3, 0): input = raw_input class Person(object): def __init__(self, name): self.name = name def visit(self, warehouse): print("This is {0}.".format(self.name)) self.deposit(warehouse) self.retrieve(warehouse) print("Thank you, come again!") def deposit(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type a thing you want to store (or empty): ").strip() if item: warehouse.store(self.name, item) def retrieve(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type something you want to take (or empty): ").strip() if item: warehouse.take(self.name, item) Pyro4-4.82/examples/warehouse/phase2/visit.py000066400000000000000000000005131416147301300211750ustar00rootroot00000000000000# This is the code that visits the warehouse. import sys import Pyro4 from person import Person if sys.version_info < (3, 0): input = raw_input uri = input("Enter the uri of the warehouse: ").strip() warehouse = Pyro4.Proxy(uri) janet = Person("Janet") henry = Person("Henry") janet.visit(warehouse) henry.visit(warehouse) Pyro4-4.82/examples/warehouse/phase2/warehouse.py000066400000000000000000000012771416147301300220510ustar00rootroot00000000000000from __future__ import print_function import Pyro4 @Pyro4.expose @Pyro4.behavior(instance_mode="single") class Warehouse(object): def __init__(self): self.contents = ["chair", "bike", "flashlight", "laptop", "couch"] def list_contents(self): return self.contents def take(self, name, item): self.contents.remove(item) print("{0} took the {1}.".format(name, item)) def store(self, name, item): self.contents.append(item) print("{0} stored the {1}.".format(name, item)) def main(): Pyro4.Daemon.serveSimple( { Warehouse: "example.warehouse" }, ns=False) if __name__ == "__main__": main() Pyro4-4.82/examples/warehouse/phase3/000077500000000000000000000000001416147301300174675ustar00rootroot00000000000000Pyro4-4.82/examples/warehouse/phase3/person.py000066400000000000000000000015221416147301300213470ustar00rootroot00000000000000from __future__ import print_function import sys if sys.version_info < (3, 0): input = raw_input class Person(object): def __init__(self, name): self.name = name def visit(self, warehouse): print("This is {0}.".format(self.name)) self.deposit(warehouse) self.retrieve(warehouse) print("Thank you, come again!") def deposit(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type a thing you want to store (or empty): ").strip() if item: warehouse.store(self.name, item) def retrieve(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type something you want to take (or empty): ").strip() if item: warehouse.take(self.name, item) Pyro4-4.82/examples/warehouse/phase3/visit.py000066400000000000000000000004611416147301300212000ustar00rootroot00000000000000# This is the code that visits the warehouse. import sys import Pyro4 import Pyro4.util from person import Person sys.excepthook = Pyro4.util.excepthook warehouse = Pyro4.Proxy("PYRONAME:example.warehouse") janet = Person("Janet") henry = Person("Henry") janet.visit(warehouse) henry.visit(warehouse) Pyro4-4.82/examples/warehouse/phase3/warehouse.py000066400000000000000000000012761416147301300220510ustar00rootroot00000000000000from __future__ import print_function import Pyro4 @Pyro4.expose @Pyro4.behavior(instance_mode="single") class Warehouse(object): def __init__(self): self.contents = ["chair", "bike", "flashlight", "laptop", "couch"] def list_contents(self): return self.contents def take(self, name, item): self.contents.remove(item) print("{0} took the {1}.".format(name, item)) def store(self, name, item): self.contents.append(item) print("{0} stored the {1}.".format(name, item)) def main(): Pyro4.Daemon.serveSimple( { Warehouse: "example.warehouse" }, ns=True) if __name__ == "__main__": main() Pyro4-4.82/requirements.txt000066400000000000000000000001741416147301300157520ustar00rootroot00000000000000serpent >=1.27,<1.30 ; python_version < "3.2" serpent >=1.27 ; python_version >= "3.2" selectors34 ; python_version < "3.4" Pyro4-4.82/setup.cfg000066400000000000000000000005501416147301300143050ustar00rootroot00000000000000[metadata] license = MIT license_file = LICENSE [bdist_rpm] doc_files = LICENSE docs/ [build_sphinx] source-dir = docs/source build-dir = build/sphinx [upload_sphinx] upload-dir = build/sphinx/html [wheel] universal = 1 [pycodestyle] max-line-length = 140 ignore = E402,E731,W504 exclude = .git,__pycache__,.tox,docs,tests,build,dist,examples Pyro4-4.82/setup.py000066400000000000000000000103041416147301300141740ustar00rootroot00000000000000from __future__ import print_function import sys import re import unittest try: # try setuptools first, to get access to build_sphinx and test commands from setuptools import setup using_setuptools = True except ImportError: from distutils.core import setup using_setuptools = False def pyro_test_suite(): testloader = unittest.TestLoader() testsuite = testloader.discover("tests/PyroTests", pattern="test*.py") return testsuite if __name__ == '__main__': with open("src/Pyro4/constants.py") as constants_file: # extract the VERSION definition from the Pyro4.constants module without importing it version_line = next(line for line in constants_file if line.startswith("VERSION")) pyro4_version = re.match("VERSION ?= ?['\"](.+)['\"]", version_line).group(1) print('Pyro version = %s' % pyro4_version) setup( name="Pyro4", version=pyro4_version, license="MIT", description="distributed object middleware for Python (RPC)", long_description="""Pyro means PYthon Remote Objects. It is a library that enables you to build applications in which objects can talk to eachother over the network, with minimal programming effort. You can just use normal Python method calls, with almost every possible parameter and return value type, and Pyro takes care of locating the right object on the right computer to execute the method. It is designed to be very easy to use, and to generally stay out of your way. But it also provides a set of powerful features that enables you to build distributed applications rapidly and effortlessly. Pyro is a pure Python library and runs on many different platforms and Python versions. The source code repository is on Github: https://github.com/irmen/Pyro4 The documentation can be found here: http://pyro4.readthedocs.io """, author="Irmen de Jong", author_email="irmen@razorvine.net", keywords=["distributed objects", "RPC", "remote method call", "IPC"], url="http://pyro4.readthedocs.io", package_dir={'': 'src'}, packages=['Pyro4', 'Pyro4.socketserver', 'Pyro4.test', 'Pyro4.utils'], scripts=[], platforms="any", test_suite="setup.pyro_test_suite", install_requires=[ "serpent>=1.27,<1.30 ; python_version<'3.2'", "serpent>=1.27 ; python_version>='3.2'", ], extras_require={ ":python_version<'3.4'": ["selectors34"] }, requires=["serpent"], classifiers=[ "Development Status :: 5 - Production/Stable", "Development Status :: 6 - Mature", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Natural Language :: Dutch", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Software Development :: Object Brokering", "Topic :: System :: Distributed Computing", "Topic :: System :: Networking" ], entry_points={ 'console_scripts': [ 'pyro4-ns=Pyro4.naming:main', 'pyro4-nsc=Pyro4.nsc:main', 'pyro4-test-echoserver=Pyro4.test.echoserver:main', 'pyro4-check-config=Pyro4.configuration:main', 'pyro4-flameserver=Pyro4.utils.flameserver:main', 'pyro4-httpgateway=Pyro4.utils.httpgateway:main' ] }, options={"install": {"optimize": 0}} ) if len(sys.argv) >= 2 and sys.argv[1].startswith("install"): print("\nOnly the Pyro library has been installed (version %s)." % pyro4_version) print("If you want to install the tests, the examples, and/or the manual,") print("you have to copy them manually to the desired location.") Pyro4-4.82/src/000077500000000000000000000000001416147301300132535ustar00rootroot00000000000000Pyro4-4.82/src/Pyro4/000077500000000000000000000000001416147301300142705ustar00rootroot00000000000000Pyro4-4.82/src/Pyro4/__init__.py000066400000000000000000000054451416147301300164110ustar00rootroot00000000000000""" Pyro package. Some generic init stuff to set up logging etc. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import sys from Pyro4.constants import VERSION as __version__ if sys.version_info < (2, 7): import warnings warnings.warn("This Pyro version is unsupported on Python versions older than 2.7", ImportWarning) def _configLogging(): """Do some basic config of the logging module at package import time. The configuring is done only if the PYRO_LOGLEVEL env var is set. If you want to use your own logging config, make sure you do that before any Pyro imports. Then Pyro will skip the autoconfig. Set the env var PYRO_LOGFILE to change the name of the autoconfigured log file (default is pyro.log in the current dir). Use '{stderr}' to make the log go to the standard error output.""" import os import logging level = os.environ.get("PYRO_LOGLEVEL") logfilename = os.environ.get("PYRO_LOGFILE", "pyro.log") if logfilename == "{stderr}": logfilename = None if level not in (None, ""): levelvalue = getattr(logging, level) if len(logging.root.handlers) == 0: # configure the logging with some sensible defaults. try: if logfilename: import tempfile logfile_dir = os.path.dirname(os.path.expanduser(logfilename)) tempfile = tempfile.TemporaryFile(dir=logfile_dir) tempfile.close() except OSError: # cannot write in the desired logfile directory, use the default console logger logging.basicConfig(level=levelvalue) logging.getLogger("Pyro4").warn("unable to write to the desired logfile (access rights?), falling back to console logger") else: # set up a basic logfile in current directory logging.basicConfig( level=levelvalue, filename=logfilename, datefmt="%Y-%m-%d %H:%M:%S", format="[%(asctime)s.%(msecs)03d,%(name)s,%(levelname)s] %(message)s" ) log = logging.getLogger("Pyro4") log.info("Pyro log configured using built-in defaults, level=%s", level) else: # PYRO_LOGLEVEL is not set, disable Pyro logging. No message is printed about this fact. log = logging.getLogger("Pyro4") log.setLevel(9999) _configLogging() del _configLogging # import the required Pyro symbols into this package from Pyro4.configuration import config from Pyro4.core import URI, Proxy, Daemon, callback, batch, asyncproxy, oneway, expose, behavior, current_context from Pyro4.core import _locateNS as locateNS, _resolve as resolve from Pyro4.futures import Future Pyro4-4.82/src/Pyro4/configuration.py000066400000000000000000000153541416147301300175210ustar00rootroot00000000000000""" Configuration settings. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ # Env vars used at package import time (see __init__.py): # PYRO_LOGLEVEL (enable Pyro log config and set level) # PYRO_LOGFILE (the name of the logfile if you don't like the default) from __future__ import print_function import os import platform import pickle import socket from Pyro4 import constants class Configuration(object): __slots__ = ("HOST", "NS_HOST", "NS_PORT", "NS_BCPORT", "NS_BCHOST", "NS_AUTOCLEAN", "COMPRESSION", "SERVERTYPE", "COMMTIMEOUT", "POLLTIMEOUT", "ONEWAY_THREADED", "DETAILED_TRACEBACK", "SOCK_REUSE", "SOCK_NODELAY", "PREFER_IP_VERSION", "THREADPOOL_SIZE", "THREADPOOL_SIZE_MIN", "AUTOPROXY", "PICKLE_PROTOCOL_VERSION", "BROADCAST_ADDRS", "NATHOST", "NATPORT", "MAX_MESSAGE_SIZE", "FLAME_ENABLED", "SERIALIZER", "SERIALIZERS_ACCEPTED", "LOGWIRE", "METADATA", "REQUIRE_EXPOSE", "USE_MSG_WAITALL", "JSON_MODULE", "MAX_RETRIES", "DILL_PROTOCOL_VERSION", "ITER_STREAMING", "ITER_STREAM_LIFETIME", "ITER_STREAM_LINGER", "SSL", "SSL_REQUIRECLIENTCERT", "SSL_CACERTS", "SSL_SERVERCERT", "SSL_SERVERKEY", "SSL_SERVERKEYPASSWD", "SSL_CLIENTCERT", "SSL_CLIENTKEY", "SSL_CLIENTKEYPASSWD") def __init__(self): self.reset() def reset(self, useenvironment=True): """ Set default config items. If useenvironment is False, won't read environment variables settings (useful if you can't trust your env). """ self.HOST = "localhost" # don't expose us to the outside world by default self.NS_HOST = self.HOST self.NS_PORT = 9090 # tcp self.NS_BCPORT = 9091 # udp self.NS_BCHOST = None self.NS_AUTOCLEAN = 0.0 self.NATHOST = None self.NATPORT = 0 self.COMPRESSION = False self.SERVERTYPE = "thread" self.COMMTIMEOUT = 0.0 self.POLLTIMEOUT = 2.0 # seconds self.SOCK_REUSE = True # so_reuseaddr on server sockets? self.SOCK_NODELAY = False # tcp_nodelay on socket? self.ONEWAY_THREADED = True # oneway calls run in their own thread self.DETAILED_TRACEBACK = False self.THREADPOOL_SIZE = 40 self.THREADPOOL_SIZE_MIN = 4 self.AUTOPROXY = True self.MAX_MESSAGE_SIZE = 0 # 0 = unlimited self.BROADCAST_ADDRS = ", 0.0.0.0" # comma separated list of broadcast addresses self.FLAME_ENABLED = False self.PREFER_IP_VERSION = 4 # 4, 6 or 0 (let OS choose according to RFC 3484) self.SERIALIZER = "serpent" self.SERIALIZERS_ACCEPTED = "serpent,marshal,json" # these are the 'safe' serializers that are always available self.LOGWIRE = False # log wire-level messages self.PICKLE_PROTOCOL_VERSION = pickle.HIGHEST_PROTOCOL try: import dill self.DILL_PROTOCOL_VERSION = dill.HIGHEST_PROTOCOL # Highest protocol except ImportError: self.DILL_PROTOCOL_VERSION = -1 self.METADATA = True # get metadata from server on proxy connect self.REQUIRE_EXPOSE = True # require @expose to make members remotely accessible (if False, everything is accessible) self.USE_MSG_WAITALL = hasattr(socket, "MSG_WAITALL") and platform.system() != "Windows" # waitall is not reliable on windows self.JSON_MODULE = "json" self.MAX_RETRIES = 0 self.ITER_STREAMING = True self.ITER_STREAM_LIFETIME = 0.0 self.ITER_STREAM_LINGER = 30.0 self.SSL = False self.SSL_SERVERCERT = "" self.SSL_SERVERKEY = "" self.SSL_SERVERKEYPASSWD = "" self.SSL_REQUIRECLIENTCERT = False self.SSL_CLIENTCERT = "" self.SSL_CLIENTKEY = "" self.SSL_CLIENTKEYPASSWD = "" self.SSL_CACERTS = "" if useenvironment: # process environment variables PREFIX = "PYRO_" for symbol in self.__slots__: if PREFIX + symbol in os.environ: value = getattr(self, symbol) envvalue = os.environ[PREFIX + symbol] if value is not None: valuetype = type(value) if valuetype is bool: # booleans are special envvalue = envvalue.lower() if envvalue in ("0", "off", "no", "false"): envvalue = False elif envvalue in ("1", "yes", "on", "true"): envvalue = True else: raise ValueError("invalid boolean value: %s%s=%s" % (PREFIX, symbol, envvalue)) else: envvalue = valuetype(envvalue) # just cast the value to the appropriate type setattr(self, symbol, envvalue) self.SERIALIZERS_ACCEPTED = set(self.SERIALIZERS_ACCEPTED.split(',')) def asDict(self): """returns the current config as a regular dictionary""" result = {} for item in self.__slots__: result[item] = getattr(self, item) return result def parseAddressesString(self, addresses): """ Parses the addresses string which contains one or more ip addresses separated by a comma. Returns a sequence of these addresses. '' is replaced by the empty string. """ result = [] for addr in addresses.split(','): addr = addr.strip() if addr == "''": addr = "" result.append(addr) return result def dump(self): # easy config diagnostics if hasattr(platform, "python_implementation"): implementation = platform.python_implementation() else: implementation = "???" config = self.asDict() config["LOGFILE"] = os.environ.get("PYRO_LOGFILE") config["LOGLEVEL"] = os.environ.get("PYRO_LOGLEVEL") result = ["Pyro version: %s" % constants.VERSION, "Loaded from: %s" % os.path.dirname(__file__), "Python version: %s %s (%s, %s)" % (implementation, platform.python_version(), platform.system(), os.name), "Protocol version: %d" % constants.PROTOCOL_VERSION, "Currently active configuration settings:"] for n, v in sorted(config.items()): result.append("%s = %s" % (n, v)) return "\n".join(result) config = Configuration() # entrypoint from script def main(): print(config.dump()) if __name__ == "__main__": main() Pyro4-4.82/src/Pyro4/constants.py000066400000000000000000000007611416147301300166620ustar00rootroot00000000000000""" Definitions of various hard coded constants. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ # Pyro version VERSION = "4.82" # standard object name for the Daemon object DAEMON_NAME = "Pyro.Daemon" # standard name for the Name server itself NAMESERVER_NAME = "Pyro.NameServer" # standard name for Flame server FLAME_NAME = "Pyro.Flame" # wire protocol version. Note that if this gets updated, Pyrolite might need an update too. PROTOCOL_VERSION = 48 Pyro4-4.82/src/Pyro4/core.py000066400000000000000000003007031416147301300155750ustar00rootroot00000000000000""" Core logic (uri, daemon, proxy stuff). Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function, division import inspect import re import logging import sys import ssl import os import time import threading import uuid import base64 import warnings import socket import random from Pyro4 import errors, socketutil, util, constants, message, futures from Pyro4.configuration import config __all__ = ["URI", "Proxy", "Daemon", "current_context", "callback", "batch", "asyncproxy", "expose", "behavior", "oneway", "SerializedBlob", "_resolve", "_locateNS"] if sys.version_info >= (3, 0): basestring = str log = logging.getLogger("Pyro4.core") class URI(object): """ Pyro object URI (universal resource identifier). The uri format is like this: ``PYRO:objectid@location`` where location is one of: - ``hostname:port`` (tcp/ip socket on given port) - ``./u:sockname`` (Unix domain socket on localhost) There is also a 'Magic format' for simple name resolution using Name server: ``PYRONAME:objectname[@location]`` (optional name server location, can also omit location port) And one that looks up things in the name server by metadata: ``PYROMETA:meta1,meta2,...[@location]`` (optional name server location, can also omit location port) You can write the protocol in lowercase if you like (``pyro:...``) but it will automatically be converted to uppercase internally. """ uriRegEx = re.compile(r"(?P[Pp][Yy][Rr][Oo][a-zA-Z]*):(?P\S+?)(@(?P.+))?$") def __init__(self, uri): if isinstance(uri, URI): state = uri.__getstate__() self.__setstate__(state) return if not isinstance(uri, basestring): raise TypeError("uri parameter object is of wrong type") self.sockname = self.host = self.port = None match = self.uriRegEx.match(uri) if not match: raise errors.PyroError("invalid uri") self.protocol = match.group("protocol").upper() self.object = match.group("object") location = match.group("location") if self.protocol == "PYRONAME": self._parseLocation(location, config.NS_PORT) elif self.protocol == "PYRO": if not location: raise errors.PyroError("invalid uri") self._parseLocation(location, None) elif self.protocol == "PYROMETA": self.object = set(m.strip() for m in self.object.split(",")) self._parseLocation(location, config.NS_PORT) else: raise errors.PyroError("invalid uri (protocol)") def _parseLocation(self, location, defaultPort): if not location: return if location.startswith("./u:"): self.sockname = location[4:] if (not self.sockname) or ':' in self.sockname: raise errors.PyroError("invalid uri (location)") else: if location.startswith("["): # ipv6 if location.startswith("[["): # possible mistake: double-bracketing raise errors.PyroError("invalid ipv6 address: enclosed in too many brackets") ipv6locationmatch = re.match(r"\[([0-9a-fA-F:%]+)](:(\d+))?", location) if not ipv6locationmatch: raise errors.PyroError("invalid ipv6 address: the part between brackets must be a numeric ipv6 address") self.host, _, self.port = ipv6locationmatch.groups() else: self.host, _, self.port = location.partition(":") if not self.port: self.port = defaultPort try: self.port = int(self.port) except (ValueError, TypeError): raise errors.PyroError("invalid port in uri, port=" + str(self.port)) @staticmethod def isUnixsockLocation(location): """determine if a location string is for a Unix domain socket""" return location.startswith("./u:") @property def location(self): """property containing the location string, for instance ``"servername.you.com:5555"``""" if self.host: if ":" in self.host: # ipv6 return "[%s]:%d" % (self.host, self.port) else: return "%s:%d" % (self.host, self.port) elif self.sockname: return "./u:" + self.sockname else: return None def asString(self): """the string representation of this object""" if self.protocol == "PYROMETA": result = "PYROMETA:" + ",".join(self.object) else: result = self.protocol + ":" + self.object location = self.location if location: result += "@" + location return result def __str__(self): string = self.asString() if sys.version_info < (3, 0) and type(string) is unicode: return string.encode("ascii", "replace") return string def __unicode__(self): return self.asString() def __repr__(self): return "<%s.%s at 0x%x; %s>" % (self.__class__.__module__, self.__class__.__name__, id(self), str(self)) def __eq__(self, other): if not isinstance(other, URI): return False return (self.protocol, self.object, self.sockname, self.host, self.port) ==\ (other.protocol, other.object, other.sockname, other.host, other.port) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash((self.protocol, str(self.object), self.sockname, self.host, self.port)) # note: getstate/setstate are not needed if we use pickle protocol 2, # but this way it helps pickle to make the representation smaller by omitting all attribute names. def __getstate__(self): return self.protocol, self.object, self.sockname, self.host, self.port def __setstate__(self, state): self.protocol, self.object, self.sockname, self.host, self.port = state def __getstate_for_dict__(self): return self.__getstate__() def __setstate_from_dict__(self, state): self.__setstate__(state) class _RemoteMethod(object): """method call abstraction""" def __init__(self, send, name, max_retries): self.__send = send self.__name = name self.__max_retries = max_retries def __getattr__(self, name): return _RemoteMethod(self.__send, "%s.%s" % (self.__name, name), self.__max_retries) def __call__(self, *args, **kwargs): for attempt in range(self.__max_retries + 1): try: return self.__send(self.__name, args, kwargs) except (errors.ConnectionClosedError, errors.TimeoutError): # only retry for recoverable network errors if attempt >= self.__max_retries: # last attempt, raise the exception raise class Proxy(object): """ Pyro proxy for a remote object. Intercepts method calls and dispatches them to the remote object. .. automethod:: _pyroBind .. automethod:: _pyroRelease .. automethod:: _pyroReconnect .. automethod:: _pyroBatch .. automethod:: _pyroAsync .. automethod:: _pyroAnnotations .. automethod:: _pyroResponseAnnotations .. automethod:: _pyroValidateHandshake .. autoattribute:: _pyroTimeout .. autoattribute:: _pyroHmacKey .. attribute:: _pyroMaxRetries Number of retries to perform on communication calls by this proxy, allows you to override the default setting. .. attribute:: _pyroSerializer Name of the serializer to use by this proxy, allows you to override the default setting. .. attribute:: _pyroHandshake The data object that should be sent in the initial connection handshake message. Can be any serializable object. """ __pyroAttributes = frozenset( ["__getnewargs__", "__getnewargs_ex__", "__getinitargs__", "_pyroConnection", "_pyroUri", "_pyroOneway", "_pyroMethods", "_pyroAttrs", "_pyroTimeout", "_pyroSeq", "_pyroHmacKey", "_pyroRawWireResponse", "_pyroHandshake", "_pyroMaxRetries", "_pyroSerializer", "_Proxy__async", "_Proxy__pyroHmacKey", "_Proxy__pyroTimeout", "_Proxy__pyroConnLock"]) def __init__(self, uri, connected_socket=None): if connected_socket: uri = URI("PYRO:" + uri + "@<>:0") if isinstance(uri, basestring): uri = URI(uri) elif not isinstance(uri, URI): raise TypeError("expected Pyro URI") self._pyroUri = uri self._pyroConnection = None self._pyroSerializer = None # can be set to the name of a serializer to override the global one per-proxy self._pyroMethods = set() # all methods of the remote object, gotten from meta-data self._pyroAttrs = set() # attributes of the remote object, gotten from meta-data self._pyroOneway = set() # oneway-methods of the remote object, gotten from meta-data self._pyroSeq = 0 # message sequence number self._pyroRawWireResponse = False # internal switch to enable wire level responses self._pyroHandshake = "hello" # the data object that should be sent in the initial connection handshake message self._pyroMaxRetries = config.MAX_RETRIES self.__pyroHmacKey = None self.__pyroTimeout = config.COMMTIMEOUT self.__pyroConnLock = threading.RLock() util.get_serializer(config.SERIALIZER) # assert that the configured serializer is available self.__async = False current_context.annotations = {} current_context.response_annotations = {} if connected_socket: self.__pyroCreateConnection(False, connected_socket) @property def _pyroHmacKey(self): """the HMAC key (bytes) that this proxy uses""" return self.__pyroHmacKey @_pyroHmacKey.setter def _pyroHmacKey(self, value): # if needed, convert the hmac value to bytes first if value and sys.version_info >= (3, 0) and type(value) is not bytes: value = value.encode("utf-8") # convert to bytes self.__pyroHmacKey = value def __del__(self): if hasattr(self, "_pyroConnection"): self._pyroRelease() def __getattr__(self, name): if name in Proxy.__pyroAttributes: # allows it to be safely pickled raise AttributeError(name) if config.METADATA: # get metadata if it's not there yet if not self._pyroMethods and not self._pyroAttrs: self._pyroGetMetadata() if name in self._pyroAttrs: return self._pyroInvoke("__getattr__", (name,), None) if config.METADATA and name not in self._pyroMethods: # client side check if the requested attr actually exists raise AttributeError("remote object '%s' has no exposed attribute or method '%s'" % (self._pyroUri, name)) if self.__async: return _AsyncRemoteMethod(self, name, self._pyroMaxRetries) return _RemoteMethod(self._pyroInvoke, name, self._pyroMaxRetries) def __setattr__(self, name, value): if name in Proxy.__pyroAttributes: return super(Proxy, self).__setattr__(name, value) # one of the special pyro attributes if config.METADATA: # get metadata if it's not there yet if not self._pyroMethods and not self._pyroAttrs: self._pyroGetMetadata() if name in self._pyroAttrs: return self._pyroInvoke("__setattr__", (name, value), None) # remote attribute if config.METADATA: # client side validation if the requested attr actually exists raise AttributeError("remote object '%s' has no exposed attribute '%s'" % (self._pyroUri, name)) # metadata disabled, just treat it as a local attribute on the proxy: return super(Proxy, self).__setattr__(name, value) def __repr__(self): if self._pyroConnection: connected = "connected " + self._pyroConnection.family() else: connected = "not connected" return "<%s.%s at 0x%x; %s; for %s>" % (self.__class__.__module__, self.__class__.__name__, id(self), connected, self._pyroUri) def __unicode__(self): return str(self) def __getstate_for_dict__(self): encodedHmac = None if self._pyroHmacKey is not None: encodedHmac = "b64:" + (base64.b64encode(self._pyroHmacKey).decode("ascii")) # for backwards compatibility reasons we also put the timeout and maxretries into the state return self._pyroUri.asString(), tuple(self._pyroOneway), tuple(self._pyroMethods), tuple(self._pyroAttrs),\ self.__pyroTimeout, encodedHmac, self._pyroHandshake, self._pyroMaxRetries, self._pyroSerializer def __setstate_from_dict__(self, state): uri = URI(state[0]) oneway = set(state[1]) methods = set(state[2]) attrs = set(state[3]) timeout = state[4] hmac_key = state[5] handshake = state[6] max_retries = state[7] serializer = None if len(state) < 9 else state[8] if hmac_key: if hmac_key.startswith("b64:"): hmac_key = base64.b64decode(hmac_key[4:].encode("ascii")) else: raise errors.ProtocolError("hmac encoding error") self.__setstate__((uri, oneway, methods, attrs, timeout, hmac_key, handshake, max_retries, serializer)) def __getstate__(self): # for backwards compatibility reasons we also put the timeout and maxretries into the state return self._pyroUri, self._pyroOneway, self._pyroMethods, self._pyroAttrs, self.__pyroTimeout, \ self._pyroHmacKey, self._pyroHandshake, self._pyroMaxRetries, self._pyroSerializer def __setstate__(self, state): # Note that the timeout and maxretries are also part of the state (for backwards compatibility reasons), # but we're not using them here. Instead we get the configured values from the 'local' config. self._pyroUri, self._pyroOneway, self._pyroMethods, self._pyroAttrs, _, self._pyroHmacKey, self._pyroHandshake = state[:7] self._pyroSerializer = None if len(state) < 9 else state[8] self.__pyroTimeout = config.COMMTIMEOUT self._pyroMaxRetries = config.MAX_RETRIES self._pyroConnection = None self._pyroSeq = 0 self._pyroRawWireResponse = False self.__pyroConnLock = threading.RLock() self.__async = False def __copy__(self): uriCopy = URI(self._pyroUri) p = type(self)(uriCopy) p._pyroOneway = set(self._pyroOneway) p._pyroMethods = set(self._pyroMethods) p._pyroAttrs = set(self._pyroAttrs) p._pyroSerializer = self._pyroSerializer p._pyroTimeout = self._pyroTimeout p._pyroHandshake = self._pyroHandshake p._pyroHmacKey = self._pyroHmacKey p._pyroRawWireResponse = self._pyroRawWireResponse p._pyroMaxRetries = self._pyroMaxRetries p.__async = self.__async return p def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self._pyroRelease() def __eq__(self, other): if other is self: return True return isinstance(other, Proxy) and other._pyroUri == self._pyroUri def __ne__(self, other): if other and isinstance(other, Proxy): return other._pyroUri != self._pyroUri return True def __hash__(self): return hash(self._pyroUri) def __dir__(self): result = dir(self.__class__) + list(self.__dict__.keys()) return sorted(set(result) | self._pyroMethods | self._pyroAttrs) def _pyroRelease(self): """release the connection to the pyro daemon""" with self.__pyroConnLock: if self._pyroConnection is not None: if self._pyroConnection.keep_open: return self._pyroConnection.close() self._pyroConnection = None log.debug("connection released") def _pyroBind(self): """ Bind this proxy to the exact object from the uri. That means that the proxy's uri will be updated with a direct PYRO uri, if it isn't one yet. If the proxy is already bound, it will not bind again. """ return self.__pyroCreateConnection(True) def __pyroGetTimeout(self): return self.__pyroTimeout def __pyroSetTimeout(self, timeout): self.__pyroTimeout = timeout if self._pyroConnection is not None: self._pyroConnection.timeout = timeout _pyroTimeout = property(__pyroGetTimeout, __pyroSetTimeout, doc=""" The timeout in seconds for calls on this proxy. Defaults to ``None``. If the timeout expires before the remote method call returns, Pyro will raise a :exc:`Pyro4.errors.TimeoutError`""") def _pyroInvoke(self, methodname, vargs, kwargs, flags=0, objectId=None): """perform the remote method call communication""" current_context.response_annotations = {} with self.__pyroConnLock: if self._pyroConnection is None: self.__pyroCreateConnection() serializer = util.get_serializer(self._pyroSerializer or config.SERIALIZER) objectId = objectId or self._pyroConnection.objectId annotations = self.__annotations() if vargs and isinstance(vargs[0], SerializedBlob): # special serialization of a 'blob' that stays serialized data, compressed, flags = self.__serializeBlobArgs(vargs, kwargs, annotations, flags, objectId, methodname, serializer) else: # normal serialization of the remote call data, compressed = serializer.serializeCall(objectId, methodname, vargs, kwargs, compress=config.COMPRESSION) if compressed: flags |= message.FLAGS_COMPRESSED if methodname in self._pyroOneway: flags |= message.FLAGS_ONEWAY self._pyroSeq = (self._pyroSeq + 1) & 0xffff msg = message.Message(message.MSG_INVOKE, data, serializer.serializer_id, flags, self._pyroSeq, annotations=annotations, hmac_key=self._pyroHmacKey) if config.LOGWIRE: _log_wiredata(log, "proxy wiredata sending", msg) try: self._pyroConnection.send(msg.to_bytes()) del msg # invite GC to collect the object, don't wait for out-of-scope if flags & message.FLAGS_ONEWAY: return None # oneway call, no response data else: msg = message.Message.recv(self._pyroConnection, [message.MSG_RESULT], hmac_key=self._pyroHmacKey) if config.LOGWIRE: _log_wiredata(log, "proxy wiredata received", msg) self.__pyroCheckSequence(msg.seq) if msg.serializer_id != serializer.serializer_id: error = "invalid serializer in response: %d" % msg.serializer_id log.error(error) raise errors.SerializeError(error) if msg.annotations: current_context.response_annotations = msg.annotations self._pyroResponseAnnotations(msg.annotations, msg.type) if self._pyroRawWireResponse: msg.decompress_if_needed() return msg data = serializer.deserializeData(msg.data, compressed=msg.flags & message.FLAGS_COMPRESSED) if msg.flags & message.FLAGS_ITEMSTREAMRESULT: streamId = bytes(msg.annotations.get("STRM", b"")).decode() if not streamId: raise errors.ProtocolError("result of call is an iterator, but the server is not configured to allow streaming") return _StreamResultIterator(streamId, self) if msg.flags & message.FLAGS_EXCEPTION: if sys.platform == "cli": util.fixIronPythonExceptionForPickle(data, False) raise data # if you see this in your traceback, you should probably inspect the remote traceback as well else: return data except (errors.CommunicationError, KeyboardInterrupt): # Communication error during read. To avoid corrupt transfers, we close the connection. # Otherwise we might receive the previous reply as a result of a new method call! # Special case for keyboardinterrupt: people pressing ^C to abort the client # may be catching the keyboardinterrupt in their code. We should probably be on the # safe side and release the proxy connection in this case too, because they might # be reusing the proxy object after catching the exception... self._pyroRelease() raise def __pyroCheckSequence(self, seq): if seq != self._pyroSeq: err = "invoke: reply sequence out of sync, got %d expected %d" % (seq, self._pyroSeq) log.error(err) raise errors.ProtocolError(err) def __pyroCreateConnection(self, replaceUri=False, connected_socket=None): """ Connects this proxy to the remote Pyro daemon. Does connection handshake. Returns true if a new connection was made, false if an existing one was already present. """ def connect_and_handshake(conn): try: if self._pyroConnection is not None: return False # already connected if config.SSL: sslContext = socketutil.getSSLcontext(clientcert=config.SSL_CLIENTCERT, clientkey=config.SSL_CLIENTKEY, keypassword=config.SSL_CLIENTKEYPASSWD, cacerts=config.SSL_CACERTS) else: sslContext = None sock = socketutil.createSocket(connect=connect_location, reuseaddr=config.SOCK_REUSE, timeout=self.__pyroTimeout, nodelay=config.SOCK_NODELAY, sslContext=sslContext) conn = socketutil.SocketConnection(sock, uri.object) # Do handshake. serializer = util.get_serializer(self._pyroSerializer or config.SERIALIZER) data = {"handshake": self._pyroHandshake} if config.METADATA: # the object id is only used/needed when piggybacking the metadata on the connection response # make sure to pass the resolved object id instead of the logical id data["object"] = uri.object flags = message.FLAGS_META_ON_CONNECT else: flags = 0 data, compressed = serializer.serializeData(data, config.COMPRESSION) if compressed: flags |= message.FLAGS_COMPRESSED msg = message.Message(message.MSG_CONNECT, data, serializer.serializer_id, flags, self._pyroSeq, annotations=self.__annotations(False), hmac_key=self._pyroHmacKey) if config.LOGWIRE: _log_wiredata(log, "proxy connect sending", msg) conn.send(msg.to_bytes()) msg = message.Message.recv(conn, [message.MSG_CONNECTOK, message.MSG_CONNECTFAIL], hmac_key=self._pyroHmacKey) if config.LOGWIRE: _log_wiredata(log, "proxy connect response received", msg) except Exception as x: if conn: conn.close() err = "cannot connect to %s: %s" % (connect_location, x) log.error(err) if isinstance(x, errors.CommunicationError): raise else: ce = errors.CommunicationError(err) if sys.version_info >= (3, 0): ce.__cause__ = x raise ce else: handshake_response = "?" if msg.data: serializer = util.get_serializer_by_id(msg.serializer_id) handshake_response = serializer.deserializeData(msg.data, compressed=msg.flags & message.FLAGS_COMPRESSED) if msg.type == message.MSG_CONNECTFAIL: if sys.version_info < (3, 0): error = "connection to %s rejected: %s" % (connect_location, handshake_response.decode()) else: error = "connection to %s rejected: %s" % (connect_location, handshake_response) conn.close() log.error(error) raise errors.CommunicationError(error) elif msg.type == message.MSG_CONNECTOK: if msg.flags & message.FLAGS_META_ON_CONNECT: self.__processMetadata(handshake_response["meta"]) handshake_response = handshake_response["handshake"] self._pyroConnection = conn if replaceUri: self._pyroUri = uri self._pyroValidateHandshake(handshake_response) log.debug("connected to %s - %s - %s", self._pyroUri, conn.family(), "SSL" if sslContext else "unencrypted") if msg.annotations: self._pyroResponseAnnotations(msg.annotations, msg.type) else: conn.close() err = "cannot connect to %s: invalid msg type %d received" % (connect_location, msg.type) log.error(err) raise errors.ProtocolError(err) with self.__pyroConnLock: if self._pyroConnection is not None: return False # already connected if connected_socket: if config.SSL and not isinstance(connected_socket, ssl.SSLSocket): raise socket.error("SSL configured for Pyro but existing socket is not a SSL socket") uri = self._pyroUri else: uri = _resolve(self._pyroUri, self._pyroHmacKey) # socket connection (normal or Unix domain socket) conn = None log.debug("connecting to %s", uri) connect_location = uri.sockname or (uri.host, uri.port) if connected_socket: self._pyroConnection = socketutil.SocketConnection(connected_socket, uri.object, True) else: connect_and_handshake(conn) if config.METADATA: # obtain metadata if this feature is enabled, and the metadata is not known yet if self._pyroMethods or self._pyroAttrs: log.debug("reusing existing metadata") else: self._pyroGetMetadata(uri.object) return True def _pyroGetMetadata(self, objectId=None, known_metadata=None): """ Get metadata from server (methods, attrs, oneway, ...) and remember them in some attributes of the proxy. Usually this will already be known due to the default behavior of the connect handshake, where the connect response also includes the metadata. """ objectId = objectId or self._pyroUri.object log.debug("getting metadata for object %s", objectId) if self._pyroConnection is None and not known_metadata: try: self.__pyroCreateConnection() except errors.PyroError: log.error("problem getting metadata: cannot connect") raise if self._pyroMethods or self._pyroAttrs: return # metadata has already been retrieved as part of creating the connection try: # invoke the get_metadata method on the daemon result = known_metadata or self._pyroInvoke("get_metadata", [objectId], {}, objectId=constants.DAEMON_NAME) self.__processMetadata(result) except errors.PyroError: log.exception("problem getting metadata") raise def __processMetadata(self, metadata): if not metadata: return self._pyroOneway = set(metadata["oneway"]) self._pyroMethods = set(metadata["methods"]) self._pyroAttrs = set(metadata["attrs"]) if log.isEnabledFor(logging.DEBUG): log.debug("from meta: methods=%s, oneway methods=%s, attributes=%s", sorted(self._pyroMethods), sorted(self._pyroOneway), sorted(self._pyroAttrs)) if not self._pyroMethods and not self._pyroAttrs: raise errors.PyroError("remote object doesn't expose any methods or attributes. Did you forget setting @expose on them?") def _pyroReconnect(self, tries=100000000): """ (Re)connect the proxy to the daemon containing the pyro object which the proxy is for. In contrast to the _pyroBind method, this one first releases the connection (if the proxy is still connected) and retries making a new connection until it succeeds or the given amount of tries ran out. """ self._pyroRelease() while tries: try: self.__pyroCreateConnection() return except errors.CommunicationError: tries -= 1 if tries: time.sleep(2) msg = "failed to reconnect" log.error(msg) raise errors.ConnectionClosedError(msg) def _pyroBatch(self): """returns a helper class that lets you create batched method calls on the proxy""" return _BatchProxyAdapter(self) def _pyroAsync(self, asynchronous=True): """turns the proxy into asynchronous mode so you can do asynchronous method calls, or sets it back to normal sync mode if you set asynchronous=False. This setting is strictly on a per-proxy basis (unless an exact clone is made via copy.copy).""" self.__async = asynchronous if sys.version_info < (3, 7): # async keyword backwards compatibility _pyroAsync_37 = _pyroAsync def _pyroAsync(self, asynchronous=True, **kwargs): if kwargs: kword = list(kwargs.keys()) if kword != ["async"]: raise TypeError("_pyroAsync() got an unexpected keyword argument '{:s}'".format(kword[0])) asynchronous = kwargs["async"] return Proxy._pyroAsync_37(self, asynchronous) def _pyroInvokeBatch(self, calls, oneway=False): flags = message.FLAGS_BATCH if oneway: flags |= message.FLAGS_ONEWAY return self._pyroInvoke("", calls, None, flags) def _pyroAnnotations(self): """ Override to return a dict with custom user annotations to be sent with each request message. Code using Pyro 4.56 or newer can skip this and instead set the annotations directly on the context object. """ return {} def _pyroResponseAnnotations(self, annotations, msgtype): """ Process any response annotations (dictionary set by the daemon). Usually this contains the internal Pyro annotations such as hmac and correlation id, and if you override the annotations method in the daemon, can contain your own annotations as well. Code using Pyro 4.56 or newer can skip this and instead read the response_annotations directly from the context object. """ pass def _pyroValidateHandshake(self, response): """ Process and validate the initial connection handshake response data received from the daemon. Simply return without error if everything is ok. Raise an exception if something is wrong and the connection should not be made. """ return def __annotations(self, clear=True): annotations = current_context.annotations if current_context.correlation_id: annotations["CORR"] = current_context.correlation_id.bytes else: annotations.pop("CORR", None) annotations.update(self._pyroAnnotations()) if clear: current_context.annotations = {} return annotations def __serializeBlobArgs(self, vargs, kwargs, annotations, flags, objectId, methodname, serializer): """ Special handling of a "blob" argument that has to stay serialized until explicitly deserialized in client code. This makes efficient, transparent gateways or dispatchers and such possible: they don't have to de/reserialize the message and are independent from the serialized class definitions. Annotations are passed in because some blob metadata is added. They're not part of the blob itself. """ if len(vargs) > 1 or kwargs: raise errors.SerializeError("if SerializedBlob is used, it must be the only argument") blob = vargs[0] flags |= message.FLAGS_KEEPSERIALIZED # Pass the objectId and methodname separately in an annotation because currently, # they are embedded inside the serialized message data. And we're not deserializing that, # so we have to have another means of knowing the object and method it is meant for... # A better solution is perhaps to split the actual remote method arguments from the # control data (object + methodname) but that requires a major protocol change. # The code below is not as nice but it works without any protocol change and doesn't # require a hack either - so it's actually not bad like this. import marshal annotations["BLBI"] = marshal.dumps((blob.info, objectId, methodname)) if blob._contains_blob: # directly pass through the already serialized msg data from within the blob protocol_msg = blob._data data, compressed = protocol_msg.data, protocol_msg.flags & message.FLAGS_COMPRESSED else: # replaces SerializedBlob argument with the data to be serialized data, compressed = serializer.serializeCall(objectId, methodname, blob._data, kwargs, compress=config.COMPRESSION) return data, compressed, flags class _StreamResultIterator(object): """ Pyro returns this as a result of a remote call which returns an iterator or generator. It is a normal iterable and produces elements on demand from the remote iterator. You can simply use it in for loops, list comprehensions etc. """ def __init__(self, streamId, proxy): self.streamId = streamId self.proxy = proxy self.pyroseq = proxy._pyroSeq def __iter__(self): return self def next(self): # python 2.x support return self.__next__() def __next__(self): if self.proxy is None: raise StopIteration if self.proxy._pyroConnection is None: raise errors.ConnectionClosedError("the proxy for this stream result has been closed") self.pyroseq += 1 try: return self.proxy._pyroInvoke("get_next_stream_item", [self.streamId], {}, objectId=constants.DAEMON_NAME) except (StopIteration, GeneratorExit): # when the iterator is exhausted, the proxy is removed to avoid unneeded close_stream calls later # (the server has closed its part of the stream by itself already) self.proxy = None raise def __del__(self): self.close() def close(self): if self.proxy and self.proxy._pyroConnection is not None: if self.pyroseq == self.proxy._pyroSeq: # we're still in sync, it's okay to use the same proxy to close this stream self.proxy._pyroInvoke("close_stream", [self.streamId], {}, flags=message.FLAGS_ONEWAY, objectId=constants.DAEMON_NAME) else: # The proxy's sequence number has diverged. # One of the reasons this can happen is because this call is being done from python's GC where # it decides to gc old iterator objects *during a new call on the proxy*. # If we use the same proxy and do a call in between, the other call on the proxy will get an out of sync seq and crash! # We create a temporary second proxy to call close_stream on. This is inefficient, but avoids the problem. try: with self.proxy.__copy__() as closingProxy: closingProxy._pyroInvoke("close_stream", [self.streamId], {}, flags=message.FLAGS_ONEWAY, objectId=constants.DAEMON_NAME) except errors.CommunicationError: pass self.proxy = None class _BatchedRemoteMethod(object): """method call abstraction that is used with batched calls""" def __init__(self, calls, name): self.__calls = calls self.__name = name def __getattr__(self, name): return _BatchedRemoteMethod(self.__calls, "%s.%s" % (self.__name, name)) def __call__(self, *args, **kwargs): self.__calls.append((self.__name, args, kwargs)) class _BatchProxyAdapter(object): """Helper class that lets you batch multiple method calls into one. It is constructed with a reference to the normal proxy that will carry out the batched calls. Call methods on this object that you want to batch, and finally call the batch proxy itself. That call will return a generator for the results of every method call in the batch (in sequence).""" def __init__(self, proxy): self.__proxy = proxy self.__calls = [] def __getattr__(self, name): return _BatchedRemoteMethod(self.__calls, name) def __enter__(self): return self def __exit__(self, *args): pass def __copy__(self): copy = type(self)(self.__proxy) copy.__calls = list(self.__calls) return copy def __resultsgenerator(self, results): for result in results: if isinstance(result, futures._ExceptionWrapper): result.raiseIt() # re-raise the remote exception locally. else: yield result # it is a regular result object, yield that and continue. def __call__(self, oneway=False, asynchronous=False): if oneway and asynchronous: raise errors.PyroError("async oneway calls make no sense") if asynchronous: return _AsyncRemoteMethod(self, "", self.__proxy._pyroMaxRetries)() else: results = self.__proxy._pyroInvokeBatch(self.__calls, oneway) self.__calls = [] # clear for re-use if not oneway: return self.__resultsgenerator(results) if sys.version_info < (3, 7): # async keyword backwards compatibility call_37 = __call__ def __call__(self, oneway=False, **kwargs): if kwargs: kword = list(kwargs.keys()) if kword != ["async"] and kword != ["asynchronous"]: raise TypeError("__call__() got an unexpected keyword argument '{:s}'".format(kword[0])) if kword == ["async"]: kwargs = {"asynchronous": kwargs["async"]} kwargs["oneway"] = oneway return _BatchProxyAdapter.call_37(self, **kwargs) def _pyroInvoke(self, name, args, kwargs): # ignore all parameters, we just need to execute the batch results = self.__proxy._pyroInvokeBatch(self.__calls) self.__calls = [] # clear for re-use return self.__resultsgenerator(results) class _AsyncRemoteMethod(object): """asynchronous method call abstraction (call will run in a background thread)""" def __init__(self, proxy, name, max_retries): self.__proxy = proxy self.__name = name self.__max_retries = max_retries def __getattr__(self, name): return _AsyncRemoteMethod(self.__proxy, "%s.%s" % (self.__name, name), self.__max_retries) def __call__(self, *args, **kwargs): result = futures.FutureResult() thread = threading.Thread(target=self.__asynccall, args=(result, args, kwargs)) thread.setDaemon(True) thread.start() return result def __asynccall(self, asyncresult, args, kwargs): for attempt in range(self.__max_retries + 1): try: # use a copy of the proxy otherwise calls would still be done in sequence, # and use contextmanager to close the proxy after we're done with self.__proxy.__copy__() as proxy: delay = 0.1 + random.random() / 5 while not proxy._pyroConnection: try: proxy._pyroBind() except errors.CommunicationError as x: if "no free workers" not in str(x): raise time.sleep(delay) # wait a bit until a worker might be available again delay += 0.4 + random.random() / 2 if 0 < config.COMMTIMEOUT / 2 < delay: raise value = proxy._pyroInvoke(self.__name, args, kwargs) asyncresult.value = value return except (errors.ConnectionClosedError, errors.TimeoutError) as x: # only retry for recoverable network errors if attempt >= self.__max_retries: # ignore any exceptions here, return them as part of the asynchronous result instead asyncresult.value = futures._ExceptionWrapper(x) return except Exception as x: # ignore any exceptions here, return them as part of the asynchronous result instead asyncresult.value = futures._ExceptionWrapper(x) return def batch(proxy): """convenience method to get a batch proxy adapter""" return proxy._pyroBatch() def asyncproxy(proxy, asynchronous=True): """convenience method to set proxy to asynchronous or sync mode.""" proxy._pyroAsync(asynchronous) def pyroObjectToAutoProxy(obj): """reduce function that automatically replaces Pyro objects by a Proxy""" if config.AUTOPROXY: daemon = getattr(obj, "_pyroDaemon", None) if daemon: # only return a proxy if the object is a registered pyro object return daemon.proxyFor(obj) return obj # decorators def callback(method): """ decorator to mark a method to be a 'callback'. This will make Pyro raise any errors also on the callback side, and not only on the side that does the callback call. """ method._pyroCallback = True return method def oneway(method): """ decorator to mark a method to be oneway (client won't wait for a response) """ method._pyroOneway = True return method def expose(method_or_class): """ Decorator to mark a method or class to be exposed for remote calls (relevant when REQUIRE_EXPOSE=True) You can apply it to a method or a class as a whole. If you need to change the default instance mode or instance creator, also use a @behavior decorator. """ if inspect.isdatadescriptor(method_or_class): func = method_or_class.fget or method_or_class.fset or method_or_class.fdel if util.is_private_attribute(func.__name__): raise AttributeError("exposing private names (starting with _) is not allowed") func._pyroExposed = True return method_or_class attrname = getattr(method_or_class, "__name__", None) if not attrname or isinstance(method_or_class, (classmethod, staticmethod)): # we could be dealing with a descriptor (classmethod/staticmethod), this means the order of the decorators is wrong if inspect.ismethoddescriptor(method_or_class): attrname = method_or_class.__get__(None, dict).__name__ raise AttributeError("using @expose on a classmethod/staticmethod must be done " "after @classmethod/@staticmethod. Method: " + attrname) else: raise AttributeError("@expose cannot determine what this is: " + repr(method_or_class)) if util.is_private_attribute(attrname): raise AttributeError("exposing private names (starting with _) is not allowed") if inspect.isclass(method_or_class): clazz = method_or_class log.debug("exposing all members of %r", clazz) for name in clazz.__dict__: if util.is_private_attribute(name): continue thing = getattr(clazz, name) if inspect.isfunction(thing) or inspect.ismethoddescriptor(thing): thing._pyroExposed = True elif inspect.ismethod(thing): thing.__func__._pyroExposed = True elif inspect.isdatadescriptor(thing): if getattr(thing, "fset", None): thing.fset._pyroExposed = True if getattr(thing, "fget", None): thing.fget._pyroExposed = True if getattr(thing, "fdel", None): thing.fdel._pyroExposed = True clazz._pyroExposed = True return clazz method_or_class._pyroExposed = True return method_or_class def behavior(instance_mode="session", instance_creator=None): """ Decorator to specify the server behavior of your Pyro class. """ def _behavior(clazz): if not inspect.isclass(clazz): raise TypeError("behavior decorator can only be used on a class") if instance_mode not in ("single", "session", "percall"): raise ValueError("invalid instance mode: " + instance_mode) if instance_creator and not callable(instance_creator): raise TypeError("instance_creator must be a callable") clazz._pyroInstancing = (instance_mode, instance_creator) return clazz if not isinstance(instance_mode, basestring): raise SyntaxError("behavior decorator is missing argument(s)") return _behavior @expose class DaemonObject(object): """The part of the daemon that is exposed as a Pyro object.""" def __init__(self, daemon): self.daemon = daemon def registered(self): """returns a list of all object names registered in this daemon""" return list(self.daemon.objectsById.keys()) def ping(self): """a simple do-nothing method for testing purposes""" pass def info(self): """return some descriptive information about the daemon""" return "%s bound on %s, NAT %s, %d objects registered. Servertype: %s" % ( constants.DAEMON_NAME, self.daemon.locationStr, self.daemon.natLocationStr, len(self.daemon.objectsById), self.daemon.transportServer) def get_metadata(self, objectId, as_lists=False): """ Get metadata for the given object (exposed methods, oneways, attributes). If you get an error in your proxy saying that 'DaemonObject' has no attribute 'get_metadata', you're probably connecting to an older Pyro version (4.26 or earlier). Either upgrade the Pyro version or set METADATA config item to False in your client code. """ obj = self.daemon.objectsById.get(objectId) if obj is not None: metadata = util.get_exposed_members(obj, only_exposed=config.REQUIRE_EXPOSE, as_lists=as_lists) if config.REQUIRE_EXPOSE and not metadata["methods"] and not metadata["attrs"]: # Something seems wrong: nothing is remotely exposed. # Possibly because older code not using @expose is now running with a more recent Pyro version # where @expose is mandatory in the default configuration. Give a hint to the user. if not inspect.isclass(obj): obj = type(obj) warnings.warn("Class %r doesn't expose any methods or attributes. Did you forget setting @expose on them?" % obj) return metadata else: log.debug("unknown object requested: %s", objectId) raise errors.DaemonError("unknown object") def get_next_stream_item(self, streamId): if streamId not in self.daemon.streaming_responses: raise errors.PyroError("item stream terminated") client, timestamp, linger_timestamp, stream = self.daemon.streaming_responses[streamId] if client is None: # reset client connection association (can be None if proxy disconnected) self.daemon.streaming_responses[streamId] = (current_context.client, timestamp, 0, stream) try: return next(stream) except Exception: # in case of error (or StopIteration!) the stream is removed del self.daemon.streaming_responses[streamId] raise def close_stream(self, streamId): if streamId in self.daemon.streaming_responses: del self.daemon.streaming_responses[streamId] class Daemon(object): """ Pyro daemon. Contains server side logic and dispatches incoming remote method calls to the appropriate objects. """ def __init__(self, host=None, port=0, unixsocket=None, nathost=None, natport=None, interface=DaemonObject, connected_socket=None): if connected_socket: nathost = natport = None else: if host is None: host = config.HOST if nathost is None: nathost = config.NATHOST if natport is None and nathost is not None: natport = config.NATPORT if nathost and unixsocket: raise ValueError("cannot use nathost together with unixsocket") if (nathost is None) ^ (natport is None): raise ValueError("must provide natport with nathost") self.__mustshutdown = threading.Event() self.__mustshutdown.set() self.__loopstopped = threading.Event() self.__loopstopped.set() if connected_socket: from Pyro4.socketserver.existingconnectionserver import SocketServer_ExistingConnection self.transportServer = SocketServer_ExistingConnection() self.transportServer.init(self, connected_socket) else: if config.SERVERTYPE == "thread": from Pyro4.socketserver.threadpoolserver import SocketServer_Threadpool self.transportServer = SocketServer_Threadpool() elif config.SERVERTYPE == "multiplex": from Pyro4.socketserver.multiplexserver import SocketServer_Multiplex self.transportServer = SocketServer_Multiplex() else: raise errors.PyroError("invalid server type '%s'" % config.SERVERTYPE) self.transportServer.init(self, host, port, unixsocket) #: The location (str of the form ``host:portnumber``) on which the Daemon is listening self.locationStr = self.transportServer.locationStr log.debug("daemon created on %s - %s (pid %d)", self.locationStr, socketutil.family_str(self.transportServer.sock), os.getpid()) natport_for_loc = natport if natport == 0: # expose internal port number as NAT port as well. (don't use port because it could be 0 and will be chosen by the OS) natport_for_loc = int(self.locationStr.split(":")[1]) #: The NAT-location (str of the form ``nathost:natportnumber``) on which the Daemon is exposed for use with NAT-routing self.natLocationStr = "%s:%d" % (nathost, natport_for_loc) if nathost else None if self.natLocationStr: log.debug("NAT address is %s", self.natLocationStr) pyroObject = interface(self) pyroObject._pyroId = constants.DAEMON_NAME #: Dictionary from Pyro object id to the actual Pyro object registered by this id self.objectsById = {pyroObject._pyroId: pyroObject} # assert that the configured serializers are available, and remember their ids: self.__serializer_ids = {util.get_serializer(ser_name).serializer_id for ser_name in config.SERIALIZERS_ACCEPTED} log.debug("accepted serializers: %s" % config.SERIALIZERS_ACCEPTED) log.debug("pyro protocol version: %d pickle version: %d" % (constants.PROTOCOL_VERSION, config.PICKLE_PROTOCOL_VERSION)) self.__pyroHmacKey = None self._pyroInstances = {} # pyro objects for instance_mode=single (singletons, just one per daemon) self.streaming_responses = {} # stream_id -> (client, creation_timestamp, linger_timestamp, stream) self.housekeeper_lock = threading.Lock() self.create_single_instance_lock = threading.Lock() self.__mustshutdown.clear() @property def _pyroHmacKey(self): return self.__pyroHmacKey @_pyroHmacKey.setter def _pyroHmacKey(self, value): # if needed, convert the hmac value to bytes first if value and sys.version_info >= (3, 0) and type(value) is not bytes: value = value.encode("utf-8") # convert to bytes self.__pyroHmacKey = value @property def sock(self): """the server socket used by the daemon""" return self.transportServer.sock @property def sockets(self): """list of all sockets used by the daemon (server socket and all active client sockets)""" return self.transportServer.sockets @property def selector(self): """the multiplexing selector used, if using the multiplex server type""" return self.transportServer.selector @staticmethod def serveSimple(objects, host=None, port=0, daemon=None, ns=True, verbose=True): """ Basic method to fire up a daemon (or supply one yourself). objects is a dict containing objects to register as keys, and their names (or None) as values. If ns is true they will be registered in the naming server as well, otherwise they just stay local. If you need to publish on a unix domain socket you can't use this shortcut method. See the documentation on 'publishing objects' (in chapter: Servers) for more details. """ if daemon is None: daemon = Daemon(host, port) with daemon: if ns: ns = _locateNS() for obj, name in objects.items(): if ns: localname = None # name is used for the name server else: localname = name # no name server, use name in daemon uri = daemon.register(obj, localname) if verbose: print("Object {0}:\n uri = {1}".format(repr(obj), uri)) if name and ns: ns.register(name, uri) if verbose: print(" name = {0}".format(name)) if verbose: print("Pyro daemon running.") daemon.requestLoop() def requestLoop(self, loopCondition=lambda: True): """ Goes in a loop to service incoming requests, until someone breaks this or calls shutdown from another thread. """ self.__mustshutdown.clear() log.info("daemon %s entering requestloop", self.locationStr) try: self.__loopstopped.clear() condition = lambda: not self.__mustshutdown.isSet() and loopCondition() self.transportServer.loop(loopCondition=condition) finally: self.__loopstopped.set() log.debug("daemon exits requestloop") def events(self, eventsockets): """for use in an external event loop: handle any requests that are pending for this daemon""" return self.transportServer.events(eventsockets) def shutdown(self): """Cleanly terminate a daemon that is running in the requestloop.""" log.debug("daemon shutting down") self.streaming_responses = {} time.sleep(0.02) self.__mustshutdown.set() if self.transportServer: self.transportServer.shutdown() time.sleep(0.02) self.close() self.__loopstopped.wait(timeout=5) # use timeout to avoid deadlock situations @property def _shutting_down(self): return self.__mustshutdown.is_set() def _handshake(self, conn, denied_reason=None): """ Perform connection handshake with new clients. Client sends a MSG_CONNECT message with a serialized data payload. If all is well, return with a CONNECT_OK message. The reason we're not doing this with a MSG_INVOKE method call on the daemon (like when retrieving the metadata) is because we need to force the clients to get past an initial connect handshake before letting them invoke any method. Return True for successful handshake, False if something was wrong. If a denied_reason is given, the handshake will fail with the given reason. """ serializer_id = util.MarshalSerializer.serializer_id msg_seq = 0 try: msg = message.Message.recv(conn, [message.MSG_CONNECT], hmac_key=self._pyroHmacKey) msg_seq = msg.seq if denied_reason: raise Exception(denied_reason) if config.LOGWIRE: _log_wiredata(log, "daemon handshake received", msg) if msg.serializer_id not in self.__serializer_ids: raise errors.SerializeError("message used serializer that is not accepted: %d" % msg.serializer_id) if "CORR" in msg.annotations: current_context.correlation_id = uuid.UUID(bytes=msg.annotations["CORR"]) else: current_context.correlation_id = uuid.uuid4() serializer_id = msg.serializer_id serializer = util.get_serializer_by_id(serializer_id) data = serializer.deserializeData(msg.data, msg.flags & message.FLAGS_COMPRESSED) handshake_response = self.validateHandshake(conn, data["handshake"]) if msg.flags & message.FLAGS_META_ON_CONNECT: # Usually this flag will be enabled, which results in including the object metadata # in the handshake response. This avoids a separate remote call to get_metadata. flags = message.FLAGS_META_ON_CONNECT handshake_response = { "handshake": handshake_response, "meta": self.objectsById[constants.DAEMON_NAME].get_metadata(data["object"], as_lists=True) } else: flags = 0 data, compressed = serializer.serializeData(handshake_response, config.COMPRESSION) msgtype = message.MSG_CONNECTOK if compressed: flags |= message.FLAGS_COMPRESSED except errors.ConnectionClosedError: log.debug("handshake failed, connection closed early") return False except Exception as x: log.debug("handshake failed, reason:", exc_info=True) serializer = util.get_serializer_by_id(serializer_id) data, compressed = serializer.serializeData(str(x), False) msgtype = message.MSG_CONNECTFAIL flags = message.FLAGS_COMPRESSED if compressed else 0 # We need a minimal amount of response data or the socket will remain blocked # on some systems... (messages smaller than 40 bytes) msg = message.Message(msgtype, data, serializer_id, flags, msg_seq, annotations=self.__annotations(), hmac_key=self._pyroHmacKey) if config.LOGWIRE: _log_wiredata(log, "daemon handshake response", msg) conn.send(msg.to_bytes()) return msg.type == message.MSG_CONNECTOK def validateHandshake(self, conn, data): """ Override this to create a connection validator for new client connections. It should return a response data object normally if the connection is okay, or should raise an exception if the connection should be denied. """ return "hello" def clientDisconnect(self, conn): """ Override this to handle a client disconnect. Conn is the SocketConnection object that was disconnected. """ pass def handleRequest(self, conn): """ Handle incoming Pyro request. Catches any exception that may occur and wraps it in a reply to the calling side, as to not make this server side loop terminate due to exceptions caused by remote invocations. """ request_flags = 0 request_seq = 0 request_serializer_id = util.MarshalSerializer.serializer_id wasBatched = False isCallback = False try: msg = message.Message.recv(conn, [message.MSG_INVOKE, message.MSG_PING], hmac_key=self._pyroHmacKey) except errors.CommunicationError as x: # we couldn't even get data from the client, this is an immediate error # log.info("error receiving data from client %s: %s", conn.sock.getpeername(), x) raise x try: request_flags = msg.flags request_seq = msg.seq request_serializer_id = msg.serializer_id current_context.correlation_id = uuid.UUID(bytes=msg.annotations["CORR"]) if "CORR" in msg.annotations else uuid.uuid4() if config.LOGWIRE: _log_wiredata(log, "daemon wiredata received", msg) if msg.type == message.MSG_PING: # return same seq, but ignore any data (it's a ping, not an echo). Nothing is deserialized. msg = message.Message(message.MSG_PING, b"pong", msg.serializer_id, 0, msg.seq, annotations=self.__annotations(), hmac_key=self._pyroHmacKey) if config.LOGWIRE: _log_wiredata(log, "daemon wiredata sending", msg) conn.send(msg.to_bytes()) return if msg.serializer_id not in self.__serializer_ids: raise errors.SerializeError("message used serializer that is not accepted: %d" % msg.serializer_id) serializer = util.get_serializer_by_id(msg.serializer_id) if request_flags & message.FLAGS_KEEPSERIALIZED: # pass on the wire protocol message blob unchanged objId, method, vargs, kwargs = self.__deserializeBlobArgs(msg) else: # normal deserialization of remote call arguments objId, method, vargs, kwargs = serializer.deserializeCall(msg.data, compressed=msg.flags & message.FLAGS_COMPRESSED) current_context.client = conn try: current_context.client_sock_addr = conn.sock.getpeername() # store, because on oneway calls, socket will be disconnected except socket.error: current_context.client_sock_addr = None # sometimes getpeername() doesn't work... current_context.seq = msg.seq current_context.annotations = msg.annotations current_context.msg_flags = msg.flags current_context.serializer_id = msg.serializer_id del msg # invite GC to collect the object, don't wait for out-of-scope obj = self.objectsById.get(objId) if obj is not None: if inspect.isclass(obj): obj = self._getInstance(obj, conn) if request_flags & message.FLAGS_BATCH: # batched method calls, loop over them all and collect all results data = [] for method, vargs, kwargs in vargs: method = util.getAttribute(obj, method) try: result = method(*vargs, **kwargs) # this is the actual method call to the Pyro object except Exception: xt, xv = sys.exc_info()[0:2] log.debug("Exception occurred while handling batched request: %s", xv) xv._pyroTraceback = util.formatTraceback(detailed=config.DETAILED_TRACEBACK) if sys.platform == "cli": util.fixIronPythonExceptionForPickle(xv, True) # piggyback attributes data.append(futures._ExceptionWrapper(xv)) break # stop processing the rest of the batch else: data.append(result) # note that we don't support streaming results in batch mode wasBatched = True else: # normal single method call if method == "__getattr__": # special case for direct attribute access (only exposed @properties are accessible) data = util.get_exposed_property_value(obj, vargs[0], only_exposed=config.REQUIRE_EXPOSE) elif method == "__setattr__": # special case for direct attribute access (only exposed @properties are accessible) data = util.set_exposed_property_value(obj, vargs[0], vargs[1], only_exposed=config.REQUIRE_EXPOSE) else: method = util.getAttribute(obj, method) if request_flags & message.FLAGS_ONEWAY and config.ONEWAY_THREADED: # oneway call to be run inside its own thread _OnewayCallThread(target=method, args=vargs, kwargs=kwargs).start() else: isCallback = getattr(method, "_pyroCallback", False) data = method(*vargs, **kwargs) # this is the actual method call to the Pyro object if not request_flags & message.FLAGS_ONEWAY: isStream, data = self._streamResponse(data, conn) if isStream: # throw an exception as well as setting message flags # this way, it is backwards compatible with older pyro versions. exc = errors.ProtocolError("result of call is an iterator") ann = {"STRM": data.encode()} if data else {} self._sendExceptionResponse(conn, request_seq, serializer.serializer_id, exc, None, annotations=ann, flags=message.FLAGS_ITEMSTREAMRESULT) return else: log.debug("unknown object requested: %s", objId) raise errors.DaemonError("unknown object") if request_flags & message.FLAGS_ONEWAY: return # oneway call, don't send a response else: data, compressed = serializer.serializeData(data, compress=config.COMPRESSION) response_flags = 0 if compressed: response_flags |= message.FLAGS_COMPRESSED if wasBatched: response_flags |= message.FLAGS_BATCH msg = message.Message(message.MSG_RESULT, data, serializer.serializer_id, response_flags, request_seq, annotations=self.__annotations(), hmac_key=self._pyroHmacKey) current_context.response_annotations = {} if config.LOGWIRE: _log_wiredata(log, "daemon wiredata sending", msg) conn.send(msg.to_bytes()) except Exception: xt, xv = sys.exc_info()[0:2] msg = getattr(xv, "pyroMsg", None) if msg: request_seq = msg.seq request_serializer_id = msg.serializer_id if xt is not errors.ConnectionClosedError: if xt not in (StopIteration, GeneratorExit): log.debug("Exception occurred while handling request: %r", xv) if not request_flags & message.FLAGS_ONEWAY: if isinstance(xv, errors.SerializeError) or not isinstance(xv, errors.CommunicationError): # only return the error to the client if it wasn't a oneway call, and not a communication error # (in these cases, it makes no sense to try to report the error back to the client...) tblines = util.formatTraceback(detailed=config.DETAILED_TRACEBACK) self._sendExceptionResponse(conn, request_seq, request_serializer_id, xv, tblines) if isCallback or isinstance(xv, (errors.CommunicationError, errors.SecurityError)): raise # re-raise if flagged as callback, communication or security error. def _clientDisconnect(self, conn): if config.ITER_STREAM_LINGER > 0: # client goes away, keep streams around for a bit longer (allow reconnect) for streamId in list(self.streaming_responses): info = self.streaming_responses.get(streamId, None) if info and info[0] is conn: _, timestamp, _, stream = info self.streaming_responses[streamId] = (None, timestamp, time.time(), stream) else: # client goes away, close any streams it had open as well for streamId in list(self.streaming_responses): info = self.streaming_responses.get(streamId, None) if info and info[0] is conn: del self.streaming_responses[streamId] self.clientDisconnect(conn) # user overridable hook def _housekeeping(self): """ Perform periodical housekeeping actions (cleanups etc) """ if self._shutting_down: return with self.housekeeper_lock: if self.streaming_responses: if config.ITER_STREAM_LIFETIME > 0: # cleanup iter streams that are past their lifetime for streamId in list(self.streaming_responses.keys()): info = self.streaming_responses.get(streamId, None) if info: last_use_period = time.time() - info[1] if 0 < config.ITER_STREAM_LIFETIME < last_use_period: del self.streaming_responses[streamId] if config.ITER_STREAM_LINGER > 0: # cleanup iter streams that are past their linger time for streamId in list(self.streaming_responses.keys()): info = self.streaming_responses.get(streamId, None) if info and info[2]: linger_period = time.time() - info[2] if linger_period > config.ITER_STREAM_LINGER: del self.streaming_responses[streamId] self.housekeeping() def housekeeping(self): """ Override this to add custom periodic housekeeping (cleanup) logic. This will be called every few seconds by the running daemon's request loop. """ pass def _getInstance(self, clazz, conn): """ Find or create a new instance of the class """ def createInstance(clazz, creator): try: if creator: obj = creator(clazz) if isinstance(obj, clazz): return obj raise TypeError("instance creator returned object of different type") return clazz() except Exception: log.exception("could not create pyro object instance") raise instance_mode, instance_creator = clazz._pyroInstancing if instance_mode == "single": # create and use one singleton instance of this class (not a global singleton, just exactly one per daemon) with self.create_single_instance_lock: instance = self._pyroInstances.get(clazz) if not instance: log.debug("instancemode %s: creating new pyro object for %s", instance_mode, clazz) instance = createInstance(clazz, instance_creator) self._pyroInstances[clazz] = instance return instance elif instance_mode == "session": # Create and use one instance for this proxy connection # the instances are kept on the connection object. # (this is the default instance mode when using new style @expose) instance = conn.pyroInstances.get(clazz) if not instance: log.debug("instancemode %s: creating new pyro object for %s", instance_mode, clazz) instance = createInstance(clazz, instance_creator) conn.pyroInstances[clazz] = instance return instance elif instance_mode == "percall": # create and use a new instance just for this call log.debug("instancemode %s: creating new pyro object for %s", instance_mode, clazz) return createInstance(clazz, instance_creator) else: raise errors.DaemonError("invalid instancemode in registered class") def _sendExceptionResponse(self, connection, seq, serializer_id, exc_value, tbinfo, flags=0, annotations=None): """send an exception back including the local traceback info""" exc_value._pyroTraceback = tbinfo if sys.platform == "cli": util.fixIronPythonExceptionForPickle(exc_value, True) # piggyback attributes serializer = util.get_serializer_by_id(serializer_id) try: data, compressed = serializer.serializeData(exc_value) except: # the exception object couldn't be serialized, use a generic PyroError instead xt, xv, tb = sys.exc_info() msg = "Error serializing exception: %s. Original exception: %s: %s" % (str(xv), type(exc_value), str(exc_value)) exc_value = errors.PyroError(msg) exc_value._pyroTraceback = tbinfo if sys.platform == "cli": util.fixIronPythonExceptionForPickle(exc_value, True) # piggyback attributes data, compressed = serializer.serializeData(exc_value) flags |= message.FLAGS_EXCEPTION if compressed: flags |= message.FLAGS_COMPRESSED annotations = dict(annotations or {}) annotations.update(self.annotations()) msg = message.Message(message.MSG_RESULT, data, serializer.serializer_id, flags, seq, annotations=annotations, hmac_key=self._pyroHmacKey) if config.LOGWIRE: _log_wiredata(log, "daemon wiredata sending (error response)", msg) connection.send(msg.to_bytes()) def register(self, obj_or_class, objectId=None, force=False): """ Register a Pyro object under the given id. Note that this object is now only known inside this daemon, it is not automatically available in a name server. This method returns a URI for the registered object. Pyro checks if an object is already registered, unless you set force=True. You can register a class or an object (instance) directly. For a class, Pyro will create instances of it to handle the remote calls according to the instance_mode (set via @expose on the class). The default there is one object per session (=proxy connection). If you register an object directly, Pyro will use that single object for *all* remote calls. """ if objectId: if not isinstance(objectId, basestring): raise TypeError("objectId must be a string or None") else: objectId = "obj_" + uuid.uuid4().hex # generate a new objectId if inspect.isclass(obj_or_class): if not hasattr(obj_or_class, "_pyroInstancing"): obj_or_class._pyroInstancing = ("session", None) if not force: if hasattr(obj_or_class, "_pyroId") and obj_or_class._pyroId != "": # check for empty string is needed for Cython raise errors.DaemonError("object or class already has a Pyro id") if objectId in self.objectsById: raise errors.DaemonError("an object or class is already registered with that id") # set some pyro attributes obj_or_class._pyroId = objectId obj_or_class._pyroDaemon = self if config.AUTOPROXY: # register a custom serializer for the type to automatically return proxies # we need to do this for all known serializers for ser in util._serializers.values(): if inspect.isclass(obj_or_class): ser.register_type_replacement(obj_or_class, pyroObjectToAutoProxy) else: ser.register_type_replacement(type(obj_or_class), pyroObjectToAutoProxy) # register the object/class in the mapping self.objectsById[obj_or_class._pyroId] = obj_or_class return self.uriFor(objectId) def unregister(self, objectOrId): """ Remove a class or object from the known objects inside this daemon. You can unregister the class/object directly, or with its id. """ if objectOrId is None: raise ValueError("object or objectid argument expected") if not isinstance(objectOrId, basestring): objectId = getattr(objectOrId, "_pyroId", None) if objectId is None: raise errors.DaemonError("object isn't registered") else: objectId = objectOrId objectOrId = None if objectId == constants.DAEMON_NAME: return if objectId in self.objectsById: del self.objectsById[objectId] if objectOrId is not None: del objectOrId._pyroId del objectOrId._pyroDaemon # Don't remove the custom type serializer because there may be # other registered objects of the same type still depending on it. def uriFor(self, objectOrId, nat=True): """ Get a URI for the given object (or object id) from this daemon. Only a daemon can hand out proper uris because the access location is contained in them. Note that unregistered objects cannot be given an uri, but unregistered object names can (it's just a string we're creating in that case). If nat is set to False, the configured NAT address (if any) is ignored and it will return an URI for the internal address. """ if not isinstance(objectOrId, basestring): objectOrId = getattr(objectOrId, "_pyroId", None) if objectOrId is None or objectOrId not in self.objectsById: raise errors.DaemonError("object isn't registered in this daemon") if nat: loc = self.natLocationStr or self.locationStr else: loc = self.locationStr return URI("PYRO:%s@%s" % (objectOrId, loc)) def resetMetadataCache(self, objectOrId, nat=True): """Reset cache of metadata when a Daemon has available methods/attributes dynamically updated. Clients will have to get a new proxy to see changes""" uri = self.uriFor(objectOrId, nat) # can only be cached if registered, else no-op if uri.object in self.objectsById: registered_object = self.objectsById[uri.object] # Clear cache regardless of how it is accessed util.reset_exposed_members(registered_object, config.REQUIRE_EXPOSE, as_lists=True) util.reset_exposed_members(registered_object, config.REQUIRE_EXPOSE, as_lists=False) def proxyFor(self, objectOrId, nat=True): """ Get a fully initialized Pyro Proxy for the given object (or object id) for this daemon. If nat is False, the configured NAT address (if any) is ignored. The object or id must be registered in this daemon, or you'll get an exception. (you can't get a proxy for an unknown object) """ uri = self.uriFor(objectOrId, nat) proxy = Proxy(uri) try: registered_object = self.objectsById[uri.object] except KeyError: raise errors.DaemonError("object isn't registered in this daemon") meta = util.get_exposed_members(registered_object, only_exposed=config.REQUIRE_EXPOSE) proxy._pyroGetMetadata(known_metadata=meta) return proxy def close(self): """Close down the server and release resources""" self.__mustshutdown.set() self.streaming_responses = {} if self.transportServer: log.debug("daemon closing") self.transportServer.close() self.transportServer = None def annotations(self): """Override to return a dict with custom user annotations to be sent with each response message.""" return {} def combine(self, daemon): """ Combines the event loop of the other daemon in the current daemon's loop. You can then simply run the current daemon's requestLoop to serve both daemons. This works fine on the multiplex server type, but doesn't work with the threaded server type. """ log.debug("combining event loop with other daemon") self.transportServer.combine_loop(daemon.transportServer) def __annotations(self): annotations = current_context.response_annotations if current_context.correlation_id: annotations["CORR"] = current_context.correlation_id.bytes else: annotations.pop("CORR", None) annotations.update(self.annotations()) return annotations def __repr__(self): if hasattr(self, "locationStr"): family = socketutil.family_str(self.sock) return "<%s.%s at 0x%x; %s - %s; %d objects>" % (self.__class__.__module__, self.__class__.__name__, id(self), self.locationStr, family, len(self.objectsById)) else: # daemon objects may come back from serialized form without being properly initialized (by design) return "<%s.%s at 0x%x; unusable>" % (self.__class__.__module__, self.__class__.__name__, id(self)) def __enter__(self): if not self.transportServer: raise errors.PyroError("cannot reuse this object") return self def __exit__(self, exc_type, exc_value, traceback): self.close() def __getstate__(self): # A little hack to make it possible to serialize Pyro objects, because they can reference a daemon, # but it is not meant to be able to properly serialize/deserialize Daemon objects. return {} def __getstate_for_dict__(self): return tuple(self.__getstate__()) def __setstate_from_dict__(self, state): pass if sys.version_info < (3, 0): __lazy_dict_iterator_types = (type({}.iterkeys()), type({}.itervalues()), type({}.iteritems())) else: __lazy_dict_iterator_types = (type({}.keys()), type({}.values()), type({}.items())) def _streamResponse(self, data, client): if sys.version_info < (3, 4): from collections import Iterator else: from collections.abc import Iterator if isinstance(data, Iterator) or inspect.isgenerator(data): if config.ITER_STREAMING: if type(data) in self.__lazy_dict_iterator_types: raise errors.PyroError("won't serialize or stream lazy dict iterators, convert to list yourself") stream_id = str(uuid.uuid4()) self.streaming_responses[stream_id] = (client, time.time(), 0, data) return True, stream_id return True, None return False, data def __deserializeBlobArgs(self, protocolmsg): import marshal blobinfo = protocolmsg.annotations["BLBI"] if sys.platform == "cli" and type(blobinfo) is not str: # Ironpython's marshal expects str... blobinfo = str(blobinfo) blobinfo, objId, method = marshal.loads(blobinfo) blob = SerializedBlob(blobinfo, protocolmsg, is_blob=True) return objId, method, (blob,), {} # object, method, vargs, kwargs # serpent serializer initialization try: import serpent def pyro_class_serpent_serializer(obj, serializer, stream, level): # Override the default way that a Pyro URI/proxy/daemon is serialized. # Because it defines a __getstate__ it would otherwise just become a tuple, # and not be deserialized as a class. d = util.SerializerBase.class_to_dict(obj) serializer.ser_builtins_dict(d, stream, level) # register the special serializers for the pyro objects with Serpent serpent.register_class(URI, pyro_class_serpent_serializer) serpent.register_class(Proxy, pyro_class_serpent_serializer) serpent.register_class(Daemon, pyro_class_serpent_serializer) serpent.register_class(futures._ExceptionWrapper, pyro_class_serpent_serializer) except ImportError: pass def serialize_core_object_to_dict(obj): return { "__class__": "Pyro4.core." + obj.__class__.__name__, "state": obj.__getstate_for_dict__() } util.SerializerBase.register_class_to_dict(URI, serialize_core_object_to_dict, serpent_too=False) util.SerializerBase.register_class_to_dict(Proxy, serialize_core_object_to_dict, serpent_too=False) util.SerializerBase.register_class_to_dict(Daemon, serialize_core_object_to_dict, serpent_too=False) util.SerializerBase.register_class_to_dict(futures._ExceptionWrapper, futures._ExceptionWrapper.__serialized_dict__, serpent_too=False) def _log_wiredata(logger, text, msg): """logs all the given properties of the wire message in the given logger""" corr = str(uuid.UUID(bytes=msg.annotations["CORR"])) if "CORR" in msg.annotations else "?" logger.debug("%s: msgtype=%d flags=0x%x ser=%d seq=%d corr=%s\nannotations=%r\ndata=%r" % (text, msg.type, msg.flags, msg.serializer_id, msg.seq, corr, msg.annotations, msg.data)) class _CallContext(threading.local): def __init__(self): # per-thread initialization self.client = None self.client_sock_addr = None self.seq = 0 self.msg_flags = 0 self.serializer_id = 0 self.annotations = {} self.response_annotations = {} self.correlation_id = None def to_global(self): if sys.platform != "cli": return dict(self.__dict__) # ironpython somehow has problems getting at the values, so do it manually: return { "client": self.client, "seq": self.seq, "msg_flags": self.msg_flags, "serializer_id": self.serializer_id, "annotations": self.annotations, "response_annotations": self.response_annotations, "correlation_id": self.correlation_id, "client_sock_addr": self.client_sock_addr } def from_global(self, values): self.client = values["client"] self.seq = values["seq"] self.msg_flags = values["msg_flags"] self.serializer_id = values["serializer_id"] self.annotations = values["annotations"] self.response_annotations = values["response_annotations"] self.correlation_id = values["correlation_id"] self.client_sock_addr = values["client_sock_addr"] def track_resource(self, resource): """keep a weak reference to the resource to be tracked for this connection""" if self.client: self.client.tracked_resources.add(resource) else: raise errors.PyroError("cannot track resource on a connectionless call") def untrack_resource(self, resource): """no longer track the resource for this connection""" if self.client: self.client.tracked_resources.discard(resource) else: raise errors.PyroError("cannot untrack resource on a connectionless call") class _OnewayCallThread(threading.Thread): def __init__(self, target, args, kwargs): super(_OnewayCallThread, self).__init__(target=target, args=args, kwargs=kwargs, name="oneway-call") self.daemon = True self.parent_context = current_context.to_global() def run(self): current_context.from_global(self.parent_context) super(_OnewayCallThread, self).run() # name server utility function, here to avoid cyclic dependencies def _resolve(uri, hmac_key=None): """ Resolve a 'magic' uri (PYRONAME, PYROMETA) into the direct PYRO uri. It finds a name server, and use that to resolve a PYRONAME uri into the direct PYRO uri pointing to the named object. If uri is already a PYRO uri, it is returned unmodified. You can consider this a shortcut function so that you don't have to locate and use a name server proxy yourself. Note: if you need to resolve more than a few names, consider using the name server directly instead of repeatedly calling this function, to avoid the name server lookup overhead from each call. """ if isinstance(uri, basestring): uri = URI(uri) elif not isinstance(uri, URI): raise TypeError("can only resolve Pyro URIs") if uri.protocol == "PYRO": return uri log.debug("resolving %s", uri) if uri.protocol == "PYRONAME": with _locateNS(uri.host, uri.port, hmac_key=hmac_key) as nameserver: return nameserver.lookup(uri.object) elif uri.protocol == "PYROMETA": with _locateNS(uri.host, uri.port, hmac_key=hmac_key) as nameserver: candidates = nameserver.list(metadata_all=uri.object) if candidates: candidate = random.choice(list(candidates.values())) log.debug("resolved to candidate %s", candidate) return URI(candidate) raise errors.NamingError("no registrations available with desired metadata properties %s" % uri.object) else: raise errors.PyroError("invalid uri protocol") # name server utility function, here to avoid cyclic dependencies def _locateNS(host=None, port=None, broadcast=True, hmac_key=None): """Get a proxy for a name server somewhere in the network.""" if host is None: # first try localhost if we have a good chance of finding it there if config.NS_HOST in ("localhost", "::1") or config.NS_HOST.startswith("127."): if ":" in config.NS_HOST: # ipv6 hosts = ["[%s]" % config.NS_HOST] else: # Some systems (Debian Linux) have 127.0.1.1 in the hosts file assigned to the hostname, # try this too for convenience sake (only if it's actually used as a valid ip address) try: socket.gethostbyaddr("127.0.1.1") hosts = [config.NS_HOST] if config.NS_HOST == "127.0.1.1" else [config.NS_HOST, "127.0.1.1"] except socket.error: hosts = [config.NS_HOST] for host in hosts: uristring = "PYRO:%s@%s:%d" % (constants.NAMESERVER_NAME, host, port or config.NS_PORT) log.debug("locating the NS: %s", uristring) proxy = Proxy(uristring) proxy._pyroHmacKey = hmac_key try: proxy._pyroBind() log.debug("located NS") return proxy except errors.PyroError: pass if config.PREFER_IP_VERSION == 6: broadcast = False # ipv6 doesn't have broadcast. We should probably use multicast.... if broadcast: # broadcast lookup if not port: port = config.NS_BCPORT log.debug("broadcast locate") sock = socketutil.createBroadcastSocket(reuseaddr=config.SOCK_REUSE, timeout=0.7) for _ in range(3): try: for bcaddr in config.parseAddressesString(config.BROADCAST_ADDRS): try: sock.sendto(b"GET_NSURI", 0, (bcaddr, port)) except socket.error as x: err = getattr(x, "errno", x.args[0]) # handle some errno's that some platforms like to throw: if err not in socketutil.ERRNO_EADDRNOTAVAIL and err not in socketutil.ERRNO_EADDRINUSE: raise data, _ = sock.recvfrom(100) sock.close() if sys.version_info >= (3, 0): data = data.decode("iso-8859-1") log.debug("located NS: %s", data) proxy = Proxy(data) proxy._pyroHmacKey = hmac_key return proxy except socket.timeout: continue try: sock.shutdown(socket.SHUT_RDWR) except (OSError, socket.error): pass sock.close() log.debug("broadcast locate failed, try direct connection on NS_HOST") else: log.debug("skipping broadcast lookup") # broadcast failed or skipped, try PYRO directly on specific host host = config.NS_HOST port = config.NS_PORT # pyro direct lookup if not port: port = config.NS_PORT if URI.isUnixsockLocation(host): uristring = "PYRO:%s@%s" % (constants.NAMESERVER_NAME, host) else: # if not a unix socket, check for ipv6 if ":" in host: host = "[%s]" % host uristring = "PYRO:%s@%s:%d" % (constants.NAMESERVER_NAME, host, port) uri = URI(uristring) log.debug("locating the NS: %s", uri) proxy = Proxy(uri) proxy._pyroHmacKey = hmac_key try: proxy._pyroBind() log.debug("located NS") return proxy except errors.PyroError as x: e = errors.NamingError("Failed to locate the nameserver") if sys.version_info >= (3, 0): e.__cause__ = x raise e class SerializedBlob(object): """ Used to wrap some data to make Pyro pass this object transparently (it keeps the serialized payload as-is) Only when you need to access the actual client data you can deserialize on demand. This makes efficient, transparent gateways or dispatchers and such possible: they don't have to de/reserialize the message and are independent from the serialized class definitions. You have to pass this as the only parameter to a remote method call for Pyro to understand it. Init arguments: ``info`` = some (small) descriptive data about the blob. Can be a simple id or name or guid. Must be marshallable. ``data`` = the actual client data payload that you want to transfer in the blob. Can be anything that you would otherwise have used as regular remote call arguments. """ def __init__(self, info, data, is_blob=False): self.info = info self._data = data self._contains_blob = is_blob def deserialized(self): """Retrieves the client data stored in this blob. Deserializes the data automatically if required.""" if self._contains_blob: protocol_msg = self._data serializer = util.get_serializer_by_id(protocol_msg.serializer_id) _, _, data, _ = serializer.deserializeData(protocol_msg.data, protocol_msg.flags & message.FLAGS_COMPRESSED) return data else: return self._data # call context thread local current_context = _CallContext() """the context object for the current call. (thread-local)""" # 'async' keyword backwards compatibility for Python versions older than 3.7. New code should not use this! if sys.version_info < (3, 7): def asyncproxy(proxy, asynchronous=True, **kwargs): """convenience method to set proxy to asynchronous or sync mode.""" if kwargs: kword = list(kwargs.keys()) if kword != ["async"]: raise TypeError("asyncproxy() got an unexpected keyword argument '{:s}'".format(kword[0])) asynchronous = kwargs["async"] proxy._pyroAsync(asynchronous) current_module = sys.modules[__name__] pyro4_module = __import__("Pyro4") current_module.__dict__["async"] = pyro4_module.__dict__["async"] = asyncproxy Pyro4-4.82/src/Pyro4/errors.py000066400000000000000000000025601416147301300161610ustar00rootroot00000000000000""" Definition of the various exceptions that are used in Pyro. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ class PyroError(Exception): """Generic base of all Pyro-specific errors.""" pass class CommunicationError(PyroError): """Base class for the errors related to network communication problems.""" pass class ConnectionClosedError(CommunicationError): """The connection was unexpectedly closed.""" pass class TimeoutError(CommunicationError): """ A call could not be completed within the set timeout period, or the network caused a timeout. """ pass class ProtocolError(CommunicationError): """Pyro received a message that didn't match the active Pyro network protocol, or there was a protocol related error.""" pass class MessageTooLargeError(ProtocolError): """Pyro received a message or was trying to send a message that exceeds the maximum message size as configured.""" pass class NamingError(PyroError): """There was a problem related to the name server or object names.""" pass class DaemonError(PyroError): """The Daemon encountered a problem.""" pass class SecurityError(PyroError): """A security related error occurred.""" pass class SerializeError(ProtocolError): """Something went wrong while (de)serializing data.""" pass Pyro4-4.82/src/Pyro4/futures.py000066400000000000000000000210551416147301300163420ustar00rootroot00000000000000""" Support for Futures (asynchronously executed callables). If you're using Python 3.2 or newer, also see http://docs.python.org/3/library/concurrent.futures.html#future-objects Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import sys import functools import logging import threading import time __all__ = ["Future", "FutureResult", "_ExceptionWrapper"] log = logging.getLogger("Pyro4.futures") class Future(object): """ Holds a callable that will be executed asynchronously and provide its result value some time in the future. This is a more general implementation than the AsyncRemoteMethod, which only works with Pyro proxies (and provides a bit different syntax). This class has a few extra features as well (delay, canceling). """ def __init__(self, somecallable): self.callable = somecallable self.chain = [] self.exceptionhandler = None self.call_delay = 0 self.cancelled = False self.completed = False def __call__(self, *args, **kwargs): """ Start the future call with the provided arguments. Control flow returns immediately, with a FutureResult object. """ if self.completed or not hasattr(self, "chain"): raise RuntimeError("the future has already been evaluated") if self.cancelled: raise RuntimeError("the future has been cancelled") chain = self.chain del self.chain # make it impossible to add new calls to the chain once we started executing it result = FutureResult() # notice that the call chain doesn't sit on the result object thread = threading.Thread(target=self.__asynccall, args=(result, chain, args, kwargs)) thread.setDaemon(True) thread.start() return result def __asynccall(self, asyncresult, chain, args, kwargs): while self.call_delay > 0 and not self.cancelled: delay = min(self.call_delay, 2) time.sleep(delay) self.call_delay -= delay if self.cancelled: self.completed = True asyncresult.set_cancelled() return try: self.completed = True self.cancelled = False value = self.callable(*args, **kwargs) # now walk the callchain, passing on the previous value as first argument for call, args, kwargs in chain: call = functools.partial(call, value) value = call(*args, **kwargs) asyncresult.value = value except Exception as x: if self.exceptionhandler: self.exceptionhandler(x) asyncresult.value = _ExceptionWrapper(x) def delay(self, seconds): """ Delay the evaluation of the future for the given number of seconds. Return True if successful otherwise False if the future has already been evaluated. """ if self.completed: return False self.call_delay = seconds return True def cancel(self): """ Cancels the execution of the future altogether. If the execution hasn't been started yet, the cancellation is successful and returns True. Otherwise, it failed and returns False. """ if self.completed: return False self.cancelled = True return True def then(self, call, *args, **kwargs): """ Add a callable to the call chain, to be invoked when the results become available. The result of the current call will be used as the first argument for the next call. Optional extra arguments can be provided in args and kwargs. Returns self so you can easily chain then() calls. """ self.chain.append((call, args, kwargs)) return self def iferror(self, exceptionhandler): """ Specify the exception handler to be invoked (with the exception object as only argument) when calculating the result raises an exception. If no exception handler is set, any exception raised in the asynchronous call will be silently ignored. Returns self so you can easily chain other calls. """ self.exceptionhandler = exceptionhandler return self class FutureResult(object): """ The result object for asynchronous Pyro calls. Unfortunatley it should be similar to the more general Future class but it is still somewhat limited (no delay, no canceling). """ def __init__(self): self.__ready = threading.Event() self.callchain = [] self.valueLock = threading.Lock() self.exceptionhandler = None def wait(self, timeout=None): """ Wait for the result to become available, with optional timeout (in seconds). Returns True if the result is ready, or False if it still isn't ready. """ result = self.__ready.wait(timeout) if result is None: # older pythons return None from wait() return self.__ready.isSet() return result @property def ready(self): """Boolean that contains the readiness of the asynchronous result""" return self.__ready.isSet() def get_value(self): self.__ready.wait() if isinstance(self.__value, _ExceptionWrapper): self.__value.raiseIt() else: return self.__value def set_value(self, value): with self.valueLock: self.__value = value # walk the call chain if the result is not an exception, otherwise invoke the errorhandler (if any) if isinstance(value, _ExceptionWrapper): if self.exceptionhandler: self.exceptionhandler(value.exception) else: for call, args, kwargs in self.callchain: call = functools.partial(call, self.__value) self.__value = call(*args, **kwargs) if isinstance(self.__value, _ExceptionWrapper): break self.callchain = [] self.__ready.set() value = property(get_value, set_value, None, "The result value of the call. Reading it will block if not available yet.") def set_cancelled(self): self.set_value(_ExceptionWrapper(RuntimeError("future has been cancelled"))) def then(self, call, *args, **kwargs): """ Add a callable to the call chain, to be invoked when the results become available. The result of the current call will be used as the first argument for the next call. Optional extra arguments can be provided in args and kwargs. Returns self so you can easily chain then() calls. """ with self.valueLock: if self.__ready.isSet(): # value is already known, we need to process it immediately (can't use the call chain anymore) call = functools.partial(call, self.__value) self.__value = call(*args, **kwargs) else: # add the call to the call chain, it will be processed later when the result arrives self.callchain.append((call, args, kwargs)) return self def iferror(self, exceptionhandler): """ Specify the exception handler to be invoked (with the exception object as only argument) when asking for the result raises an exception. If no exception handler is set, any exception result will be silently ignored (unless you explicitly ask for the value). Returns self so you can easily chain other calls. """ self.exceptionhandler = exceptionhandler return self class _ExceptionWrapper(object): """Class that wraps a remote exception. If this is returned, Pyro will re-throw the exception on the receiving side. Usually this is taken care of by a special response message flag, but in the case of batched calls this flag is useless and another mechanism was needed.""" def __init__(self, exception): self.exception = exception def raiseIt(self): from Pyro4.util import fixIronPythonExceptionForPickle # XXX circular if sys.platform == "cli": fixIronPythonExceptionForPickle(self.exception, False) raise self.exception def __serialized_dict__(self): """serialized form as a dictionary""" from Pyro4.util import SerializerBase # XXX circular return { "__class__": "Pyro4.futures._ExceptionWrapper", "exception": SerializerBase.class_to_dict(self.exception) } Pyro4-4.82/src/Pyro4/message.py000077500000000000000000000255121416147301300162760ustar00rootroot00000000000000""" The pyro wire protocol message. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import hashlib import hmac import struct import logging import sys import zlib from Pyro4 import errors, constants from Pyro4.configuration import config __all__ = ["Message", "secure_compare"] log = logging.getLogger("Pyro4.message") MSG_CONNECT = 1 MSG_CONNECTOK = 2 MSG_CONNECTFAIL = 3 MSG_INVOKE = 4 MSG_RESULT = 5 MSG_PING = 6 FLAGS_EXCEPTION = 1 << 0 FLAGS_COMPRESSED = 1 << 1 FLAGS_ONEWAY = 1 << 2 FLAGS_BATCH = 1 << 3 FLAGS_META_ON_CONNECT = 1 << 4 FLAGS_ITEMSTREAMRESULT = 1 << 5 FLAGS_KEEPSERIALIZED = 1 << 6 class Message(object): """ Pyro write protocol message. Wire messages contains of a fixed size header, an optional set of annotation chunks, and then the payload data. This class doesn't deal with the payload data: (de)serialization and handling of that data is done elsewhere. Annotation chunks are only parsed, except the 'HMAC' chunk: that is created and validated because it is used as a message digest. The header format is:: 4 id ('PYRO') 2 protocol version 2 message type 2 message flags 2 sequence number 4 data length (i.e. 2 Gb data size limitation) 2 data serialization format (serializer id) 2 annotations length (total of all chunks, 0 if no annotation chunks present) 2 (reserved) 2 checksum After the header, zero or more annotation chunks may follow, of the format:: 4 id (ASCII) 2 chunk length x annotation chunk databytes After that, the actual payload data bytes follow. The sequencenumber is used to check if response messages correspond to the actual request message. This prevents the situation where Pyro would perhaps return the response data from another remote call (which would not result in an error otherwise!) This could happen for instance if the socket data stream gets out of sync, perhaps due To some form of signal that interrupts I/O. The header checksum is a simple sum of the header fields to make reasonably sure that we are dealing with an actual correct PYRO protocol header and not some random data that happens to start with the 'PYRO' protocol identifier. Pyro now uses two annotation chunks that you should not touch yourself: 'HMAC' contains the hmac digest of the message data bytes and all of the annotation chunk data bytes (except those of the HMAC chunk itself). 'CORR' contains the correlation id (guid bytes) Other chunk names are free to use for custom purposes, but Pyro has the right to reserve more of them for internal use in the future. """ __slots__ = ["type", "flags", "seq", "data", "data_size", "serializer_id", "annotations", "annotations_size", "hmac_key"] header_format = '!4sHHHHiHHHH' header_size = struct.calcsize(header_format) checksum_magic = 0x34E9 def __init__(self, msgType, databytes, serializer_id, flags, seq, annotations=None, hmac_key=None): self.type = msgType self.flags = flags self.seq = seq self.data = databytes self.data_size = len(self.data) self.serializer_id = serializer_id self.annotations = dict(annotations or {}) self.hmac_key = hmac_key if self.hmac_key: self.annotations["HMAC"] = self.hmac() # should be done last because it calculates hmac over other annotations self.annotations_size = sum([6 + len(v) for v in self.annotations.values()]) if 0 < config.MAX_MESSAGE_SIZE < (self.data_size + self.annotations_size): raise errors.MessageTooLargeError("max message size exceeded (%d where max=%d)" % (self.data_size + self.annotations_size, config.MAX_MESSAGE_SIZE)) def __repr__(self): return "<%s.%s at %x; type=%d flags=%d seq=%d datasize=%d #ann=%d>" %\ (self.__module__, self.__class__.__name__, id(self), self.type, self.flags, self.seq, self.data_size, len(self.annotations)) def to_bytes(self): """creates a byte stream containing the header followed by annotations (if any) followed by the data""" return self.__header_bytes() + self.__annotations_bytes() + self.data def __header_bytes(self): if not (0 <= self.data_size <= 0x7fffffff): raise ValueError("invalid message size (outside range 0..2Gb)") checksum = (self.type + constants.PROTOCOL_VERSION + self.data_size + self.annotations_size + self.serializer_id + self.flags + self.seq + self.checksum_magic) & 0xffff return struct.pack(self.header_format, b"PYRO", constants.PROTOCOL_VERSION, self.type, self.flags, self.seq, self.data_size, self.serializer_id, self.annotations_size, 0, checksum) def __annotations_bytes(self): if self.annotations: a = [] for k, v in self.annotations.items(): if len(k) != 4: raise errors.ProtocolError("annotation key must be of length 4") if sys.version_info >= (3, 0): k = k.encode("ASCII") a.append(struct.pack("!4sH", k, len(v))) a.append(v) return b"".join(a) return b"" # Note: this 'chunked' way of sending is not used because it triggers Nagle's algorithm # on some systems (linux). This causes big delays, unless you change the socket option # TCP_NODELAY to disable the algorithm. What also works, is sending all the message bytes # in one go: connection.send(message.to_bytes()). This is what Pyro does. def send(self, connection): """send the message as bytes over the connection""" connection.send(self.__header_bytes()) if self.annotations: connection.send(self.__annotations_bytes()) connection.send(self.data) @classmethod def from_header(cls, headerData): """Parses a message header. Does not yet process the annotations chunks and message data.""" if not headerData or len(headerData) != cls.header_size: raise errors.ProtocolError("header data size mismatch") tag, ver, msg_type, flags, seq, data_size, serializer_id, anns_size, _, checksum = struct.unpack(cls.header_format, headerData) if tag != b"PYRO" or ver != constants.PROTOCOL_VERSION: raise errors.ProtocolError("invalid data or unsupported protocol version") if checksum != (msg_type + ver + data_size + anns_size + flags + serializer_id + seq + cls.checksum_magic) & 0xffff: raise errors.ProtocolError("header checksum mismatch") msg = Message(msg_type, b"", serializer_id, flags, seq) msg.data_size = data_size msg.annotations_size = anns_size return msg @classmethod def recv(cls, connection, requiredMsgTypes=None, hmac_key=None): """ Receives a pyro message from a given connection. Accepts the given message types (None=any, or pass a sequence). Also reads annotation chunks and the actual payload data. Validates a HMAC chunk if present. """ msg = cls.from_header(connection.recv(cls.header_size)) msg.hmac_key = hmac_key if 0 < config.MAX_MESSAGE_SIZE < (msg.data_size + msg.annotations_size): errorMsg = "max message size exceeded (%d where max=%d)" % (msg.data_size + msg.annotations_size, config.MAX_MESSAGE_SIZE) log.error("connection " + str(connection) + ": " + errorMsg) connection.close() # close the socket because at this point we can't return the correct seqnr for returning an errormsg exc = errors.MessageTooLargeError(errorMsg) exc.pyroMsg = msg raise exc if requiredMsgTypes and msg.type not in requiredMsgTypes: err = "invalid msg type %d received" % msg.type log.error(err) exc = errors.ProtocolError(err) exc.pyroMsg = msg raise exc if msg.annotations_size: # read annotation chunks annotations_data = connection.recv(msg.annotations_size) msg.annotations = {} i = 0 while i < msg.annotations_size: anno, length = struct.unpack("!4sH", annotations_data[i:i + 6]) if sys.version_info >= (3, 0): anno = anno.decode("ASCII") msg.annotations[anno] = annotations_data[i + 6:i + 6 + length] if sys.platform == "cli": msg.annotations[anno] = bytes(msg.annotations[anno]) i += 6 + length # read data msg.data = connection.recv(msg.data_size) if "HMAC" in msg.annotations and hmac_key: if not secure_compare(msg.annotations["HMAC"], msg.hmac()): exc = errors.SecurityError("message hmac mismatch") exc.pyroMsg = msg raise exc elif ("HMAC" in msg.annotations) != bool(hmac_key): # Not allowed: message contains hmac but hmac_key is not set, or vice versa. err = "hmac key config not symmetric" log.warning(err) exc = errors.SecurityError(err) exc.pyroMsg = msg raise exc return msg def hmac(self): """returns the hmac of the data and the annotation chunk values (except HMAC chunk itself)""" mac = hmac.new(self.hmac_key, self.data, digestmod=hashlib.sha1) for k, v in sorted(self.annotations.items()): # note: sorted because we need fixed order to get the same hmac if k != "HMAC": mac.update(v) return mac.digest() if sys.platform != "cli" else bytes(mac.digest()) @staticmethod def ping(pyroConnection, hmac_key=None): """Convenience method to send a 'ping' message and wait for the 'pong' response""" ping = Message(MSG_PING, b"ping", 42, 0, 0, hmac_key=hmac_key) pyroConnection.send(ping.to_bytes()) Message.recv(pyroConnection, [MSG_PING]) def decompress_if_needed(self): """Decompress the message data if it is compressed.""" if self.flags & FLAGS_COMPRESSED: self.data = zlib.decompress(self.data) self.flags &= ~FLAGS_COMPRESSED self.data_size = len(self.data) return self try: from hmac import compare_digest as secure_compare except ImportError: # Python version doesn't have it natively, use a python fallback implementation import operator try: reduce except NameError: from functools import reduce def secure_compare(a, b): if type(a) != type(b): raise TypeError("arguments must both be same type") if len(a) != len(b): return False return reduce(operator.and_, map(operator.eq, a, b), True) Pyro4-4.82/src/Pyro4/naming.py000066400000000000000000000615421416147301300161230ustar00rootroot00000000000000""" Name Server and helper functions. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import warnings import re import logging import socket import sys import os import time import threading from Pyro4.errors import NamingError, PyroError, ProtocolError from Pyro4 import core, socketutil, constants from Pyro4.configuration import config from Pyro4.core import _locateNS as locateNS, _resolve as resolve # API compatibility with older versions __all__ = ["locateNS", "resolve", "type_meta", "startNSloop", "startNS"] if sys.version_info >= (3, 0): basestring = str log = logging.getLogger("Pyro4.naming") class MemoryStorage(dict): """ Storage implementation that is just an in-memory dict. (because it inherits from dict it is automatically a collections.MutableMapping) Stopping the nameserver will make the server instantly forget about everything. """ def __init__(self, **kwargs): super(MemoryStorage, self).__init__(**kwargs) def __setitem__(self, key, value): uri, metadata = value super(MemoryStorage, self).__setitem__(key, (uri, metadata or frozenset())) def optimized_prefix_list(self, prefix, return_metadata=False): return None def optimized_regex_list(self, regex, return_metadata=False): return None def optimized_metadata_search(self, metadata_all=None, metadata_any=None, return_metadata=False): return None def everything(self, return_metadata=False): if return_metadata: return self.copy() return {name: uri for name, (uri, metadata) in self.items()} def remove_items(self, items): for item in items: try: del self[item] except KeyError: pass def close(self): pass @core.expose class NameServer(object): """ Pyro name server. Provides a simple flat name space to map logical object names to Pyro URIs. Default storage is done in an in-memory dictionary. You can provide custom storage types. """ def __init__(self, storageProvider=None): self.storage = storageProvider if storageProvider is None: self.storage = MemoryStorage() log.debug("using volatile in-memory dict storage") self.lock = threading.RLock() def count(self): """Returns the number of name registrations.""" return len(self.storage) def lookup(self, name, return_metadata=False): """ Lookup the given name, returns an URI if found. Returns tuple (uri, metadata) if return_metadata is True. """ try: uri, metadata = self.storage[name] uri = core.URI(uri) if return_metadata: metadata = list(metadata) if metadata else [] return uri, metadata return uri except KeyError: raise NamingError("unknown name: " + name) def register(self, name, uri, safe=False, metadata=None): """Register a name with an URI. If safe is true, name cannot be registered twice. The uri can be a string or an URI object. Metadata must be None, or a collection of strings.""" if isinstance(uri, core.URI): uri = uri.asString() elif not isinstance(uri, basestring): raise TypeError("only URIs or strings can be registered") else: core.URI(uri) # check if uri is valid if not isinstance(name, basestring): raise TypeError("name must be a str") if isinstance(metadata, basestring): raise TypeError("metadata should not be a str, but another iterable (set, list, etc)") metadata and iter(metadata) # validate that metadata is iterable with self.lock: if safe and name in self.storage: raise NamingError("name already registered: " + name) if metadata: metadata = set(metadata) self.storage[name] = uri, metadata def set_metadata(self, name, metadata): """update the metadata for an existing registration""" if not isinstance(name, basestring): raise TypeError("name must be a str") if isinstance(metadata, basestring): raise TypeError("metadata should not be a str, but another iterable (set, list, etc)") metadata and iter(metadata) # validate that metadata is iterable with self.lock: try: uri, old_meta = self.storage[name] if metadata: metadata = set(metadata) self.storage[name] = uri, metadata except KeyError: raise NamingError("unknown name: " + name) def remove(self, name=None, prefix=None, regex=None): """Remove a registration. returns the number of items removed.""" if name and name in self.storage and name != constants.NAMESERVER_NAME: with self.lock: del self.storage[name] return 1 if prefix: items = list(self.list(prefix=prefix).keys()) if constants.NAMESERVER_NAME in items: items.remove(constants.NAMESERVER_NAME) self.storage.remove_items(items) return len(items) if regex: items = list(self.list(regex=regex).keys()) if constants.NAMESERVER_NAME in items: items.remove(constants.NAMESERVER_NAME) self.storage.remove_items(items) return len(items) return 0 # noinspection PyNoneFunctionAssignment def list(self, prefix=None, regex=None, metadata_all=None, metadata_any=None, return_metadata=False): """Retrieve the registered items as a dictionary name-to-URI. The URIs in the resulting dict are strings, not URI objects. You can filter by prefix or by regex or by metadata subset (separately)""" def fix_set(result): # for python 2 compatibility we cannot send sets to the default (serpent) serializer. # that's why we will convert them to lists here. if return_metadata: fixed = {} for name, data in result.items(): fixed[name] = (data[0], list(data[1])) return fixed return result if sum(1 for x in [prefix, regex, metadata_all, metadata_any] if x is not None) > 1: raise ValueError("you can only filter on one thing at a time") with self.lock: if prefix: result = self.storage.optimized_prefix_list(prefix, return_metadata) if result is not None: return fix_set(result) result = {} for name in self.storage: if name.startswith(prefix): result[name] = self.storage[name] if return_metadata else self.storage[name][0] return fix_set(result) elif regex: result = self.storage.optimized_regex_list(regex, return_metadata) if result is not None: return fix_set(result) result = {} try: regex = re.compile(regex) except re.error as x: raise NamingError("invalid regex: " + str(x)) else: for name in self.storage: if regex.match(name): result[name] = self.storage[name] if return_metadata else self.storage[name][0] return fix_set(result) elif metadata_all: # return the entries which have all of the given metadata as (a subset of) their metadata if isinstance(metadata_all, basestring): raise TypeError("metadata_all should not be a str, but another iterable (set, list, etc)") metadata_all and iter(metadata_all) # validate that metadata is iterable result = self.storage.optimized_metadata_search(metadata_all=metadata_all, return_metadata=return_metadata) if result is not None: return fix_set(result) metadata_all = frozenset(metadata_all) result = {} for name, (uri, meta) in self.storage.everything(return_metadata=True).items(): if metadata_all.issubset(meta): result[name] = (uri, meta) if return_metadata else uri return fix_set(result) elif metadata_any: # return the entries which have any of the given metadata as part of their metadata if isinstance(metadata_any, basestring): raise TypeError("metadata_any should not be a str, but another iterable (set, list, etc)") metadata_any and iter(metadata_any) # validate that metadata is iterable result = self.storage.optimized_metadata_search(metadata_any=metadata_any, return_metadata=return_metadata) if result is not None: return fix_set(result) metadata_any = frozenset(metadata_any) result = {} for name, (uri, meta) in self.storage.everything(return_metadata=True).items(): if metadata_any & meta: result[name] = (uri, meta) if return_metadata else uri return fix_set(result) else: # just return (a copy of) everything return fix_set(self.storage.everything(return_metadata)) def ping(self): """A simple test method to check if the name server is running correctly.""" pass class NameServerDaemon(core.Daemon): """Daemon that contains the Name Server.""" def __init__(self, host=None, port=None, unixsocket=None, nathost=None, natport=None, storage=None): if host is None: host = config.HOST if port is None: port = config.NS_PORT if nathost is None: nathost = config.NATHOST if natport is None: natport = config.NATPORT or None storage = storage or "memory" if storage == "memory": log.debug("using volatile in-memory dict storage") self.nameserver = NameServer(MemoryStorage()) elif storage.startswith("dbm:") and len(storage) > 4: dbmfile = storage[4:] log.debug("using persistent dbm storage in file %s", dbmfile) from Pyro4.naming_storage import DbmStorage self.nameserver = NameServer(DbmStorage(dbmfile)) warning = "Warning: the DbmStorage doesn't support metadata." log.warning(warning) print(warning) elif storage.startswith("sql:") and len(storage) > 4: sqlfile = storage[4:] log.debug("using persistent sql storage in file %s", sqlfile) from Pyro4.naming_storage import SqlStorage self.nameserver = NameServer(SqlStorage(sqlfile)) else: raise ValueError("invalid storage type '%s'" % storage) existing_count = self.nameserver.count() if existing_count > 0: log.debug("number of existing entries in storage: %d", existing_count) super(NameServerDaemon, self).__init__(host, port, unixsocket, nathost=nathost, natport=natport) self.register(self.nameserver, constants.NAMESERVER_NAME) metadata = {"class:Pyro4.naming.NameServer"} self.nameserver.register(constants.NAMESERVER_NAME, self.uriFor(self.nameserver), metadata=metadata) if config.NS_AUTOCLEAN > 0: if not AutoCleaner.override_autoclean_min and config.NS_AUTOCLEAN < AutoCleaner.min_autoclean_value: raise ValueError("NS_AUTOCLEAN cannot be smaller than " + str(AutoCleaner.min_autoclean_value)) log.debug("autoclean enabled") self.cleaner_thread = AutoCleaner(self.nameserver) self.cleaner_thread.start() else: log.debug("autoclean not enabled") self.cleaner_thread = None log.info("nameserver daemon created") def close(self): super(NameServerDaemon, self).close() if self.nameserver is not None: self.nameserver.storage.close() self.nameserver = None if self.cleaner_thread: self.cleaner_thread.stop = True self.cleaner_thread.join() self.cleaner_thread = None def __enter__(self): if not self.nameserver: raise PyroError("cannot reuse this object") return self def __exit__(self, exc_type, exc_value, traceback): if self.nameserver is not None: self.nameserver.storage.close() self.nameserver = None if self.cleaner_thread: self.cleaner_thread.stop = True self.cleaner_thread.join() self.cleaner_thread = None return super(NameServerDaemon, self).__exit__(exc_type, exc_value, traceback) def handleRequest(self, conn): try: return super(NameServerDaemon, self).handleRequest(conn) except ProtocolError as x: # Notify the user that a protocol error occurred. # This is useful for instance when a wrong serializer is used, it helps # a lot to immediately see what is going wrong. warnings.warn("Pyro protocol error occurred: " + str(x)) raise class AutoCleaner(threading.Thread): """ Takes care of checking every registration in the name server. If it cannot be contacted anymore, it will be removed after ~20 seconds. """ min_autoclean_value = 3 max_unreachable_time = 20.0 loop_delay = 2.0 override_autoclean_min = False # only for unit test purposes def __init__(self, nameserver): assert config.NS_AUTOCLEAN > 0 if not self.override_autoclean_min and config.NS_AUTOCLEAN < self.min_autoclean_value: raise ValueError("NS_AUTOCLEAN cannot be smaller than " + str(self.min_autoclean_value)) super(AutoCleaner, self).__init__() self.nameserver = nameserver self.stop = False self.daemon = True self.last_cleaned = time.time() self.unreachable = {} # name->since when def run(self): while not self.stop: time.sleep(self.loop_delay) time_since_last_autoclean = time.time() - self.last_cleaned if time_since_last_autoclean < config.NS_AUTOCLEAN: continue for name, uri in self.nameserver.list().items(): if name in (constants.DAEMON_NAME, constants.NAMESERVER_NAME): continue try: uri_obj = core.URI(uri) timeout = config.COMMTIMEOUT or 5 sock = socketutil.createSocket(connect=(uri_obj.host, uri_obj.port), timeout=timeout) sock.close() # if we get here, the listed server is still answering on its port if name in self.unreachable: del self.unreachable[name] except socket.error: if name not in self.unreachable: self.unreachable[name] = time.time() if time.time() - self.unreachable[name] >= self.max_unreachable_time: log.info("autoclean: unregistering %s; cannot connect uri %s for %d sec", name, uri, self.max_unreachable_time) self.nameserver.remove(name) del self.unreachable[name] continue self.last_cleaned = time.time() if self.unreachable: log.debug("autoclean: %d/%d names currently unreachable", len(self.unreachable), self.nameserver.count()) class BroadcastServer(object): class TransportServerAdapter(object): # this adapter is used to be able to pass the BroadcastServer to Daemon.combine() to integrate the event loops. def __init__(self, bcserver): self.sockets = [bcserver] def events(self, eventobjects): for bc in eventobjects: bc.processRequest() def __init__(self, nsUri, bchost=None, bcport=None, ipv6=False): self.transportServer = self.TransportServerAdapter(self) self.nsUri = nsUri if bcport is None: bcport = config.NS_BCPORT if bchost is None: bchost = config.NS_BCHOST if ":" in nsUri.host or ipv6: # match nameserver's ip version bchost = bchost or "::" self.sock = socketutil.createBroadcastSocket((bchost, bcport, 0, 0), reuseaddr=config.SOCK_REUSE, timeout=2.0) else: self.sock = socketutil.createBroadcastSocket((bchost, bcport), reuseaddr=config.SOCK_REUSE, timeout=2.0) self._sockaddr = self.sock.getsockname() bchost = bchost or self._sockaddr[0] bcport = bcport or self._sockaddr[1] if ":" in bchost: # ipv6 self.locationStr = "[%s]:%d" % (bchost, bcport) else: self.locationStr = "%s:%d" % (bchost, bcport) log.info("ns broadcast server created on %s - %s", self.locationStr, socketutil.family_str(self.sock)) self.running = True def close(self): log.debug("ns broadcast server closing") self.running = False try: self.sock.shutdown(socket.SHUT_RDWR) except (OSError, socket.error): pass self.sock.close() def getPort(self): return self.sock.getsockname()[1] def fileno(self): return self.sock.fileno() def runInThread(self): """Run the broadcast server loop in its own thread.""" thread = threading.Thread(target=self.__requestLoop) thread.setDaemon(True) thread.start() log.debug("broadcast server loop running in own thread") return thread def __requestLoop(self): while self.running: self.processRequest() log.debug("broadcast server loop terminating") def processRequest(self): try: data, addr = self.sock.recvfrom(100) if data == b"GET_NSURI": responsedata = core.URI(self.nsUri) if responsedata.host == "0.0.0.0": # replace INADDR_ANY address by the interface IP address that connects to the requesting client try: interface_ip = socketutil.getInterfaceAddress(addr[0]) responsedata.host = interface_ip except socket.error: pass log.debug("responding to broadcast request from %s: interface %s", addr[0], responsedata.host) responsedata = str(responsedata).encode("iso-8859-1") self.sock.sendto(responsedata, 0, addr) except socket.error: pass except SystemError: if sys.platform == "cli" and not self.running: # ironpython throws these systemerrors when shutting down... we can ignore them. pass else: raise def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() def startNSloop(host=None, port=None, enableBroadcast=True, bchost=None, bcport=None, unixsocket=None, nathost=None, natport=None, storage=None, hmac=None): """utility function that starts a new Name server and enters its requestloop.""" daemon = NameServerDaemon(host, port, unixsocket, nathost=nathost, natport=natport, storage=storage) daemon._pyroHmacKey = hmac nsUri = daemon.uriFor(daemon.nameserver) internalUri = daemon.uriFor(daemon.nameserver, nat=False) bcserver = None if unixsocket: hostip = "Unix domain socket" else: hostip = daemon.sock.getsockname()[0] if daemon.sock.family == socket.AF_INET6: # ipv6 doesn't have broadcast. We should probably use multicast instead... print("Not starting broadcast server for IPv6.") log.info("Not starting NS broadcast server because NS is using IPv6") enableBroadcast = False elif hostip.startswith("127.") or hostip == "::1": print("Not starting broadcast server for localhost.") log.info("Not starting NS broadcast server because NS is bound to localhost") enableBroadcast = False if enableBroadcast: # Make sure to pass the internal uri to the broadcast responder. # It is almost always useless to let it return the external uri, # because external systems won't be able to talk to this thing anyway. bcserver = BroadcastServer(internalUri, bchost, bcport, ipv6=daemon.sock.family == socket.AF_INET6) print("Broadcast server running on %s" % bcserver.locationStr) bcserver.runInThread() existing = daemon.nameserver.count() if existing > 1: # don't count our own nameserver registration print("Persistent store contains %d existing registrations." % existing) print("NS running on %s (%s)" % (daemon.locationStr, hostip)) if not hmac: print("Warning: HMAC key not set. Anyone can connect to this server!") if daemon.natLocationStr: print("internal URI = %s" % internalUri) print("external URI = %s" % nsUri) else: print("URI = %s" % nsUri) try: daemon.requestLoop() finally: daemon.close() if bcserver is not None: bcserver.close() print("NS shut down.") def startNS(host=None, port=None, enableBroadcast=True, bchost=None, bcport=None, unixsocket=None, nathost=None, natport=None, storage=None, hmac=None): """utility fuction to quickly get a Name server daemon to be used in your own event loops. Returns (nameserverUri, nameserverDaemon, broadcastServer).""" daemon = NameServerDaemon(host, port, unixsocket, nathost=nathost, natport=natport, storage=storage) daemon._pyroHmacKey = hmac bcserver = None nsUri = daemon.uriFor(daemon.nameserver) if not unixsocket: hostip = daemon.sock.getsockname()[0] if hostip.startswith("127."): # not starting broadcast server for localhost. enableBroadcast = False if enableBroadcast: internalUri = daemon.uriFor(daemon.nameserver, nat=False) bcserver = BroadcastServer(internalUri, bchost, bcport, ipv6=daemon.sock.family == socket.AF_INET6) return nsUri, daemon, bcserver def type_meta(class_or_object, prefix="class:"): """extracts type metadata from the given class or object, can be used as Name server metadata.""" if hasattr(class_or_object, "__mro__"): return {prefix + c.__module__ + "." + c.__name__ for c in class_or_object.__mro__ if c.__module__ not in ("builtins", "__builtin__")} if hasattr(class_or_object, "__class__"): return type_meta(class_or_object.__class__) return frozenset() def main(args=None): from optparse import OptionParser parser = OptionParser() parser.add_option("-n", "--host", dest="host", help="hostname to bind server on") parser.add_option("-p", "--port", dest="port", type="int", help="port to bind server on (0=random)") parser.add_option("-u", "--unixsocket", help="Unix domain socket name to bind server on") parser.add_option("-s", "--storage", help="Storage system to use (memory, dbm:file, sql:file)", default="memory") parser.add_option("", "--bchost", dest="bchost", help="hostname to bind broadcast server on (default is \"\")") parser.add_option("", "--bcport", dest="bcport", type="int", help="port to bind broadcast server on (0=random)") parser.add_option("", "--nathost", dest="nathost", help="external hostname in case of NAT") parser.add_option("", "--natport", dest="natport", type="int", help="external port in case of NAT") parser.add_option("-x", "--nobc", dest="enablebc", action="store_false", default=True, help="don't start a broadcast server") parser.add_option("-k", "--key", help="the HMAC key to use (deprecated)") options, args = parser.parse_args(args) if options.key: warnings.warn("using -k to supply HMAC key on the command line is a security problem " "and is deprecated since Pyro 4.72. See the documentation for an alternative.") if "PYRO_HMAC_KEY" in os.environ: if options.key: raise SystemExit("error: don't use -k and PYRO_HMAC_KEY at the same time") options.key = os.environ["PYRO_HMAC_KEY"] startNSloop(options.host, options.port, enableBroadcast=options.enablebc, bchost=options.bchost, bcport=options.bcport, unixsocket=options.unixsocket, nathost=options.nathost, natport=options.natport, storage=options.storage, hmac=options.key) if __name__ == "__main__": main() Pyro4-4.82/src/Pyro4/naming_storage.py000066400000000000000000000433251416147301300176460ustar00rootroot00000000000000""" Name Server persistent storage implementations. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import re import logging import sys import threading if sys.version_info <= (3, 4): from collections import MutableMapping else: from collections.abc import MutableMapping from contextlib import closing from Pyro4.errors import NamingError try: import anydbm as dbm # python 2 except ImportError: try: import dbm # python 3 except ImportError: dbm = None except Exception as x: # pypy can generate a distutils error somehow if dbm is not available dbm = None try: import sqlite3 except ImportError: sqlite3 = None log = logging.getLogger("Pyro4.naming_storage") class SqlStorage(MutableMapping): """ Sqlite-based storage. It is just a single (name,uri) table for the names and another table for the metadata. Sqlite db connection objects aren't thread-safe, so a new connection is created in every method. """ def __init__(self, dbfile): if dbfile == ":memory:": raise ValueError("We don't support the sqlite :memory: database type. Just use the default volatile in-memory store.") self.dbfile = dbfile with closing(sqlite3.connect(dbfile)) as db: db.execute("PRAGMA foreign_keys=ON") try: db.execute("SELECT COUNT(*) FROM pyro_names").fetchone() except sqlite3.OperationalError: # the table does not yet exist self._create_schema(db) else: # check if we need to update the existing schema try: db.execute("SELECT COUNT(*) FROM pyro_metadata").fetchone() except sqlite3.OperationalError: # metadata schema needs to be created and existing data migrated db.execute("ALTER TABLE pyro_names RENAME TO pyro_names_old") self._create_schema(db) db.execute("INSERT INTO pyro_names(name, uri) SELECT name, uri FROM pyro_names_old") db.execute("DROP TABLE pyro_names_old") db.commit() def _create_schema(self, db): db.execute("""CREATE TABLE pyro_names ( id integer PRIMARY KEY, name nvarchar NOT NULL UNIQUE, uri nvarchar NOT NULL );""") db.execute("""CREATE TABLE pyro_metadata ( object integer NOT NULL, metadata nvarchar NOT NULL, FOREIGN KEY(object) REFERENCES pyro_names(id) );""") def __getattr__(self, item): raise NotImplementedError("SqlStorage doesn't implement method/attribute '" + item + "'") def __getitem__(self, item): try: with closing(sqlite3.connect(self.dbfile)) as db: result = db.execute("SELECT id, uri FROM pyro_names WHERE name=?", (item,)).fetchone() if result: dbid, uri = result metadata = {m[0] for m in db.execute("SELECT metadata FROM pyro_metadata WHERE object=?", (dbid,)).fetchall()} return uri, metadata else: raise KeyError(item) except sqlite3.DatabaseError as e: raise NamingError("sqlite error in getitem: " + str(e)) def __setitem__(self, key, value): uri, metadata = value try: with closing(sqlite3.connect(self.dbfile)) as db: cursor = db.cursor() cursor.execute("PRAGMA foreign_keys=ON") dbid = cursor.execute("SELECT id FROM pyro_names WHERE name=?", (key,)).fetchone() if dbid: dbid = dbid[0] cursor.execute("DELETE FROM pyro_metadata WHERE object=?", (dbid,)) cursor.execute("DELETE FROM pyro_names WHERE id=?", (dbid,)) cursor.execute("INSERT INTO pyro_names(name, uri) VALUES(?,?)", (key, uri)) if metadata: object_id = cursor.lastrowid for m in metadata: cursor.execute("INSERT INTO pyro_metadata(object, metadata) VALUES (?,?)", (object_id, m)) cursor.close() db.commit() except sqlite3.DatabaseError as e: raise NamingError("sqlite error in setitem: " + str(e)) def __len__(self): try: with closing(sqlite3.connect(self.dbfile)) as db: return db.execute("SELECT count(*) FROM pyro_names").fetchone()[0] except sqlite3.DatabaseError as e: raise NamingError("sqlite error in len: " + str(e)) def __contains__(self, item): try: with closing(sqlite3.connect(self.dbfile)) as db: return db.execute("SELECT EXISTS(SELECT 1 FROM pyro_names WHERE name=? LIMIT 1)", (item,)).fetchone()[0] except sqlite3.DatabaseError as e: raise NamingError("sqlite error in contains: " + str(e)) def __delitem__(self, key): try: with closing(sqlite3.connect(self.dbfile)) as db: db.execute("PRAGMA foreign_keys=ON") dbid = db.execute("SELECT id FROM pyro_names WHERE name=?", (key,)).fetchone() if dbid: dbid = dbid[0] db.execute("DELETE FROM pyro_metadata WHERE object=?", (dbid,)) db.execute("DELETE FROM pyro_names WHERE id=?", (dbid,)) db.commit() except sqlite3.DatabaseError as e: raise NamingError("sqlite error in delitem: " + str(e)) def __iter__(self): try: with closing(sqlite3.connect(self.dbfile)) as db: result = db.execute("SELECT name FROM pyro_names") return iter([n[0] for n in result.fetchall()]) except sqlite3.DatabaseError as e: raise NamingError("sqlite error in iter: " + str(e)) def clear(self): try: with closing(sqlite3.connect(self.dbfile)) as db: db.execute("PRAGMA foreign_keys=ON") db.execute("DELETE FROM pyro_metadata") db.execute("DELETE FROM pyro_names") db.commit() with closing(sqlite3.connect(self.dbfile, isolation_level=None)) as db: db.execute("VACUUM") # this cannot run inside a transaction. except sqlite3.DatabaseError as e: raise NamingError("sqlite error in clear: " + str(e)) def optimized_prefix_list(self, prefix, return_metadata=False): try: with closing(sqlite3.connect(self.dbfile)) as db: names = {} if return_metadata: for dbid, name, uri in db.execute("SELECT id, name, uri FROM pyro_names WHERE name LIKE ?", (prefix + '%',)).fetchall(): metadata = {m[0] for m in db.execute("SELECT metadata FROM pyro_metadata WHERE object=?", (dbid,)).fetchall()} names[name] = uri, metadata else: for name, uri in db.execute("SELECT name, uri FROM pyro_names WHERE name LIKE ?", (prefix + '%',)).fetchall(): names[name] = uri return names except sqlite3.DatabaseError as e: raise NamingError("sqlite error in optimized_prefix_list: " + str(e)) def optimized_regex_list(self, regex, return_metadata=False): # defining a regex function isn't much better than simply regexing ourselves over the full table. return None def optimized_metadata_search(self, metadata_all=None, metadata_any=None, return_metadata=False): try: with closing(sqlite3.connect(self.dbfile)) as db: if metadata_any: # any of the given metadata params = list(metadata_any) sql = "SELECT id, name, uri FROM pyro_names WHERE id IN (SELECT object FROM pyro_metadata WHERE metadata IN ({seq}))" \ .format(seq=",".join(['?'] * len(metadata_any))) else: # all of the given metadata params = list(metadata_all) params.append(len(metadata_all)) sql = "SELECT id, name, uri FROM pyro_names WHERE id IN (SELECT object FROM pyro_metadata WHERE metadata IN ({seq}) " \ "GROUP BY object HAVING COUNT(metadata)=?)".format(seq=",".join(['?'] * len(metadata_all))) result = db.execute(sql, params).fetchall() if return_metadata: names = {} for dbid, name, uri in result: metadata = {m[0] for m in db.execute("SELECT metadata FROM pyro_metadata WHERE object=?", (dbid,)).fetchall()} names[name] = uri, metadata else: names = {name: uri for (dbid, name, uri) in result} return names except sqlite3.DatabaseError as e: raise NamingError("sqlite error in optimized_metadata_search: " + str(e)) def remove_items(self, items): try: with closing(sqlite3.connect(self.dbfile)) as db: db.execute("PRAGMA foreign_keys=ON") for item in items: dbid = db.execute("SELECT id FROM pyro_names WHERE name=?", (item,)).fetchone() if dbid: dbid = dbid[0] db.execute("DELETE FROM pyro_metadata WHERE object=?", (dbid,)) db.execute("DELETE FROM pyro_names WHERE id=?", (dbid,)) db.commit() except sqlite3.DatabaseError as e: raise NamingError("sqlite error in remove_items: " + str(e)) def everything(self, return_metadata=False): try: with closing(sqlite3.connect(self.dbfile)) as db: names = {} if return_metadata: for dbid, name, uri in db.execute("SELECT id, name, uri FROM pyro_names").fetchall(): metadata = {m[0] for m in db.execute("SELECT metadata FROM pyro_metadata WHERE object=?", (dbid,)).fetchall()} names[name] = uri, metadata else: for name, uri in db.execute("SELECT name, uri FROM pyro_names").fetchall(): names[name] = uri return names except sqlite3.DatabaseError as e: raise NamingError("sqlite error in everything: " + str(e)) def close(self): pass class DbmStorage(MutableMapping): """ Storage implementation that uses a persistent dbm file. Because dbm only supports strings as key/value, we encode/decode them in utf-8. Dbm files cannot be accessed concurrently, so a strict concurrency model is used where only one operation is processed at the same time (this is very slow when compared to the in-memory storage) DbmStorage does NOT support storing metadata! It only accepts empty metadata, and always returns empty metadata. """ def __init__(self, dbmfile): self.dbmfile = dbmfile db = dbm.open(self.dbmfile, "c", mode=0o600) db.close() self.lock = threading.Lock() def __getattr__(self, item): raise NotImplementedError("DbmStorage doesn't implement method/attribute '" + item + "'") def __getitem__(self, item): item = item.encode("utf-8") with self.lock: try: with closing(dbm.open(self.dbmfile)) as db: return db[item].decode("utf-8"), frozenset() # always return empty metadata except dbm.error as e: raise NamingError("dbm error in getitem: " + str(e)) def __setitem__(self, key, value): uri, metadata = value if metadata: log.warning("DbmStorage doesn't support metadata, silently discarded") key = key.encode("utf-8") uri = uri.encode("utf-8") with self.lock: try: with closing(dbm.open(self.dbmfile, "w")) as db: db[key] = uri except dbm.error as e: raise NamingError("dbm error in setitem: " + str(e)) def __len__(self): with self.lock: try: with closing(dbm.open(self.dbmfile)) as db: return len(db) except dbm.error as e: raise NamingError("dbm error in len: " + str(e)) def __contains__(self, item): item = item.encode("utf-8") with self.lock: try: with closing(dbm.open(self.dbmfile)) as db: return item in db except dbm.error as e: raise NamingError("dbm error in contains: " + str(e)) def __delitem__(self, key): key = key.encode("utf-8") with self.lock: try: with closing(dbm.open(self.dbmfile, "w")) as db: del db[key] except dbm.error as e: raise NamingError("dbm error in delitem: " + str(e)) def __iter__(self): with self.lock: try: with closing(dbm.open(self.dbmfile)) as db: return iter([key.decode("utf-8") for key in db.keys()]) except dbm.error as e: raise NamingError("dbm error in iter: " + str(e)) def clear(self): with self.lock: try: with closing(dbm.open(self.dbmfile, "w")) as db: if hasattr(db, "clear"): db.clear() else: for key in db.keys(): del db[key] except dbm.error as e: raise NamingError("dbm error in clear: " + str(e)) def optimized_prefix_list(self, prefix, return_metadata=False): with self.lock: try: with closing(dbm.open(self.dbmfile)) as db: result = {} if hasattr(db, "items"): for key, value in db.items(): key = key.decode("utf-8") if key.startswith(prefix): uri = value.decode("utf-8") result[key] = (uri, frozenset()) if return_metadata else uri # always return empty metadata else: for key in db.keys(): keystr = key.decode("utf-8") if keystr.startswith(prefix): uri = db[key].decode("utf-8") result[keystr] = (uri, frozenset()) if return_metadata else uri # always return empty metadata return result except dbm.error as e: raise NamingError("dbm error in optimized_prefix_list: " + str(e)) def optimized_regex_list(self, regex, return_metadata=False): try: regex = re.compile(regex + "$") # add end of string marker except re.error as x: raise NamingError("invalid regex: " + str(x)) with self.lock: try: with closing(dbm.open(self.dbmfile)) as db: result = {} if hasattr(db, "items"): for key, value in db.items(): key = key.decode("utf-8") if regex.match(key): uri = value.decode("utf-8") result[key] = (uri, frozenset()) if return_metadata else uri # always return empty metadata else: for key in db.keys(): keystr = key.decode("utf-8") if regex.match(keystr): uri = db[key].decode("utf-8") result[keystr] = (uri, frozenset()) if return_metadata else uri # always return empty metadata return result except dbm.error as e: raise NamingError("dbm error in optimized_regex_list: " + str(e)) def optimized_metadata_search(self, metadata_all=None, metadata_any=None, return_metadata=False): if metadata_all or metadata_any: raise NamingError("DbmStorage doesn't support metadata") return self.everything(return_metadata) def remove_items(self, items): with self.lock: try: with closing(dbm.open(self.dbmfile, "w")) as db: for item in items: try: del db[item.encode("utf-8")] except KeyError: pass except dbm.error as e: raise NamingError("dbm error in remove_items: " + str(e)) def everything(self, return_metadata=False): with self.lock: try: with closing(dbm.open(self.dbmfile)) as db: result = {} if hasattr(db, "items"): for key, value in db.items(): uri = value.decode("utf-8") result[key.decode("utf-8")] = (uri, frozenset()) if return_metadata else uri # always return empty metadata else: for key in db.keys(): uri = db[key].decode("utf-8") result[key.decode("utf-8")] = (uri, frozenset()) if return_metadata else uri # always return empty metadata return result except dbm.error as e: raise NamingError("dbm error in everything: " + str(e)) def close(self): pass Pyro4-4.82/src/Pyro4/nsc.py000066400000000000000000000131201416147301300154220ustar00rootroot00000000000000""" Name server control tool. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import sys import os import warnings from Pyro4 import errors, naming if sys.version_info < (3, 0): input = raw_input def handleCommand(nameserver, options, args): def printListResult(resultdict, title=""): print("--------START LIST %s" % title) for name, (uri, metadata) in sorted(resultdict.items()): print("%s --> %s" % (name, uri)) if metadata: print(" metadata:", metadata) print("--------END LIST %s" % title) def cmd_ping(): nameserver.ping() print("Name server ping ok.") def cmd_listprefix(): if len(args) == 1: printListResult(nameserver.list(return_metadata=True)) else: printListResult(nameserver.list(prefix=args[1], return_metadata=True), "- prefix '%s'" % args[1]) def cmd_listregex(): if len(args) != 2: raise SystemExit("requires one argument: pattern") printListResult(nameserver.list(regex=args[1], return_metadata=True), "- regex '%s'" % args[1]) def cmd_lookup(): if len(args) != 2: raise SystemExit("requires one argument: name") uri, metadata = nameserver.lookup(args[1], return_metadata=True) print(uri) if metadata: print("metadata:", metadata) def cmd_register(): if len(args) != 3: raise SystemExit("requires two arguments: name uri") nameserver.register(args[1], args[2], safe=True) print("Registered %s" % args[1]) def cmd_remove(): if len(args) != 2: raise SystemExit("requires one argument: name") count = nameserver.remove(args[1]) if count > 0: print("Removed %s" % args[1]) else: print("Nothing removed") def cmd_removeregex(): if len(args) != 2: raise SystemExit("requires one argument: pattern") sure = input("Potentially removing lots of items from the Name server. Are you sure (y/n)?").strip() if sure in ('y', 'Y'): count = nameserver.remove(regex=args[1]) print("%d items removed." % count) def cmd_setmeta(): if len(args) < 2: raise SystemExit("requires at least 2 arguments: uri and zero or more meta tags") metadata = set(args[2:]) nameserver.set_metadata(args[1], metadata) if metadata: print("Metadata updated") else: print("Metadata cleared") def cmd_listmeta_all(): if len(args) < 2: raise SystemExit("requires at least one metadata tag argument") metadata = set(args[1:]) printListResult(nameserver.list(metadata_all=metadata, return_metadata=True), " - searched by metadata") def cmd_listmeta_any(): if len(args) < 2: raise SystemExit("requires at least one metadata tag argument") metadata = set(args[1:]) printListResult(nameserver.list(metadata_any=metadata, return_metadata=True), " - searched by metadata") commands = { "ping": cmd_ping, "list": cmd_listprefix, "listmatching": cmd_listregex, "listmeta_all": cmd_listmeta_all, "listmeta_any": cmd_listmeta_any, "lookup": cmd_lookup, "register": cmd_register, "remove": cmd_remove, "removematching": cmd_removeregex, "setmeta": cmd_setmeta } try: commands[args[0]]() except Exception as x: print("Error: %s - %s" % (type(x).__name__, x)) def main(args=None): from optparse import OptionParser usage = "usage: %prog [options] command [arguments]\nCommands: " \ "register remove removematching lookup list listmatching\n listmeta_all listmeta_any setmeta ping" parser = OptionParser(usage=usage) parser.add_option("-n", "--host", dest="host", help="hostname of the NS") parser.add_option("-p", "--port", dest="port", type="int", help="port of the NS (or bc-port if host isn't specified)") parser.add_option("-u", "--unixsocket", help="Unix domain socket name of the NS") parser.add_option("-k", "--key", help="the HMAC key to use (deprecated)") parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="verbose output") options, args = parser.parse_args(args) if options.key: warnings.warn("using -k to supply HMAC key on the command line is a security problem " "and is deprecated since Pyro 4.72. See the documentation for an alternative.") if "PYRO_HMAC_KEY" in os.environ: if options.key: raise SystemExit("error: don't use -k and PYRO_HMAC_KEY at the same time") options.key = os.environ["PYRO_HMAC_KEY"] if not args or args[0] not in ("register", "remove", "removematching", "list", "listmatching", "lookup", "listmeta_all", "listmeta_any", "setmeta", "ping"): parser.error("invalid or missing command") if options.verbose: print("Locating name server...") if options.unixsocket: options.host = "./u:" + options.unixsocket try: nameserver = naming.locateNS(options.host, options.port, hmac_key=options.key) except errors.PyroError as x: print("Error: %s" % x) return if options.verbose: print("Name server found: %s" % nameserver._pyroUri) handleCommand(nameserver, options, args) if options.verbose: print("Done.") if __name__ == "__main__": main() Pyro4-4.82/src/Pyro4/socketserver/000077500000000000000000000000001416147301300170075ustar00rootroot00000000000000Pyro4-4.82/src/Pyro4/socketserver/__init__.py000066400000000000000000000002001416147301300211100ustar00rootroot00000000000000""" Package for the various server types. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ Pyro4-4.82/src/Pyro4/socketserver/existingconnectionserver.py000066400000000000000000000100371416147301300245230ustar00rootroot00000000000000""" Socket server for a the special case of a single, already existing, connection. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import socket import sys import logging import ssl from Pyro4 import socketutil, errors, util from Pyro4.configuration import config log = logging.getLogger("Pyro4.existingconnectionserver") class SocketServer_ExistingConnection(object): def __init__(self): self.sock = self.daemon = self.locationStr = self.conn = None self.shutting_down = False def init(self, daemon, connected_socket): connected_socket.getpeername() # check that it is connected if config.SSL and not isinstance(connected_socket, ssl.SSLSocket): raise socket.error("SSL configured for Pyro but existing socket is not a SSL socket") self.daemon = daemon self.sock = connected_socket log.info("starting server on user-supplied connected socket " + str(connected_socket)) sn = connected_socket.getsockname() if hasattr(socket, "AF_UNIX") and connected_socket.family == socket.AF_UNIX: self.locationStr = "./u:" + (sn or "<>") else: host, port = sn[:2] if ":" in host: # ipv6 self.locationStr = "[%s]:%d" % (host, port) else: self.locationStr = "%s:%d" % (host, port) self.conn = socketutil.SocketConnection(connected_socket) def __repr__(self): return "<%s on %s>" % (self.__class__.__name__, self.locationStr) def __del__(self): if self.sock is not None: self.sock = None self.conn = None @property def selector(self): raise TypeError("single-connection server doesn't have multiplexing selector") @property def sockets(self): return [self.sock] def combine_loop(self, server): raise errors.PyroError("cannot combine servers when using user-supplied connected socket") def events(self, eventsockets): raise errors.PyroError("cannot combine events when using user-supplied connected socket") def shutdown(self): self.shutting_down = True self.close() self.sock = None self.conn = None def close(self): # don't close the socket itself, that's the user's responsibility self.sock = None self.conn = None def handleRequest(self): """Handles a single connection request event and returns if the connection is still active""" try: self.daemon.handleRequest(self.conn) return True except (socket.error, errors.ConnectionClosedError, errors.SecurityError) as x: # client went away or caused a security error. # close the connection silently. try: peername = self.conn.sock.getpeername() log.debug("disconnected %s", peername) except socket.error: log.debug("disconnected a client") self.shutdown() return False except errors.TimeoutError as x: # for timeout errors we're not really interested in detailed traceback info log.warning("error during handleRequest: %s" % x) return False except: # other error occurred, close the connection, but also log a warning ex_t, ex_v, ex_tb = sys.exc_info() tb = util.formatTraceback(ex_t, ex_v, ex_tb) msg = "error during handleRequest: %s; %s" % (ex_v, "".join(tb)) log.warning(msg) return False def loop(self, loopCondition=lambda: True): log.debug("entering requestloop") while loopCondition() and self.sock: try: self.handleRequest() self.daemon._housekeeping() except socket.timeout: pass # just continue the loop on a timeout except KeyboardInterrupt: log.debug("stopping on break signal") break Pyro4-4.82/src/Pyro4/socketserver/multiplexserver.py000066400000000000000000000232241416147301300226360ustar00rootroot00000000000000""" Socket server based on socket multiplexing. Doesn't use threads. Uses the best available selector (kqueue, poll, select). Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import socket import time import sys import logging import os from collections import defaultdict from Pyro4 import socketutil, errors, util from Pyro4.configuration import config if sys.version_info >= (3, 5): import selectors else: try: # first try selectors2 as it has better semantics when dealing with interrupted system calls import selectors2 as selectors except ImportError: if sys.version_info >= (3, 4): import selectors else: try: import selectors34 as selectors except ImportError: selectors = None log = logging.getLogger("Pyro4.multiplexserver") class SocketServer_Multiplex(object): """Multiplexed transport server for socket connections (uses select, poll, kqueue, ...)""" def __init__(self): self.sock = self.daemon = self.locationStr = None if selectors is None: raise RuntimeError("This Python installation doesn't have the 'selectors2' or 'selectors34' module installed, " + "which is required to use Pyro's multiplex server. Install it, or use the threadpool server instead.") self.selector = selectors.DefaultSelector() self.shutting_down = False def init(self, daemon, host, port, unixsocket=None): log.info("starting multiplexed socketserver") log.debug("selector implementation: %s.%s", self.selector.__class__.__module__, self.selector.__class__.__name__) self.sock = None bind_location = unixsocket if unixsocket else (host, port) if config.SSL: sslContext = socketutil.getSSLcontext(servercert=config.SSL_SERVERCERT, serverkey=config.SSL_SERVERKEY, keypassword=config.SSL_SERVERKEYPASSWD, cacerts=config.SSL_CACERTS) log.info("using SSL, cert=%s key=%s cacerts=%s", config.SSL_SERVERCERT, config.SSL_SERVERKEY, config.SSL_CACERTS) else: sslContext = None log.info("not using SSL") self.sock = socketutil.createSocket(bind=bind_location, reuseaddr=config.SOCK_REUSE, timeout=config.COMMTIMEOUT, noinherit=True, nodelay=config.SOCK_NODELAY, sslContext=sslContext) self.daemon = daemon self._socketaddr = sockaddr = self.sock.getsockname() if not unixsocket and sockaddr[0].startswith("127."): if host is None or host.lower() != "localhost" and not host.startswith("127."): log.warning("weird DNS setup: %s resolves to localhost (127.x.x.x)", host) if unixsocket: self.locationStr = "./u:" + unixsocket else: host = host or sockaddr[0] port = port or sockaddr[1] if ":" in host: # ipv6 self.locationStr = "[%s]:%d" % (host, port) else: self.locationStr = "%s:%d" % (host, port) self.selector.register(self.sock, selectors.EVENT_READ, self) def __repr__(self): return "<%s on %s; %d connections>" % (self.__class__.__name__, self.locationStr, len(self.selector.get_map()) - 1) def __del__(self): if self.sock is not None: self.selector.close() self.sock.close() self.sock = None def events(self, eventsockets): """handle events that occur on one of the sockets of this server""" for s in eventsockets: if self.shutting_down: return if s is self.sock: # server socket, means new connection conn = self._handleConnection(self.sock) if conn: self.selector.register(conn, selectors.EVENT_READ, self) else: # must be client socket, means remote call active = self.handleRequest(s) if not active: try: self.daemon._clientDisconnect(s) except Exception as x: log.warning("Error in clientDisconnect: " + str(x)) self.selector.unregister(s) s.close() self.daemon._housekeeping() def _handleConnection(self, sock): try: if sock is None: return csock, caddr = sock.accept() if hasattr(csock, "getpeercert"): log.debug("connected %s - SSL", caddr) else: log.debug("connected %s - unencrypted", caddr) if config.COMMTIMEOUT: csock.settimeout(config.COMMTIMEOUT) except (socket.error, OSError) as x: err = getattr(x, "errno", x.args[0]) if err in socketutil.ERRNO_BADF or err in socketutil.ERRNO_ENOTSOCK: # our server socket got destroyed raise errors.ConnectionClosedError("server socket closed") # socket errors may not lead to a server abort, so we log it and continue err = getattr(x, "errno", x.args[0]) log.warning("accept() failed '%s' with errno=%d, shouldn't happen", x, err) return None try: conn = socketutil.SocketConnection(csock) if self.daemon._handshake(conn): return conn conn.close() except: # catch all errors, otherwise the event loop could terminate ex_t, ex_v, ex_tb = sys.exc_info() tb = util.formatTraceback(ex_t, ex_v, ex_tb) log.warning("error during connect/handshake: %s; %s", ex_v, "\n".join(tb)) try: csock.shutdown(socket.SHUT_RDWR) except (OSError, socket.error): pass csock.close() return None def shutdown(self): self.shutting_down = True self.wakeup() time.sleep(0.05) self.close() self.sock = None def close(self): self.selector.close() if self.sock: sockname = None try: sockname = self.sock.getsockname() except (socket.error, OSError): pass self.sock.close() if type(sockname) is str: # it was a Unix domain socket, remove it from the filesystem if os.path.exists(sockname): os.remove(sockname) self.sock = None @property def sockets(self): registrations = self.selector.get_map() if registrations: return [sk.fileobj for sk in registrations.values()] else: return [] def wakeup(self): """bit of a hack to trigger a blocking server to get out of the loop, useful at clean shutdowns""" socketutil.interruptSocket(self._socketaddr) def handleRequest(self, conn): """Handles a single connection request event and returns if the connection is still active""" try: self.daemon.handleRequest(conn) return True except (socket.error, errors.ConnectionClosedError, errors.SecurityError): # client went away or caused a security error. # close the connection silently. try: peername = conn.sock.getpeername() log.debug("disconnected %s", peername) except socket.error: log.debug("disconnected a client") return False except errors.TimeoutError as x: # for timeout errors we're not really interested in detailed traceback info log.warning("error during handleRequest: %s" % x) return False except: # other error occurred, close the connection, but also log a warning ex_t, ex_v, ex_tb = sys.exc_info() tb = util.formatTraceback(ex_t, ex_v, ex_tb) msg = "error during handleRequest: %s; %s" % (ex_v, "".join(tb)) log.warning(msg) return False def loop(self, loopCondition=lambda: True): log.debug("entering multiplexed requestloop") while loopCondition(): try: try: events = self.selector.select(config.POLLTIMEOUT) except OSError: events = [] # get all the socket connection objects that have a READ event # (the WRITE events are ignored here, they're registered to let timeouts work etc) events_per_server = defaultdict(list) for key, mask in events: if mask & selectors.EVENT_READ: events_per_server[key.data].append(key.fileobj) for server, fileobjs in events_per_server.items(): server.events(fileobjs) if not events_per_server: self.daemon._housekeeping() except socket.timeout: pass # just continue the loop on a timeout except KeyboardInterrupt: log.debug("stopping on break signal") break def combine_loop(self, server): for sock in server.sockets: self.selector.register(sock, selectors.EVENT_READ, server) server.selector = self.selector Pyro4-4.82/src/Pyro4/socketserver/threadpool.py000066400000000000000000000103471416147301300215270ustar00rootroot00000000000000""" Thread pool job processor with variable number of worker threads (between max/min amount). Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import with_statement import time import logging import threading from Pyro4.configuration import config log = logging.getLogger("Pyro4.threadpool") class PoolError(Exception): pass class NoFreeWorkersError(PoolError): pass class Worker(threading.Thread): def __init__(self, pool): super(Worker, self).__init__() self.daemon = True self.name = "Pyro-Worker-%d" % id(self) self.job_available = threading.Event() self.job = None self.pool = pool def process(self, job): self.job = job self.job_available.set() def run(self): while True: self.job_available.wait() self.job_available.clear() if self.job is None: break try: self.job() except Exception as x: log.exception("unhandled exception from job in worker thread %s: %s", self.name, x) self.job = None self.pool.notify_done(self) self.pool = None class Pool(object): """ A job processing pool that is using a pool of worker threads. The amount of worker threads in the pool is configurable and scales between min/max size. """ def __init__(self): if config.THREADPOOL_SIZE < 1 or config.THREADPOOL_SIZE_MIN < 1: raise ValueError("threadpool sizes must be greater than zero") if config.THREADPOOL_SIZE_MIN > config.THREADPOOL_SIZE: raise ValueError("minimum threadpool size must be less than or equal to max size") self.idle = set() self.busy = set() self.closed = False for _ in range(config.THREADPOOL_SIZE_MIN): worker = Worker(self) self.idle.add(worker) worker.start() log.debug("worker pool created with initial size %d", self.num_workers()) self.count_lock = threading.Lock() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def close(self): if not self.closed: log.debug("closing down") for w in list(self.busy): w.process(None) for w in list(self.idle): w.process(None) self.closed = True time.sleep(0.1) idle, self.idle = self.idle, set() busy, self.busy = self.busy, set() # check if the threads that are joined are not the current thread, # otherwise Python 2.x crashes with "cannot join current thread". current_thread = threading.current_thread() while idle: p = idle.pop() if p is not current_thread: p.join(timeout=0.1) while busy: p = busy.pop() if p is not current_thread: p.join(timeout=0.1) def __repr__(self): return "<%s.%s at 0x%x; %d busy workers; %d idle workers>" % \ (self.__class__.__module__, self.__class__.__name__, id(self), len(self.busy), len(self.idle)) def num_workers(self): return len(self.busy) + len(self.idle) def process(self, job): if self.closed: raise PoolError("job queue is closed") if self.idle: worker = self.idle.pop() elif self.num_workers() < config.THREADPOOL_SIZE: worker = Worker(self) worker.start() else: raise NoFreeWorkersError("no free workers available, increase thread pool size") self.busy.add(worker) worker.process(job) log.debug("worker counts: %d busy, %d idle", len(self.busy), len(self.idle)) def notify_done(self, worker): if worker in self.busy: self.busy.remove(worker) if self.closed: worker.process(None) return if len(self.idle) >= config.THREADPOOL_SIZE_MIN: worker.process(None) else: self.idle.add(worker) log.debug("worker counts: %d busy, %d idle", len(self.busy), len(self.idle)) Pyro4-4.82/src/Pyro4/socketserver/threadpoolserver.py000066400000000000000000000235111416147301300227530ustar00rootroot00000000000000""" Socket server based on a worker thread pool. Doesn't use select. Uses a single worker thread per client connection. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import socket import logging import sys import time import threading import os from Pyro4 import socketutil, errors, util from Pyro4.configuration import config from .threadpool import Pool, NoFreeWorkersError from .multiplexserver import selectors log = logging.getLogger("Pyro4.threadpoolserver") _client_disconnect_lock = threading.Lock() class ClientConnectionJob(object): """ Takes care of a single client connection and all requests that may arrive during its life span. """ def __init__(self, clientSocket, clientAddr, daemon): self.csock = socketutil.SocketConnection(clientSocket) self.caddr = clientAddr self.daemon = daemon def __call__(self): if self.handleConnection(): try: while True: try: self.daemon.handleRequest(self.csock) except (socket.error, errors.ConnectionClosedError): # client went away. log.debug("disconnected %s", self.caddr) break except errors.SecurityError: log.debug("security error on client %s", self.caddr) break except errors.TimeoutError as x: # for timeout errors we're not really interested in detailed traceback info log.warning("error during handleRequest: %s" % x) break except: # other errors log a warning, break this loop and close the client connection ex_t, ex_v, ex_tb = sys.exc_info() tb = util.formatTraceback(ex_t, ex_v, ex_tb) msg = "error during handleRequest: %s; %s" % (ex_v, "".join(tb)) log.warning(msg) break finally: with _client_disconnect_lock: try: self.daemon._clientDisconnect(self.csock) except Exception as x: log.warning("Error in clientDisconnect: " + str(x)) self.csock.close() def handleConnection(self): # connection handshake try: if self.daemon._handshake(self.csock): return True self.csock.close() except: ex_t, ex_v, ex_tb = sys.exc_info() tb = util.formatTraceback(ex_t, ex_v, ex_tb) log.warning("error during connect/handshake: %s; %s", ex_v, "\n".join(tb)) self.csock.close() return False def denyConnection(self, reason): log.warning("client connection was denied: " + reason) # return failed handshake self.daemon._handshake(self.csock, denied_reason=reason) self.csock.close() class Housekeeper(threading.Thread): def __init__(self, daemon): super(Housekeeper, self).__init__(name="housekeeper") self.pyroDaemon = daemon self.stop = threading.Event() self.daemon = True self.waittime = min(config.POLLTIMEOUT or 0, max(config.COMMTIMEOUT or 0, 5)) def run(self): while True: if self.stop.wait(self.waittime): break self.pyroDaemon._housekeeping() class SocketServer_Threadpool(object): """transport server for socket connections, worker thread pool version.""" def __init__(self): self.daemon = self.sock = self._socketaddr = self.locationStr = self.pool = None self.shutting_down = False self.housekeeper = None self._selector = selectors.DefaultSelector() if selectors else None def init(self, daemon, host, port, unixsocket=None): log.info("starting thread pool socketserver") self.daemon = daemon self.sock = None bind_location = unixsocket if unixsocket else (host, port) if config.SSL: sslContext = socketutil.getSSLcontext(servercert=config.SSL_SERVERCERT, serverkey=config.SSL_SERVERKEY, keypassword=config.SSL_SERVERKEYPASSWD, cacerts=config.SSL_CACERTS) log.info("using SSL, cert=%s key=%s cacerts=%s", config.SSL_SERVERCERT, config.SSL_SERVERKEY, config.SSL_CACERTS) else: sslContext = None log.info("not using SSL") self.sock = socketutil.createSocket(bind=bind_location, reuseaddr=config.SOCK_REUSE, timeout=config.COMMTIMEOUT, noinherit=True, nodelay=config.SOCK_NODELAY, sslContext=sslContext) self._socketaddr = self.sock.getsockname() if not unixsocket and self._socketaddr[0].startswith("127."): if host is None or host.lower() != "localhost" and not host.startswith("127."): log.warning("weird DNS setup: %s resolves to localhost (127.x.x.x)", host) if unixsocket: self.locationStr = "./u:" + unixsocket else: host = host or self._socketaddr[0] port = port or self._socketaddr[1] if ":" in host: # ipv6 self.locationStr = "[%s]:%d" % (host, port) else: self.locationStr = "%s:%d" % (host, port) self.pool = Pool() self.housekeeper = Housekeeper(daemon) self.housekeeper.start() if self._selector: self._selector.register(self.sock, selectors.EVENT_READ, self) def __del__(self): if self.sock is not None: self.sock.close() self.sock = None if self.pool is not None: self.pool.close() self.pool = None if self.housekeeper: self.housekeeper.stop.set() self.housekeeper.join() self.housekeeper = None def __repr__(self): return "<%s on %s; %d workers>" % (self.__class__.__name__, self.locationStr, self.pool.num_workers()) def loop(self, loopCondition=lambda: True): log.debug("threadpool server requestloop") while (self.sock is not None) and not self.shutting_down and loopCondition(): try: self.events([self.sock]) except (socket.error, OSError) as x: if not loopCondition(): # swallow the socket error if loop terminates anyway # this can occur if we are asked to shutdown, socket can be invalid then break # socket errors may not lead to a server abort, so we log it and continue err = getattr(x, "errno", x.args[0]) log.warning("socket error '%s' with errno=%d, shouldn't happen", x, err) continue except KeyboardInterrupt: log.debug("stopping on break signal") break def combine_loop(self, server): raise TypeError("You can't use the loop combiner on the threadpool server type") def events(self, eventsockets): """used for external event loops: handle events that occur on one of the sockets of this server""" # we only react on events on our own server socket. # all other (client) sockets are owned by their individual threads. assert self.sock in eventsockets try: if self._selector: events = self._selector.select(config.POLLTIMEOUT) if not events: return csock, caddr = self.sock.accept() if self.shutting_down: csock.close() return if hasattr(csock, "getpeercert"): log.debug("connected %s - SSL", caddr) else: log.debug("connected %s - unencrypted", caddr) if config.COMMTIMEOUT: csock.settimeout(config.COMMTIMEOUT) job = ClientConnectionJob(csock, caddr, self.daemon) try: self.pool.process(job) except NoFreeWorkersError: job.denyConnection("no free workers, increase server threadpool size") except socket.timeout: pass # just continue the loop on a timeout on accept def shutdown(self): self.shutting_down = True self.wakeup() time.sleep(0.05) self.close() self.sock = None def close(self): if self.housekeeper: self.housekeeper.stop.set() self.housekeeper.join() self.housekeeper = None if self.sock: sockname = None try: sockname = self.sock.getsockname() except (socket.error, OSError): pass try: self.sock.close() if type(sockname) is str: # it was a Unix domain socket, remove it from the filesystem if os.path.exists(sockname): os.remove(sockname) except Exception: pass self.sock = None self.pool.close() @property def sockets(self): # the server socket is all we care about, all client sockets are running in their own threads return [self.sock] @property def selector(self): raise TypeError("threadpool server doesn't have multiplexing selector") def wakeup(self): socketutil.interruptSocket(self._socketaddr) Pyro4-4.82/src/Pyro4/socketutil.py000066400000000000000000000565601416147301300170440ustar00rootroot00000000000000""" Low level socket utilities. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import os import socket import errno import time import sys import select import weakref try: import ssl except ImportError: ssl = None from Pyro4.configuration import config from Pyro4.errors import CommunicationError, TimeoutError, ConnectionClosedError try: InterruptedError() # new since Python 3.4 except NameError: class InterruptedError(Exception): pass # Note: other interesting errnos are EPERM, ENOBUFS, EMFILE # but it seems to me that all these signify an unrecoverable situation. # So I didn't include them in the list of retryable errors. ERRNO_RETRIES = [errno.EINTR, errno.EAGAIN, errno.EWOULDBLOCK, errno.EINPROGRESS] if hasattr(errno, "WSAEINTR"): ERRNO_RETRIES.append(errno.WSAEINTR) if hasattr(errno, "WSAEWOULDBLOCK"): ERRNO_RETRIES.append(errno.WSAEWOULDBLOCK) if hasattr(errno, "WSAEINPROGRESS"): ERRNO_RETRIES.append(errno.WSAEINPROGRESS) ERRNO_BADF = [errno.EBADF] if hasattr(errno, "WSAEBADF"): ERRNO_BADF.append(errno.WSAEBADF) ERRNO_ENOTSOCK = [errno.ENOTSOCK] if hasattr(errno, "WSAENOTSOCK"): ERRNO_ENOTSOCK.append(errno.WSAENOTSOCK) if not hasattr(socket, "SOL_TCP"): socket.SOL_TCP = socket.IPPROTO_TCP ERRNO_EADDRNOTAVAIL = [errno.EADDRNOTAVAIL] if hasattr(errno, "WSAEADDRNOTAVAIL"): ERRNO_EADDRNOTAVAIL.append(errno.WSAEADDRNOTAVAIL) ERRNO_EADDRINUSE = [errno.EADDRINUSE] if hasattr(errno, "WSAEADDRINUSE"): ERRNO_EADDRINUSE.append(errno.WSAEADDRINUSE) if sys.version_info >= (3, 0): basestring = str def getIpVersion(hostnameOrAddress): """ Determine what the IP version is of the given hostname or ip address (4 or 6). First, it resolves the hostname or address to get an IP address. Then, if the resolved IP contains a ':' it is considered to be an ipv6 address, and if it contains a '.', it is ipv4. """ address = getIpAddress(hostnameOrAddress) if "." in address: return 4 elif ":" in address: return 6 else: raise CommunicationError("Unknown IP address format" + address) def getIpAddress(hostname, workaround127=False, ipVersion=None): """ Returns the IP address for the given host. If you enable the workaround, it will use a little hack if the ip address is found to be the loopback address. The hack tries to discover an externally visible ip address instead (this only works for ipv4 addresses). Set ipVersion=6 to return ipv6 addresses, 4 to return ipv4, 0 to let OS choose the best one or None to use config.PREFER_IP_VERSION. """ def getaddr(ipVersion): if ipVersion == 6: family = socket.AF_INET6 elif ipVersion == 4: family = socket.AF_INET elif ipVersion == 0: family = socket.AF_UNSPEC else: raise ValueError("unknown value for argument ipVersion.") ip = socket.getaddrinfo(hostname or socket.gethostname(), 80, family, socket.SOCK_STREAM, socket.SOL_TCP)[0][4][0] if workaround127 and (ip.startswith("127.") or ip == "0.0.0.0"): ip = getInterfaceAddress("4.2.2.2") return ip try: if hostname and ':' in hostname and ipVersion is None: ipVersion = 0 return getaddr(config.PREFER_IP_VERSION) if ipVersion is None else getaddr(ipVersion) except socket.gaierror: if ipVersion == 6 or (ipVersion is None and config.PREFER_IP_VERSION == 6): raise socket.error("unable to determine IPV6 address") return getaddr(0) def getInterfaceAddress(ip_address): """tries to find the ip address of the interface that connects to the given host's address""" family = socket.AF_INET if getIpVersion(ip_address) == 4 else socket.AF_INET6 sock = socket.socket(family, socket.SOCK_DGRAM) try: sock.connect((ip_address, 53)) # 53=dns return sock.getsockname()[0] finally: sock.close() def __nextRetrydelay(delay): # first try a few very short delays, # if that doesn't work, increase by 0.1 sec every time if delay == 0.0: return 0.001 if delay == 0.001: return 0.01 return delay + 0.1 def receiveData(sock, size): """Retrieve a given number of bytes from a socket. It is expected the socket is able to supply that number of bytes. If it isn't, an exception is raised (you will not get a zero length result or a result that is smaller than what you asked for). The partial data that has been received however is stored in the 'partialData' attribute of the exception object.""" try: retrydelay = 0.0 msglen = 0 chunks = [] if config.USE_MSG_WAITALL and not hasattr(sock, "getpeercert"): # waitall is very convenient and if a socket error occurs, # we can assume the receive has failed. No need for a loop, # unless it is a retryable error. # Some systems have an erratic MSG_WAITALL and sometimes still return # less bytes than asked. In that case, we drop down into the normal # receive loop to finish the task. # Also note that on SSL sockets, you cannot use MSG_WAITALL (or any other flag) while True: try: data = sock.recv(size, socket.MSG_WAITALL) if len(data) == size: return data # less data than asked, drop down into normal receive loop to finish msglen = len(data) chunks = [data] break except socket.timeout: raise TimeoutError("receiving: timeout") except socket.error as x: err = getattr(x, "errno", x.args[0]) if err not in ERRNO_RETRIES: raise ConnectionClosedError("receiving: connection lost: " + str(x)) time.sleep(0.00001 + retrydelay) # a slight delay to wait before retrying retrydelay = __nextRetrydelay(retrydelay) # old fashioned recv loop, we gather chunks until the message is complete while True: try: while msglen < size: # 60k buffer limit avoids problems on certain OSes like VMS, Windows chunk = sock.recv(min(60000, size - msglen)) if not chunk: break chunks.append(chunk) msglen += len(chunk) data = b"".join(chunks) del chunks if len(data) != size: err = ConnectionClosedError("receiving: not enough data") err.partialData = data # store the message that was received until now raise err return data # yay, complete except socket.timeout: raise TimeoutError("receiving: timeout") except socket.error: x = sys.exc_info()[1] err = getattr(x, "errno", x.args[0]) if err not in ERRNO_RETRIES: raise ConnectionClosedError("receiving: connection lost: " + str(x)) time.sleep(0.00001 + retrydelay) # a slight delay to wait before retrying retrydelay = __nextRetrydelay(retrydelay) except socket.timeout: raise TimeoutError("receiving: timeout") def sendData(sock, data): """ Send some data over a socket. Some systems have problems with ``sendall()`` when the socket is in non-blocking mode. For instance, Mac OS X seems to be happy to throw EAGAIN errors too often. This function falls back to using a regular send loop if needed. """ if sock.gettimeout() is None: # socket is in blocking mode, we can use sendall normally. try: sock.sendall(data) return except socket.timeout: raise TimeoutError("sending: timeout") except socket.error as x: raise ConnectionClosedError("sending: connection lost: " + str(x)) else: # Socket is in non-blocking mode, use regular send loop. retrydelay = 0.0 while data: try: sent = sock.send(data) data = data[sent:] except socket.timeout: raise TimeoutError("sending: timeout") except socket.error as x: err = getattr(x, "errno", x.args[0]) if err not in ERRNO_RETRIES: raise ConnectionClosedError("sending: connection lost: " + str(x)) time.sleep(0.00001 + retrydelay) # a slight delay to wait before retrying retrydelay = __nextRetrydelay(retrydelay) _GLOBAL_DEFAULT_TIMEOUT = object() def createSocket(bind=None, connect=None, reuseaddr=False, keepalive=True, timeout=_GLOBAL_DEFAULT_TIMEOUT, noinherit=False, ipv6=False, nodelay=True, sslContext=None): """ Create a socket. Default socket options are keepalive and IPv4 family, and nodelay (nagle disabled). If 'bind' or 'connect' is a string, it is assumed a Unix domain socket is requested. Otherwise, a normal tcp/ip socket is used. Set ipv6=True to create an IPv6 socket rather than IPv4. Set ipv6=None to use the PREFER_IP_VERSION config setting. """ if bind and connect: raise ValueError("bind and connect cannot both be specified at the same time") forceIPv6 = ipv6 or (ipv6 is None and config.PREFER_IP_VERSION == 6) if isinstance(bind, basestring) or isinstance(connect, basestring): family = socket.AF_UNIX elif not bind and not connect: family = socket.AF_INET6 if forceIPv6 else socket.AF_INET elif type(bind) is tuple: if not bind[0]: family = socket.AF_INET6 if forceIPv6 else socket.AF_INET else: if getIpVersion(bind[0]) == 4: if forceIPv6: raise ValueError("IPv4 address is used bind argument with forceIPv6 argument:" + bind[0] + ".") family = socket.AF_INET elif getIpVersion(bind[0]) == 6: family = socket.AF_INET6 # replace bind addresses by their ipv6 counterparts (4-tuple) bind = (bind[0], bind[1], 0, 0) else: raise ValueError("unknown bind format.") elif type(connect) is tuple: if not connect[0]: family = socket.AF_INET6 if forceIPv6 else socket.AF_INET else: if getIpVersion(connect[0]) == 4: if forceIPv6: raise ValueError("IPv4 address is used in connect argument with forceIPv6 argument:" + bind[0] + ".") family = socket.AF_INET elif getIpVersion(connect[0]) == 6: family = socket.AF_INET6 # replace connect addresses by their ipv6 counterparts (4-tuple) connect = (connect[0], connect[1], 0, 0) else: raise ValueError("unknown connect format.") else: raise ValueError("unknown bind or connect format.") sock = socket.socket(family, socket.SOCK_STREAM) if sslContext: if bind: sock = sslContext.wrap_socket(sock, server_side=True) elif connect: sock = sslContext.wrap_socket(sock, server_side=False, server_hostname=connect[0]) else: sock = sslContext.wrap_socket(sock, server_side=False) if nodelay: setNoDelay(sock) if reuseaddr: setReuseAddr(sock) if noinherit: setNoInherit(sock) if timeout == 0: timeout = None if timeout is not _GLOBAL_DEFAULT_TIMEOUT: sock.settimeout(timeout) if bind: if type(bind) is tuple and bind[1] == 0: bindOnUnusedPort(sock, bind[0]) else: sock.bind(bind) try: sock.listen(100) except (OSError, IOError): pass if connect: try: sock.connect(connect) except socket.error: # This can happen when the socket is in non-blocking mode (or has a timeout configured). # We check if it is a retryable errno (usually EINPROGRESS). # If so, we use select() to wait until the socket is in writable state, # essentially rebuilding a blocking connect() call. xv = sys.exc_info()[1] errno = getattr(xv, "errno", 0) if errno in ERRNO_RETRIES: if timeout is _GLOBAL_DEFAULT_TIMEOUT or timeout < 0.1: timeout = 0.1 while True: try: sr, sw, se = select.select([], [sock], [sock], timeout) except InterruptedError: continue if sock in sw: break # yay, writable now, connect() completed elif sock in se: sock.close() # close the socket that refused to connect raise socket.error("connect failed") else: sock.close() # close the socket that refused to connect raise if keepalive: setKeepalive(sock) return sock def createBroadcastSocket(bind=None, reuseaddr=False, timeout=_GLOBAL_DEFAULT_TIMEOUT, ipv6=False): """ Create a udp broadcast socket. Set ipv6=True to create an IPv6 socket rather than IPv4. Set ipv6=None to use the PREFER_IP_VERSION config setting. """ forceIPv6 = ipv6 or (ipv6 is None and config.PREFER_IP_VERSION == 6) if not bind: family = socket.AF_INET6 if forceIPv6 else socket.AF_INET elif type(bind) is tuple: if not bind[0]: family = socket.AF_INET6 if forceIPv6 else socket.AF_INET else: if getIpVersion(bind[0]) == 4: if forceIPv6: raise ValueError("IPv4 address is used with forceIPv6 option:" + bind[0] + ".") family = socket.AF_INET elif getIpVersion(bind[0]) == 6: family = socket.AF_INET6 bind = (bind[0], bind[1], 0, 0) else: raise ValueError("unknown bind format: %r" % (bind,)) else: raise ValueError("unknown bind format: %r" % (bind,)) sock = socket.socket(family, socket.SOCK_DGRAM) if family == socket.AF_INET: sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) if reuseaddr: setReuseAddr(sock) if timeout is None: sock.settimeout(None) else: if timeout is not _GLOBAL_DEFAULT_TIMEOUT: sock.settimeout(timeout) if bind: host = bind[0] or "" port = bind[1] if port == 0: bindOnUnusedPort(sock, host) else: if len(bind) == 2: sock.bind((host, port)) # ipv4 elif len(bind) == 4: sock.bind((host, port, 0, 0)) # ipv6 else: raise ValueError("bind must be None, 2-tuple or 4-tuple") return sock def setReuseAddr(sock): """sets the SO_REUSEADDR option on the socket, if possible.""" try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) except Exception: pass def setNoDelay(sock): """sets the TCP_NODELAY option on the socket (to disable Nagle's algorithm), if possible.""" try: sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) except Exception: pass def setKeepalive(sock): """sets the SO_KEEPALIVE option on the socket, if possible.""" try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) except Exception: pass try: import fcntl def setNoInherit(sock): """Mark the given socket fd as non-inheritable to child processes""" fd = sock.fileno() flags = fcntl.fcntl(fd, fcntl.F_GETFD) fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) except ImportError: # no fcntl available, try the windows version try: if sys.platform == "cli": raise NotImplementedError("IronPython can't obtain a proper HANDLE from a socket") from ctypes import windll, WinError, wintypes # help ctypes to set the proper args for this kernel32 call on 64-bit pythons _SetHandleInformation = windll.kernel32.SetHandleInformation _SetHandleInformation.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.DWORD] _SetHandleInformation.restype = wintypes.BOOL # don't need this, but might as well def setNoInherit(sock): """Mark the given socket fd as non-inheritable to child processes""" if not _SetHandleInformation(sock.fileno(), 1, 0): raise WinError() except (ImportError, NotImplementedError): # nothing available, define a dummy function def setNoInherit(sock): """Mark the given socket fd as non-inheritable to child processes (dummy)""" pass class SocketConnection(object): """A wrapper class for plain sockets, containing various methods such as :meth:`send` and :meth:`recv`""" def __init__(self, sock, objectId=None, keep_open=False): self.sock = sock self.objectId = objectId self.pyroInstances = {} # pyro objects for instance_mode=session self.tracked_resources = weakref.WeakSet() # weakrefs to resources for this connection self.keep_open = keep_open def __del__(self): self.close() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def send(self, data): sendData(self.sock, data) def recv(self, size): return receiveData(self.sock, size) def close(self): if self.keep_open: return try: self.sock.shutdown(socket.SHUT_RDWR) except: pass try: self.sock.close() except: pass self.pyroInstances.clear() # release the session instances for rsc in self.tracked_resources: try: rsc.close() # it is assumed a 'resource' has a close method. except Exception: pass self.tracked_resources.clear() def fileno(self): return self.sock.fileno() def family(self): return family_str(self.sock) def setTimeout(self, timeout): self.sock.settimeout(timeout) def getTimeout(self): return self.sock.gettimeout() def getpeercert(self): try: return self.sock.getpeercert() except AttributeError: return None timeout = property(getTimeout, setTimeout) def family_str(sock): f = sock.family if f == socket.AF_INET: return "IPv4" if f == socket.AF_INET6: return "IPv6" if hasattr(socket, "AF_UNIX") and f == socket.AF_UNIX: return "Unix" return "???" def findProbablyUnusedPort(family=socket.AF_INET, socktype=socket.SOCK_STREAM): """Returns an unused port that should be suitable for binding (likely, but not guaranteed). This code is copied from the stdlib's test.test_support module.""" tempsock = socket.socket(family, socktype) try: port = bindOnUnusedPort(tempsock) if sys.platform == "cli": return port + 1 # the actual port is somehow still in use by the socket when using IronPython return port finally: tempsock.close() def bindOnUnusedPort(sock, host='localhost'): """Bind the socket to a free port and return the port number. This code is based on the code in the stdlib's test.test_support module.""" if sock.family in (socket.AF_INET, socket.AF_INET6) and sock.type == socket.SOCK_STREAM: if hasattr(socket, "SO_EXCLUSIVEADDRUSE"): try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) except socket.error: pass if sock.family == socket.AF_INET: if host == 'localhost': sock.bind(('127.0.0.1', 0)) else: sock.bind((host, 0)) elif sock.family == socket.AF_INET6: if host == 'localhost': sock.bind(('::1', 0, 0, 0)) else: sock.bind((host, 0, 0, 0)) else: raise CommunicationError("unsupported socket family: " + sock.family) return sock.getsockname()[1] def interruptSocket(address): """bit of a hack to trigger a blocking server to get out of the loop, useful at clean shutdowns""" try: sock = createSocket(connect=address, keepalive=False, timeout=None) try: sock.sendall(b"!" * 16) except (socket.error, AttributeError): pass try: sock.shutdown(socket.SHUT_RDWR) except (OSError, socket.error): pass sock.close() except socket.error: pass __ssl_server_context = None __ssl_client_context = None def getSSLcontext(servercert="", serverkey="", clientcert="", clientkey="", cacerts="", keypassword=""): """creates an SSL context and caches it, so you have to set the parameters correctly before doing anything""" global __ssl_client_context, __ssl_server_context if not ssl: raise ValueError("SSL requested but ssl module is not available") else: # Theoretically, the SSL support works on python versions older than the ones checked below. # however, a few important security changes were included in these versions # (disabling vulnerable cyphers and protocols by default). So change this at your own peril. if sys.version_info < (2, 7, 11): raise RuntimeError("need Python 2.7.11 or newer to properly use SSL") if servercert: if clientcert: raise ValueError("can't have both server cert and client cert") # server context if __ssl_server_context: return __ssl_server_context if not os.path.isfile(servercert): raise IOError("server cert file not found") if serverkey and not os.path.isfile(serverkey): raise IOError("server key file not found") __ssl_server_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) __ssl_server_context.load_cert_chain(servercert, serverkey or None, keypassword or None) if cacerts: if os.path.isdir(cacerts): __ssl_server_context.load_verify_locations(capath=cacerts) else: __ssl_server_context.load_verify_locations(cafile=cacerts) if config.SSL_REQUIRECLIENTCERT: __ssl_server_context.verify_mode = ssl.CERT_REQUIRED # 2-way ssl, server+client certs else: __ssl_server_context.verify_mode = ssl.CERT_NONE # 1-way ssl, server cert only return __ssl_server_context else: # client context if __ssl_client_context: return __ssl_client_context __ssl_client_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) if clientcert: if not os.path.isfile(clientcert): raise IOError("client cert file not found") __ssl_client_context.load_cert_chain(clientcert, clientkey or None, keypassword or None) if cacerts: if os.path.isdir(cacerts): __ssl_client_context.load_verify_locations(capath=cacerts) else: __ssl_client_context.load_verify_locations(cafile=cacerts) return __ssl_client_context Pyro4-4.82/src/Pyro4/test/000077500000000000000000000000001416147301300152475ustar00rootroot00000000000000Pyro4-4.82/src/Pyro4/test/__init__.py000066400000000000000000000000371416147301300173600ustar00rootroot00000000000000# just to make this a package. Pyro4-4.82/src/Pyro4/test/echoserver.py000066400000000000000000000166111416147301300177730ustar00rootroot00000000000000""" Echo server for test purposes. This is usually invoked by starting this module as a script: :command:`python -m Pyro4.test.echoserver` or simply: :command:`pyro4-test-echoserver` It is also possible to use the :class:`EchoServer` in user code but that is not terribly useful. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import sys import os import time import warnings import threading from optparse import OptionParser from Pyro4 import core, naming from Pyro4.configuration import config __all__ = ["EchoServer"] @core.expose class EchoServer(object): """ The echo server object that is provided as a Pyro object by this module. If its :attr:`verbose` attribute is set to ``True``, it will print messages as it receives calls. """ _verbose = False _must_shutdown = False def echo(self, message): """return the message""" if self._verbose: message_str = repr(message).encode(sys.stdout.encoding, errors="replace").decode(sys.stdout.encoding) print("%s - echo: %s" % (time.asctime(), message_str)) return message def error(self): """generates a simple exception without text""" if self._verbose: print("%s - error: generating exception" % time.asctime()) raise ValueError("expected error from echoserver error() method") def error_with_text(self): """generates a simple exception with message""" if self._verbose: print("%s - error: generating exception" % time.asctime()) raise ValueError("the message of the error") @core.oneway def oneway_echo(self, message): """just like echo, but oneway; the client won't wait for response""" if self._verbose: message_str = repr(message).encode(sys.stdout.encoding, errors="replace").decode(sys.stdout.encoding) print("%s - oneway_echo: %s" % (time.asctime(), message_str)) return "bogus return value" def slow(self): """returns (and prints) a message after a certain delay""" if self._verbose: print("%s - slow: waiting a bit..." % time.asctime()) time.sleep(5) if self._verbose: print("%s - slow: returning result" % time.asctime()) return "Finally, an answer!" def generator(self): """a generator function that returns some elements on demand""" yield "one" yield "two" yield "three" def nan(self): return float("nan") def inf(self): return float("inf") @core.oneway def oneway_slow(self): """prints a message after a certain delay, and returns; but the client won't wait for it""" if self._verbose: print("%s - oneway_slow: waiting a bit..." % time.asctime()) time.sleep(5) if self._verbose: print("%s - oneway_slow: returning result" % time.asctime()) return "bogus return value" def _private(self): """a 'private' method that should not be accessible""" return "should not be allowed" def __private(self): """another 'private' method that should not be accessible""" return "should not be allowed" def __dunder__(self): """a double underscore method that should be accessible normally""" return "should be allowed (dunder)" def shutdown(self): """called to signal the echo server to shut down""" if self._verbose: print("%s - shutting down" % time.asctime()) self._must_shutdown = True @property def verbose(self): return self._verbose @verbose.setter def verbose(self, onoff): self._verbose = bool(onoff) class NameServer(threading.Thread): def __init__(self, hostname, hmac=None): super(NameServer, self).__init__() self.setDaemon(1) self.hostname = hostname self.hmac = hmac self.started = threading.Event() def run(self): self.uri, self.ns_daemon, self.bc_server = naming.startNS(self.hostname, hmac=self.hmac) self.started.set() if self.bc_server: self.bc_server.runInThread() self.ns_daemon.requestLoop() def startNameServer(host, hmac=None): ns = NameServer(host, hmac=hmac) ns.start() ns.started.wait() return ns def main(args=None, returnWithoutLooping=False): parser = OptionParser() parser.add_option("-H", "--host", default="localhost", help="hostname to bind server on (default=%default)") parser.add_option("-p", "--port", type="int", default=0, help="port to bind server on") parser.add_option("-u", "--unixsocket", help="Unix domain socket name to bind server on") parser.add_option("-n", "--naming", action="store_true", default=False, help="register with nameserver") parser.add_option("-N", "--nameserver", action="store_true", default=False, help="also start a nameserver") parser.add_option("-v", "--verbose", action="store_true", default=False, help="verbose output") parser.add_option("-q", "--quiet", action="store_true", default=False, help="don't output anything") parser.add_option("-k", "--key", help="the HMAC key to use (deprecated)") options, args = parser.parse_args(args) if options.key: warnings.warn("using -k to supply HMAC key on the command line is a security problem " "and is deprecated since Pyro 4.72. See the documentation for an alternative.") if "PYRO_HMAC_KEY" in os.environ: if options.key: raise SystemExit("error: don't use -k and PYRO_HMAC_KEY at the same time") options.key = os.environ["PYRO_HMAC_KEY"] if options.verbose: options.quiet = False if not options.quiet: print("Starting Pyro's built-in test echo server.") config.SERVERTYPE = "multiplex" hmac = (options.key or "").encode("utf-8") if not hmac and not options.quiet: print("Warning: HMAC key not set. Anyone can connect to this server!") nameserver = None if options.nameserver: options.naming = True nameserver = startNameServer(options.host, hmac=hmac) d = core.Daemon(host=options.host, port=options.port, unixsocket=options.unixsocket) if hmac: d._pyroHmacKey = hmac echo = EchoServer() echo._verbose = options.verbose objectName = "test.echoserver" uri = d.register(echo, objectName) if options.naming: host, port = None, None if nameserver is not None: host, port = nameserver.uri.host, nameserver.uri.port ns = naming.locateNS(host, port, hmac_key=hmac) ns.register(objectName, uri) if options.verbose: print("using name server at %s" % ns._pyroUri) if nameserver is not None: if nameserver.bc_server: print("broadcast server running at %s" % nameserver.bc_server.locationStr) else: print("not using a broadcast server") else: if options.verbose: print("not using a name server.") if not options.quiet: print("object name: %s" % objectName) print("echo uri: %s" % uri) print("echoserver running.") if returnWithoutLooping: return d, echo, uri # for unit testing else: d.requestLoop(loopCondition=lambda: not echo._must_shutdown) d.close() if __name__ == "__main__": main() Pyro4-4.82/src/Pyro4/util.py000066400000000000000000001201141416147301300156160ustar00rootroot00000000000000""" Miscellaneous utilities, and serializers. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import array import sys import zlib import uuid import logging import linecache import traceback import inspect import struct import datetime import decimal import numbers from Pyro4 import errors from Pyro4.configuration import config try: import copyreg except ImportError: import copy_reg as copyreg log = logging.getLogger("Pyro4.util") def getPyroTraceback(ex_type=None, ex_value=None, ex_tb=None): """Returns a list of strings that form the traceback information of a Pyro exception. Any remote Pyro exception information is included. Traceback information is automatically obtained via ``sys.exc_info()`` if you do not supply the objects yourself.""" def formatRemoteTraceback(remote_tb_lines): result = [" +--- This exception occured remotely (Pyro) - Remote traceback:"] for line in remote_tb_lines: if line.endswith("\n"): line = line[:-1] lines = line.split("\n") for line2 in lines: result.append("\n | ") result.append(line2) result.append("\n +--- End of remote traceback\n") return result try: if ex_type is not None and ex_value is None and ex_tb is None: # possible old (3.x) call syntax where caller is only providing exception object if type(ex_type) is not type: raise TypeError("invalid argument: ex_type should be an exception type, or just supply no arguments at all") if ex_type is None and ex_tb is None: ex_type, ex_value, ex_tb = sys.exc_info() remote_tb = getattr(ex_value, "_pyroTraceback", None) local_tb = formatTraceback(ex_type, ex_value, ex_tb, config.DETAILED_TRACEBACK) if remote_tb: remote_tb = formatRemoteTraceback(remote_tb) return local_tb + remote_tb else: # hmm. no remote tb info, return just the local tb. return local_tb finally: # clean up cycle to traceback, to allow proper GC del ex_type, ex_value, ex_tb def formatTraceback(ex_type=None, ex_value=None, ex_tb=None, detailed=False): """Formats an exception traceback. If you ask for detailed formatting, the result will contain info on the variables in each stack frame. You don't have to provide the exception info objects, if you omit them, this function will obtain them itself using ``sys.exc_info()``.""" if ex_type is not None and ex_value is None and ex_tb is None: # possible old (3.x) call syntax where caller is only providing exception object if type(ex_type) is not type: raise TypeError("invalid argument: ex_type should be an exception type, or just supply no arguments at all") if ex_type is None and ex_tb is None: ex_type, ex_value, ex_tb = sys.exc_info() if detailed and sys.platform != "cli": # detailed tracebacks don't work in ironpython (most of the local vars are omitted) def makeStrValue(value): try: return repr(value) except: try: return str(value) except: return "" try: result = ["-" * 52 + "\n"] result.append(" EXCEPTION %s: %s\n" % (ex_type, ex_value)) result.append(" Extended stacktrace follows (most recent call last)\n") skipLocals = True # don't print the locals of the very first stack frame while ex_tb: frame = ex_tb.tb_frame sourceFileName = frame.f_code.co_filename if "self" in frame.f_locals: location = "%s.%s" % (frame.f_locals["self"].__class__.__name__, frame.f_code.co_name) else: location = frame.f_code.co_name result.append("-" * 52 + "\n") result.append("File \"%s\", line %d, in %s\n" % (sourceFileName, ex_tb.tb_lineno, location)) result.append("Source code:\n") result.append(" " + linecache.getline(sourceFileName, ex_tb.tb_lineno).strip() + "\n") if not skipLocals: names = set() names.update(getattr(frame.f_code, "co_varnames", ())) names.update(getattr(frame.f_code, "co_names", ())) names.update(getattr(frame.f_code, "co_cellvars", ())) names.update(getattr(frame.f_code, "co_freevars", ())) result.append("Local values:\n") for name2 in sorted(names): if name2 in frame.f_locals: value = frame.f_locals[name2] result.append(" %s = %s\n" % (name2, makeStrValue(value))) if name2 == "self": # print the local variables of the class instance for name3, value in vars(value).items(): result.append(" self.%s = %s\n" % (name3, makeStrValue(value))) skipLocals = False ex_tb = ex_tb.tb_next result.append("-" * 52 + "\n") result.append(" EXCEPTION %s: %s\n" % (ex_type, ex_value)) result.append("-" * 52 + "\n") return result except Exception: return ["-" * 52 + "\nError building extended traceback!!! :\n", "".join(traceback.format_exception(*sys.exc_info())) + '-' * 52 + '\n', "Original Exception follows:\n", "".join(traceback.format_exception(ex_type, ex_value, ex_tb))] else: # default traceback format. return traceback.format_exception(ex_type, ex_value, ex_tb) all_exceptions = {} if sys.version_info < (3, 0): import exceptions for name, t in vars(exceptions).items(): if type(t) is type and issubclass(t, BaseException): all_exceptions[name] = t else: import builtins for name, t in vars(builtins).items(): if type(t) is type and issubclass(t, BaseException): all_exceptions[name] = t buffer = bytearray for name, t in vars(errors).items(): if type(t) is type and issubclass(t, errors.PyroError): all_exceptions[name] = t class SerializerBase(object): """Base class for (de)serializer implementations (which must be thread safe)""" __custom_class_to_dict_registry = {} __custom_dict_to_class_registry = {} def serializeData(self, data, compress=False): """Serialize the given data object, try to compress if told so. Returns a tuple of the serialized data (bytes) and a bool indicating if it is compressed or not.""" data = self.dumps(data) return self.__compressdata(data, compress) def deserializeData(self, data, compressed=False): """Deserializes the given data (bytes). Set compressed to True to decompress the data first.""" if compressed: if sys.version_info < (3, 0): data = self._convertToBytes(data) data = zlib.decompress(data) return self.loads(data) def serializeCall(self, obj, method, vargs, kwargs, compress=False): """Serialize the given method call parameters, try to compress if told so. Returns a tuple of the serialized data and a bool indicating if it is compressed or not.""" data = self.dumpsCall(obj, method, vargs, kwargs) return self.__compressdata(data, compress) def deserializeCall(self, data, compressed=False): """Deserializes the given call data back to (object, method, vargs, kwargs) tuple. Set compressed to True to decompress the data first.""" if compressed: if sys.version_info < (3, 0): data = self._convertToBytes(data) data = zlib.decompress(data) return self.loadsCall(data) def loads(self, data): raise NotImplementedError("implement in subclass") def loadsCall(self, data): raise NotImplementedError("implement in subclass") def dumps(self, data): raise NotImplementedError("implement in subclass") def dumpsCall(self, obj, method, vargs, kwargs): raise NotImplementedError("implement in subclass") def _convertToBytes(self, data): t = type(data) if t is not bytes: if t in (bytearray, buffer): return bytes(data) if t is memoryview: return data.tobytes() return data def __compressdata(self, data, compress): if not compress or len(data) < 200: return data, False # don't waste time compressing small messages compressed = zlib.compress(data) if len(compressed) < len(data): return compressed, True return data, False @classmethod def register_type_replacement(cls, object_type, replacement_function): raise NotImplementedError("implement in subclass") @classmethod def register_class_to_dict(cls, clazz, converter, serpent_too=True): """Registers a custom function that returns a dict representation of objects of the given class. The function is called with a single parameter; the object to be converted to a dict.""" cls.__custom_class_to_dict_registry[clazz] = converter if serpent_too: try: get_serializer_by_id(SerpentSerializer.serializer_id) import serpent # @todo not needed? def serpent_converter(obj, serializer, stream, level): d = converter(obj) serializer.ser_builtins_dict(d, stream, level) serpent.register_class(clazz, serpent_converter) except errors.ProtocolError: pass @classmethod def unregister_class_to_dict(cls, clazz): """Removes the to-dict conversion function registered for the given class. Objects of the class will be serialized by the default mechanism again.""" if clazz in cls.__custom_class_to_dict_registry: del cls.__custom_class_to_dict_registry[clazz] try: get_serializer_by_id(SerpentSerializer.serializer_id) import serpent # @todo not needed? serpent.unregister_class(clazz) except errors.ProtocolError: pass @classmethod def register_dict_to_class(cls, classname, converter): """ Registers a custom converter function that creates objects from a dict with the given classname tag in it. The function is called with two parameters: the classname and the dictionary to convert to an instance of the class. This mechanism is not used for the pickle serializer. """ cls.__custom_dict_to_class_registry[classname] = converter @classmethod def unregister_dict_to_class(cls, classname): """ Removes the converter registered for the given classname. Dicts with that classname tag will be deserialized by the default mechanism again. This mechanism is not used for the pickle serializer. """ if classname in cls.__custom_dict_to_class_registry: del cls.__custom_dict_to_class_registry[classname] @classmethod def class_to_dict(cls, obj): """ Convert a non-serializable object to a dict. Partly borrowed from serpent. Not used for the pickle serializer. """ for clazz in cls.__custom_class_to_dict_registry: if isinstance(obj, clazz): return cls.__custom_class_to_dict_registry[clazz](obj) if type(obj) in (set, dict, tuple, list): # we use a ValueError to mirror the exception type returned by serpent and other serializers raise ValueError("can't serialize type " + str(obj.__class__) + " into a dict") if hasattr(obj, "_pyroDaemon"): obj._pyroDaemon = None if isinstance(obj, BaseException): # special case for exceptions return { "__class__": obj.__class__.__module__ + "." + obj.__class__.__name__, "__exception__": True, "args": obj.args, "attributes": vars(obj) # add custom exception attributes } try: value = obj.__getstate__() except AttributeError: pass else: if isinstance(value, dict): return value try: value = dict(vars(obj)) # make sure we can serialize anything that resembles a dict value["__class__"] = obj.__class__.__module__ + "." + obj.__class__.__name__ return value except TypeError: if hasattr(obj, "__slots__"): # use the __slots__ instead of the vars dict value = {} for slot in obj.__slots__: value[slot] = getattr(obj, slot) value["__class__"] = obj.__class__.__module__ + "." + obj.__class__.__name__ return value else: raise errors.SerializeError("don't know how to serialize class " + str(obj.__class__) + " using serializer " + str(cls.__name__) + ". Give it vars() or an appropriate __getstate__") @classmethod def dict_to_class(cls, data): """ Recreate an object out of a dict containing the class name and the attributes. Only a fixed set of classes are recognized. Not used for the pickle serializer. """ from Pyro4 import core, futures # XXX circular classname = data.get("__class__", "") if isinstance(classname, bytes): classname = classname.decode("utf-8") if classname in cls.__custom_dict_to_class_registry: converter = cls.__custom_dict_to_class_registry[classname] return converter(classname, data) if "__" in classname: raise errors.SecurityError("refused to deserialize types with double underscores in their name: " + classname) # for performance, the constructors below are hardcoded here instead of added on a per-class basis to the dict-to-class registry if classname.startswith("Pyro4.core."): if classname == "Pyro4.core.URI": uri = core.URI.__new__(core.URI) uri.__setstate_from_dict__(data["state"]) return uri elif classname == "Pyro4.core.Proxy": proxy = core.Proxy.__new__(core.Proxy) proxy.__setstate_from_dict__(data["state"]) return proxy elif classname == "Pyro4.core.Daemon": daemon = core.Daemon.__new__(core.Daemon) daemon.__setstate_from_dict__(data["state"]) return daemon elif classname.startswith("Pyro4.util."): if classname == "Pyro4.util.SerpentSerializer": return SerpentSerializer() elif classname == "Pyro4.util.PickleSerializer": return PickleSerializer() elif classname == "Pyro4.util.MarshalSerializer": return MarshalSerializer() elif classname == "Pyro4.util.JsonSerializer": return JsonSerializer() elif classname == "Pyro4.util.MsgpackSerializer": return MsgpackSerializer() elif classname == "Pyro4.util.CloudpickleSerializer": return CloudpickleSerializer() elif classname == "Pyro4.util.DillSerializer": return DillSerializer() elif classname.startswith("Pyro4.errors."): errortype = getattr(errors, classname.split('.', 2)[2]) if issubclass(errortype, errors.PyroError): return SerializerBase.make_exception(errortype, data) elif classname == "Pyro4.futures._ExceptionWrapper": ex = data["exception"] if isinstance(ex, dict) and "__class__" in ex: ex = SerializerBase.dict_to_class(ex) return futures._ExceptionWrapper(ex) elif data.get("__exception__", False): if classname in all_exceptions: return SerializerBase.make_exception(all_exceptions[classname], data) # python 2.x: exceptions.ValueError # python 3.x: builtins.ValueError # translate to the appropriate namespace... namespace, short_classname = classname.split('.', 1) if namespace in ("builtins", "exceptions"): if sys.version_info < (3, 0): exceptiontype = getattr(exceptions, short_classname) if issubclass(exceptiontype, BaseException): return SerializerBase.make_exception(exceptiontype, data) else: exceptiontype = getattr(builtins, short_classname) if issubclass(exceptiontype, BaseException): return SerializerBase.make_exception(exceptiontype, data) elif namespace == "sqlite3" and short_classname.endswith("Error"): import sqlite3 exceptiontype = getattr(sqlite3, short_classname) if issubclass(exceptiontype, BaseException): return SerializerBase.make_exception(exceptiontype, data) log.warning("unsupported serialized class: " + classname) raise errors.SerializeError("unsupported serialized class: " + classname) @staticmethod def make_exception(exceptiontype, data): ex = exceptiontype(*data["args"]) if "attributes" in data: # restore custom attributes on the exception object for attr, value in data["attributes"].items(): setattr(ex, attr, value) return ex def recreate_classes(self, literal): t = type(literal) if t is set: return {self.recreate_classes(x) for x in literal} if t is list: return [self.recreate_classes(x) for x in literal] if t is tuple: return tuple(self.recreate_classes(x) for x in literal) if t is dict: if "__class__" in literal: return self.dict_to_class(literal) result = {} for key, value in literal.items(): result[key] = self.recreate_classes(value) return result return literal def __eq__(self, other): """this equality method is only to support the unit tests of this class""" return isinstance(other, SerializerBase) and vars(self) == vars(other) def __ne__(self, other): return not self.__eq__(other) __hash__ = object.__hash__ class PickleSerializer(SerializerBase): """ A (de)serializer that wraps the Pickle serialization protocol. It can optionally compress the serialized data, and is thread safe. """ serializer_id = 4 # never change this def dumpsCall(self, obj, method, vargs, kwargs): return pickle.dumps((obj, method, vargs, kwargs), config.PICKLE_PROTOCOL_VERSION) def dumps(self, data): return pickle.dumps(data, config.PICKLE_PROTOCOL_VERSION) def loadsCall(self, data): data = self._convertToBytes(data) return pickle.loads(data) def loads(self, data): data = self._convertToBytes(data) return pickle.loads(data) @classmethod def register_type_replacement(cls, object_type, replacement_function): def copyreg_function(obj): return replacement_function(obj).__reduce__() if object_type is type or not inspect.isclass(object_type): raise ValueError("refusing to register replacement for a non-type or the type 'type' itself") try: copyreg.pickle(object_type, copyreg_function) except TypeError: pass class CloudpickleSerializer(SerializerBase): """ A (de)serializer that wraps the Cloudpickle serialization protocol. It can optionally compress the serialized data, and is thread safe. """ serializer_id = 7 # never change this def dumpsCall(self, obj, method, vargs, kwargs): return cloudpickle.dumps((obj, method, vargs, kwargs), config.PICKLE_PROTOCOL_VERSION) def dumps(self, data): return cloudpickle.dumps(data, config.PICKLE_PROTOCOL_VERSION) def loadsCall(self, data): return cloudpickle.loads(data) def loads(self, data): return cloudpickle.loads(data) @classmethod def register_type_replacement(cls, object_type, replacement_function): def copyreg_function(obj): return replacement_function(obj).__reduce__() if object_type is type or not inspect.isclass(object_type): raise ValueError("refusing to register replacement for a non-type or the type 'type' itself") try: copyreg.pickle(object_type, copyreg_function) except TypeError: pass class DillSerializer(SerializerBase): """ A (de)serializer that wraps the Dill serialization protocol. It can optionally compress the serialized data, and is thread safe. """ serializer_id = 5 # never change this def dumpsCall(self, obj, method, vargs, kwargs): return dill.dumps((obj, method, vargs, kwargs), config.DILL_PROTOCOL_VERSION) def dumps(self, data): return dill.dumps(data, config.DILL_PROTOCOL_VERSION) def loadsCall(self, data): return dill.loads(data) def loads(self, data): return dill.loads(data) @classmethod def register_type_replacement(cls, object_type, replacement_function): def copyreg_function(obj): return replacement_function(obj).__reduce__() if object_type is type or not inspect.isclass(object_type): raise ValueError("refusing to register replacement for a non-type or the type 'type' itself") try: copyreg.pickle(object_type, copyreg_function) except TypeError: pass class MarshalSerializer(SerializerBase): """(de)serializer that wraps the marshal serialization protocol.""" serializer_id = 3 # never change this def dumpsCall(self, obj, method, vargs, kwargs): vargs = [self.convert_obj_into_marshallable(value) for value in vargs] kwargs = {key: self.convert_obj_into_marshallable(value) for key, value in kwargs.items()} return marshal.dumps((obj, method, vargs, kwargs)) def dumps(self, data): return marshal.dumps(self.convert_obj_into_marshallable(data)) if sys.platform == "cli": def loadsCall(self, data): if type(data) is not str: # Ironpython's marshal expects str... data = str(data) obj, method, vargs, kwargs = marshal.loads(data) vargs = self.recreate_classes(vargs) kwargs = self.recreate_classes(kwargs) return obj, method, vargs, kwargs def loads(self, data): if type(data) is not str: # Ironpython's marshal expects str... data = str(data) return self.recreate_classes(marshal.loads(data)) else: def loadsCall(self, data): data = self._convertToBytes(data) obj, method, vargs, kwargs = marshal.loads(data) vargs = self.recreate_classes(vargs) kwargs = self.recreate_classes(kwargs) return obj, method, vargs, kwargs def loads(self, data): data = self._convertToBytes(data) return self.recreate_classes(marshal.loads(data)) marshalable_types = (str, int, float, type(None), bool, complex, bytes, bytearray, tuple, set, frozenset, list, dict) if sys.version_info < (3, 0): marshalable_types += (unicode,) def convert_obj_into_marshallable(self, obj): if isinstance(obj, self.marshalable_types): return obj if isinstance(obj, array.array): if obj.typecode == 'c': return obj.tostring() if obj.typecode == 'u': return obj.tounicode() return obj.tolist() return self.class_to_dict(obj) @classmethod def class_to_dict(cls, obj): if isinstance(obj, uuid.UUID): return str(obj) return super(MarshalSerializer, cls).class_to_dict(obj) @classmethod def register_type_replacement(cls, object_type, replacement_function): pass # marshal serializer doesn't support per-type hooks class SerpentSerializer(SerializerBase): """(de)serializer that wraps the serpent serialization protocol.""" serializer_id = 1 # never change this def dumpsCall(self, obj, method, vargs, kwargs): return serpent.dumps((obj, method, vargs, kwargs), module_in_classname=True) def dumps(self, data): return serpent.dumps(data, module_in_classname=True) def loadsCall(self, data): obj, method, vargs, kwargs = serpent.loads(data) vargs = self.recreate_classes(vargs) kwargs = self.recreate_classes(kwargs) return obj, method, vargs, kwargs def loads(self, data): return self.recreate_classes(serpent.loads(data)) @classmethod def register_type_replacement(cls, object_type, replacement_function): def custom_serializer(object, serpent_serializer, outputstream, indentlevel): replaced = replacement_function(object) if replaced is object: serpent_serializer.ser_default_class(replaced, outputstream, indentlevel) else: serpent_serializer._serialize(replaced, outputstream, indentlevel) if object_type is type or not inspect.isclass(object_type): raise ValueError("refusing to register replacement for a non-type or the type 'type' itself") serpent.register_class(object_type, custom_serializer) @classmethod def dict_to_class(cls, data): if data.get("__class__") == "float": return float(data["value"]) # serpent encodes a float nan as a special class dict like this return super(SerpentSerializer, cls).dict_to_class(data) class JsonSerializer(SerializerBase): """(de)serializer that wraps the json serialization protocol.""" serializer_id = 2 # never change this __type_replacements = {} def dumpsCall(self, obj, method, vargs, kwargs): data = {"object": obj, "method": method, "params": vargs, "kwargs": kwargs} data = json.dumps(data, ensure_ascii=False, default=self.default) return data.encode("utf-8") def dumps(self, data): data = json.dumps(data, ensure_ascii=False, default=self.default) return data.encode("utf-8") def loadsCall(self, data): data = self._convertToBytes(data).decode("utf-8") data = json.loads(data) vargs = self.recreate_classes(data["params"]) kwargs = self.recreate_classes(data["kwargs"]) return data["object"], data["method"], vargs, kwargs def loads(self, data): data = self._convertToBytes(data).decode("utf-8") return self.recreate_classes(json.loads(data)) def default(self, obj): replacer = self.__type_replacements.get(type(obj), None) if replacer: obj = replacer(obj) if isinstance(obj, set): return tuple(obj) # json module can't deal with sets so we make a tuple out of it if isinstance(obj, uuid.UUID): return str(obj) if isinstance(obj, (datetime.datetime, datetime.date)): return obj.isoformat() if isinstance(obj, decimal.Decimal): return str(obj) if isinstance(obj, array.array): if obj.typecode == 'c': return obj.tostring() if obj.typecode == 'u': return obj.tounicode() return obj.tolist() return self.class_to_dict(obj) @classmethod def register_type_replacement(cls, object_type, replacement_function): if object_type is type or not inspect.isclass(object_type): raise ValueError("refusing to register replacement for a non-type or the type 'type' itself") cls.__type_replacements[object_type] = replacement_function class MsgpackSerializer(SerializerBase): """(de)serializer that wraps the msgpack serialization protocol.""" serializer_id = 6 # never change this __type_replacements = {} def dumpsCall(self, obj, method, vargs, kwargs): return msgpack.packb((obj, method, vargs, kwargs), use_bin_type=True, default=self.default) def dumps(self, data): return msgpack.packb(data, use_bin_type=True, default=self.default) def loadsCall(self, data): data = self._convertToBytes(data) obj, method, vargs, kwargs = msgpack.unpackb(data, raw=False, object_hook=self.object_hook) return obj, method, vargs, kwargs def loads(self, data): data = self._convertToBytes(data) return msgpack.unpackb(data, raw=False, object_hook=self.object_hook, ext_hook=self.ext_hook) def default(self, obj): replacer = self.__type_replacements.get(type(obj), None) if replacer: obj = replacer(obj) if isinstance(obj, set): return tuple(obj) # msgpack module can't deal with sets so we make a tuple out of it if isinstance(obj, uuid.UUID): return str(obj) if isinstance(obj, bytearray): return bytes(obj) if isinstance(obj, complex): return msgpack.ExtType(0x30, struct.pack("dd", obj.real, obj.imag)) if isinstance(obj, datetime.datetime): if obj.tzinfo: raise errors.SerializeError("msgpack cannot serialize datetime with timezone info") return msgpack.ExtType(0x32, struct.pack("d", obj.timestamp())) if isinstance(obj, datetime.date): return msgpack.ExtType(0x33, struct.pack("l", obj.toordinal())) if isinstance(obj, decimal.Decimal): return str(obj) if isinstance(obj, numbers.Number): return msgpack.ExtType(0x31, str(obj).encode("ascii")) # long if isinstance(obj, array.array): if obj.typecode == 'c': return obj.tostring() if obj.typecode == 'u': return obj.tounicode() return obj.tolist() return self.class_to_dict(obj) def object_hook(self, obj): if "__class__" in obj: return self.dict_to_class(obj) return obj def ext_hook(self, code, data): if code == 0x30: real, imag = struct.unpack("dd", data) return complex(real, imag) if code == 0x31: return int(data) if code == 0x32: return datetime.datetime.fromtimestamp(struct.unpack("d", data)[0]) if code == 0x33: return datetime.date.fromordinal(struct.unpack("l", data)[0]) raise errors.SerializeError("invalid ext code for msgpack: " + str(code)) @classmethod def register_type_replacement(cls, object_type, replacement_function): if object_type is type or not inspect.isclass(object_type): raise ValueError("refusing to register replacement for a non-type or the type 'type' itself") cls.__type_replacements[object_type] = replacement_function """The various serializers that are supported""" _serializers = {} _serializers_by_id = {} def get_serializer(name): try: return _serializers[name] except KeyError: raise errors.SerializeError("serializer '%s' is unknown or not available" % name) def get_serializer_by_id(sid): try: return _serializers_by_id[sid] except KeyError: raise errors.SerializeError("no serializer available for id %d" % sid) # determine the serializers that are supported try: import cPickle as pickle except ImportError: import pickle assert config.PICKLE_PROTOCOL_VERSION >= 2, "pickle protocol needs to be 2 or higher" _ser = PickleSerializer() _serializers["pickle"] = _ser _serializers_by_id[_ser.serializer_id] = _ser import marshal _ser = MarshalSerializer() _serializers["marshal"] = _ser _serializers_by_id[_ser.serializer_id] = _ser try: import cloudpickle _ser = CloudpickleSerializer() _serializers["cloudpickle"] = _ser _serializers_by_id[_ser.serializer_id] = _ser except ImportError: pass try: import dill _ser = DillSerializer() _serializers["dill"] = _ser _serializers_by_id[_ser.serializer_id] = _ser except ImportError: pass try: try: import importlib json = importlib.import_module(config.JSON_MODULE) except ImportError: json = __import__(config.JSON_MODULE) _ser = JsonSerializer() _serializers["json"] = _ser _serializers_by_id[_ser.serializer_id] = _ser except ImportError: pass try: import serpent _ser = SerpentSerializer() _serializers["serpent"] = _ser _serializers_by_id[_ser.serializer_id] = _ser except ImportError: log.warning("serpent serializer is not available") try: import msgpack _ser = MsgpackSerializer() _serializers["msgpack"] = _ser _serializers_by_id[_ser.serializer_id] = _ser except ImportError: pass del _ser def getAttribute(obj, attr): """ Resolves an attribute name to an object. Raises an AttributeError if any attribute in the chain starts with a '``_``'. Doesn't resolve a dotted name, because that is a security vulnerability. It treats it as a single attribute name (and the lookup will likely fail). """ if is_private_attribute(attr): raise AttributeError("attempt to access private attribute '%s'" % attr) else: obj = getattr(obj, attr) if not config.REQUIRE_EXPOSE or getattr(obj, "_pyroExposed", False): return obj raise AttributeError("attempt to access unexposed attribute '%s'" % attr) def excepthook(ex_type, ex_value, ex_tb): """An exception hook you can use for ``sys.excepthook``, to automatically print remote Pyro tracebacks""" traceback = "".join(getPyroTraceback(ex_type, ex_value, ex_tb)) sys.stderr.write(traceback) def fixIronPythonExceptionForPickle(exceptionObject, addAttributes): """ Function to hack around a bug in IronPython where it doesn't pickle exception attributes. We piggyback them into the exception's args. Bug report is at https://github.com/IronLanguages/main/issues/943 Bug is still present in Ironpython 2.7.7 """ if hasattr(exceptionObject, "args"): if addAttributes: # piggyback the attributes on the exception args instead. ironpythonArgs = vars(exceptionObject) ironpythonArgs["__ironpythonargs__"] = True exceptionObject.args += (ironpythonArgs,) else: # check if there is a piggybacked object in the args # if there is, extract the exception attributes from it. if len(exceptionObject.args) > 0: piggyback = exceptionObject.args[-1] if type(piggyback) is dict and piggyback.get("__ironpythonargs__"): del piggyback["__ironpythonargs__"] exceptionObject.args = exceptionObject.args[:-1] exceptionObject.__dict__.update(piggyback) __exposed_member_cache = {} def reset_exposed_members(obj, only_exposed=True, as_lists=False): """Delete any cached exposed members forcing recalculation on next request""" if not inspect.isclass(obj): obj = obj.__class__ cache_key = (obj, only_exposed, as_lists) __exposed_member_cache.pop(cache_key, None) def get_exposed_members(obj, only_exposed=True, as_lists=False, use_cache=True): """ Return public and exposed members of the given object's class. You can also provide a class directly. Private members are ignored no matter what (names starting with underscore). If only_exposed is True, only members tagged with the @expose decorator are returned. If it is False, all public members are returned. The return value consists of the exposed methods, exposed attributes, and methods tagged as @oneway. (All this is used as meta data that Pyro sends to the proxy if it asks for it) as_lists is meant for python 2 compatibility. """ if not inspect.isclass(obj): obj = obj.__class__ cache_key = (obj, only_exposed, as_lists) if use_cache and cache_key in __exposed_member_cache: return __exposed_member_cache[cache_key] methods = set() # all methods oneway = set() # oneway methods attrs = set() # attributes for m in dir(obj): # also lists names inherited from super classes if is_private_attribute(m): continue v = getattr(obj, m) if inspect.ismethod(v) or inspect.isfunction(v) or inspect.ismethoddescriptor(v): if getattr(v, "_pyroExposed", not only_exposed): methods.add(m) # check if the method is marked with the 'oneway' decorator: if getattr(v, "_pyroOneway", False): oneway.add(m) elif inspect.isdatadescriptor(v): func = getattr(v, "fget", None) or getattr(v, "fset", None) or getattr(v, "fdel", None) if func is not None and getattr(func, "_pyroExposed", not only_exposed): attrs.add(m) # Note that we don't expose plain class attributes no matter what. # it is a syntax error to add a decorator on them, and it is not possible # to give them a _pyroExposed tag either. # The way to expose attributes is by using properties for them. # This automatically solves the protection/security issue: you have to # explicitly decide to make an attribute into a @property (and to @expose it # if REQUIRE_EXPOSED=True) before it is remotely accessible. if as_lists: methods = list(methods) oneway = list(oneway) attrs = list(attrs) result = { "methods": methods, "oneway": oneway, "attrs": attrs } __exposed_member_cache[cache_key] = result return result def get_exposed_property_value(obj, propname, only_exposed=True): """ Return the value of an @exposed @property. If the requested property is not a @property or not exposed, an AttributeError is raised instead. """ v = getattr(obj.__class__, propname) if inspect.isdatadescriptor(v): if v.fget and getattr(v.fget, "_pyroExposed", not only_exposed): return v.fget(obj) raise AttributeError("attempt to access unexposed or unknown remote attribute '%s'" % propname) def set_exposed_property_value(obj, propname, value, only_exposed=True): """ Sets the value of an @exposed @property. If the requested property is not a @property or not exposed, an AttributeError is raised instead. """ v = getattr(obj.__class__, propname) if inspect.isdatadescriptor(v): pfunc = v.fget or v.fset or v.fdel if v.fset and getattr(pfunc, "_pyroExposed", not only_exposed): return v.fset(obj, value) raise AttributeError("attempt to access unexposed or unknown remote attribute '%s'" % propname) _private_dunder_methods = frozenset([ "__init__", "__init_subclass__", "__class__", "__module__", "__weakref__", "__call__", "__new__", "__del__", "__repr__", "__unicode__", "__str__", "__format__", "__nonzero__", "__bool__", "__coerce__", "__cmp__", "__eq__", "__ne__", "__hash__", "__ge__", "__gt__", "__le__", "__lt__", "__dir__", "__enter__", "__exit__", "__copy__", "__deepcopy__", "__sizeof__", "__getattr__", "__setattr__", "__hasattr__", "__getattribute__", "__delattr__", "__instancecheck__", "__subclasscheck__", "__getinitargs__", "__getnewargs__", "__getstate__", "__setstate__", "__reduce__", "__reduce_ex__", "__getstate_for_dict__", "__setstate_from_dict__", "__subclasshook__" ]) def is_private_attribute(attr_name): """returns if the attribute name is to be considered private or not.""" if attr_name in _private_dunder_methods: return True if not attr_name.startswith('_'): return False if len(attr_name) > 4 and attr_name.startswith("__") and attr_name.endswith("__"): return False return True Pyro4-4.82/src/Pyro4/utils/000077500000000000000000000000001416147301300154305ustar00rootroot00000000000000Pyro4-4.82/src/Pyro4/utils/__init__.py000066400000000000000000000000371416147301300175410ustar00rootroot00000000000000# just to make this a package. Pyro4-4.82/src/Pyro4/utils/flame.py000066400000000000000000000271541416147301300170770ustar00rootroot00000000000000""" Pyro FLAME: Foreign Location Automatic Module Exposer. Easy but potentially very dangerous way of exposing remote modules and builtins. Flame requires the pickle serializer to be used. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import sys import types import code import os import stat from Pyro4 import constants, errors, core from Pyro4.configuration import config try: import importlib except ImportError: importlib = None try: import builtins except ImportError: import __builtin__ as builtins try: from cStringIO import StringIO except ImportError: from io import StringIO __all__ = ["connect", "start", "createModule", "Flame"] # Exec is a statement in Py2, a function in Py3 # Workaround as written by Ned Batchelder on his blog. if sys.version_info > (3, 0): def exec_function(source, filename, global_map): source = fixExecSourceNewlines(source) exec(compile(source, filename, "exec"), global_map) else: # OK, this is pretty gross. In Py2, exec was a statement, but that will # be a syntax error if we try to put it in a Py3 file, even if it isn't # executed. So hide it inside an evaluated string literal instead. eval(compile("""\ def exec_function(source, filename, global_map): source=fixExecSourceNewlines(source) exec compile(source, filename, "exec") in global_map """, "", "exec")) def fixExecSourceNewlines(source): if sys.version_info < (2, 7) or sys.version_info[:2] in ((3, 0), (3, 1)): # for python versions prior to 2.7 (and 3.0/3.1), compile is kinda picky. # it needs unix type newlines and a trailing newline to work correctly. source = source.replace("\r\n", "\n") source = source.rstrip() + "\n" # remove trailing whitespace that might cause IndentationErrors source = source.rstrip() return source class FlameModule(object): """Proxy to a remote module.""" def __init__(self, flameserver, module): # store a proxy to the flameserver regardless of autoproxy setting self.flameserver = core.Proxy(flameserver._pyroDaemon.uriFor(flameserver)) self.module = module def __getattr__(self, item): if item in ("__getnewargs__", "__getnewargs_ex__", "__getinitargs__"): raise AttributeError(item) return core._RemoteMethod(self.__invoke, "%s.%s" % (self.module, item), 0) def __getstate__(self): return self.__dict__ def __setstate__(self, args): self.__dict__ = args def __invoke(self, module, args, kwargs): return self.flameserver.invokeModule(module, args, kwargs) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.flameserver._pyroRelease() def __repr__(self): return "<%s.%s at 0x%x; module '%s' at %s>" % (self.__class__.__module__, self.__class__.__name__, id(self), self.module, self.flameserver._pyroUri.location) class FlameBuiltin(object): """Proxy to a remote builtin function.""" def __init__(self, flameserver, builtin): # store a proxy to the flameserver regardless of autoproxy setting self.flameserver = core.Proxy(flameserver._pyroDaemon.uriFor(flameserver)) self.builtin = builtin def __call__(self, *args, **kwargs): return self.flameserver.invokeBuiltin(self.builtin, args, kwargs) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.flameserver._pyroRelease() def __repr__(self): return "<%s.%s at 0x%x; builtin '%s' at %s>" % (self.__class__.__module__, self.__class__.__name__, id(self), self.builtin, self.flameserver._pyroUri.location) class RemoteInteractiveConsole(object): """Proxy to a remote interactive console.""" class LineSendingConsole(code.InteractiveConsole): """makes sure the lines are sent to the remote console""" def __init__(self, remoteconsole): code.InteractiveConsole.__init__(self, filename="") self.remoteconsole = remoteconsole def push(self, line): output, more = self.remoteconsole.push_and_get_output(line) if output: sys.stdout.write(output) return more def __init__(self, remoteconsoleuri): # store a proxy to the console regardless of autoproxy setting self.remoteconsole = core.Proxy(remoteconsoleuri) def interact(self): console = self.LineSendingConsole(self.remoteconsole) console.interact(banner=self.remoteconsole.get_banner()) print("(Remote session ended)") def close(self): self.remoteconsole.terminate() self.remoteconsole._pyroRelease() def terminate(self): self.close() def __repr__(self): return "<%s.%s at 0x%x; for %s>" % (self.__class__.__module__, self.__class__.__name__, id(self), self.remoteconsole._pyroUri.location) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() @core.expose class InteractiveConsole(code.InteractiveConsole): """Interactive console wrapper that saves output written to stdout so it can be returned as value""" def push_and_get_output(self, line): output, more = "", False stdout_save = sys.stdout try: sys.stdout = StringIO() more = self.push(line) output = sys.stdout.getvalue() sys.stdout.close() finally: sys.stdout = stdout_save return output, more def get_banner(self): return self.banner # custom banner string, set by Pyro daemon def write(self, data): sys.stdout.write(data) # stdout instead of stderr def terminate(self): self._pyroDaemon.unregister(self) self.resetbuffer() @core.expose class Flame(object): """ The actual FLAME server logic. Usually created by using :py:meth:`core.Daemon.startFlame`. Be *very* cautious before starting this: it allows the clients full access to everything on your system. """ def __init__(self): if set(config.SERIALIZERS_ACCEPTED) != {"pickle"}: raise RuntimeError("flame requires the pickle serializer exclusively") def module(self, name): """ Import a module on the server given by the module name and returns a proxy to it. The returned proxy does not support direct attribute access, if you want that, you should use the ``evaluate`` method instead. """ if importlib: importlib.import_module(name) else: __import__(name) return FlameModule(self, name) def builtin(self, name): """returns a proxy to the given builtin on the server""" return FlameBuiltin(self, name) def execute(self, code): """execute a piece of code""" exec_function(code, "", globals()) def evaluate(self, expression): """evaluate an expression and return its result""" return eval(expression) def sendmodule(self, modulename, modulesource): """ Send the source of a module to the server and make the server load it. Note that you still have to actually ``import`` it on the server to access it. Sending a module again will replace the previous one with the new. """ createModule(modulename, modulesource) def getmodule(self, modulename): """obtain the source code from a module on the server""" import inspect module = __import__(modulename, globals={}, locals={}) return inspect.getsource(module) def sendfile(self, filename, filedata): """store a new file on the server""" with open(filename, "wb") as targetfile: os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR) # readable/writable by owner only targetfile.write(filedata) def getfile(self, filename): """read any accessible file from the server""" with open(filename, "rb") as diskfile: return diskfile.read() def console(self): """get a proxy for a remote interactive console session""" console = InteractiveConsole(filename="") uri = self._pyroDaemon.register(console) console.banner = "Python %s on %s\n(Remote console on %s)" % (sys.version, sys.platform, uri.location) return RemoteInteractiveConsole(uri) @core.expose def invokeBuiltin(self, builtin, args, kwargs): return getattr(builtins, builtin)(*args, **kwargs) @core.expose def invokeModule(self, dottedname, args, kwargs): # dottedname is something like "os.path.walk" so strip off the module name modulename, dottedname = dottedname.split('.', 1) module = sys.modules[modulename] # Look up the actual method to call. # Because Flame already opens all doors, security wise, we allow ourselves to # look up a dotted name via object traversal. The security implication of that # is overshadowed by the security implications of enabling Flame in the first place. # We also don't check for access to 'private' methods. Same reasons. method = module for attr in dottedname.split('.'): method = getattr(method, attr) return method(*args, **kwargs) def createModule(name, source, filename="", namespace=None): """ Utility function to create a new module with the given name (dotted notation allowed), directly from the source string. Adds it to sys.modules, and returns the new module object. If you provide a namespace dict (such as ``globals()``), it will import the module into that namespace too. """ path = "" components = name.split('.') module = types.ModuleType("pyro-flame-module-context") for component in components: # build the module hierarchy. path += '.' + component real_path = path[1:] if real_path in sys.modules: # use already loaded modules instead of overwriting them module = sys.modules[real_path] else: setattr(module, component, types.ModuleType(real_path)) module = getattr(module, component) sys.modules[real_path] = module exec_function(source, filename, module.__dict__) if namespace is not None: namespace[components[0]] = __import__(name) return module def start(daemon): """ Create and register a Flame server in the given daemon. Be *very* cautious before starting this: it allows the clients full access to everything on your system. """ if config.FLAME_ENABLED: if set(config.SERIALIZERS_ACCEPTED) != {"pickle"}: raise errors.SerializeError("Flame requires the pickle serializer exclusively") return daemon.register(Flame(), constants.FLAME_NAME) else: raise errors.SecurityError("Flame is disabled in the server configuration") def connect(location, hmac_key=None): """ Connect to a Flame server on the given location, for instance localhost:9999 or ./u:unixsock This is just a convenience function to creates an appropriate Pyro proxy. """ if config.SERIALIZER != "pickle": raise errors.SerializeError("Flame requires the pickle serializer") proxy = core.Proxy("PYRO:%s@%s" % (constants.FLAME_NAME, location)) if hmac_key: proxy._pyroHmacKey = hmac_key proxy._pyroBind() return proxy Pyro4-4.82/src/Pyro4/utils/flameserver.py000066400000000000000000000050411416147301300203150ustar00rootroot00000000000000""" Pyro FLAME: Foreign Location Automatic Module Exposer. Easy but potentially very dangerous way of exposing remote modules and builtins. This is the commandline server. You can start this module as a script from the command line, to easily get a flame server running: :command:`python -m Pyro4.utils.flameserver` or simply: :command:`pyro4-flameserver` You have to explicitly enable Flame first though by setting the FLAME_ENABLED config item. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import sys import os import warnings from Pyro4.configuration import config from Pyro4 import core from Pyro4.utils import flame def main(args=None, returnWithoutLooping=False): from optparse import OptionParser parser = OptionParser() parser.add_option("-H", "--host", default="localhost", help="hostname to bind server on (default=%default)") parser.add_option("-p", "--port", type="int", default=0, help="port to bind server on") parser.add_option("-u", "--unixsocket", help="Unix domain socket name to bind server on") parser.add_option("-q", "--quiet", action="store_true", default=False, help="don't output anything") parser.add_option("-k", "--key", help="the HMAC key to use (deprecated)") options, args = parser.parse_args(args) if options.key: warnings.warn("using -k to supply HMAC key on the command line is a security problem " "and is deprecated since Pyro 4.72. See the documentation for an alternative.") if "PYRO_HMAC_KEY" in os.environ: if options.key: raise SystemExit("error: don't use -k and PYRO_HMAC_KEY at the same time") options.key = os.environ["PYRO_HMAC_KEY"] if not options.quiet: print("Starting Pyro Flame server.") hmac = (options.key or "").encode("utf-8") if not hmac and not options.quiet: print("Warning: HMAC key not set. Anyone can connect to this server!") config.SERIALIZERS_ACCEPTED = {"pickle"} # flame requires pickle serializer, doesn't work with the others. daemon = core.Daemon(host=options.host, port=options.port, unixsocket=options.unixsocket) if hmac: daemon._pyroHmacKey = hmac uri = flame.start(daemon) if not options.quiet: print("server uri: %s" % uri) print("server is running.") if returnWithoutLooping: return daemon, uri # for unit testing else: daemon.requestLoop() daemon.close() return 0 if __name__ == "__main__": sys.exit(main()) Pyro4-4.82/src/Pyro4/utils/httpgateway.py000066400000000000000000000405561416147301300203550ustar00rootroot00000000000000""" HTTP gateway: connects the web browser's world of javascript+http and Pyro. Creates a stateless HTTP server that essentially is a proxy for the Pyro objects behind it. It exposes the Pyro objects through a HTTP interface and uses the JSON serializer, so that you can immediately process the response data in the browser. You can start this module as a script from the command line, to easily get a http gateway server running: :command:`python -m Pyro4.utils.httpgateway` or simply: :command:`pyro4-httpgateway` It is also possible to import the 'pyro_app' function and stick that into a WSGI server of your choice, to have more control. The javascript code in the web page of the gateway server works with the same-origin browser policy because it is served by the gateway itself. If you want to access it from scripts in different sites, you have to work around this or embed the gateway app in your site. Non-browser clients that access the http api have no problems. See the `http` example for two of such clients (node.js and python). Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import sys import re import cgi import os import uuid import warnings from wsgiref.simple_server import make_server import traceback from Pyro4.util import json # don't import stdlib json directly, we want to use the JSON_MODULE config item from Pyro4.configuration import config from Pyro4 import constants, errors, core, message, util, naming __all__ = ["pyro_app", "main"] _nameserver = None def get_nameserver(hmac=None): global _nameserver if not _nameserver: _nameserver = naming.locateNS(hmac_key=hmac) try: _nameserver.ping() return _nameserver except errors.ConnectionClosedError: _nameserver = None print("Connection with nameserver lost, reconnecting...") return get_nameserver(hmac) def invalid_request(start_response): """Called if invalid http method.""" start_response('405 Method Not Allowed', [('Content-Type', 'text/plain')]) return [b'Error 405: Method Not Allowed'] def not_found(start_response): """Called if Url not found.""" start_response('404 Not Found', [('Content-Type', 'text/plain')]) return [b'Error 404: Not Found'] def redirect(start_response, target): """Called to do a redirect""" start_response('302 Found', [('Location', target)]) return [] index_page_template = """ Pyro HTTP gateway

Pyro HTTP gateway

Use http+json to talk to Pyro objects. Docs.

Note: performance isn't a key concern here; it is a stateless server. It does a name lookup and uses a new Pyro proxy for each request.

Currently exposed contents of name server on {hostname}:

(Limited to 10 entries, exposed name pattern = '{ns_regex}')

{name_server_contents_list}

Name server examples: (these examples are working if you expose the Pyro.NameServer object)

Echoserver examples: (these examples are working if you expose the test.echoserver object)

Pyro response data (via Ajax):

Call:
   
Response:
   

Pyro version: {pyro_version} — © Irmen de Jong

""" def return_homepage(environ, start_response): try: nameserver = get_nameserver(hmac=pyro_app.hmac_key) except errors.NamingError as x: print("Name server error:", x) start_response('500 Internal Server Error', [('Content-Type', 'text/plain')]) return [b"Cannot connect to the Pyro name server. Is it running? Refresh page to retry."] start_response('200 OK', [('Content-Type', 'text/html')]) nslist = [""] names = sorted(list(nameserver.list(regex=pyro_app.ns_regex).keys())[:10]) with core.batch(nameserver) as nsbatch: for name in names: nsbatch.lookup(name) for name, uri in zip(names, nsbatch()): attributes = "-" try: with core.Proxy(uri) as proxy: proxy._pyroHmacKey = pyro_app.hmac_key proxy._pyroBind() methods = "   ".join(proxy._pyroMethods) or "-" attributes = [ "{attribute}" .format(name=name, attribute=attribute) for attribute in proxy._pyroAttrs ] attributes = "   ".join(attributes) or "-" except errors.PyroError as x: stderr = environ["wsgi.errors"] print("ERROR getting metadata for {0}:".format(uri), file=stderr) traceback.print_exc(file=stderr) methods = "??error:%s??" % str(x) nslist.append( "" .format(name=name, methods=methods, attributes=attributes)) nslist.append("
Namemethodsattributes (zero-param methods)
{name}{methods}{attributes}
") index_page = index_page_template.format(ns_regex=pyro_app.ns_regex, name_server_contents_list="".join(nslist), pyro_version=constants.VERSION, hostname=nameserver._pyroUri.location) return [index_page.encode("utf-8")] def process_pyro_request(environ, path, parameters, start_response): pyro_options = environ.get("HTTP_X_PYRO_OPTIONS", "").split(",") if not path: return return_homepage(environ, start_response) matches = re.match(r"(.+)/(.+)", path) if not matches: return not_found(start_response) object_name, method = matches.groups() if pyro_app.gateway_key: gateway_key = environ.get("HTTP_X_PYRO_GATEWAY_KEY", "") or parameters.get("$key", "") gateway_key = gateway_key.encode("utf-8") if gateway_key != pyro_app.gateway_key: start_response('403 Forbidden', [('Content-Type', 'text/plain')]) return [b"403 Forbidden - incorrect gateway api key"] if "$key" in parameters: del parameters["$key"] if pyro_app.ns_regex and not re.match(pyro_app.ns_regex, object_name): start_response('403 Forbidden', [('Content-Type', 'text/plain')]) return [b"403 Forbidden - access to the requested object has been denied"] try: nameserver = get_nameserver(hmac=pyro_app.hmac_key) uri = nameserver.lookup(object_name) with core.Proxy(uri) as proxy: header_corr_id = environ.get("HTTP_X_PYRO_CORRELATION_ID", "") if header_corr_id: core.current_context.correlation_id = uuid.UUID(header_corr_id) # use the correlation id from the request header else: core.current_context.correlation_id = uuid.uuid4() # set new correlation id proxy._pyroHmacKey = pyro_app.hmac_key proxy._pyroGetMetadata() if "oneway" in pyro_options: proxy._pyroOneway.add(method) if method == "$meta": result = {"methods": tuple(proxy._pyroMethods), "attributes": tuple(proxy._pyroAttrs)} reply = json.dumps(result).encode("utf-8") start_response('200 OK', [('Content-Type', 'application/json; charset=utf-8'), ('X-Pyro-Correlation-Id', str(core.current_context.correlation_id))]) return [reply] else: proxy._pyroRawWireResponse = True # we want to access the raw response json if method in proxy._pyroAttrs: # retrieve the attribute assert not parameters, "attribute lookup can't have query parameters" msg = getattr(proxy, method) else: # call the remote method msg = getattr(proxy, method)(**parameters) if msg is None or "oneway" in pyro_options: # was a oneway call, no response available start_response('200 OK', [('Content-Type', 'application/json; charset=utf-8'), ('X-Pyro-Correlation-Id', str(core.current_context.correlation_id))]) return [] elif msg.flags & message.FLAGS_EXCEPTION: # got an exception response so send a 500 status start_response('500 Internal Server Error', [('Content-Type', 'application/json; charset=utf-8')]) return [msg.data] else: # normal response start_response('200 OK', [('Content-Type', 'application/json; charset=utf-8'), ('X-Pyro-Correlation-Id', str(core.current_context.correlation_id))]) return [msg.data] except Exception as x: stderr = environ["wsgi.errors"] print("ERROR handling {0} with params {1}:".format(path, parameters), file=stderr) traceback.print_exc(file=stderr) start_response('500 Internal Server Error', [('Content-Type', 'application/json; charset=utf-8')]) reply = json.dumps(util.SerializerBase.class_to_dict(x)).encode("utf-8") return [reply] def pyro_app(environ, start_response): """ The WSGI app function that is used to process the requests. You can stick this into a wsgi server of your choice, or use the main() method to use the default wsgiref server. """ config.SERIALIZER = "json" # we only talk json through the http proxy config.COMMTIMEOUT = pyro_app.comm_timeout method = environ.get("REQUEST_METHOD") path = environ.get('PATH_INFO', '').lstrip('/') if not path: return redirect(start_response, "/pyro/") if path.startswith("pyro/"): if method in ("GET", "POST"): parameters = singlyfy_parameters(cgi.parse(environ['wsgi.input'], environ)) return process_pyro_request(environ, path[5:], parameters, start_response) else: return invalid_request(start_response) return not_found(start_response) def singlyfy_parameters(parameters): """ Makes a cgi-parsed parameter dictionary into a dict where the values that are just a list of a single value, are converted to just that single value. """ for key, value in parameters.items(): if isinstance(value, (list, tuple)) and len(value) == 1: parameters[key] = value[0] return parameters pyro_app.ns_regex = r"http\." pyro_app.hmac_key = None pyro_app.gateway_key = None pyro_app.comm_timeout = config.COMMTIMEOUT def main(args=None): from optparse import OptionParser parser = OptionParser() parser.add_option("-H", "--host", default="localhost", help="hostname to bind server on (default=%default)") parser.add_option("-p", "--port", type="int", default=8080, help="port to bind server on (default=%default)") parser.add_option("-e", "--expose", default=pyro_app.ns_regex, help="a regex of object names to expose (default=%default)") parser.add_option("-k", "--pyrokey", help="the HMAC key to use to connect with Pyro (deprecated)") parser.add_option("-g", "--gatewaykey", help="the api key to use to connect to the gateway itself") parser.add_option("-t", "--timeout", type="float", default=pyro_app.comm_timeout, help="Pyro timeout value to use (COMMTIMEOUT setting, default=%default)") options, args = parser.parse_args(args) if options.pyrokey or options.gatewaykey: warnings.warn("using -k and/or -g to supply keys on the command line is a security problem " "and is deprecated since Pyro 4.72. See the documentation for an alternative.") if "PYRO_HMAC_KEY" in os.environ: if options.pyrokey: raise SystemExit("error: don't use -k and PYRO_HMAC_KEY at the same time") options.pyrokey = os.environ["PYRO_HMAC_KEY"] if "PYRO_HTTPGATEWAY_KEY" in os.environ: if options.gatewaykey: raise SystemExit("error: don't use -g and PYRO_HTTPGATEWAY_KEY at the same time") options.gatewaykey = os.environ["PYRO_HTTPGATEWAY_KEY"] pyro_app.hmac_key = (options.pyrokey or "").encode("utf-8") pyro_app.gateway_key = (options.gatewaykey or "").encode("utf-8") pyro_app.ns_regex = options.expose pyro_app.comm_timeout = config.COMMTIMEOUT = options.timeout if pyro_app.ns_regex: print("Exposing objects with names matching: ", pyro_app.ns_regex) else: print("Warning: exposing all objects (no expose regex set)") try: ns = get_nameserver(hmac=pyro_app.hmac_key) except errors.PyroError: print("Not yet connected to a name server.") else: print("Connected to name server at: ", ns._pyroUri) server = make_server(options.host, options.port, pyro_app) print("Pyro HTTP gateway running on http://{0}:{1}/pyro/".format(*server.socket.getsockname())) server.serve_forever() server.server_close() return 0 if __name__ == "__main__": sys.exit(main()) Pyro4-4.82/test_requirements.txt000066400000000000000000000001021416147301300170000ustar00rootroot00000000000000-r requirements.txt cloudpickle>=0.4.0 dill>=0.2.6 msgpack>=0.5.2 Pyro4-4.82/tests/000077500000000000000000000000001416147301300136265ustar00rootroot00000000000000Pyro4-4.82/tests/PyroTests/000077500000000000000000000000001416147301300156025ustar00rootroot00000000000000Pyro4-4.82/tests/PyroTests/__init__.py000066400000000000000000000000211416147301300177040ustar00rootroot00000000000000# just a package Pyro4-4.82/tests/PyroTests/test_core.py000066400000000000000000001336661416147301300201620ustar00rootroot00000000000000""" Tests for the core logic. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import copy import logging import os import sys import time import uuid import socket import unittest import warnings import Pyro4.core import Pyro4.errors import Pyro4.constants import Pyro4.futures from Pyro4.configuration import config from testsupport import * if (3, 0) <= sys.version_info < (3, 4): import imp reload = imp.reload elif sys.version_info >= (3, 4): import importlib reload = importlib.reload class CoreTests(unittest.TestCase): def setUp(self): warnings.filterwarnings("ignore") def tearDown(self): warnings.resetwarnings() def testProxyNoHmac(self): # check that proxy without hmac is possible with Pyro4.core.Proxy("PYRO:object@host:9999") as p: pass def testDaemonNoHmac(self): # check that daemon without hmac is possible d = Pyro4.core.Daemon() d.shutdown() def testConfig(self): self.assertTrue(type(config.COMPRESSION) is bool) self.assertTrue(type(config.NS_PORT) is int) cfgdict = config.asDict() self.assertTrue(type(cfgdict) is dict) self.assertIn("COMPRESSION", cfgdict) self.assertEqual(config.COMPRESSION, cfgdict["COMPRESSION"]) def testConfigDefaults(self): # some security sensitive settings: config.reset(False) # reset the config to default self.assertTrue(config.REQUIRE_EXPOSE) self.assertEqual("localhost", config.HOST) self.assertEqual("localhost", config.NS_HOST) self.assertFalse(config.FLAME_ENABLED) self.assertEqual("serpent", config.SERIALIZER) self.assertEqual({"json", "serpent", "marshal"}, config.SERIALIZERS_ACCEPTED) def testConfigValid(self): try: config.XYZ_FOOBAR = True # don't want to allow weird config names self.fail("expected exception for weird config item") except AttributeError: pass def testConfigParseBool(self): config = Pyro4.configuration.Configuration() self.assertTrue(type(config.COMPRESSION) is bool) os.environ["PYRO_COMPRESSION"] = "yes" config.reset() self.assertTrue(config.COMPRESSION) os.environ["PYRO_COMPRESSION"] = "off" config.reset() self.assertFalse(config.COMPRESSION) os.environ["PYRO_COMPRESSION"] = "foobar" self.assertRaises(ValueError, config.reset) del os.environ["PYRO_COMPRESSION"] config.reset() def testConfigDump(self): config = Pyro4.configuration.Configuration() dump = config.dump() self.assertIn("version:", dump) self.assertIn("LOGLEVEL", dump) def testLogInit(self): _ = logging.getLogger("Pyro4") os.environ["PYRO_LOGLEVEL"] = "DEBUG" os.environ["PYRO_LOGFILE"] = "{stderr}" reload(Pyro4) _ = logging.getLogger("Pyro4") os.environ["PYRO_LOGFILE"] = "Pyro.log" reload(Pyro4) _ = logging.getLogger("Pyro4") del os.environ["PYRO_LOGLEVEL"] del os.environ["PYRO_LOGFILE"] reload(Pyro4) _ = logging.getLogger("Pyro4") def testUriStrAndRepr(self): uri = "PYRONAME:some_obj_name" p = Pyro4.core.URI(uri) self.assertEqual(uri, str(p)) uri = "PYRONAME:some_obj_name@host.com" p = Pyro4.core.URI(uri) self.assertEqual(uri + ":" + str(config.NS_PORT), str(p)) # a PYRONAME uri with a hostname gets a port too if omitted uri = "PYRONAME:some_obj_name@host.com:8888" p = Pyro4.core.URI(uri) self.assertEqual(uri, str(p)) expected = "" % id(p) self.assertEqual(expected, repr(p)) uri = "PYRO:12345@host.com:9999" p = Pyro4.core.URI(uri) self.assertEqual(uri, str(p)) self.assertEqual(uri, p.asString()) uri = "PYRO:12345@./u:sockname" p = Pyro4.core.URI(uri) self.assertEqual(uri, str(p)) uri = "PYRO:12345@./u:sockname" unicodeuri = unicode(uri) p = Pyro4.core.URI(unicodeuri) self.assertEqual(uri, str(p)) self.assertEqual(unicodeuri, unicode(p)) self.assertTrue(type(p.sockname) is unicode) uri = "PYRO:12345@./u:sock name with strings" p = Pyro4.core.URI(uri) self.assertEqual(uri, str(p)) def testUriParsingPyro(self): p = Pyro4.core.URI("PYRONAME:some_obj_name") self.assertEqual("PYRONAME", p.protocol) self.assertEqual("some_obj_name", p.object) self.assertIsNone(p.host) self.assertIsNone(p.sockname) self.assertIsNone(p.port) p = Pyro4.core.URI("PYRONAME:some_obj_name@host.com:9999") self.assertEqual("PYRONAME", p.protocol) self.assertEqual("some_obj_name", p.object) self.assertEqual("host.com", p.host) self.assertEqual(9999, p.port) p = Pyro4.core.URI("PYRO:12345@host.com:4444") self.assertEqual("PYRO", p.protocol) self.assertEqual("12345", p.object) self.assertEqual("host.com", p.host) self.assertIsNone(p.sockname) self.assertEqual(4444, p.port) self.assertEqual("host.com:4444", p.location) p = Pyro4.core.URI("PYRO:12345@./u:sockname") self.assertEqual("12345", p.object) self.assertEqual("sockname", p.sockname) p = Pyro4.core.URI("PYRO:12345@./u:/tmp/sockname") self.assertEqual("12345", p.object) self.assertEqual("/tmp/sockname", p.sockname) p = Pyro4.core.URI("PYRO:12345@./u:/path with spaces/sockname ") self.assertEqual("12345", p.object) self.assertEqual("/path with spaces/sockname ", p.sockname) p = Pyro4.core.URI("PYRO:12345@./u:../sockname") self.assertEqual("12345", p.object) self.assertEqual("../sockname", p.sockname) p = Pyro4.core.URI("pyro:12345@host.com:4444") self.assertEqual("PYRO", p.protocol) self.assertEqual("12345", p.object) self.assertEqual("host.com", p.host) self.assertIsNone(p.sockname) self.assertEqual(4444, p.port) def testUriParsingIpv6(self): p = Pyro4.core.URI("pyro:12345@[::1]:4444") self.assertEqual("::1", p.host) self.assertEqual("[::1]:4444", p.location) with self.assertRaises(Pyro4.errors.PyroError) as e: Pyro4.core.URI("pyro:12345@[[::1]]:4444") self.assertEqual("invalid ipv6 address: enclosed in too many brackets", str(e.exception)) with self.assertRaises(Pyro4.errors.PyroError) as e: Pyro4.core.URI("pyro:12345@[must_be_numeric_here]:4444") self.assertEqual("invalid ipv6 address: the part between brackets must be a numeric ipv6 address", str(e.exception)) def testUriParsingPyroname(self): p = Pyro4.core.URI("PYRONAME:objectname") self.assertEqual("PYRONAME", p.protocol) self.assertEqual("objectname", p.object) self.assertIsNone(p.host) self.assertIsNone(p.port) p = Pyro4.core.URI("PYRONAME:objectname@nameserverhost") self.assertEqual("PYRONAME", p.protocol) self.assertEqual("objectname", p.object) self.assertEqual("nameserverhost", p.host) self.assertEqual(config.NS_PORT, p.port) # Pyroname uri with host gets a port too if not specified p = Pyro4.core.URI("PYRONAME:objectname@nameserverhost:4444") self.assertEqual("PYRONAME", p.protocol) self.assertEqual("objectname", p.object) self.assertEqual("nameserverhost", p.host) self.assertEqual(4444, p.port) p = Pyro4.core.URI("PyroName:some_obj_name@host.com:9999") self.assertEqual("PYRONAME", p.protocol) p = Pyro4.core.URI("pyroname:some_obj_name@host.com:9999") self.assertEqual("PYRONAME", p.protocol) def testUriParsingPyrometa(self): p = Pyro4.core.URI("PYROMETA:meta") self.assertEqual("PYROMETA", p.protocol) self.assertEqual({"meta"}, p.object) self.assertIsNone(p.host) self.assertIsNone(p.port) p = Pyro4.core.URI("PYROMETA:meta1,meta2,meta2@nameserverhost") self.assertEqual("PYROMETA", p.protocol) self.assertEqual({"meta1", "meta2"}, p.object) self.assertEqual("nameserverhost", p.host) self.assertEqual(config.NS_PORT, p.port) # PyroMeta uri with host gets a port too if not specified p = Pyro4.core.URI("PYROMETA:meta@nameserverhost:4444") self.assertEqual("PYROMETA", p.protocol) self.assertEqual({"meta"}, p.object) self.assertEqual("nameserverhost", p.host) self.assertEqual(4444, p.port) p = Pyro4.core.URI("PyroMeta:meta1,meta2@host.com:9999") self.assertEqual("PYROMETA", p.protocol) p = Pyro4.core.URI("PyroMeta:meta1,meta2@host.com:9999") self.assertEqual("PYROMETA", p.protocol) def testInvalidUris(self): self.assertRaises(TypeError, Pyro4.core.URI, None) self.assertRaises(TypeError, Pyro4.core.URI, 99999) self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, " ") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "a") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYR") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRO") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRO:") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRO::") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRO:a") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRO:x@") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRO:x@hostname") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRO:@hostname:portstr") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRO:@hostname:7766") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRO:objid@hostname:7766:bogus") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRO:obj id@hostname:7766") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYROLOC:objname") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYROLOC:objname@host") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYROLOC:objectname@hostname:4444") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRONAME:") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRONAME:obj name@nameserver:bogus") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRONAME:objname@nameserver:bogus") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRONAME:objname@nameserver:7766:bogus") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYROMETA:") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYROMETA:meta@nameserver:bogus") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYROMETA:meta@nameserver:7766:bogus") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYROMETA:meta1, m2 ,m3@nameserver:7766:bogus") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "FOOBAR:") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "FOOBAR:objid@hostname:7766") self.assertRaises(Pyro4.errors.PyroError, Pyro4.core.URI, "PYRO:12345@./u:sockname:9999") def testUriUnicode(self): p = Pyro4.core.URI(unicode("PYRO:12345@host.com:4444")) self.assertEqual("PYRO", p.protocol) self.assertEqual("12345", p.object) self.assertEqual("host.com", p.host) self.assertTrue(type(p.protocol) is unicode) self.assertTrue(type(p.object) is unicode) self.assertTrue(type(p.host) is unicode) self.assertIsNone(p.sockname) self.assertEqual(4444, p.port) uri = "PYRO:12345@hostname:9999" p = Pyro4.core.URI(uri) pu = Pyro4.core.URI(unicode(uri)) self.assertEqual("PYRO", pu.protocol) self.assertEqual("hostname", pu.host) self.assertEqual(p, pu) self.assertEqual(str(p), str(pu)) unicodeuri = "PYRO:weirdchars" + unichr(0x20ac) + "@host" + unichr(0x20AC) + ".com:4444" pu = Pyro4.core.URI(unicodeuri) self.assertEqual("PYRO", pu.protocol) self.assertEqual("host" + unichr(0x20AC) + ".com", pu.host) self.assertEqual("weirdchars" + unichr(0x20AC), pu.object) if sys.version_info <= (3, 0): self.assertEqual("PYRO:weirdchars?@host?.com:4444", pu.__str__()) expected = "" % id(pu) self.assertEqual(expected, repr(pu)) else: self.assertEqual("PYRO:weirdchars" + unichr(0x20ac) + "@host" + unichr(0x20ac) + ".com:4444", pu.__str__()) expected = ("") % id(pu) self.assertEqual(expected, repr(pu)) self.assertEqual("PYRO:weirdchars" + unichr(0x20ac) + "@host" + unichr(0x20ac) + ".com:4444", pu.asString()) self.assertEqual("PYRO:weirdchars" + unichr(0x20ac) + "@host" + unichr(0x20ac) + ".com:4444", unicode(pu)) def testUriCopy(self): p1 = Pyro4.core.URI("PYRO:12345@hostname:9999") p2 = Pyro4.core.URI(p1) p3 = copy.copy(p1) self.assertEqual(p1.protocol, p2.protocol) self.assertEqual(p1.host, p2.host) self.assertEqual(p1.port, p2.port) self.assertEqual(p1.object, p2.object) self.assertEqual(p1, p2) self.assertEqual(p1.protocol, p3.protocol) self.assertEqual(p1.host, p3.host) self.assertEqual(p1.port, p3.port) self.assertEqual(p1.object, p3.object) self.assertEqual(p1, p3) def testUriSubclassCopy(self): class SubURI(Pyro4.core.URI): pass u = SubURI("PYRO:12345@hostname:9999") u2 = copy.copy(u) self.assertIsInstance(u2, SubURI) def testUriEqual(self): p1 = Pyro4.core.URI("PYRO:12345@host.com:9999") p2 = Pyro4.core.URI("PYRO:12345@host.com:9999") p3 = Pyro4.core.URI("PYRO:99999@host.com:4444") self.assertEqual(p1, p2) self.assertNotEqual(p1, p3) self.assertNotEqual(p2, p3) self.assertTrue(p1 == p2) self.assertFalse(p1 == p3) self.assertFalse(p2 == p3) self.assertFalse(p1 != p2) self.assertTrue(p1 != p3) self.assertTrue(p2 != p3) self.assertTrue(hash(p1) == hash(p2)) self.assertTrue(hash(p1) != hash(p3)) p2.port = 4444 p2.object = "99999" self.assertNotEqual(p1, p2) self.assertEqual(p2, p3) self.assertFalse(p1 == p2) self.assertTrue(p2 == p3) self.assertTrue(p1 != p2) self.assertFalse(p2 != p3) self.assertTrue(hash(p1) != hash(p2)) self.assertTrue(hash(p2) == hash(p3)) self.assertFalse(p1 == 42) self.assertTrue(p1 != 42) def testLocation(self): self.assertTrue(Pyro4.core.URI.isUnixsockLocation("./u:name")) self.assertFalse(Pyro4.core.URI.isUnixsockLocation("./p:name")) self.assertFalse(Pyro4.core.URI.isUnixsockLocation("./x:name")) self.assertFalse(Pyro4.core.URI.isUnixsockLocation("foobar")) def testProxyConnectedSocket(self): s1, s2 = socket.socketpair() s1.settimeout(0.1) s2.settimeout(0.1) try: Pyro4.config.METADATA = False with self.assertRaises(Pyro4.errors.PyroError): Pyro4.Proxy("test") with Pyro4.Proxy("test", connected_socket=s1) as p: self.assertEquals("<>:0", p._pyroUri.location) self.assertIsNotNone(p._pyroConnection) p._pyroRelease() self.assertIsNotNone(p._pyroConnection) self.assertIsNotNone(p._pyroConnection) finally: Pyro4.config.METADATA = True def testProxyCopy(self): u = Pyro4.core.URI("PYRO:12345@hostname:9999") p1 = Pyro4.core.Proxy(u) p2 = copy.copy(p1) # check that most basic copy also works self.assertEqual(p1, p2) self.assertEqual(set(), p2._pyroOneway) p1._pyroAttrs = set("abc") p1._pyroTimeout = 42 p1._pyroOneway = set("def") p1._pyroMethods = set("ghi") p1._pyroHmacKey = b"secret" p1._pyroHandshake = "apples" p2 = copy.copy(p1) self.assertEqual(p1, p2) self.assertEqual(p1._pyroUri, p2._pyroUri) self.assertEqual(p1._pyroOneway, p2._pyroOneway) self.assertEqual(p1._pyroMethods, p2._pyroMethods) self.assertEqual(p1._pyroAttrs, p2._pyroAttrs) self.assertEqual(p1._pyroTimeout, p2._pyroTimeout) self.assertEqual(p1._pyroHmacKey, p2._pyroHmacKey) self.assertEqual(p1._pyroHandshake, p2._pyroHandshake) p1._pyroRelease() p2._pyroRelease() def testProxySubclassCopy(self): class ProxySub(Pyro4.core.Proxy): pass p = ProxySub("PYRO:12345@hostname:9999") p2 = copy.copy(p) self.assertIsInstance(p2, ProxySub) p._pyroRelease() p2._pyroRelease() def testAsyncProxyCopy(self): try: config.METADATA = False with Pyro4.core.Proxy("PYRO:12345@hostname:9999") as proxy: proxy._pyroAsync() p2 = copy.copy(proxy) asynccall = p2.foobar() self.assertIsInstance(asynccall, Pyro4.futures.FutureResult) finally: config.METADATA = True def testBatchProxyAdapterCopy(self): with Pyro4.core.Proxy("PYRO:12345@hostname:9999") as proxy: batchproxy = proxy._pyroBatch() p2 = copy.copy(batchproxy) self.assertIsInstance(p2, Pyro4.core._BatchProxyAdapter) def testProxyOffline(self): # only offline stuff here. # online stuff needs a running daemon, so we do that in another test, to keep this one simple self.assertRaises(TypeError, Pyro4.core.Proxy, 999) # wrong arg p1 = Pyro4.core.Proxy("PYRO:9999@localhost:15555") p2 = Pyro4.core.Proxy(Pyro4.core.URI("PYRO:9999@localhost:15555")) self.assertEqual(p1._pyroUri, p2._pyroUri) self.assertIsNone(p1._pyroConnection) p1._pyroRelease() p1._pyroRelease() # try copying a not-connected proxy p3 = copy.copy(p1) self.assertIsNone(p3._pyroConnection) self.assertIsNone(p1._pyroConnection) self.assertEqual(p3._pyroUri, p1._pyroUri) self.assertIsNot(p3._pyroUri, p1._pyroUri) p3._pyroRelease() def testProxyRepr(self): with Pyro4.core.Proxy("PYRO:9999@localhost:15555") as p: address = id(p) expected = "" % address self.assertEqual(expected, repr(p)) self.assertEqual(unicode(expected), unicode(p)) def testProxySerializerOverride(self): serializer = config.SERIALIZER try: config.SERIALIZER = "~invalid~" _ = Pyro4.core.Proxy("PYRO:obj@localhost:5555") self.fail("must raise exception") except Pyro4.errors.SerializeError as x: self.assertIn("~invalid~", str(x)) self.assertIn("unknown", str(x)) finally: config.SERIALIZER = serializer try: proxy = Pyro4.core.Proxy("PYRO:obj@localhost:5555") proxy._pyroSerializer = "~invalidoverride~" proxy._pyroConnection = "FAKE" proxy.methodcall() self.fail("must raise exception") except Pyro4.errors.SerializeError as x: self.assertIn("~invalidoverride~", str(x)) self.assertIn("unknown", str(x)) finally: proxy._pyroConnection = None config.SERIALIZER = serializer def testProxyDir(self): # PyPy tries to call deprecated __members__ and __methods__ # that causes a CommunicationError since we use a fake URI class ProxyWithFixedGettattr(Pyro4.core.Proxy): def __getattr__(self, name): if name in ('__members__', '__methods__'): raise AttributeError(name) return super(Pyro4.core.Proxy, self).__getattr__(name) ProxyClass = ProxyWithFixedGettattr if 'pypy' in sys.version.lower() else Pyro4.core.Proxy p = ProxyClass("PYRO:9999@localhost:15555") # make sure that __dir__ implementation works the same way as dir() dir_result = dir(p) if sys.version_info < (3, 3): # before 3.3 python's object class didn't have __dir__ method dir_result.remove('__dir__') old_dir_method = getattr(Pyro4.core.Proxy, '__dir__') try: delattr(Pyro4.core.Proxy, '__dir__') self.assertEqual(dir_result, dir(p)) finally: setattr(Pyro4.core.Proxy, '__dir__', old_dir_method) p._pyroRelease() def testProxyDirMetadata(self): with Pyro4.core.Proxy("PYRO:9999@localhost:15555") as p: # metadata isn't loaded self.assertIn('__hash__', dir(p)) self.assertNotIn('ping', dir(p)) # emulate obtaining metadata p._pyroAttrs = {"prop"} p._pyroMethods = {"ping"} self.assertIn('__hash__', dir(p)) self.assertIn('prop', dir(p)) self.assertIn('ping', dir(p)) def testProxySettings(self): p1 = Pyro4.core.Proxy("PYRO:9999@localhost:15555") p2 = Pyro4.core.Proxy("PYRO:9999@localhost:15555") p1._pyroOneway.add("method") p1._pyroAttrs.add("attr") p1._pyroMethods.add("method2") self.assertIn("method", p1._pyroOneway) self.assertIn("attr", p1._pyroAttrs) self.assertIn("method2", p1._pyroMethods) self.assertNotIn("method", p2._pyroOneway) self.assertNotIn("attr", p2._pyroAttrs) self.assertNotIn("method2", p2._pyroMethods) self.assertIsNot(p1._pyroOneway, p2._pyroOneway, "p1 and p2 should have different oneway tables") self.assertIsNot(p1._pyroAttrs, p2._pyroAttrs, "p1 and p2 should have different attr tables") self.assertIsNot(p1._pyroMethods, p2._pyroMethods, "p1 and p2 should have different method tables") p1._pyroRelease() p2._pyroRelease() def testProxyWithStmt(self): class ConnectionMock(object): closeCalled = False keep_open = False def close(self): self.closeCalled = True connMock = ConnectionMock() # first without a 'with' statement p = Pyro4.core.Proxy("PYRO:9999@localhost:15555") p._pyroConnection = connMock self.assertFalse(connMock.closeCalled) p._pyroRelease() self.assertIsNone(p._pyroConnection) self.assertTrue(connMock.closeCalled) connMock = ConnectionMock() with Pyro4.core.Proxy("PYRO:9999@localhost:15555") as p: p._pyroConnection = connMock self.assertIsNone(p._pyroConnection) self.assertTrue(connMock.closeCalled) connMock = ConnectionMock() try: with Pyro4.core.Proxy("PYRO:9999@localhost:15555") as p: p._pyroConnection = connMock print(1 // 0) # cause an error self.fail("expected error") except ZeroDivisionError: pass self.assertIsNone(p._pyroConnection) self.assertTrue(connMock.closeCalled) p = Pyro4.core.Proxy("PYRO:9999@localhost:15555") with p: self.assertIsNotNone(p._pyroUri) with p: self.assertIsNotNone(p._pyroUri) p._pyroRelease() def testProxyHmac(self): class ConnectionMock(object): def __init__(self): self.msgbytes = None self.keep_open = False def close(self): pass def send(self, msgbytes): self.msgbytes = msgbytes def recv(self, size): raise Pyro4.errors.ConnectionClosedError("mock") proxy = Pyro4.core.Proxy("PYRO:foobar@localhost:59999") proxy._pyroHmacKey = b"secret" conn_mock = ConnectionMock() proxy._pyroConnection = conn_mock with self.assertRaises(Pyro4.errors.ConnectionClosedError): proxy.foo() self.assertIn(b"HMAC", conn_mock.msgbytes) conn_mock = ConnectionMock() proxy._pyroConnection = conn_mock proxy._pyroHmacKey = None with self.assertRaises(Pyro4.errors.ConnectionClosedError): proxy.foo() self.assertNotIn(b"HMAC", conn_mock.msgbytes) self.assertIsNone(proxy._pyroHmacKey) proxy._pyroRelease() def testNoConnect(self): wrongUri = Pyro4.core.URI("PYRO:foobar@localhost:59999") with Pyro4.core.Proxy(wrongUri) as p: try: p.ping() self.fail("CommunicationError expected") except Pyro4.errors.CommunicationError: pass def testTimeoutGetSet(self): class ConnectionMock(object): def __init__(self): self.timeout = config.COMMTIMEOUT self.keep_open = False def close(self): pass config.COMMTIMEOUT = None p = Pyro4.core.Proxy("PYRO:obj@host:555") self.assertIsNone(p._pyroTimeout) p._pyroTimeout = 5 self.assertEqual(5, p._pyroTimeout) p = Pyro4.core.Proxy("PYRO:obj@host:555") p._pyroConnection = ConnectionMock() self.assertIsNone(p._pyroTimeout) p._pyroTimeout = 5 self.assertEqual(5, p._pyroTimeout) self.assertEqual(5, p._pyroConnection.timeout) config.COMMTIMEOUT = 2 p = Pyro4.core.Proxy("PYRO:obj@host:555") p._pyroConnection = ConnectionMock() self.assertEqual(2, p._pyroTimeout) self.assertEqual(2, p._pyroConnection.timeout) p._pyroTimeout = None self.assertIsNone(p._pyroTimeout) self.assertIsNone(p._pyroConnection.timeout) config.COMMTIMEOUT = None p._pyroRelease() def testCallbackDecorator(self): # just test the decorator itself, testing the callback # exception handling is kinda hard in unit tests. Maybe later. class Test(object): @Pyro4.core.callback def method(self): pass def method2(self): pass t = Test() self.assertEqual(True, getattr(t.method, "_pyroCallback")) self.assertEqual(False, getattr(t.method2, "_pyroCallback", False)) def testProxyEquality(self): p1 = Pyro4.core.Proxy("PYRO:thing@localhost:15555") p2 = Pyro4.core.Proxy("PYRO:thing@localhost:15555") p3 = Pyro4.core.Proxy("PYRO:other@machine:16666") self.assertTrue(p1 == p2) self.assertFalse(p1 != p2) self.assertFalse(p1 == p3) self.assertTrue(p1 != p3) self.assertTrue(hash(p1) == hash(p2)) self.assertFalse(hash(p1) == hash(p3)) self.assertFalse(p1 == 42) self.assertTrue(p1 != 42) p1._pyroRelease() p2._pyroRelease() p3._pyroRelease() def testCallContext(self): ctx = Pyro4.core.current_context corr_id = uuid.UUID('1897022f-c481-4117-a4cc-cbd1ca100582') ctx.correlation_id = corr_id d = ctx.to_global() self.assertIsInstance(d, dict) self.assertEqual(corr_id, d["correlation_id"]) corr_id2 = uuid.UUID('67b05ad9-2d6a-4ed8-8ed5-95cba68b4cf9') d["correlation_id"] = corr_id2 ctx.from_global(d) self.assertEqual(corr_id2, Pyro4.core.current_context.correlation_id) Pyro4.core.current_context.correlation_id = None class ExposeDecoratorTests(unittest.TestCase): # note: the bulk of the tests for the @expose decorator are found in the test_util module def testExposeInstancemodeDefault(self): @Pyro4.core.expose class TestClassOne: def method(self): pass class TestClassTwo: @Pyro4.core.expose def method(self): pass class TestClassThree: def method(self): pass with Pyro4.core.Daemon() as daemon: daemon.register(TestClassOne) daemon.register(TestClassTwo) daemon.register(TestClassThree) self.assertEqual(("session", None), TestClassOne._pyroInstancing) self.assertEqual(("session", None), TestClassTwo._pyroInstancing) self.assertEqual(("session", None), TestClassThree._pyroInstancing) class BehaviorDecoratorTests(unittest.TestCase): def testBehaviorInstancemodeInvalid(self): with self.assertRaises(ValueError): @Pyro4.core.behavior(instance_mode="kaputt") class TestClass: def method(self): pass def testBehaviorRequiresParams(self): with self.assertRaises(SyntaxError) as x: @Pyro4.core.behavior class TestClass: def method(self): pass self.assertIn("is missing argument", str(x.exception)) def testBehaviorInstancecreatorInvalid(self): with self.assertRaises(TypeError): @Pyro4.core.behavior(instance_creator=12345) class TestClass: def method(self): pass def testBehaviorOnMethodInvalid(self): with self.assertRaises(TypeError): class TestClass: @Pyro4.core.behavior(instance_mode="~invalidmode~") def method(self): pass with self.assertRaises(TypeError): class TestClass: @Pyro4.core.behavior(instance_mode="percall", instance_creator=float) def method(self): pass with self.assertRaises(TypeError): class TestClass: @Pyro4.core.behavior() def method(self): pass def testBehaviorInstancing(self): @Pyro4.core.behavior(instance_mode="percall", instance_creator=float) class TestClass: def method(self): pass im, ic = TestClass._pyroInstancing self.assertEqual("percall", im) self.assertIs(float, ic) def testBehaviorWithExposeKeepsCorrectValues(self): @Pyro4.core.behavior(instance_mode="percall", instance_creator=float) @Pyro4.core.expose class TestClass: pass im, ic = TestClass._pyroInstancing self.assertEqual("percall", im) self.assertIs(float, ic) @Pyro4.core.expose @Pyro4.core.behavior(instance_mode="percall", instance_creator=float) class TestClass2: pass im, ic = TestClass2._pyroInstancing self.assertEqual("percall", im) self.assertIs(float, ic) class RemoteMethodTests(unittest.TestCase): class BatchProxyMock(object): def __init__(self): self.result = [] self._pyroMaxRetries = 0 def __copy__(self): return self def __enter__(self): return self def __exit__(self, *args): pass def _pyroBatch(self): return Pyro4.core._BatchProxyAdapter(self) def _pyroInvokeBatch(self, calls, oneway=False): self.result = [] for methodname, args, kwargs in calls: if methodname == "error": self.result.append(Pyro4.futures._ExceptionWrapper(ValueError("some exception"))) break # stop processing the rest, this is what Pyro should do in case of an error in a batch elif methodname == "pause": time.sleep(args[0]) self.result.append("INVOKED %s args=%s kwargs=%s" % (methodname, args, kwargs)) if oneway: return else: return self.result class AsyncProxyMock(object): def __copy__(self): return self def __enter__(self): return self def __exit__(self, *args): pass def _pyroAsync(self, asynchronous=True): return self def __getattr__(self, item): return Pyro4.core._AsyncRemoteMethod(self, item, 5) def _pyroInvoke(self, methodname, vargs, kwargs, flags=0): if methodname == "pause_and_divide": time.sleep(vargs[0]) return vargs[1] // vargs[2] else: raise NotImplementedError(methodname) def testRemoteMethodMetaOff(self): try: config.METADATA = False class ProxyMock(object): def invoke(self, name, args, kwargs): return "INVOKED name=%s args=%s kwargs=%s" % (name, args, kwargs) def __getattr__(self, name): return Pyro4.core._RemoteMethod(self.invoke, name, max_retries=0) o = ProxyMock() self.assertEqual("INVOKED name=foo args=(1,) kwargs={}", o.foo(1)) # normal self.assertEqual("INVOKED name=foo.bar args=(1,) kwargs={}", o.foo.bar(1)) # dotted self.assertEqual("INVOKED name=foo.bar args=(1, 'hello') kwargs={'a': True}", o.foo.bar(1, "hello", a=True)) p = Pyro4.core.Proxy("PYRO:obj@host:666") a = p.someattribute self.assertIsInstance(a, Pyro4.core._RemoteMethod, "attribute access should just be a RemoteMethod") a2 = a.nestedattribute.nested2 self.assertIsInstance(a2, Pyro4.core._RemoteMethod, "nested attribute should just be another RemoteMethod") p._pyroRelease() finally: config.METADATA = True def testRemoteMethodMetaOn(self): p = Pyro4.core.Proxy("PYRO:obj@localhost:59999") with self.assertRaises(Pyro4.errors.CommunicationError): _ = p.someattribute # triggers attempt to get metadata p._pyroRelease() def testBatchMethod(self): proxy = self.BatchProxyMock() batch = Pyro4.core.batch(proxy) self.assertIsNone(batch.foo(42)) self.assertIsNone(batch.bar("abc")) self.assertIsNone(batch.baz(42, "abc", arg=999)) self.assertIsNone(batch.error()) # generate an exception self.assertIsNone(batch.foo(42)) # this call should not be performed after the error results = batch() result = next(results) self.assertEqual("INVOKED foo args=(42,) kwargs={}", result) result = next(results) self.assertEqual("INVOKED bar args=('abc',) kwargs={}", result) result = next(results) self.assertEqual("INVOKED baz args=(42, 'abc') kwargs={'arg': 999}", result) self.assertRaises(ValueError, next, results) # the call to error() should generate an exception self.assertRaises(StopIteration, next, results) # and now there should not be any more results self.assertEqual(4, len(proxy.result)) # should have done 4 calls, not 5 batch._pyroRelease() def testBatchMethodOneway(self): proxy = self.BatchProxyMock() batch = Pyro4.core.batch(proxy) self.assertIsNone(batch.foo(42)) self.assertIsNone(batch.bar("abc")) self.assertIsNone(batch.baz(42, "abc", arg=999)) self.assertIsNone(batch.error()) # generate an exception self.assertIsNone(batch.foo(42)) # this call should not be performed after the error results = batch(oneway=True) self.assertIsNone(results) # oneway always returns None self.assertEqual(4, len(proxy.result)) # should have done 4 calls, not 5 self.assertRaises(Pyro4.errors.PyroError, batch, oneway=True, asynchronous=True) # oneway+asynchronous=booboo def testBatchMethodAsync(self): proxy = self.BatchProxyMock() batch = Pyro4.core.batch(proxy) self.assertIsNone(batch.foo(42)) self.assertIsNone(batch.bar("abc")) self.assertIsNone(batch.pause(0.5)) # pause shouldn't matter with asynchronous self.assertIsNone(batch.baz(42, "abc", arg=999)) begin = time.time() asyncresult = batch(asynchronous=True) duration = time.time() - begin self.assertLess(duration, 0.2, "batch oneway with pause should still return almost immediately") results = asyncresult.value self.assertEqual(4, len(proxy.result)) # should have done 4 calls result = next(results) self.assertEqual("INVOKED foo args=(42,) kwargs={}", result) result = next(results) self.assertEqual("INVOKED bar args=('abc',) kwargs={}", result) result = next(results) self.assertEqual("INVOKED pause args=(0.5,) kwargs={}", result) result = next(results) self.assertEqual("INVOKED baz args=(42, 'abc') kwargs={'arg': 999}", result) self.assertRaises(StopIteration, next, results) # and now there should not be any more results def testBatchMethodReuse(self): proxy = self.BatchProxyMock() batch = Pyro4.core.batch(proxy) batch.foo(1) batch.foo(2) results = batch() self.assertEqual(['INVOKED foo args=(1,) kwargs={}', 'INVOKED foo args=(2,) kwargs={}'], list(results)) # re-use the batch proxy: batch.foo(3) batch.foo(4) results = batch() self.assertEqual(['INVOKED foo args=(3,) kwargs={}', 'INVOKED foo args=(4,) kwargs={}'], list(results)) results = batch() self.assertEqual(0, len(list(results))) def testChangedAsyncLegacyApi(self): try: Pyro4.config.METADATA = False proxy = Pyro4.Proxy("PYRO:test@addr:5555") self.assertIsNone(proxy._pyroAsync()) m = proxy.foo self.assertIsInstance(m, Pyro4.core._AsyncRemoteMethod) self.assertIsNone(proxy._pyroAsync(False)) m = proxy.foo self.assertIsInstance(m, Pyro4.core._RemoteMethod) self.assertIsNone(Pyro4.asyncproxy(proxy)) m = proxy.foo self.assertIsInstance(m, Pyro4.core._AsyncRemoteMethod) self.assertIsNone(Pyro4.asyncproxy(proxy, False)) m = proxy.foo self.assertIsInstance(m, Pyro4.core._RemoteMethod) finally: Pyro4.config.METADATA = True def testAsyncMethod(self): proxy = self.AsyncProxyMock() Pyro4.core.asyncproxy(proxy) begin = time.time() result = proxy.pause_and_divide(0.2, 10, 2) # returns immediately duration = time.time() - begin self.assertLess(duration, 0.1) self.assertFalse(result.ready) _ = result.value self.assertTrue(result.ready) proxy._pyroRelease() def testAsyncCallbackMethod(self): class AsyncFunctionHolder(object): asyncFunctionCount = 0 def asyncFunction(self, value, amount=1): self.asyncFunctionCount += 1 return value + amount proxy = self.AsyncProxyMock() Pyro4.core.asyncproxy(proxy) result = proxy.pause_and_divide(0.2, 10, 2) # returns immediately holder = AsyncFunctionHolder() result.then(holder.asyncFunction, amount=2) \ .then(holder.asyncFunction, amount=4) \ .then(holder.asyncFunction) value = result.value self.assertEqual(10 // 2 + 2 + 4 + 1, value) self.assertEqual(3, holder.asyncFunctionCount) proxy._pyroRelease() def testCrashingAsyncCallbackMethod(self): def normalAsyncFunction(value, x): return value + x def crashingAsyncFunction(value): return 1 // 0 # crash proxy = self.AsyncProxyMock() Pyro4.core.asyncproxy(proxy) result = proxy.pause_and_divide(0.2, 10, 2) # returns immediately result.then(crashingAsyncFunction).then(normalAsyncFunction, 2) try: _ = result.value self.fail("expected exception") except ZeroDivisionError: pass # ok proxy._pyroRelease() def testAsyncMethodTimeout(self): proxy = self.AsyncProxyMock() Pyro4.core.asyncproxy(proxy) result = proxy.pause_and_divide(1, 10, 2) # returns immediately self.assertFalse(result.ready) self.assertFalse(result.wait(0.5)) # won't be ready after 0.5 sec self.assertTrue(result.wait(1)) # will be ready within 1 seconds more self.assertTrue(result.ready) self.assertEqual(5, result.value) proxy._pyroRelease() class TestSimpleServe(unittest.TestCase): class DaemonWrapper(Pyro4.core.Daemon): def requestLoop(self, *args): # override with empty method to fall out of the serveSimple call pass def testSimpleServe(self): with TestSimpleServe.DaemonWrapper() as d: o1 = MyThingPartlyExposed(1) o2 = MyThingPartlyExposed(2) objects = {o1: "test.o1", o2: None} Pyro4.core.Daemon.serveSimple(objects, daemon=d, ns=False, verbose=False) self.assertEqual(3, len(d.objectsById)) self.assertIn("test.o1", d.objectsById) self.assertIn(o1, d.objectsById.values()) self.assertIn(o2, d.objectsById.values()) def testSimpleServeSameNames(self): with TestSimpleServe.DaemonWrapper() as d: o1 = MyThingPartlyExposed(1) o2 = MyThingPartlyExposed(2) o3 = MyThingPartlyExposed(3) objects = {o1: "test.name", o2: "test.name", o3: "test.othername"} with self.assertRaises(Pyro4.errors.DaemonError): Pyro4.core.Daemon.serveSimple(objects, daemon=d, ns=False, verbose=False) def futurestestfunc(a, b, extra=None): if extra is None: return a + b else: return a + b + extra def crashingfuturestestfunc(a): return 1 // 0 # crash class FuturesErrorHandlerStorage(object): def __init__(self): self.error = None def errorhandler(self, err): self.error = err class TestFutures(unittest.TestCase): def testSimpleFuture(self): f = Pyro4.futures.Future(futurestestfunc) r = f(4, 5) self.assertIsInstance(r, Pyro4.futures.FutureResult) value = r.value self.assertEqual(9, value) def testFutureChain(self): f = Pyro4.futures.Future(futurestestfunc) \ .then(futurestestfunc, 6) \ .then(futurestestfunc, 7) \ .then(futurestestfunc, 8) \ .then(futurestestfunc, 9, extra=10) r = f(4, 5) value = r.value self.assertEqual(4 + 5 + 6 + 7 + 8 + 9 + 10, value) def testCrashingChain(self): f = Pyro4.futures.Future(futurestestfunc) \ .then(futurestestfunc, 6) \ .then(crashingfuturestestfunc) \ .then(futurestestfunc, 8) r = f(4, 5) try: _ = r.value self.fail("expected exception") except ZeroDivisionError: pass # ok def testErrorHandler(self): storage = FuturesErrorHandlerStorage() f = Pyro4.futures.Future(crashingfuturestestfunc) \ .then(futurestestfunc, 5) \ .iferror(storage.errorhandler) \ .then(futurestestfunc, 6) self.assertIsNone(storage.error) r = f(42) try: _ = r.value except ZeroDivisionError: pass # ok self.assertIsInstance(storage.error, ZeroDivisionError) def testFutureResultChainSlow(self): f = Pyro4.futures.Future(futurestestfunc) result = f(4, 5) time.sleep(.02) result.then(futurestestfunc, 6) time.sleep(.02) result.then(futurestestfunc, 7) time.sleep(.02) result.then(futurestestfunc, 8) time.sleep(.02) result.then(futurestestfunc, 9) time.sleep(.02) result.then(futurestestfunc, 10) time.sleep(.02) value = result.value self.assertEqual(4 + 5 + 6 + 7 + 8 + 9 + 10, value) def testFutureResultChain(self): f = Pyro4.futures.Future(futurestestfunc) result = f(4, 5).then(futurestestfunc, 6).then(futurestestfunc, 7).then(futurestestfunc, 8).then(futurestestfunc, 9).then(futurestestfunc, 10) value = result.value self.assertEqual(4 + 5 + 6 + 7 + 8 + 9 + 10, value) with self.assertRaises(RuntimeError): f(4, 5) # cannot evaluate the same future more than once def testFutureDelay(self): f = Pyro4.futures.Future(futurestestfunc) b = f.delay(0) self.assertTrue(b) begin = time.time() f(4, 5).value duration = time.time() - begin self.assertLess(duration, 0.1) f = Pyro4.futures.Future(futurestestfunc) b = f.delay(1) self.assertTrue(b) begin = time.time() r = f(4, 5) duration = time.time() - begin self.assertLess(duration, 0.1) begin = time.time() r.value duration = time.time() - begin self.assertGreaterEqual(duration, 0.98) self.assertLess(duration, 1.1) self.assertFalse(f.delay(10)) def testFutureCancel(self): f = Pyro4.futures.Future(futurestestfunc) f.delay(10) b = f.cancel() self.assertTrue(b) with self.assertRaises(RuntimeError) as x: f(4, 5) self.assertTrue("cancelled" in str(x.exception)) f = Pyro4.futures.Future(futurestestfunc) f.delay(10) result = f(4, 5) b = f.cancel() self.assertTrue(b) success = result.wait(3) self.assertTrue(success) with self.assertRaises(RuntimeError) as x: result.value self.assertTrue("cancelled" in str(x.exception)) f = Pyro4.futures.Future(futurestestfunc) result = f(4, 5) result.value b = f.cancel() self.assertFalse(b) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() Pyro4-4.82/tests/PyroTests/test_daemon.py000066400000000000000000000766171416147301300204770ustar00rootroot00000000000000""" Tests for the daemon. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import sys import time import socket import uuid import unittest import Pyro4.core import Pyro4.naming import Pyro4.constants import Pyro4.socketutil import Pyro4.message import Pyro4.util from Pyro4.errors import DaemonError, PyroError, SerializeError from Pyro4.configuration import config from Pyro4.core import current_context from testsupport import * class MyObj(object): def __init__(self, arg): self.arg = arg def __eq__(self, other): return self.arg == other.arg __hash__ = object.__hash__ class CustomDaemonInterface(Pyro4.core.DaemonObject): def __init__(self, daemon): super(CustomDaemonInterface, self).__init__(daemon) def custom_daemon_method(self): return 42 class DaemonTests(unittest.TestCase): # We create a daemon, but notice that we are not actually running the requestloop. # 'on-line' tests are all taking place in another test, to keep this one simple. def setUp(self): config.POLLTIMEOUT = 0.1 def sendHandshakeMessage(self, conn, correlation_id=None): ser = Pyro4.util.get_serializer_by_id(Pyro4.util.MarshalSerializer.serializer_id) data, _ = ser.serializeData({"handshake": "hello", "object": Pyro4.constants.DAEMON_NAME}, False) annotations = {"CORR": correlation_id.bytes} if correlation_id else None msg = Pyro4.message.Message(Pyro4.message.MSG_CONNECT, data, Pyro4.util.MarshalSerializer.serializer_id, 0, 99, annotations=annotations) msg.send(conn) def testSerializerConfig(self): self.assertIsInstance(config.SERIALIZERS_ACCEPTED, set) self.assertIsInstance(config.SERIALIZER, basestring) self.assertGreater(len(config.SERIALIZERS_ACCEPTED), 1) def testSerializerAccepted(self): self.assertIn("marshal", config.SERIALIZERS_ACCEPTED) self.assertNotIn("pickle", config.SERIALIZERS_ACCEPTED) self.assertNotIn("cloudpickle", config.SERIALIZERS_ACCEPTED) self.assertNotIn("dill", config.SERIALIZERS_ACCEPTED) with Pyro4.core.Daemon(port=0) as d: msg = Pyro4.message.Message(Pyro4.message.MSG_INVOKE, b"", Pyro4.util.MarshalSerializer.serializer_id, 0, 0, hmac_key=d._pyroHmacKey) cm = ConnectionMock(msg) d.handleRequest(cm) # marshal serializer should be accepted msg = Pyro4.message.Message(Pyro4.message.MSG_INVOKE, b"", Pyro4.util.PickleSerializer.serializer_id, 0, 0, hmac_key=d._pyroHmacKey) cm = ConnectionMock(msg) try: d.handleRequest(cm) self.fail("should crash") except Pyro4.errors.ProtocolError as x: self.assertIn("serializer that is not accepted", str(x)) pass msg = Pyro4.message.Message(Pyro4.message.MSG_INVOKE, b"", Pyro4.util.CloudpickleSerializer.serializer_id, 0, 0, hmac_key=d._pyroHmacKey) cm = ConnectionMock(msg) try: d.handleRequest(cm) self.fail("should crash") except Pyro4.errors.ProtocolError as x: self.assertTrue("no serializer available for id" in str(x) or "serializer that is not accepted" in str(x)) msg = Pyro4.message.Message(Pyro4.message.MSG_INVOKE, b"", Pyro4.util.DillSerializer.serializer_id, 0, 0, hmac_key=d._pyroHmacKey) cm = ConnectionMock(msg) try: d.handleRequest(cm) self.fail("should crash") except Pyro4.errors.ProtocolError as x: self.assertTrue("no serializer available for id" in str(x) or "serializer that is not accepted" in str(x)) def testDaemon(self): with Pyro4.core.Daemon(port=0) as d: hostname, port = d.locationStr.split(":") port = int(port) self.assertIn(Pyro4.constants.DAEMON_NAME, d.objectsById) self.assertEqual("PYRO:" + Pyro4.constants.DAEMON_NAME + "@" + d.locationStr, str(d.uriFor(Pyro4.constants.DAEMON_NAME))) # check the string representations expected = "" % (id(d), d.locationStr, Pyro4.socketutil.family_str(d.sock)) self.assertEqual(expected, str(d)) self.assertEqual(expected, unicode(d)) self.assertEqual(expected, repr(d)) sockname = d.sock.getsockname() self.assertEqual(port, sockname[1]) daemonobj = d.objectsById[Pyro4.constants.DAEMON_NAME] daemonobj.ping() daemonobj.registered() try: daemonobj.shutdown() self.fail("should not succeed to call unexposed method") except AttributeError: pass def testDaemonCustomInterface(self): with Pyro4.core.Daemon(port=0, interface=CustomDaemonInterface) as d: obj = d.objectsById[Pyro4.constants.DAEMON_NAME] self.assertEqual(42, obj.custom_daemon_method()) def testDaemonConnectedSocket(self): try: Pyro4.config.SERVERTYPE = "thread" with Pyro4.core.Daemon() as d: self.assertTrue("Thread" in d.transportServer.__class__.__name__) s1, s2 = socket.socketpair() with Pyro4.core.Daemon(connected_socket=s1) as d: self.assertTrue(d.locationStr=="./u:<>" or d.locationStr.startswith("127.0.")) self.assertFalse("Thread" in d.transportServer.__class__.__name__) self.assertTrue("Existing" in d.transportServer.__class__.__name__) Pyro4.config.SERVERTYPE = "multiplex" with Pyro4.core.Daemon() as d: self.assertTrue("Multiplex" in d.transportServer.__class__.__name__) s1, s2 = socket.socketpair() with Pyro4.core.Daemon(connected_socket=s1) as d: self.assertTrue(d.locationStr=="./u:<>" or d.locationStr.startswith("127.0.")) self.assertFalse("Multiplex" in d.transportServer.__class__.__name__) self.assertTrue("Existing" in d.transportServer.__class__.__name__) finally: Pyro4.config.SERVERTYPE = "thread" @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "unix domain sockets required") def testDaemonUnixSocket(self): SOCKNAME = "test_unixsocket" with Pyro4.core.Daemon(unixsocket=SOCKNAME) as d: locationstr = "./u:" + SOCKNAME self.assertEqual(locationstr, d.locationStr) self.assertEqual("PYRO:" + Pyro4.constants.DAEMON_NAME + "@" + locationstr, str(d.uriFor(Pyro4.constants.DAEMON_NAME))) # check the string representations expected = "" % (id(d), locationstr) self.assertEqual(expected, str(d)) self.assertEqual(SOCKNAME, d.sock.getsockname()) self.assertEqual(socket.AF_UNIX, d.sock.family) @unittest.skipUnless(hasattr(socket, "AF_UNIX") and sys.platform.startswith("linux"), "unix domain sockets required and linux os") def testDaemonUnixSocketAbstractNS(self): SOCKNAME = "\0test_unixsocket" # mind the \0 at the start, for a Linux abstract namespace socket with Pyro4.core.Daemon(unixsocket=SOCKNAME) as d: locationstr = "./u:" + SOCKNAME self.assertEqual(locationstr, d.locationStr) self.assertEqual("PYRO:" + Pyro4.constants.DAEMON_NAME + "@" + locationstr, str(d.uriFor(Pyro4.constants.DAEMON_NAME))) # check the string representations expected = "" % (id(d), locationstr) self.assertEqual(expected, str(d)) sn_bytes = tobytes(SOCKNAME) self.assertEqual(sn_bytes, d.sock.getsockname()) self.assertEqual(socket.AF_UNIX, d.sock.family) def testServertypeThread(self): old_servertype = config.SERVERTYPE config.SERVERTYPE = "thread" with Pyro4.core.Daemon(port=0) as d: self.assertIn(d.sock, d.sockets, "daemon's socketlist should contain the server socket") self.assertTrue(len(d.sockets) == 1, "daemon without connections should have just 1 socket") config.SERVERTYPE = old_servertype def testServertypeMultiplex(self): old_servertype = config.SERVERTYPE config.SERVERTYPE = "multiplex" with Pyro4.core.Daemon(port=0) as d: self.assertIn(d.sock, d.sockets, "daemon's socketlist should contain the server socket") self.assertTrue(len(d.sockets) == 1, "daemon without connections should have just 1 socket") config.SERVERTYPE = old_servertype def testServertypeFoobar(self): old_servertype = config.SERVERTYPE config.SERVERTYPE = "foobar" self.assertRaises(PyroError, Pyro4.core.Daemon) config.SERVERTYPE = old_servertype def testRegisterTwice(self): with Pyro4.core.Daemon(port=0) as d: o1 = MyObj("object1") d.register(o1) with self.assertRaises(DaemonError) as x: d.register(o1) self.assertEqual("object or class already has a Pyro id", str(x.exception)) d.unregister(o1) d.register(o1, "samename") o2 = MyObj("object2") with self.assertRaises(DaemonError) as x: d.register(o2, "samename") self.assertEqual("an object or class is already registered with that id", str(x.exception)) self.assertTrue(hasattr(o1, "_pyroId")) self.assertTrue(hasattr(o1, "_pyroDaemon")) d.unregister(o1) self.assertFalse(hasattr(o1, "_pyroId")) self.assertFalse(hasattr(o1, "_pyroDaemon")) o1._pyroId = "FOOBAR" with self.assertRaises(DaemonError) as x: d.register(o1) self.assertEqual("object or class already has a Pyro id", str(x.exception)) o1._pyroId = "" d.register(o1) # with empty-string _pyroId register should work def testRegisterTwiceForced(self): with Pyro4.core.Daemon(port=0) as d: o1 = MyObj("object1") d.register(o1, "name1") d.register(o1, "name2", force=True) d.register(o1, "name1", force=True) self.assertIs(d.objectsById["name1"], d.objectsById["name2"]) d.unregister(o1) o1._pyroId = "FOOBAR_ID" d.register(o1, "newname", force=True) self.assertEqual("newname", o1._pyroId) self.assertIn("newname", d.objectsById) def testRegisterEtc(self): with Pyro4.core.Daemon(port=0) as d: self.assertEqual(1, len(d.objectsById)) o1 = MyObj("object1") o2 = MyObj("object2") d.register(o1) self.assertRaises(DaemonError, d.register, o2, Pyro4.constants.DAEMON_NAME) # cannot use daemon name d.register(o2, "obj2a") self.assertEqual(3, len(d.objectsById)) self.assertEqual(o1, d.objectsById[o1._pyroId]) self.assertEqual(o2, d.objectsById["obj2a"]) self.assertEqual("obj2a", o2._pyroId) self.assertEqual(d, o2._pyroDaemon) # test unregister d.unregister("unexisting_thingie") self.assertRaises(ValueError, d.unregister, None) d.unregister("obj2a") d.unregister(o1._pyroId) self.assertEqual(1, len(d.objectsById)) self.assertNotIn(o1._pyroId, d.objectsById) self.assertNotIn(o2._pyroId, d.objectsById) # test unregister objects del o2._pyroId d.register(o2) objectid = o2._pyroId self.assertIn(objectid, d.objectsById) self.assertEqual(2, len(d.objectsById)) d.unregister(o2) # no more _pyro attributes must remain after unregistering for attr in vars(o2): self.assertFalse(attr.startswith("_pyro")) self.assertEqual(1, len(d.objectsById)) self.assertNotIn(objectid, d.objectsById) self.assertRaises(DaemonError, d.unregister, [1, 2, 3]) # test unregister daemon name d.unregister(Pyro4.constants.DAEMON_NAME) self.assertIn(Pyro4.constants.DAEMON_NAME, d.objectsById) # weird args w = MyObj("weird") self.assertRaises(AttributeError, d.register, None) self.assertRaises(AttributeError, d.register, 4444) self.assertRaises(TypeError, d.register, w, 666) # uri return value from register uri = d.register(MyObj("xyz")) self.assertIsInstance(uri, Pyro4.core.URI) uri = d.register(MyObj("xyz"), "test.register") self.assertEqual("test.register", uri.object) def testRegisterClass(self): with Pyro4.core.Daemon(port=0) as d: self.assertEqual(1, len(d.objectsById)) d.register(MyObj) with self.assertRaises(DaemonError): d.register(MyObj) self.assertEqual(2, len(d.objectsById)) d.uriFor(MyObj) # unregister: d.unregister(MyObj) self.assertEqual(1, len(d.objectsById)) def testRegisterUnicode(self): with Pyro4.core.Daemon(port=0) as d: myobj1 = MyObj("hello1") myobj2 = MyObj("hello2") myobj3 = MyObj("hello3") uri1 = d.register(myobj1, "str_name") uri2 = d.register(myobj2, unicode("unicode_name")) uri3 = d.register(myobj3, "unicode_" + unichr(0x20ac)) self.assertEqual(4, len(d.objectsById)) uri = d.uriFor(myobj1) self.assertEqual(uri1, uri) _ = Pyro4.core.Proxy(uri) uri = d.uriFor(myobj2) self.assertEqual(uri2, uri) _ = Pyro4.core.Proxy(uri) uri = d.uriFor(myobj3) self.assertEqual(uri3, uri) _ = Pyro4.core.Proxy(uri) uri = d.uriFor("str_name") self.assertEqual(uri1, uri) _ = Pyro4.core.Proxy(uri) uri = d.uriFor(unicode("unicode_name")) self.assertEqual(uri2, uri) _ = Pyro4.core.Proxy(uri) uri = d.uriFor("unicode_" + unichr(0x20ac)) self.assertEqual(uri3, uri) _ = Pyro4.core.Proxy(uri) def testDaemonObject(self): with Pyro4.core.Daemon(port=0) as d: daemon = Pyro4.core.DaemonObject(d) obj1 = MyObj("object1") obj2 = MyObj("object2") obj3 = MyObj("object2") d.register(obj1, "obj1") d.register(obj2, "obj2") d.register(obj3) daemon.ping() registered = daemon.registered() self.assertTrue(type(registered) is list) self.assertEqual(4, len(registered)) self.assertIn("obj1", registered) self.assertIn("obj2", registered) self.assertIn(obj3._pyroId, registered) try: daemon.shutdown() self.fail("should not succeed to call unexposed method") except AttributeError: pass def testUriFor(self): d = Pyro4.core.Daemon(port=0) try: o1 = MyObj("object1") o2 = MyObj("object2") self.assertRaises(DaemonError, d.uriFor, o1) self.assertRaises(DaemonError, d.uriFor, o2) d.register(o1, None) d.register(o2, "object_two") o3 = MyObj("object3") self.assertRaises(DaemonError, d.uriFor, o3) # can't get an uri for an unregistered object (note: unregistered name is ok) u1 = d.uriFor(o1) u2 = d.uriFor(o2._pyroId) u3 = d.uriFor("unexisting_thingie") # unregistered name is no problem, it's just an uri we're requesting u4 = d.uriFor(o2) self.assertEqual(Pyro4.core.URI, type(u1)) self.assertEqual("PYRO", u1.protocol) self.assertEqual("PYRO", u2.protocol) self.assertEqual("PYRO", u3.protocol) self.assertEqual("PYRO", u4.protocol) self.assertEqual("object_two", u4.object) self.assertEqual(Pyro4.core.URI("PYRO:unexisting_thingie@" + d.locationStr), u3) finally: d.close() def testDaemonWithStmt(self): d = Pyro4.core.Daemon() self.assertIsNotNone(d.transportServer) d.close() # closes the transportserver and sets it to None self.assertIsNone(d.transportServer) with Pyro4.core.Daemon() as d: self.assertIsNotNone(d.transportServer) pass self.assertIsNone(d.transportServer) try: with Pyro4.core.Daemon() as d: print(1 // 0) # cause an error self.fail("expected error") except ZeroDivisionError: pass self.assertIsNone(d.transportServer) d = Pyro4.core.Daemon() with d: pass try: with d: pass self.fail("expected error") except PyroError: # you cannot re-use a daemon object in multiple with statements pass d.close() def testRequestloopCondition(self): with Pyro4.core.Daemon(port=0) as d: condition = lambda: False start = time.time() d.requestLoop(loopCondition=condition) # this should return almost immediately duration = time.time() - start self.assertLess(duration, 0.4) def testSimpleHandshake(self): conn = ConnectionMock() with Pyro4.core.Daemon(port=0) as d: self.sendHandshakeMessage(conn) success = d._handshake(conn) self.assertTrue(success) msg = Pyro4.message.Message.recv(conn, hmac_key=d._pyroHmacKey) self.assertEqual(Pyro4.message.MSG_CONNECTOK, msg.type) self.assertEqual(99, msg.seq) def testHandshakeDenied(self): class HandshakeFailDaemon(Pyro4.core.Daemon): def validateHandshake(self, conn, data): raise ValueError("handshake fail validation error") conn = ConnectionMock() with HandshakeFailDaemon(port=0) as d: self.sendHandshakeMessage(conn) success = d._handshake(conn) self.assertFalse(success) msg = Pyro4.message.Message.recv(conn, hmac_key=d._pyroHmacKey) self.assertEqual(Pyro4.message.MSG_CONNECTFAIL, msg.type) self.assertEqual(99, msg.seq) self.assertTrue(b"handshake fail validation error" in msg.data) with Pyro4.core.Daemon(port=0) as d: self.sendHandshakeMessage(conn) success = d._handshake(conn, denied_reason="no way, handshake denied") self.assertFalse(success) msg = Pyro4.message.Message.recv(conn, hmac_key=d._pyroHmacKey) self.assertEqual(Pyro4.message.MSG_CONNECTFAIL, msg.type) self.assertEqual(99, msg.seq) self.assertTrue(b"no way, handshake denied" in msg.data) def testCustomHandshake(self): conn = ConnectionMock() class CustomHandshakeDaemon(Pyro4.core.Daemon): def validateHandshake(self, conn, data): return ["sure", "have", "fun"] def annotations(self): return {"XYZZ": b"custom annotation set by daemon"} with CustomHandshakeDaemon(port=0) as d: corr_id = uuid.uuid4() self.sendHandshakeMessage(conn, correlation_id=corr_id) self.assertNotEqual(corr_id, current_context.correlation_id) success = d._handshake(conn) self.assertTrue(success) self.assertEqual(corr_id, current_context.correlation_id) msg = Pyro4.message.Message.recv(conn, hmac_key=d._pyroHmacKey) self.assertEqual(Pyro4.message.MSG_CONNECTOK, msg.type) self.assertEqual(99, msg.seq) self.assertEqual(2, len(msg.annotations)) self.assertEqual(corr_id.bytes, msg.annotations["CORR"]) self.assertEqual(b"custom annotation set by daemon", msg.annotations["XYZZ"]) ser = Pyro4.util.get_serializer_by_id(msg.serializer_id) data = ser.deserializeData(msg.data, msg.flags & Pyro4.message.FLAGS_COMPRESSED) self.assertEqual(["sure", "have", "fun"], data) def testNAT(self): with Pyro4.core.Daemon() as d: self.assertIsNone(d.natLocationStr) with Pyro4.core.Daemon(nathost="nathosttest", natport=12345) as d: self.assertEqual("nathosttest:12345", d.natLocationStr) self.assertNotEqual(d.locationStr, d.natLocationStr) uri = d.register(MyObj(1)) self.assertEqual("nathosttest:12345", uri.location) uri = d.uriFor("object") self.assertEqual("nathosttest:12345", uri.location) uri = d.uriFor("object", nat=False) self.assertNotEqual("nathosttest:12345", uri.location) d = Pyro4.core.Daemon(nathost="bla") self.assertTrue(d.natLocationStr.startswith("bla:")) try: _ = Pyro4.core.Daemon(natport=5555) self.fail("expected error") except ValueError: pass try: _ = Pyro4.core.Daemon(nathost="bla", natport=5555, unixsocket="testsock") self.fail("expected error") except ValueError: pass def testNATzeroPort(self): servertype = config.SERVERTYPE try: config.SERVERTYPE = "multiplex" with Pyro4.core.Daemon(nathost="nathosttest", natport=99999) as d: host, port = d.locationStr.split(":") self.assertNotEqual(99999, port) self.assertEqual("nathosttest:99999", d.natLocationStr) with Pyro4.core.Daemon(nathost="nathosttest", natport=0) as d: host, port = d.locationStr.split(":") self.assertEqual("nathosttest:%s" % port, d.natLocationStr) config.SERVERTYPE = "thread" with Pyro4.core.Daemon(nathost="nathosttest", natport=99999) as d: host, port = d.locationStr.split(":") self.assertNotEqual(99999, port) self.assertEqual("nathosttest:99999", d.natLocationStr) with Pyro4.core.Daemon(nathost="nathosttest", natport=0) as d: host, port = d.locationStr.split(":") self.assertEqual("nathosttest:%s" % port, d.natLocationStr) finally: config.SERVERTYPE = servertype def testNATconfig(self): try: config.NATHOST = None config.NATPORT = 0 with Pyro4.core.Daemon() as d: self.assertIsNone(d.natLocationStr) config.NATHOST = "nathosttest" config.NATPORT = 12345 with Pyro4.core.Daemon() as d: self.assertEqual("nathosttest:12345", d.natLocationStr) finally: config.NATHOST = None config.NATPORT = 0 def testBehaviorDefaults(self): class TestClass: pass with Pyro4.core.Daemon() as d: d.register(TestClass) instance_mode, instance_creator = TestClass._pyroInstancing self.assertEqual("session", instance_mode) self.assertIsNone(instance_creator) def testInstanceCreationSingle(self): def creator(clazz): return clazz("testname") @Pyro4.core.behavior(instance_mode="single", instance_creator=creator) class TestClass: def __init__(self, name): self.name = name conn = Pyro4.socketutil.SocketConnection(socket.socket()) d = Pyro4.core.Daemon() instance1 = d._getInstance(TestClass, conn) instance2 = d._getInstance(TestClass, conn) self.assertEqual("testname", instance1.name) self.assertIs(instance1, instance2) self.assertTrue(TestClass in d._pyroInstances) self.assertIs(instance1, d._pyroInstances[TestClass]) self.assertFalse(TestClass in conn.pyroInstances) def testBehaviorDefaultsIsSession(self): class ClassWithDefaults: def __init__(self): self.name = "yep" conn1 = Pyro4.socketutil.SocketConnection(socket.socket()) conn2 = Pyro4.socketutil.SocketConnection(socket.socket()) d = Pyro4.core.Daemon() d.register(ClassWithDefaults) instance1a = d._getInstance(ClassWithDefaults, conn1) instance1b = d._getInstance(ClassWithDefaults, conn1) instance2a = d._getInstance(ClassWithDefaults, conn2) instance2b = d._getInstance(ClassWithDefaults, conn2) self.assertIs(instance1a, instance1b) self.assertIs(instance2a, instance2b) self.assertIsNot(instance1a, instance2a) self.assertFalse(ClassWithDefaults in d._pyroInstances) self.assertTrue(ClassWithDefaults in conn1.pyroInstances) self.assertTrue(ClassWithDefaults in conn2.pyroInstances) self.assertIs(instance1a, conn1.pyroInstances[ClassWithDefaults]) self.assertIs(instance2a, conn2.pyroInstances[ClassWithDefaults]) def testInstanceCreationSession(self): def creator(clazz): return clazz("testname") @Pyro4.core.behavior(instance_mode="session", instance_creator=creator) class ClassWithDecorator: def __init__(self, name): self.name = name conn1 = Pyro4.socketutil.SocketConnection(socket.socket()) conn2 = Pyro4.socketutil.SocketConnection(socket.socket()) d = Pyro4.core.Daemon() d.register(ClassWithDecorator) # check the class with the decorator first instance1a = d._getInstance(ClassWithDecorator, conn1) instance1b = d._getInstance(ClassWithDecorator, conn1) instance2a = d._getInstance(ClassWithDecorator, conn2) instance2b = d._getInstance(ClassWithDecorator, conn2) self.assertIs(instance1a, instance1b) self.assertIs(instance2a, instance2b) self.assertIsNot(instance1a, instance2a) self.assertFalse(ClassWithDecorator in d._pyroInstances) self.assertTrue(ClassWithDecorator in conn1.pyroInstances) self.assertTrue(ClassWithDecorator in conn2.pyroInstances) self.assertIs(instance1a, conn1.pyroInstances[ClassWithDecorator]) self.assertIs(instance2a, conn2.pyroInstances[ClassWithDecorator]) def testInstanceCreationPerCall(self): def creator(clazz): return clazz("testname") @Pyro4.core.behavior(instance_mode="percall", instance_creator=creator) class TestClass: def __init__(self, name): self.name = name with Pyro4.socketutil.SocketConnection(socket.socket()) as conn: with Pyro4.core.Daemon() as d: instance1 = d._getInstance(TestClass, conn) instance2 = d._getInstance(TestClass, conn) self.assertIsNot(instance1, instance2) self.assertFalse(TestClass in d._pyroInstances) self.assertFalse(TestClass in conn.pyroInstances) def testInstanceCreationWrongType(self): def creator(clazz): return Pyro4.core.URI("PYRO:test@localhost:9999") @Pyro4.core.behavior(instance_creator=creator) class TestClass: def method(self): pass with Pyro4.socketutil.SocketConnection(socket.socket()) as conn: with Pyro4.core.Daemon() as d: with self.assertRaises(TypeError): d._getInstance(TestClass, conn) def testCombine(self): d1 = Pyro4.core.Daemon() d2 = Pyro4.core.Daemon() with self.assertRaises(TypeError): d1.combine(d2) d1.close() d2.close() try: config.SERVERTYPE = "multiplex" d1 = Pyro4.core.Daemon() d2 = Pyro4.core.Daemon() nsuri, nsd, bcd = Pyro4.naming.startNS(host="", bchost="") d1_selector = d1.transportServer.selector d1.combine(d2) d1.combine(nsd) d1.combine(bcd) self.assertIs(d1_selector, d1.transportServer.selector) self.assertIs(d1_selector, d2.transportServer.selector) self.assertIs(d1_selector, nsd.transportServer.selector) self.assertIs(d1_selector, bcd.transportServer.selector) self.assertEqual(4, len(d1.sockets)) self.assertIn(d1.sock, d1.sockets) self.assertIn(d2.sock, d1.sockets) self.assertIn(nsd.sock, d1.sockets) self.assertIn(bcd, d1.sockets) bcd.close() nsd.close() d2.close() d1.close() finally: config.SERVERTYPE = "thread" class MetaInfoTests(unittest.TestCase): def testMeta(self): with Pyro4.core.Daemon() as d: daemon_obj = d.objectsById[Pyro4.constants.DAEMON_NAME] self.assertTrue(len(daemon_obj.info()) > 10) meta = daemon_obj.get_metadata(Pyro4.constants.DAEMON_NAME) self.assertEqual({"get_metadata", "get_next_stream_item", "close_stream", "info", "ping", "registered"}, meta["methods"]) def testMetaSerialization(self): with Pyro4.core.Daemon() as d: daemon_obj = d.objectsById[Pyro4.constants.DAEMON_NAME] meta = daemon_obj.get_metadata(Pyro4.constants.DAEMON_NAME) for ser_id in [Pyro4.util.JsonSerializer.serializer_id, Pyro4.util.MarshalSerializer.serializer_id, Pyro4.util.PickleSerializer.serializer_id, Pyro4.util.SerpentSerializer.serializer_id]: serializer = Pyro4.util.get_serializer_by_id(ser_id) data = serializer.dumps(meta) _ = serializer.loads(data) try: serializer = Pyro4.util.get_serializer_by_id(Pyro4.util.DillSerializer.serializer_id) except SerializeError: # dill doesn't work with ironpython so we allow an error here in that case if sys.platform != "cli": raise else: data = serializer.dumps(meta) _ = serializer.loads(data) def testMetaResetCache(self): class Dummy: @Pyro4.core.expose def method(self): pass with Pyro4.core.Daemon() as d: dummy = Dummy() uri = d.register(dummy) daemon_obj = d.objectsById[Pyro4.constants.DAEMON_NAME] meta = daemon_obj.get_metadata(uri.object) self.assertNotIn("newly_added_method", meta["methods"]) self.assertNotIn("newly_added_method_two", meta["methods"]) Dummy.newly_added_method = Pyro4.core.expose(lambda self: None) meta = daemon_obj.get_metadata(uri.object) self.assertNotIn("newly_added_method", meta["methods"]) d.resetMetadataCache(uri.object) meta = daemon_obj.get_metadata(uri.object) self.assertIn("newly_added_method", meta["methods"]) Dummy.newly_added_method_two = Pyro4.core.expose(lambda self: None) d.resetMetadataCache(dummy) meta = daemon_obj.get_metadata(uri.object) self.assertIn("newly_added_method_two", meta["methods"]) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() Pyro4-4.82/tests/PyroTests/test_echoserver.py000066400000000000000000000055541416147301300213710ustar00rootroot00000000000000""" Tests for the built-in test echo server. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import time import unittest from threading import Thread, Event import Pyro4.test.echoserver as echoserver import Pyro4.util from Pyro4.configuration import config class EchoServerThread(Thread): def __init__(self): super(EchoServerThread, self).__init__() self.setDaemon(True) self.started = Event() self.echodaemon = self.echoserver = self.uri = None def run(self): self.echodaemon, self.echoserver, self.uri = echoserver.main(args=["-q"], returnWithoutLooping=True) self.started.set() self.echodaemon.requestLoop(loopCondition=lambda: not self.echoserver._must_shutdown) class TestEchoserver(unittest.TestCase): def setUp(self): self.echoserverthread = EchoServerThread() self.echoserverthread.start() self.echoserverthread.started.wait() self.uri = self.echoserverthread.uri def tearDown(self): self.echoserverthread.echodaemon.shutdown() time.sleep(0.02) self.echoserverthread.join() config.SERVERTYPE = "thread" def testExposed(self): e = Pyro4.test.echoserver.EchoServer() self.assertTrue(hasattr(e, "_pyroExposed")) def testEcho(self): with Pyro4.core.Proxy(self.uri) as echo: try: self.assertEqual("hello", echo.echo("hello")) self.assertEqual(None, echo.echo(None)) self.assertEqual([1, 2, 3], echo.echo([1, 2, 3])) finally: echo.shutdown() def testError(self): with Pyro4.core.Proxy(self.uri) as echo: try: echo.error() self.fail("expected exception") except Exception as x: tb = "".join(Pyro4.util.getPyroTraceback()) self.assertIn("Remote traceback", tb) self.assertIn("ValueError", tb) self.assertEqual("expected error from echoserver error() method", str(x)) try: echo.error_with_text() self.fail("expected exception") except Exception as x: tb = "".join(Pyro4.util.getPyroTraceback()) self.assertIn("Remote traceback", tb) self.assertIn("ValueError", tb) self.assertEqual("the message of the error", str(x)) def testGenerator(self): with Pyro4.core.Proxy(self.uri) as echo: remotegenerator = echo.generator() self.assertIsInstance(remotegenerator, Pyro4.core._StreamResultIterator) next(remotegenerator) next(remotegenerator) next(remotegenerator) with self.assertRaises(StopIteration): next(remotegenerator) if __name__ == "__main__": unittest.main() Pyro4-4.82/tests/PyroTests/test_flame.py000066400000000000000000000104541416147301300203030ustar00rootroot00000000000000""" Tests for Pyro Flame. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import sys import unittest import Pyro4.utils.flame import Pyro4.utils.flameserver import Pyro4.core import Pyro4.errors import Pyro4.constants from Pyro4.configuration import config from testsupport import * class FlameDisabledTests(unittest.TestCase): def testFlameDisabled(self): with Pyro4.core.Daemon() as d: with self.assertRaises(Pyro4.errors.SecurityError) as ex: Pyro4.utils.flame.start(d) self.assertIn("disabled", str(ex.exception)) # default should be disabled even when pickle is activated def testRequirePickleExclusively(self): with Pyro4.core.Daemon() as d: config.FLAME_ENABLED = True sers = config.SERIALIZERS_ACCEPTED config.SERIALIZERS_ACCEPTED = {"json", "serpent", "marshal"} self.assertRaises(Pyro4.errors.SerializeError, Pyro4.utils.flame.start, d) # require pickle config.SERIALIZERS_ACCEPTED.add("pickle") self.assertRaises(Pyro4.errors.SerializeError, Pyro4.utils.flame.start, d) # require pickle exclusively config.SERIALIZERS_ACCEPTED = {"pickle"} Pyro4.utils.flame.start(d) config.SERIALIZERS_ACCEPTED = sers config.FLAME_ENABLED = False class FlameTests(unittest.TestCase): def setUp(self): config.FLAME_ENABLED = True self._serializers = config.SERIALIZERS_ACCEPTED config.SERIALIZERS_ACCEPTED = {"pickle"} def tearDown(self): config.FLAME_ENABLED = False config.SERIALIZERS_ACCEPTED = self._serializers def testExposed(self): e = Pyro4.utils.flame.Flame() self.assertTrue(hasattr(e, "_pyroExposed")) def testCreateModule(self): module = Pyro4.utils.flame.createModule("testmodule", "def x(y): return y*y") self.assertEqual(9, module.x(3)) module = Pyro4.utils.flame.createModule("testmodule2.submodule.subsub", "def x(y): return y*y") self.assertEqual(9, module.x(3)) import testmodule2.submodule.subsub self.assertEqual(9, testmodule2.submodule.subsub.x(3)) def testCreateModuleNamespace(self): namespace = {} Pyro4.utils.flame.createModule("testmodule2.submodule.subsub", "def x(y): return y*y", namespace=namespace) self.assertEqual(9, namespace["testmodule2"].submodule.subsub.x(3)) def testExecFunction(self): namespace = {} Pyro4.utils.flame.exec_function("foobar=5+6", "", namespace) self.assertEqual(11, namespace["foobar"]) def testExecFunctionNewlines(self): namespace = {} Pyro4.utils.flame.exec_function("if True:\r\n foobar=5+6\r\n ", "", namespace) self.assertEqual(11, namespace["foobar"]) def testFlameModule(self): with Pyro4.core.Daemon() as d: Pyro4.utils.flame.start(d) flameserver = d.objectsById[Pyro4.constants.FLAME_NAME] with Pyro4.utils.flame.FlameModule(flameserver, "sys") as m: self.assertIn("module 'sys' at", str(m)) self.assertIsInstance(m.exc_info, Pyro4.core._RemoteMethod) def testFlameBuiltin(self): with Pyro4.core.Daemon() as d: Pyro4.utils.flame.start(d) flameserver = d.objectsById[Pyro4.constants.FLAME_NAME] with Pyro4.utils.flame.FlameBuiltin(flameserver, "max") as builtin: self.assertTrue(hasattr(builtin, "__call__")) self.assertIn("builtin 'max' at", str(builtin)) def testFlameserverMain(self): oldstdout = sys.stdout oldstderr = sys.stderr try: sys.stdout = StringIO() sys.stderr = StringIO() self.assertRaises(SystemExit, Pyro4.utils.flameserver.main, ["--invalidarg"]) self.assertTrue("no such option" in sys.stderr.getvalue()) sys.stderr.truncate(0) sys.stdout.truncate(0) self.assertRaises(SystemExit, Pyro4.utils.flameserver.main, ["-h"]) self.assertTrue("show this help message" in sys.stdout.getvalue()) finally: sys.stdout = oldstdout sys.stderr = oldstderr if __name__ == "__main__": # import sys; sys.argv = ['', 'Test.testName'] unittest.main() Pyro4-4.82/tests/PyroTests/test_httpgateway.py000066400000000000000000000157671416147301300215740ustar00rootroot00000000000000""" Tests for the http gateway. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import json from wsgiref.util import setup_testing_defaults import io import unittest import Pyro4.utils.httpgateway import Pyro4.errors import Pyro4.core from Pyro4.naming import NameServer from Pyro4.configuration import config # a bit of hackery to avoid having to launch a live name server def get_nameserver_dummy(hmac=None): class NameServerDummyProxy(NameServer): def __init__(self): super(NameServerDummyProxy, self).__init__() self._pyroUri = Pyro4.core.URI("PYRO:dummy12345@localhost:59999") self.register("http.ObjectName", "PYRO:dummy12345@localhost:59999") def _pyroBatch(self): return self def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): pass def __call__(self, *args, **kwargs): return ["Name1", "Name2", "Name3"] return NameServerDummyProxy() class WSGITestBase(unittest.TestCase): """Base class for unit-tests. Provides up a simple interface to make requests as though they came through a wsgi interface from a user.""" def setUp(self): """Set up a fresh testing environment before each test.""" self.cookies = [] def request(self, application, url, query_string="", post_data=b""): """Hand a request to the application as if sent by a client. @param application: The callable wsgi application to test. @param url: The URL to make the request against. @param query_string: Url parameters. @param post_data: bytes to post.""" self.response_started = False method = 'POST' if post_data else 'GET' temp = io.BytesIO(post_data) environ = { 'PATH_INFO': url, 'REQUEST_METHOD': method, 'CONTENT_LENGTH': len(post_data), 'QUERY_STRING': query_string, 'wsgi.input': temp, } if method == "POST": environ["CONTENT_TYPE"] = "application/x-www-form-urlencoded" setup_testing_defaults(environ) if self.cookies: environ['HTTP_COOKIE'] = ';'.join(self.cookies) response = b'' for ret in application(environ, self._start_response): assert self.response_started response += ret temp.close() return response def _start_response(self, status, headers): """A callback passed into the application, to simulate a wsgi environment. @param status: The response status of the application ("200", "404", etc) @param headers: Any headers to begin the response with. """ assert not self.response_started self.response_started = True self.status = status self.headers = headers for header in headers: # Parse out any cookies and save them to send with later requests. if header[0] == 'Set-Cookie': var = header[1].split(';', 1) if len(var) > 1 and var[1][0:9] == ' Max-Age=': if int(var[1][9:]) > 0: # An approximation, since our cookies never expire unless # explicitly deleted (by setting Max-Age=0). self.cookies.append(var[0]) else: index = self.cookies.index(var[0]) self.cookies.pop(index) def new_session(self): """Start a new session (or pretend to be a different user) by deleting all current cookies.""" self.cookies = [] class TestHttpGateway(WSGITestBase): def setUp(self): super(TestHttpGateway, self).setUp() self.old_get_ns = Pyro4.utils.httpgateway.get_nameserver Pyro4.utils.httpgateway.get_nameserver = get_nameserver_dummy config.COMMTIMEOUT = 0.3 def tearDown(self): super(TestHttpGateway, self).tearDown() Pyro4.utils.httpgateway.get_nameserver = self.old_get_ns config.COMMTIMEOUT = 0.0 def testParams(self): multiparams = { "first": [1], "second": [1, 2, 3], "third": 42 } checkparams = { "first": 1, "second": [1, 2, 3], "third": 42 } params = Pyro4.utils.httpgateway.singlyfy_parameters(multiparams) self.assertEqual(checkparams, params) params = Pyro4.utils.httpgateway.singlyfy_parameters(multiparams) self.assertEqual(checkparams, params) def testRedirect(self): result = self.request(Pyro4.utils.httpgateway.pyro_app, "/") self.assertEqual("302 Found", self.status) self.assertEqual([('Location', '/pyro/')], self.headers) self.assertEqual(b"", result) def testWebpage(self): result = self.request(Pyro4.utils.httpgateway.pyro_app, "/pyro/") self.assertEqual("200 OK", self.status) self.assertTrue(result.startswith(b"")) self.assertTrue(len(result) > 1000) def testMethodCallGET(self): result = self.request(Pyro4.utils.httpgateway.pyro_app, "/pyro/http.ObjectName/method", query_string="param=42¶m2=hello") # the call will result in a communication error because the dummy uri points to something that is not available self.assertEqual("500 Internal Server Error", self.status) j = json.loads(result.decode("utf-8")) self.assertTrue(j["__exception__"]) self.assertEqual("Pyro4.errors.CommunicationError", j["__class__"]) def testMethodCallPOST(self): result = self.request(Pyro4.utils.httpgateway.pyro_app, "/pyro/http.ObjectName/method", post_data=b"param=42¶m2=hello") # the call will result in a communication error because the dummy uri points to something that is not available self.assertEqual("500 Internal Server Error", self.status) j = json.loads(result.decode("utf-8")) self.assertTrue(j["__exception__"]) self.assertEqual("Pyro4.errors.CommunicationError", j["__class__"]) def testNameDeniedPattern(self): result = self.request(Pyro4.utils.httpgateway.pyro_app, "/pyro/Pyro.NameServer/method") # the call will result in a access denied error because the uri points to a non-exposed name self.assertEqual("403 Forbidden", self.status) def testNameDeniedNotRegistered(self): result = self.request(Pyro4.utils.httpgateway.pyro_app, "/pyro/http.NotRegisteredName/method") # the call will result in a communication error because the dummy uri points to something that is not registered self.assertEqual("500 Internal Server Error", self.status) j = json.loads(result.decode("utf-8")) self.assertTrue(j["__exception__"]) self.assertEqual("Pyro4.errors.NamingError", j["__class__"]) def testExposedPattern(self): self.assertEqual(r"http\.", Pyro4.utils.httpgateway.pyro_app.ns_regex) if __name__ == "__main__": unittest.main() Pyro4-4.82/tests/PyroTests/test_ironpython.py000066400000000000000000000055171416147301300214340ustar00rootroot00000000000000""" Tests for some Ironpython peculiarities. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import sys import pickle import unittest import Pyro4.util if sys.platform == "cli": class IronPythonWeirdnessTests(unittest.TestCase): def testExceptionWithAttrsPickle(self): # ironpython doesn't pickle exception attributes # Bug report is at https://github.com/IronLanguages/main/issues/943 # Bug is still present in Ironpython 2.7.7 ex = ValueError("some exception") ex.custom_attribute = 42 ex2 = pickle.loads(pickle.dumps(ex)) self.assertTrue(hasattr(ex, "custom_attribute")) self.assertFalse(hasattr(ex2, "custom_attribute")) # custom attribute will be gone after pickling self.assertNotEqual(ex2, ex) # the object won't be equal def testExceptionReduce(self): # ironpython doesn't pickle exception attributes # Bug report is at https://github.com/IronLanguages/main/issues/943 # Bug is still present in Ironpython 2.7.7 ex = ValueError("some exception") ex.custom_attribute = 42 r = ex.__reduce__() # the reduce result should be: # (ValueError, ("some exception",), {"custom_attribute": 42}) # but in Ironpython the custom attributes are not returned. self.assertNotEqual((ValueError, ("some exception",), {"custom_attribute": 42}), r) self.assertEqual((ValueError, ("some exception",)), r) def testTbFrame(self): # there's some stuff missing on traceback frames # this prevents a detailed stack trace to be printed by # the functions in util.py, for instance. def crash(): a = 1 b = 0 return a // b try: crash() except: ex_t, ex_v, ex_tb = sys.exc_info() while ex_tb.tb_next: ex_tb = ex_tb.tb_next self.assertIsNone(ex_tb.tb_frame.f_back) # should not be none... :( def testExceptionArgs(self): x = ZeroDivisionError("division by zero", "arg1", "arg2") x.customattribute = 42 Pyro4.util.fixIronPythonExceptionForPickle(x, True) arg = x.args[-1] self.assertIsInstance(arg, dict) self.assertTrue(arg["__ironpythonargs__"]) self.assertEqual(42, arg["customattribute"]) x = ZeroDivisionError("division by zero", "arg1", "arg2") x.args += ({"__ironpythonargs__": True, "customattribute2": 99},) Pyro4.util.fixIronPythonExceptionForPickle(x, False) self.assertEqual(99, x.customattribute2) if __name__ == "__main__": unittest.main() Pyro4-4.82/tests/PyroTests/test_message.py000066400000000000000000000231201416147301300206350ustar00rootroot00000000000000""" Tests for pyro write protocol message. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import hashlib import hmac import unittest import zlib import Pyro4.message import Pyro4.constants import Pyro4.util import Pyro4.errors from Pyro4.message import Message from Pyro4.configuration import config from testsupport import ConnectionMock def pyrohmac(data, hmac_key, annotations={}): mac = hmac.new(hmac_key, data, digestmod=hashlib.sha1) for k, v in annotations.items(): if k != "HMAC": mac.update(v) return mac.digest() class MessageTestsHmac(unittest.TestCase): def setUp(self): self.ser = Pyro4.util.get_serializer(config.SERIALIZER) def testMessage(self): Message(99, b"", self.ser.serializer_id, 0, 0, hmac_key=b"secret") # doesn't check msg type here self.assertRaises(Pyro4.errors.ProtocolError, Message.from_header, "FOOBAR") msg = Message(Pyro4.message.MSG_CONNECT, b"hello", self.ser.serializer_id, 0, 0, hmac_key=b"secret") self.assertEqual(Pyro4.message.MSG_CONNECT, msg.type) self.assertEqual(5, msg.data_size) self.assertEqual(b"hello", msg.data) self.assertEqual(4 + 2 + 20, msg.annotations_size) mac = pyrohmac(b"hello", b"secret", msg.annotations) self.assertDictEqual({"HMAC": mac}, msg.annotations) hdr = msg.to_bytes()[:24] msg = Message.from_header(hdr) self.assertEqual(Pyro4.message.MSG_CONNECT, msg.type) self.assertEqual(4 + 2 + 20, msg.annotations_size) self.assertEqual(5, msg.data_size) hdr = Message(Pyro4.message.MSG_RESULT, b"", self.ser.serializer_id, 0, 0, hmac_key=b"secret").to_bytes()[:24] msg = Message.from_header(hdr) self.assertEqual(Pyro4.message.MSG_RESULT, msg.type) self.assertEqual(4 + 2 + 20, msg.annotations_size) self.assertEqual(0, msg.data_size) hdr = Message(Pyro4.message.MSG_RESULT, b"hello", 12345, 60006, 30003, hmac_key=b"secret").to_bytes()[:24] msg = Message.from_header(hdr) self.assertEqual(Pyro4.message.MSG_RESULT, msg.type) self.assertEqual(60006, msg.flags) self.assertEqual(5, msg.data_size) self.assertEqual(12345, msg.serializer_id) self.assertEqual(30003, msg.seq) msg = Message(255, b"", self.ser.serializer_id, 0, 255, hmac_key=b"secret").to_bytes() self.assertEqual(50, len(msg)) msg = Message(1, b"", self.ser.serializer_id, 0, 255, hmac_key=b"secret").to_bytes() self.assertEqual(50, len(msg)) msg = Message(1, b"", self.ser.serializer_id, flags=253, seq=254, hmac_key=b"secret").to_bytes() self.assertEqual(50, len(msg)) # compression is a job of the code supplying the data, so the messagefactory should leave it untouched data = b"x" * 1000 msg = Message(Pyro4.message.MSG_INVOKE, data, self.ser.serializer_id, 0, 0, hmac_key=b"secret").to_bytes() msg2 = Message(Pyro4.message.MSG_INVOKE, data, self.ser.serializer_id, Pyro4.message.FLAGS_COMPRESSED, 0, hmac_key=b"secret").to_bytes() self.assertEqual(len(msg), len(msg2)) def testMessageHeaderDatasize(self): msg = Message(Pyro4.message.MSG_RESULT, b"hello", 12345, 60006, 30003, hmac_key=b"secret") msg.data_size = 0x12345678 # hack it to a large value to see if it comes back ok hdr = msg.to_bytes()[:24] msg = Message.from_header(hdr) self.assertEqual(Pyro4.message.MSG_RESULT, msg.type) self.assertEqual(60006, msg.flags) self.assertEqual(0x12345678, msg.data_size) self.assertEqual(12345, msg.serializer_id) self.assertEqual(30003, msg.seq) def testAnnotations(self): annotations = {"TEST": b"abcde"} msg = Message(Pyro4.message.MSG_CONNECT, b"hello", self.ser.serializer_id, 0, 0, annotations, b"secret") data = msg.to_bytes() annotations_size = 4 + 2 + 20 + 4 + 2 + 5 self.assertEqual(msg.header_size + 5 + annotations_size, len(data)) self.assertEqual(annotations_size, msg.annotations_size) self.assertEqual(2, len(msg.annotations)) self.assertEqual(b"abcde", msg.annotations["TEST"]) mac = pyrohmac(b"hello", b"secret", annotations) self.assertEqual(mac, msg.annotations["HMAC"]) def testAnnotationsIdLength4(self): try: msg = Message(Pyro4.message.MSG_CONNECT, b"hello", self.ser.serializer_id, 0, 0, {"TOOLONG": b"abcde"}, b"secret") _ = msg.to_bytes() self.fail("should fail, too long") except Pyro4.errors.ProtocolError: pass try: msg = Message(Pyro4.message.MSG_CONNECT, b"hello", self.ser.serializer_id, 0, 0, {"QQ": b"abcde"}, b"secret") _ = msg.to_bytes() self.fail("should fail, too short") except Pyro4.errors.ProtocolError: pass def testRecvAnnotations(self): annotations = {"TEST": b"abcde"} msg = Message(Pyro4.message.MSG_CONNECT, b"hello", self.ser.serializer_id, 0, 0, annotations, b"secret") c = ConnectionMock() c.send(msg.to_bytes()) msg = Message.recv(c, hmac_key=b"secret") self.assertEqual(0, len(c.received)) self.assertEqual(5, msg.data_size) self.assertEqual(b"hello", msg.data) self.assertEqual(b"abcde", msg.annotations["TEST"]) self.assertIn("HMAC", msg.annotations) def testProtocolVersion(self): version = Pyro4.constants.PROTOCOL_VERSION Pyro4.constants.PROTOCOL_VERSION = 0 # fake invalid protocol version number msg = Message(Pyro4.message.MSG_RESULT, b"", self.ser.serializer_id, 0, 1, hmac_key=b"secret").to_bytes() Pyro4.constants.PROTOCOL_VERSION = version self.assertRaises(Pyro4.errors.ProtocolError, Message.from_header, msg) def testHmac(self): data = Message(Pyro4.message.MSG_RESULT, b"test", 42, 0, 1, hmac_key=b"test key").to_bytes() c = ConnectionMock(data) # test checking of different hmacs try: Message.recv(c, hmac_key=None) self.fail("crash expected") except Pyro4.errors.SecurityError as x: self.assertIn("hmac key config", str(x)) c = ConnectionMock(data) try: Message.recv(c, hmac_key=b"T3ST-K3Y") self.fail("crash expected") except Pyro4.errors.SecurityError as x: self.assertIn("hmac", str(x)) # test that it works again when providing the correct key c = ConnectionMock(data) msg = Message.recv(c, hmac_key=b"test key") self.assertEqual(b"test key", msg.hmac_key) def testHmacMethod(self): data = Message(Pyro4.message.MSG_RESULT, b"test", 42, 0, 1, hmac_key=b"test key") digest = data.hmac() self.assertTrue(len(digest) > 10) data = Message(Pyro4.message.MSG_RESULT, b"test", 42, 0, 1) with self.assertRaises(TypeError): data.hmac() def testSecureCompare(self): self.assertFalse(Pyro4.message.secure_compare("apple", "banana")) self.assertFalse(Pyro4.message.secure_compare(b"apple", b"banana")) self.assertTrue(Pyro4.message.secure_compare("apple", "apple")) self.assertTrue(Pyro4.message.secure_compare(b"apple", b"apple")) with self.assertRaises(TypeError): Pyro4.message.secure_compare(999, "typemismatch") def testChecksum(self): msg = Message(Pyro4.message.MSG_RESULT, b"test", 42, 0, 1, hmac_key=b"secret") c = ConnectionMock() c.send(msg.to_bytes()) # corrupt the checksum bytes data = c.received data = data[:msg.header_size - 2] + b'\x00\x00' + data[msg.header_size:] c = ConnectionMock(data) try: Message.recv(c) self.fail("crash expected") except Pyro4.errors.ProtocolError as x: self.assertIn("checksum", str(x)) def testCompression(self): data = b"The quick brown fox jumps over the lazy dog."*10 compressed_data = zlib.compress(data) flags = Pyro4.message.FLAGS_COMPRESSED msg = Message(Pyro4.message.MSG_INVOKE, compressed_data, 42, flags, 1, hmac_key=b"secret") self.assertNotEqual(data, msg.data) data_size = msg.data_size self.assertLess(data_size, len(data)) msg.decompress_if_needed() self.assertEqual(data, msg.data) self.assertEqual(0, msg.flags) self.assertGreater(msg.data_size, data_size) class MessageTestsNoHmac(unittest.TestCase): def testRecvNoAnnotations(self): msg = Message(Pyro4.message.MSG_CONNECT, b"hello", 42, 0, 0) c = ConnectionMock() c.send(msg.to_bytes()) msg = Message.recv(c) self.assertEqual(0, len(c.received)) self.assertEqual(5, msg.data_size) self.assertEqual(b"hello", msg.data) self.assertEqual(0, msg.annotations_size) self.assertEqual(0, len(msg.annotations)) def testMaxDataSize(self): msg = Message(Pyro4.message.MSG_CONNECT, b"hello", 42, 0, 0) msg.data_size = 0x7fffffff # still within 32 bits signed limits msg.to_bytes() msg.data_size = 0x80000000 # overflow, Pyro has a 2 gigabyte message size limitation with self.assertRaises(ValueError) as ex: msg.to_bytes() self.assertEqual("invalid message size (outside range 0..2Gb)", str(ex.exception)) msg.data_size = -42 with self.assertRaises(ValueError) as ex: msg.to_bytes() self.assertEqual("invalid message size (outside range 0..2Gb)", str(ex.exception)) if __name__ == "__main__": unittest.main() Pyro4-4.82/tests/PyroTests/test_naming.py000066400000000000000000000331571416147301300204750ustar00rootroot00000000000000""" Tests for the name server (online/running). Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import time import threading import unittest import Pyro4.core import Pyro4.naming import Pyro4.socketutil import Pyro4.constants from Pyro4.errors import CommunicationError, NamingError from Pyro4.configuration import config class NSLoopThread(threading.Thread): def __init__(self, nameserver): super(NSLoopThread, self).__init__() self.setDaemon(True) self.nameserver = nameserver self.running = threading.Event() self.running.clear() def run(self): self.running.set() try: self.nameserver.requestLoop() except CommunicationError: pass # ignore pyro communication errors class BCSetupTests(unittest.TestCase): def testBCstart(self): myIpAddress = Pyro4.socketutil.getIpAddress("", workaround127=True) nsUri, nameserver, bcserver = Pyro4.naming.startNS(host=myIpAddress, port=0, bcport=0, enableBroadcast=False) self.assertIsNone(bcserver) nameserver.close() nsUri, nameserver, bcserver = Pyro4.naming.startNS(host=myIpAddress, port=0, bcport=0, enableBroadcast=True) self.assertIsNotNone(bcserver, "expected a BC server to be running. Check DNS setup (hostname must not resolve to loopback address") self.assertGreater(bcserver.fileno(), 1) self.assertIsNotNone(bcserver.sock) nameserver.close() bcserver.close() class NameServerTests(unittest.TestCase): def setUp(self): config.POLLTIMEOUT = 0.1 myIpAddress = Pyro4.socketutil.getIpAddress("", workaround127=True) self.nsUri, self.nameserver, self.bcserver = Pyro4.naming.startNS(host=myIpAddress, port=0, bcport=0) self.assertIsNotNone(self.bcserver, "expected a BC server to be running") self.bcserver.runInThread() self.daemonthread = NSLoopThread(self.nameserver) self.daemonthread.start() self.daemonthread.running.wait() time.sleep(0.05) self.old_bcPort = config.NS_BCPORT self.old_nsPort = config.NS_PORT self.old_nsHost = config.NS_HOST config.NS_PORT = self.nsUri.port config.NS_HOST = myIpAddress config.NS_BCPORT = self.bcserver.getPort() def tearDown(self): time.sleep(0.01) self.nameserver.shutdown() self.bcserver.close() self.daemonthread.join() config.NS_HOST = self.old_nsHost config.NS_PORT = self.old_nsPort config.NS_BCPORT = self.old_bcPort def testLookupUnixsockParsing(self): # this must not raise AttributeError, it did before because of a parse bug with self.assertRaises(NamingError): Pyro4.naming.locateNS("./u:/tmp/pyro4-naming.usock") def testLookupAndRegister(self): ns = Pyro4.naming.locateNS() # broadcast lookup self.assertIsInstance(ns, Pyro4.core.Proxy) ns._pyroRelease() ns = Pyro4.naming.locateNS(self.nsUri.host) # normal lookup self.assertIsInstance(ns, Pyro4.core.Proxy) uri = ns._pyroUri self.assertEqual("PYRO", uri.protocol) self.assertEqual(self.nsUri.host, uri.host) self.assertEqual(config.NS_PORT, uri.port) self.assertIsNone(ns._pyroHmacKey) ns._pyroRelease() ns = Pyro4.naming.locateNS(self.nsUri.host, config.NS_PORT, hmac_key=None) uri = ns._pyroUri self.assertEqual("PYRO", uri.protocol) self.assertEqual(self.nsUri.host, uri.host) self.assertEqual(config.NS_PORT, uri.port) self.assertIsNone(ns._pyroHmacKey) # check that we cannot register a stupid type self.assertRaises(TypeError, ns.register, "unittest.object1", 5555) # we can register str or URI, lookup always returns URI ns.register("unittest.object2", "PYRO:55555@host.com:4444") self.assertEqual(Pyro4.core.URI("PYRO:55555@host.com:4444"), ns.lookup("unittest.object2")) ns.register("unittest.object3", Pyro4.core.URI("PYRO:66666@host.com:4444")) self.assertEqual(Pyro4.core.URI("PYRO:66666@host.com:4444"), ns.lookup("unittest.object3")) ns._pyroRelease() def testLookupInvalidHmac(self): with self.assertRaises(NamingError): Pyro4.naming.locateNS(self.nsUri.host, config.NS_PORT, hmac_key="invalidkey") def testDaemonPyroObj(self): uri = self.nsUri uri.object = Pyro4.constants.DAEMON_NAME with Pyro4.core.Proxy(uri) as daemonobj: daemonobj.ping() daemonobj.registered() try: daemonobj.shutdown() self.fail("should not succeed to call unexposed method on daemon") except AttributeError: pass def testMulti(self): uristr = str(self.nsUri) p = Pyro4.core.Proxy(uristr) p._pyroBind() p._pyroRelease() uri = Pyro4.naming.resolve(uristr) p = Pyro4.core.Proxy(uri) p._pyroBind() p._pyroRelease() uri = Pyro4.naming.resolve(uristr) p = Pyro4.core.Proxy(uri) p._pyroBind() p._pyroRelease() uri = Pyro4.naming.resolve(uristr) p = Pyro4.core.Proxy(uri) p._pyroBind() p._pyroRelease() uri = Pyro4.naming.resolve(uristr) p = Pyro4.core.Proxy(uri) p._pyroBind() p._pyroRelease() uri = Pyro4.naming.resolve(uristr) p = Pyro4.core.Proxy(uri) p._pyroBind() p._pyroRelease() daemonUri = "PYRO:" + Pyro4.constants.DAEMON_NAME + "@" + uri.location _ = Pyro4.naming.resolve(daemonUri) _ = Pyro4.naming.resolve(daemonUri) _ = Pyro4.naming.resolve(daemonUri) _ = Pyro4.naming.resolve(daemonUri) _ = Pyro4.naming.resolve(daemonUri) _ = Pyro4.naming.resolve(daemonUri) uri = Pyro4.naming.resolve(daemonUri) pyronameUri = "PYRONAME:" + Pyro4.constants.NAMESERVER_NAME + "@" + uri.location _ = Pyro4.naming.resolve(pyronameUri) _ = Pyro4.naming.resolve(pyronameUri) _ = Pyro4.naming.resolve(pyronameUri) _ = Pyro4.naming.resolve(pyronameUri) _ = Pyro4.naming.resolve(pyronameUri) _ = Pyro4.naming.resolve(pyronameUri) def testResolve(self): resolved1 = Pyro4.naming.resolve(Pyro4.core.URI("PYRO:12345@host.com:4444"), hmac_key=None) resolved2 = Pyro4.naming.resolve("PYRO:12345@host.com:4444", hmac_key=None) self.assertTrue(type(resolved1) is Pyro4.core.URI) self.assertEqual(resolved1, resolved2) self.assertEqual("PYRO:12345@host.com:4444", str(resolved1)) ns = Pyro4.naming.locateNS(self.nsUri.host, self.nsUri.port) host = "[" + self.nsUri.host + "]" if ":" in self.nsUri.host else self.nsUri.host uri = Pyro4.naming.resolve("PYRONAME:" + Pyro4.constants.NAMESERVER_NAME + "@" + host + ":" + str(self.nsUri.port)) self.assertEqual("PYRO", uri.protocol) self.assertEqual(self.nsUri.host, uri.host) self.assertEqual(Pyro4.constants.NAMESERVER_NAME, uri.object) self.assertEqual(uri, ns._pyroUri) ns._pyroRelease() # broadcast lookup self.assertRaises(NamingError, Pyro4.naming.resolve, "PYRONAME:unknown_object") uri = Pyro4.naming.resolve("PYRONAME:" + Pyro4.constants.NAMESERVER_NAME) self.assertEqual(Pyro4.core.URI, type(uri)) self.assertEqual("PYRO", uri.protocol) # test some errors self.assertRaises(NamingError, Pyro4.naming.resolve, "PYRONAME:unknown_object@" + host) self.assertRaises(TypeError, Pyro4.naming.resolve, 999) # wrong arg type def testRefuseDottedNames(self): old_metadata = config.METADATA config.METADATA = False with Pyro4.naming.locateNS(self.nsUri.host, self.nsUri.port) as ns: # the name server should never have dotted names enabled self.assertRaises(AttributeError, ns.namespace.keys) self.assertIsNotNone(ns._pyroConnection) self.assertIsNone(ns._pyroConnection) config.METADATA = old_metadata def testAutoClean(self): try: config.NS_AUTOCLEAN = 0.0 config.COMMTIMEOUT = 0.5 Pyro4.naming.AutoCleaner.max_unreachable_time = 1 Pyro4.naming.AutoCleaner.loop_delay = 0.5 Pyro4.naming.AutoCleaner.override_autoclean_min = True with Pyro4.naming.NameServerDaemon(port=0) as ns: self.assertIsNone(ns.cleaner_thread) config.NS_AUTOCLEAN = 0.2 with Pyro4.naming.NameServerDaemon(port=0) as ns: self.assertIsNotNone(ns.cleaner_thread) ns.nameserver.register("test", "PYRO:test@localhost:59999") self.assertEqual(2, ns.nameserver.count()) time.sleep(4) self.assertEqual(1, ns.nameserver.count(), "registration should be cleaned up") self.assertIsNone(ns.cleaner_thread) finally: Pyro4.naming.AutoCleaner.override_autoclean_min = False Pyro4.naming.AutoCleaner.max_unreachable_time = 20 Pyro4.naming.AutoCleaner.loop_delay = 2 config.NS_AUTOCLEAN = 0.0 config.COMMTIMEOUT = 0.0 class NameServerTests0000(unittest.TestCase): def setUp(self): config.POLLTIMEOUT = 0.1 self.nsUri, self.nameserver, self.bcserver = Pyro4.naming.startNS(host="", port=0, bcport=0) host_check = self.nsUri.host self.assertEqual("0.0.0.0", host_check, "for hostname \"\" the resulting ip must be 0.0.0.0 (or ipv6 equivalent)") self.assertIsNotNone(self.bcserver, "expected a BC server to be running") self.bcthread = self.bcserver.runInThread() self.old_bcPort = config.NS_BCPORT self.old_nsPort = config.NS_PORT self.old_nsHost = config.NS_HOST config.NS_PORT = self.nsUri.port config.NS_HOST = self.nsUri.host config.NS_BCPORT = self.bcserver.getPort() def tearDown(self): time.sleep(0.01) self.nameserver.shutdown() self.bcserver.close() self.bcthread.join() config.NS_HOST = self.old_nsHost config.NS_PORT = self.old_nsPort config.NS_BCPORT = self.old_bcPort def testBCLookup0000(self): ns = Pyro4.naming.locateNS() # broadcast lookup self.assertIsInstance(ns, Pyro4.core.Proxy) self.assertNotEqual("0.0.0.0", ns._pyroUri.host, "returned location must not be 0.0.0.0 when running on 0.0.0.0") ns._pyroRelease() class NameServerTestsHmac(unittest.TestCase): def setUp(self): config.POLLTIMEOUT = 0.1 myIpAddress = Pyro4.socketutil.getIpAddress("", workaround127=True) self.nsUri, self.nameserver, self.bcserver = Pyro4.naming.startNS(host=myIpAddress, port=0, bcport=0, hmac=b"test_key") self.assertIsNotNone(self.bcserver, "expected a BC server to be running") self.bcserver.runInThread() self.daemonthread = NSLoopThread(self.nameserver) self.daemonthread.start() self.daemonthread.running.wait() time.sleep(0.05) self.old_bcPort = config.NS_BCPORT self.old_nsPort = config.NS_PORT self.old_nsHost = config.NS_HOST config.NS_PORT = self.nsUri.port config.NS_HOST = myIpAddress config.NS_BCPORT = self.bcserver.getPort() def tearDown(self): time.sleep(0.01) self.nameserver.shutdown() self.bcserver.close() self.daemonthread.join() config.NS_HOST = self.old_nsHost config.NS_PORT = self.old_nsPort config.NS_BCPORT = self.old_bcPort def testLookupAndRegister(self): ns = Pyro4.naming.locateNS() # broadcast lookup without providing hmac still works self.assertIsInstance(ns, Pyro4.core.Proxy) self.assertIsNone(ns._pyroHmacKey) #... but no hmac is set on the proxy ns._pyroRelease() ns = Pyro4.naming.locateNS(hmac_key=b"test_key") # broadcast lookup providing hmac self.assertIsInstance(ns, Pyro4.core.Proxy) self.assertEqual(b"test_key", ns._pyroHmacKey) # ... sets the hmac on the proxy ns._pyroRelease() ns = Pyro4.naming.locateNS(self.nsUri.host, config.NS_PORT, hmac_key=b"test_key") uri = ns._pyroUri self.assertEqual("PYRO", uri.protocol) self.assertEqual(self.nsUri.host, uri.host) self.assertEqual(config.NS_PORT, uri.port) self.assertEqual(b"test_key", ns._pyroHmacKey) ns._pyroRelease() def testResolve(self): uri = Pyro4.naming.resolve("PYRONAME:Pyro.NameServer", hmac_key=b"test_key") self.assertEqual("PYRO", uri.protocol) self.assertEqual(self.nsUri.host, uri.host) self.assertEqual(config.NS_PORT, uri.port) def testPyroname(self): with Pyro4.core.Proxy("PYRONAME:Pyro.NameServer") as p: p._pyroHmacKey = b"test_key" p.ping() # the resolve() that is done should also use the hmac key def testResolveWrongHmac(self): with self.assertRaises(CommunicationError) as ex: Pyro4.naming.resolve("PYRONAME:Pyro.NameServer", hmac_key=b"wrong_key") emsg = str(ex.exception) self.assertTrue(emsg.startswith("cannot connect to ")) self.assertTrue(emsg.endswith("message hmac mismatch")) def testResolveAsymmetricHmacUsage(self): with self.assertRaises(CommunicationError) as ex: Pyro4.naming.resolve("PYRONAME:Pyro.NameServer", hmac_key=None) emsg = str(ex.exception) self.assertTrue(emsg.startswith("cannot connect to ")) self.assertTrue(emsg.endswith("hmac key config not symmetric")) if __name__ == "__main__": unittest.main() Pyro4-4.82/tests/PyroTests/test_naming2.py000066400000000000000000000437221416147301300205560ustar00rootroot00000000000000""" Tests for the name server (offline/basic logic). Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import sys import os import unittest import Pyro4.core import Pyro4.naming import Pyro4.naming_storage import Pyro4.nsc import Pyro4.constants import Pyro4.socketutil from Pyro4.errors import NamingError, PyroError from testsupport import * class OfflineNameServerTests(unittest.TestCase): def setUp(self): self.storageProvider = Pyro4.naming.MemoryStorage() def tearDown(self): self.storageProvider.clear() self.storageProvider.close() def testRegister(self): ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) self.storageProvider.clear() ns.ping() ns.register("test.object1", "PYRO:000000@host.com:4444") ns.register("test.object2", "PYRO:222222@host.com:4444") ns.register("test.object3", "PYRO:333333@host.com:4444") self.assertEqual("PYRO:000000@host.com:4444", str(ns.lookup("test.object1"))) ns.register("test.object1", "PYRO:111111@host.com:4444") # registering again should be ok by default self.assertEqual("PYRO:111111@host.com:4444", str(ns.lookup("test.object1")), "should be new uri") ns.register("test.sub.objectA", Pyro4.core.URI("PYRO:AAAAAA@host.com:4444")) ns.register("test.sub.objectB", Pyro4.core.URI("PYRO:BBBBBB@host.com:4444")) # if safe=True, a registration of an existing name should give a NamingError self.assertRaises(NamingError, ns.register, "test.object1", "PYRO:X@Y:5555", safe=True) self.assertRaises(TypeError, ns.register, None, None) self.assertRaises(TypeError, ns.register, 4444, 4444) self.assertRaises(TypeError, ns.register, "test.wrongtype", 4444) self.assertRaises(TypeError, ns.register, 4444, "PYRO:X@Y:5555") self.assertRaises(NamingError, ns.lookup, "unknown_object") uri = ns.lookup("test.object3") self.assertEqual(Pyro4.core.URI("PYRO:333333@host.com:4444"), uri) # lookup always returns URI ns.remove("unknown_object") ns.remove("test.object1") ns.remove("test.object2") ns.remove("test.object3") all_objs = ns.list() self.assertEqual(2, len(all_objs)) # 2 leftover objects self.assertRaises(PyroError, ns.register, "test.nonurivalue", "THISVALUEISNOTANURI") ns.storage.close() def testRemove(self): ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) self.storageProvider.clear() ns.register(Pyro4.constants.NAMESERVER_NAME, "PYRO:nameserver@host:555") for i in range(20): ns.register("test.%d" % i, "PYRO:obj@host:555") self.assertEqual(21, len(ns.list())) self.assertEqual(0, ns.remove("wrong")) self.assertEqual(0, ns.remove(prefix="wrong")) self.assertEqual(0, ns.remove(regex="wrong.*")) self.assertEqual(1, ns.remove("test.0")) self.assertEqual(20, len(ns.list())) self.assertEqual(11, ns.remove(prefix="test.1")) # 1, 10-19 self.assertEqual(8, ns.remove(regex=r"test\..")) # 2-9 self.assertEqual(1, len(ns.list())) ns.storage.close() def testRemoveProtected(self): ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) self.storageProvider.clear() ns.register(Pyro4.constants.NAMESERVER_NAME, "PYRO:nameserver@host:555") self.assertEqual(0, ns.remove(Pyro4.constants.NAMESERVER_NAME)) self.assertEqual(0, ns.remove(prefix="Pyro")) self.assertEqual(0, ns.remove(regex="Pyro.*")) self.assertIn(Pyro4.constants.NAMESERVER_NAME, ns.list()) ns.storage.close() @unittest.skipIf(sys.platform == "cli", "ironpython has a bug in anydbm/whichdb when inserting unicode keys") # see https://github.com/IronLanguages/main/issues/1165 def testUnicodeNames(self): ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) self.storageProvider.clear() uri = Pyro4.core.URI("PYRO:unicode" + unichr(0x20ac) + "@host:5555") ns.register("unicodename" + unichr(0x20ac), uri) x = ns.lookup("unicodename" + unichr(0x20ac)) self.assertEqual(uri, x) ns.storage.close() def testList(self): ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) self.storageProvider.clear() ns.register("test.objects.1", "PYRONAME:something1") ns.register("test.objects.2", "PYRONAME:something2") ns.register("test.objects.3", "PYRONAME:something3") ns.register("test.other.a", "PYRONAME:somethingA") ns.register("test.other.b", "PYRONAME:somethingB") ns.register("test.other.c", "PYRONAME:somethingC") ns.register("entirely.else", "PYRONAME:meh") objects = ns.list() self.assertEqual(7, len(objects)) objects = ns.list(prefix="nothing") self.assertEqual(0, len(objects)) objects = ns.list(prefix="test.") self.assertEqual(6, len(objects)) objects = ns.list(regex=r".+other..") self.assertEqual(3, len(objects)) self.assertIn("test.other.a", objects) self.assertEqual("PYRONAME:somethingA", objects["test.other.a"]) objects = ns.list(regex=r"\d\d\d\d\d\d\d\d\d\d") self.assertEqual(0, len(objects)) self.assertRaises(NamingError, ns.list, regex="((((((broken") ns.storage.close() def testNameserverWithStmt(self): ns = Pyro4.naming.NameServerDaemon(port=0) self.assertIsNotNone(ns.nameserver) ns.close() self.assertIsNone(ns.nameserver) with Pyro4.naming.NameServerDaemon(port=0) as ns: self.assertIsNotNone(ns.nameserver) pass self.assertIsNone(ns.nameserver) try: with Pyro4.naming.NameServerDaemon(port=0) as ns: self.assertIsNotNone(ns.nameserver) print(1 // 0) # cause an error self.fail("expected error") except ZeroDivisionError: pass self.assertIsNone(ns.nameserver) ns = Pyro4.naming.NameServerDaemon(port=0) with ns: pass try: with ns: pass self.fail("expected error") except PyroError: # you cannot re-use a name server object in multiple with statements pass ns.close() def testStartNSfunc(self): myIpAddress = Pyro4.socketutil.getIpAddress("", workaround127=True) uri1, ns1, bc1 = Pyro4.naming.startNS(host=myIpAddress, port=0, bcport=0, enableBroadcast=False) uri2, ns2, bc2 = Pyro4.naming.startNS(host=myIpAddress, port=0, bcport=0, enableBroadcast=True) self.assertIsInstance(uri1, Pyro4.core.URI) self.assertIsInstance(ns1, Pyro4.naming.NameServerDaemon) self.assertIsNone(bc1) self.assertIsInstance(bc2, Pyro4.naming.BroadcastServer) sock = bc2.sock self.assertTrue(hasattr(sock, "fileno")) _ = bc2.processRequest ns1.close() ns2.close() bc2.close() def testNSmain(self): oldstdout = sys.stdout oldstderr = sys.stderr try: sys.stdout = StringIO() sys.stderr = StringIO() self.assertRaises(SystemExit, Pyro4.naming.main, ["--invalidarg"]) self.assertTrue("no such option" in sys.stderr.getvalue()) sys.stderr.truncate(0) sys.stdout.truncate(0) self.assertRaises(SystemExit, Pyro4.naming.main, ["-h"]) self.assertTrue("show this help message" in sys.stdout.getvalue()) finally: sys.stdout = oldstdout sys.stderr = oldstderr def testNSCmain(self): oldstdout = sys.stdout oldstderr = sys.stderr try: sys.stdout = StringIO() sys.stderr = StringIO() self.assertRaises(SystemExit, Pyro4.nsc.main, ["--invalidarg"]) self.assertTrue("no such option" in sys.stderr.getvalue()) sys.stderr.truncate(0) sys.stdout.truncate(0) self.assertRaises(SystemExit, Pyro4.nsc.main, ["-h"]) self.assertTrue("show this help message" in sys.stdout.getvalue()) finally: sys.stdout = oldstdout sys.stderr = oldstderr def testNSCfunctions(self): oldstdout = sys.stdout try: sys.stdout = StringIO() ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) Pyro4.nsc.handleCommand(ns, None, ["foo"]) self.assertTrue(sys.stdout.getvalue().startswith("Error: KeyError ")) Pyro4.nsc.handleCommand(ns, None, ["ping"]) self.assertTrue(sys.stdout.getvalue().endswith("ping ok.\n")) Pyro4.nsc.handleCommand(ns, None, ["lookup", "WeirdName"]) self.assertTrue(sys.stdout.getvalue().endswith("Error: NamingError - unknown name: WeirdName\n")) Pyro4.nsc.handleCommand(ns, None, ["list"]) self.assertTrue(sys.stdout.getvalue().endswith("END LIST \n")) Pyro4.nsc.handleCommand(ns, None, ["listmatching", "name.$"]) self.assertTrue(sys.stdout.getvalue().endswith("END LIST - regex 'name.$'\n")) self.assertNotIn("name1", sys.stdout.getvalue()) Pyro4.nsc.handleCommand(ns, None, ["register", "name1", "PYRO:obj1@hostname:9999"]) self.assertTrue(sys.stdout.getvalue().endswith("Registered name1\n")) Pyro4.nsc.handleCommand(ns, None, ["remove", "name2"]) self.assertTrue(sys.stdout.getvalue().endswith("Nothing removed\n")) Pyro4.nsc.handleCommand(ns, None, ["listmatching", "name.$"]) self.assertIn("name1 --> PYRO:obj1@hostname:9999", sys.stdout.getvalue()) # Pyro4.nsc.handleCommand(ns, None, ["removematching", "name.?"]) # can't be tested, required user input finally: sys.stdout = oldstdout ns.storage.close() def testNAT(self): uri, ns, bc = Pyro4.naming.startNS(host="", port=0, enableBroadcast=True, nathost="nathosttest", natport=12345) self.assertEqual("nathosttest:12345", uri.location) self.assertEqual("nathosttest:12345", ns.uriFor("thing").location) self.assertNotEqual("nathosttest:12345", bc.nsUri.location, "broadcast location must not be the NAT location") ns.close() bc.close() def testMetadataRegisterInvalidTypes(self): ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) with self.assertRaises(TypeError): ns.register("meta1", "PYRO:meta1@localhost:1111", metadata=12345) # metadata must be iterable with self.assertRaises(TypeError): ns.register("meta1", "PYRO:meta1@localhost:1111", metadata="string") # metadata must not be str def testMetadataListInvalidTypes(self): ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) with self.assertRaises(TypeError): ns.list(metadata_all=12345) with self.assertRaises(TypeError): ns.list(metadata_all="string") with self.assertRaises(TypeError): ns.list(metadata_any=12345) with self.assertRaises(TypeError): ns.list(metadata_any="string") def testMetadata(self): self.storageProvider.clear() ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) # register some names with metadata, and perform simple lookups ns.register("meta1", "PYRO:meta1@localhost:1111", metadata={"a", "b", "c"}) ns.register("meta2", "PYRO:meta2@localhost:2222", metadata={"x", "y", "z"}) ns.register("meta3", "PYRO:meta3@localhost:3333", metadata=["p", "q", "r", "r", "q"]) uri = ns.lookup("meta1") self.assertEqual("meta1", uri.object) uri, metadata = ns.lookup("meta1", return_metadata=True) self.assertEqual("meta1", uri.object) self.assertSetEqual({"a", "b", "c"}, set(metadata)) uri = ns.lookup("meta2") self.assertEqual("meta2", uri.object) uri, metadata = ns.lookup("meta2", return_metadata=True) self.assertEqual("meta2", uri.object) self.assertSetEqual({"x", "y", "z"}, set(metadata)) uri, metadata = ns.lookup("meta3", return_metadata=True) self.assertEqual("meta3", uri.object) self.assertIsInstance(metadata, list) self.assertSetEqual({"p", "q", "r"}, set(metadata)) # get a list of everything, without and with metadata reg = ns.list() self.assertDictEqual({'meta1': 'PYRO:meta1@localhost:1111', 'meta2': 'PYRO:meta2@localhost:2222', 'meta3': 'PYRO:meta3@localhost:3333'}, reg) reg = ns.list(return_metadata=True) uri1, meta1 = reg["meta1"] uri2, meta2 = reg["meta2"] self.assertEqual("PYRO:meta1@localhost:1111", uri1) self.assertSetEqual({"a", "b", "c"}, set(meta1)) self.assertEqual("PYRO:meta2@localhost:2222", uri2) self.assertSetEqual({"x", "y", "z"}, set(meta2)) # filter on metadata subset reg = ns.list(metadata_all={"a", "c"}, return_metadata=False) self.assertEqual(1, len(reg)) self.assertEqual("PYRO:meta1@localhost:1111", reg["meta1"]) reg = ns.list(metadata_all={"a", "c"}, return_metadata=True) self.assertEqual(1, len(reg)) uri1, meta1 = reg["meta1"] self.assertEqual("PYRO:meta1@localhost:1111", uri1) self.assertSetEqual({"a", "b", "c"}, set(meta1)) reg = ns.list(metadata_all={"a", "wrong"}) self.assertEqual({}, reg) reg = ns.list(metadata_all={"a", "b", "c", "wrong"}) self.assertEqual({}, reg) reg = ns.list(metadata_all={"a", "c", "x"}) self.assertEqual({}, reg) # update some metadata with self.assertRaises(NamingError): ns.set_metadata("notexistingname", set()) ns.set_metadata("meta1", {"one", "two", "three"}) uri, meta = ns.lookup("meta1", return_metadata=True) self.assertSetEqual({"one", "two", "three"}, set(meta)) # check that a collection is converted to a set ns.set_metadata("meta1", ["one", "two", "three", "three", "two"]) uri, meta = ns.lookup("meta1", return_metadata=True) self.assertIsInstance(meta, list) self.assertSetEqual({"one", "two", "three"}, set(meta)) # remove record that has some metadata ns.remove("meta1") ns.remove("meta3") self.assertEqual(["meta2"], list(ns.list().keys())) # other list filters reg = ns.list(prefix="meta", return_metadata=True) self.assertEqual(1, len(reg)) self.assertSetEqual({"x", "y", "z"}, set(reg["meta2"][1])) reg = ns.list(regex="meta2.*", return_metadata=True) self.assertEqual(1, len(reg)) self.assertSetEqual({"x", "y", "z"}, set(reg["meta2"][1])) self.assertEqual(1, ns.count()) def testMetadataAny(self): self.storageProvider.clear() ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) # register some names with metadata, and perform simple lookups ns.register("meta1", "PYRO:meta1@localhost:1111", metadata={"a", "b", "c"}) ns.register("meta2", "PYRO:meta2@localhost:2222", metadata={"x", "y", "z"}) ns.register("meta3", "PYRO:meta3@localhost:2222", metadata={"k", "l", "m"}) result = ns.list(metadata_any={"1", "2", "3"}) self.assertEqual({}, result) result = ns.list(metadata_any={"1", "2", "a"}) self.assertEqual(1, len(result)) self.assertIn("meta1", result) result = ns.list(metadata_any={"1", "2", "a", "z"}) self.assertEqual(2, len(result)) self.assertIn("meta1", result) self.assertIn("meta2", result) def testEmptyMetadata(self): self.storageProvider.clear() ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) # register some names with metadata, and perform simple lookups ns.register("meta1", "PYRO:meta1@localhost:1111", metadata=[]) uri, meta = ns.lookup("meta1", return_metadata=True) self.assertEqual([], meta) registrations = ns.list(return_metadata=True) for name in registrations: uri, meta = registrations[name] self.assertEqual([], meta) ns.set_metadata("meta1", []) def testListNoMultipleFilters(self): ns = Pyro4.naming.NameServer(storageProvider=self.storageProvider) with self.assertRaises(ValueError): ns.list(prefix="a", regex="a") with self.assertRaises(ValueError): ns.list(prefix="a", metadata_all=[]) @unittest.skipIf(Pyro4.naming_storage.dbm is None, "dbm must be available") class OfflineNameServerTestsDbmStorage(OfflineNameServerTests): def setUp(self): super(OfflineNameServerTestsDbmStorage, self).setUp() import glob for file in glob.glob("pyro-test.dbm*"): os.remove(file) self.storageProvider = Pyro4.naming_storage.DbmStorage("pyro-test.dbm") def tearDown(self): super(OfflineNameServerTestsDbmStorage, self).tearDown() import glob for file in glob.glob("pyro-test.dbm*"): os.remove(file) @unittest.skip("dbmstorage doesn't support metadata") def testMetadata(self): pass @unittest.skip("dbmstorage doesn't support metadata") def testMetadataAny(self): pass @unittest.skipIf(Pyro4.naming_storage.sqlite3 is None, "sqlite3 must be available") class OfflineNameServerTestsSqlStorage(OfflineNameServerTests): def setUp(self): super(OfflineNameServerTestsSqlStorage, self).setUp() self.storageProvider = Pyro4.naming_storage.SqlStorage("pyro-test.sqlite") def tearDown(self): super(OfflineNameServerTestsSqlStorage, self).tearDown() import glob for file in glob.glob("pyro-test.sqlite*"): os.remove(file) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() Pyro4-4.82/tests/PyroTests/test_package.py000066400000000000000000000060321416147301300206070ustar00rootroot00000000000000""" Tests for the package structure and import names. Also checks if the key API functions are still in place. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import unittest # this tests the __all__ definitions: from Pyro4 import * from Pyro4.configuration import * from Pyro4.constants import * from Pyro4.core import * from Pyro4.errors import * from Pyro4.futures import * from Pyro4.message import * from Pyro4.naming import * from Pyro4.naming_storage import * from Pyro4.nsc import * from Pyro4.socketutil import * from Pyro4.util import * from Pyro4.socketserver.multiplexserver import * from Pyro4.socketserver.threadpoolserver import * from Pyro4.socketserver.threadpool import * from Pyro4.test.echoserver import * from Pyro4.utils.flame import * from Pyro4.utils.flameserver import * from Pyro4.utils.httpgateway import * #regular imports: import Pyro4.constants import Pyro4.core import Pyro4.errors import Pyro4.naming import Pyro4.nsc import Pyro4.socketutil import Pyro4.util class TestPackage(unittest.TestCase): def testPyro4(self): self.assertIs(Pyro4.core.Daemon, Pyro4.Daemon) self.assertIs(Pyro4.core.Proxy, Pyro4.Proxy) self.assertIs(Pyro4.core.URI, Pyro4.URI) self.assertIs(Pyro4.core.callback, Pyro4.callback) self.assertIs(Pyro4.core.oneway, Pyro4.oneway) self.assertIs(Pyro4.core.asyncproxy, Pyro4.asyncproxy) self.assertIs(Pyro4.core.batch, Pyro4.batch) self.assertIs(Pyro4.core.expose, Pyro4.expose) self.assertIs(Pyro4.core.behavior, Pyro4.behavior) self.assertIs(Pyro4.core._locateNS, Pyro4.locateNS) self.assertIs(Pyro4.core._resolve, Pyro4.resolve) self.assertIs(Pyro4.core._locateNS, Pyro4.naming.locateNS, "old API function location must still be valid") self.assertIs(Pyro4.core._resolve, Pyro4.naming.resolve, "old API function location must still be valid") self.assertIsInstance(Pyro4.current_context, Pyro4.core._CallContext) @unittest.skipIf(sys.version_info >= (3, 7), "async is kw on 3.7+") def testAsyncKeywordBackwardsCompatibility(self): # 'async' function async_function = getattr(Pyro4, "async") self.assertIs(async_function, Pyro4.asyncproxy) async_function = getattr(Pyro4.core, "async") self.assertIs(async_function, Pyro4.core.asyncproxy) # 'async' keyword on batch proxy's __call__ proxy = Pyro4.Proxy("PYRO:dummy@localhost:9999") batch = Pyro4.batch(proxy) result = batch(**{"async": True}) result.set_cancelled() result = batch(asynchronous=True) result.set_cancelled() # 'async' keyword on 'proxy._pyroAsync' method proxy._pyroAsync(**{"async": True}) proxy._pyroAsync(asynchronous=True) proxy._pyroAsync(True) # 'async' keyword on 'async' function Pyro4.asyncproxy(proxy, **{"async": True}) Pyro4.asyncproxy(proxy, asynchronous=True) Pyro4.asyncproxy(proxy, True) if __name__ == "__main__": unittest.main() Pyro4-4.82/tests/PyroTests/test_serialize.py000066400000000000000000001255531416147301300212150ustar00rootroot00000000000000""" Tests for the data serializer. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import array import sys import collections import copy import pprint import pickle import base64 import unittest import math import uuid import Pyro4.util import Pyro4.errors import Pyro4.core import Pyro4.futures from Pyro4.configuration import config from testsupport import * class SerializeTests_pickle(unittest.TestCase): SERIALIZER = "pickle" def setUp(self): self.previous_serializer = config.SERIALIZER config.SERIALIZER = self.SERIALIZER self.serializer = Pyro4.util.get_serializer(config.SERIALIZER) config.REQUIRE_EXPOSE = True def tearDown(self): config.SERIALIZER = self.previous_serializer def testSerItself(self): s = Pyro4.util.get_serializer(config.SERIALIZER) p, _ = self.serializer.serializeData(s) s2 = self.serializer.deserializeData(p) self.assertEqual(s, s2) self.assertTrue(s == s2) self.assertFalse(s != s2) def testSerUnicode(self): data = unicode("x") self.serializer.serializeData(data) self.serializer.serializeCall(data, unicode("method"), [], {}) def testSerCompression(self): d1, c1 = self.serializer.serializeData("small data", compress=True) d2, c2 = self.serializer.serializeData("small data", compress=False) self.assertFalse(c1) self.assertEqual(d1, d2) bigdata = "x" * 1000 d1, c1 = self.serializer.serializeData(bigdata, compress=False) d2, c2 = self.serializer.serializeData(bigdata, compress=True) self.assertFalse(c1) self.assertTrue(c2) self.assertTrue(len(d2) < len(d1)) self.assertEqual(bigdata, self.serializer.deserializeData(d1, compressed=False)) self.assertEqual(bigdata, self.serializer.deserializeData(d2, compressed=True)) def testSerErrors(self): e1 = Pyro4.errors.NamingError(unicode("x")) e1._pyroTraceback = ["this is the remote traceback"] orig_e = copy.copy(e1) e2 = Pyro4.errors.PyroError(unicode("x")) e3 = Pyro4.errors.ProtocolError(unicode("x")) if sys.platform == "cli": Pyro4.util.fixIronPythonExceptionForPickle(e1, True) p, _ = self.serializer.serializeData(e1) e = self.serializer.deserializeData(p) if sys.platform == "cli": Pyro4.util.fixIronPythonExceptionForPickle(e, False) self.assertIsInstance(e, Pyro4.errors.NamingError) self.assertEqual(repr(orig_e), repr(e)) self.assertEqual(["this is the remote traceback"], e._pyroTraceback, "remote traceback info should be present") p, _ = self.serializer.serializeData(e2) e = self.serializer.deserializeData(p) self.assertIsInstance(e, Pyro4.errors.PyroError) self.assertEqual(repr(e2), repr(e)) p, _ = self.serializer.serializeData(e3) e = self.serializer.deserializeData(p) self.assertIsInstance(e, Pyro4.errors.ProtocolError) self.assertEqual(repr(e3), repr(e)) def testSerializeExceptionWithAttr(self): ex = ZeroDivisionError("test error") ex._pyroTraceback = ["test traceback payload"] Pyro4.util.fixIronPythonExceptionForPickle(ex, True) # hack for ironpython data, compressed = self.serializer.serializeData(ex) ex2 = self.serializer.deserializeData(data, compressed) Pyro4.util.fixIronPythonExceptionForPickle(ex2, False) # hack for ironpython self.assertEqual(ZeroDivisionError, type(ex2)) self.assertTrue(hasattr(ex2, "_pyroTraceback")) self.assertEqual(["test traceback payload"], ex2._pyroTraceback) def testSerCoreOffline(self): uri = Pyro4.core.URI("PYRO:9999@host.com:4444") p, _ = self.serializer.serializeData(uri) uri2 = self.serializer.deserializeData(p) self.assertEqual(uri, uri2) self.assertEqual("PYRO", uri2.protocol) self.assertEqual("9999", uri2.object) self.assertEqual("host.com:4444", uri2.location) self.assertEqual(4444, uri2.port) self.assertIsNone(uri2.sockname) uri = Pyro4.core.URI("PYRO:12345@./u:/tmp/socketname") p, _ = self.serializer.serializeData(uri) uri2 = self.serializer.deserializeData(p) self.assertEqual(uri, uri2) self.assertEqual("PYRO", uri2.protocol) self.assertEqual("12345", uri2.object) self.assertEqual("./u:/tmp/socketname", uri2.location) self.assertIsNone(uri2.port) self.assertEqual("/tmp/socketname", uri2.sockname) proxy = Pyro4.core.Proxy("PYRO:9999@host.com:4444") proxy._pyroTimeout = 42 proxy._pyroMaxRetries = 78 self.assertIsNone(proxy._pyroConnection) p, _ = self.serializer.serializeData(proxy) proxy2 = self.serializer.deserializeData(p) self.assertIsNone(proxy._pyroConnection) self.assertIsNone(proxy2._pyroConnection) self.assertEqual(proxy2._pyroUri, proxy._pyroUri) self.assertEqual(0, proxy2._pyroTimeout, "must be reset to defaults") self.assertEqual(0, proxy2._pyroMaxRetries, "must be reset to defaults") def testNested(self): if self.SERIALIZER == "marshal": self.skipTest("marshal can't serialize custom objects") uri1 = Pyro4.core.URI("PYRO:1111@host.com:111") uri2 = Pyro4.core.URI("PYRO:2222@host.com:222") _ = self.serializer.serializeData(uri1) data = [uri1, uri2] p, _ = self.serializer.serializeData(data) [u1, u2] = self.serializer.deserializeData(p) self.assertEqual(uri1, u1) self.assertEqual(uri2, u2) def testSerDaemonHack(self): # This tests the hack that a Daemon should be serializable, # but only to support serializing Pyro objects. # The serialized form of a Daemon should be empty (and thus, useless) with Pyro4.core.Daemon(port=0) as daemon: d, _ = self.serializer.serializeData(daemon) d2 = self.serializer.deserializeData(d) self.assertTrue(len(d2.__dict__) == 0, "deserialized daemon should be empty") self.assertTrue("Pyro4.core.Daemon" in repr(d2)) self.assertTrue("unusable" in repr(d2)) try: config.AUTOPROXY = False obj = pprint.PrettyPrinter(stream="dummy", width=42) obj.name = "hello" daemon.register(obj) o, _ = self.serializer.serializeData(obj) if self.SERIALIZER in ("pickle", "cloudpickle", "dill"): # only pickle, cloudpickle and dill can deserialize the PrettyPrinter class without the need of explicit deserialization function o2 = self.serializer.deserializeData(o) self.assertEqual("hello", o2.name) self.assertEqual(42, o2._width) finally: config.AUTOPROXY = True def testPyroClasses(self): uri = Pyro4.core.URI("PYRO:object@host:4444") s, c = self.serializer.serializeData(uri) x = self.serializer.deserializeData(s, c) self.assertIsInstance(x, Pyro4.core.URI) self.assertEqual(uri, x) self.assertTrue("Pyro4.core.URI" in repr(uri)) self.assertEqual("PYRO:object@host:4444", str(uri)) uri = Pyro4.core.URI("PYRO:12345@./u:/tmp/socketname") s, c = self.serializer.serializeData(uri) x = self.serializer.deserializeData(s, c) self.assertIsInstance(x, Pyro4.core.URI) self.assertEqual(uri, x) proxy = Pyro4.core.Proxy(uri) proxy._pyroAttrs = set("abc") proxy._pyroMethods = set("def") proxy._pyroOneway = set("ghi") proxy._pyroTimeout = 42 proxy._pyroHmacKey = b"secret" proxy._pyroHandshake = "apples" proxy._pyroMaxRetries = 78 proxy._pyroSerializer = "serializer" s, c = self.serializer.serializeData(proxy) x = self.serializer.deserializeData(s, c) self.assertIsInstance(x, Pyro4.core.Proxy) self.assertEqual(proxy._pyroUri, x._pyroUri) self.assertEqual(set("abc"), x._pyroAttrs) self.assertEqual(set("def"), x._pyroMethods) self.assertEqual(set("ghi"), x._pyroOneway) self.assertEqual(b"secret", x._pyroHmacKey) self.assertEqual("apples", x._pyroHandshake) self.assertEqual("serializer", x._pyroSerializer) self.assertEqual(0, x._pyroTimeout, "must be reset to defaults") self.assertEqual(0, x._pyroMaxRetries, "must be reset to defaults") self.assertTrue("Pyro4.core.Proxy" in repr(x)) self.assertTrue("Pyro4.core.Proxy" in str(x)) daemon = Pyro4.core.Daemon() s, c = self.serializer.serializeData(daemon) x = self.serializer.deserializeData(s, c) self.assertIsInstance(x, Pyro4.core.Daemon) self.assertTrue("Pyro4.core.Daemon" in repr(x)) self.assertTrue("unusable" in repr(x)) self.assertTrue("Pyro4.core.Daemon" in str(x)) self.assertTrue("unusable" in str(x)) wrapper = Pyro4.futures._ExceptionWrapper(ZeroDivisionError("divided by zero")) s, c = self.serializer.serializeData(wrapper) x = self.serializer.deserializeData(s, c) self.assertIsInstance(x, Pyro4.futures._ExceptionWrapper) self.assertEqual("divided by zero", str(x.exception)) self.assertTrue("ExceptionWrapper" in repr(x)) self.assertTrue("ExceptionWrapper" in str(x)) def testPyroClassesForDict(self): uri = Pyro4.core.URI("PYRO:object@host:4444") state = uri.__getstate_for_dict__() self.assertEqual(('PYRO', 'object', None, 'host', 4444), state) uri2 = Pyro4.core.URI("PYRONAME:xxx") uri2.__setstate_from_dict__(state) self.assertEqual(uri, uri2) proxy = Pyro4.core.Proxy(uri) proxy._pyroAttrs = set("abc") proxy._pyroMethods = set("def") proxy._pyroOneway = set("ghi") proxy._pyroTimeout = 42 proxy._pyroHmacKey = b"secret" proxy._pyroHandshake = "apples" proxy._pyroMaxRetries = 78 proxy._pyroSerializer = "serializer" state = proxy.__getstate_for_dict__() b64_secret = "b64:"+base64.b64encode(b"secret").decode("utf-8") self.assertEqual(('PYRO:object@host:4444', tuple(set("ghi")), tuple(set("def")), tuple(set("abc")), 42, b64_secret, "apples", 78, "serializer"), state) proxy2 = Pyro4.core.Proxy("PYRONAME:xxx") proxy2.__setstate_from_dict__(state) self.assertEqual(proxy, proxy2) self.assertEqual(proxy._pyroUri, proxy2._pyroUri) self.assertEqual(proxy._pyroAttrs, proxy2._pyroAttrs) self.assertEqual(proxy._pyroMethods, proxy2._pyroMethods) self.assertEqual(proxy._pyroOneway, proxy2._pyroOneway) self.assertEqual(proxy._pyroHmacKey, proxy2._pyroHmacKey) self.assertEqual(proxy._pyroHandshake, proxy2._pyroHandshake) self.assertEqual(proxy._pyroSerializer, proxy2._pyroSerializer) self.assertEqual(0, proxy2._pyroTimeout, "must be reset to defaults") self.assertEqual(0, proxy2._pyroMaxRetries, "must be reset to defaults") daemon = Pyro4.core.Daemon() state = daemon.__getstate_for_dict__() self.assertEqual(tuple(), state) daemon2 = Pyro4.core.Daemon() daemon2.__setstate_from_dict__(state) def testProxySerializationCompat(self): proxy = Pyro4.core.Proxy("PYRO:object@host:4444") proxy._pyroSerializer = "serializer" pickle_state = proxy.__getstate__() self.assertEqual(9, len(pickle_state)) pickle_state = pickle_state[:8] proxy.__setstate__(pickle_state) self.assertIsNone(proxy._pyroSerializer) proxy._pyroSerializer = "serializer" serpent_state = proxy.__getstate_for_dict__() self.assertEqual(9, len(serpent_state)) serpent_state = serpent_state[:8] proxy.__setstate_from_dict__(serpent_state) self.assertIsNone(proxy._pyroSerializer) def testAutoProxyPartlyExposed(self): if self.SERIALIZER == "marshal": self.skipTest("marshal can't serialize custom objects") self.serializer.register_type_replacement(MyThingPartlyExposed, Pyro4.core.pyroObjectToAutoProxy) t1 = MyThingPartlyExposed("1") t2 = MyThingPartlyExposed("2") with Pyro4.core.Daemon() as d: d.register(t1, "thingy1") d.register(t2, "thingy2") data = [t1, ["apple", t2]] s, c = self.serializer.serializeData(data) data = self.serializer.deserializeData(s, c) self.assertEqual("apple", data[1][0]) p1 = data[0] p2 = data[1][1] self.assertIsInstance(p1, Pyro4.core.Proxy) self.assertIsInstance(p2, Pyro4.core.Proxy) self.assertEqual("thingy1", p1._pyroUri.object) self.assertEqual("thingy2", p2._pyroUri.object) self.assertEqual({"prop1", "readonly_prop1"}, p1._pyroAttrs) self.assertEqual({"exposed", "oneway"}, p1._pyroMethods) self.assertEqual({'oneway'}, p1._pyroOneway) def testAutoProxyFullExposed(self): if self.SERIALIZER == "marshal": self.skipTest("marshal can't serialize custom objects") self.serializer.register_type_replacement(MyThingPartlyExposed, Pyro4.core.pyroObjectToAutoProxy) t1 = MyThingFullExposed("1") t2 = MyThingFullExposed("2") with Pyro4.core.Daemon() as d: d.register(t1, "thingy1") d.register(t2, "thingy2") data = [t1, ["apple", t2]] s, c = self.serializer.serializeData(data) data = self.serializer.deserializeData(s, c) self.assertEqual("apple", data[1][0]) p1 = data[0] p2 = data[1][1] self.assertIsInstance(p1, Pyro4.core.Proxy) self.assertIsInstance(p2, Pyro4.core.Proxy) self.assertEqual("thingy1", p1._pyroUri.object) self.assertEqual("thingy2", p2._pyroUri.object) self.assertEqual({"prop1", "prop2", "readonly_prop1"}, p1._pyroAttrs) self.assertEqual({'classmethod', 'method', 'oneway', 'staticmethod', 'exposed', "__dunder__"}, p1._pyroMethods) self.assertEqual({'oneway'}, p1._pyroOneway) def testRegisterTypeReplacementSanity(self): if self.SERIALIZER == "marshal": self.skipTest("marshal can't serialize custom objects") self.serializer.register_type_replacement(int, lambda: None) with self.assertRaises(ValueError): self.serializer.register_type_replacement(type, lambda: None) with self.assertRaises(ValueError): self.serializer.register_type_replacement(42, lambda: None) def testCustomClassFail(self): if self.SERIALIZER in ("pickle", "cloudpickle", "dill"): self.skipTest("pickle, cloudpickle and dill simply serialize custom classes") o = pprint.PrettyPrinter(stream="dummy", width=42) s, c = self.serializer.serializeData(o) try: _ = self.serializer.deserializeData(s, c) self.fail("error expected, shouldn't deserialize unknown class") except Pyro4.errors.ProtocolError: pass def testCustomClassOk(self): if self.SERIALIZER in ("pickle", "cloudpickle", "dill"): self.skipTest("pickle, cloudpickle and dill simply serialize custom classes just fine") o = MyThingPartlyExposed("test") Pyro4.util.SerializerBase.register_class_to_dict(MyThingPartlyExposed, mything_dict) Pyro4.util.SerializerBase.register_dict_to_class("CUSTOM-Mythingymabob", mything_creator) s, c = self.serializer.serializeData(o) o2 = self.serializer.deserializeData(s, c) self.assertIsInstance(o2, MyThingPartlyExposed) self.assertEqual("test", o2.name) # unregister the deserializer Pyro4.util.SerializerBase.unregister_dict_to_class("CUSTOM-Mythingymabob") try: self.serializer.deserializeData(s, c) self.fail("must fail") except Pyro4.errors.ProtocolError: pass # ok # unregister the serializer Pyro4.util.SerializerBase.unregister_class_to_dict(MyThingPartlyExposed) s, c = self.serializer.serializeData(o) try: self.serializer.deserializeData(s, c) self.fail("must fail") except Pyro4.errors.SerializeError as x: msg = str(x) self.assertIn(msg, ["unsupported serialized class: testsupport.MyThingPartlyExposed", "unsupported serialized class: PyroTests.testsupport.MyThingPartlyExposed"]) def testData(self): data = [42, "hello"] ser, compressed = self.serializer.serializeData(data) self.assertFalse(compressed) data2 = self.serializer.deserializeData(ser, compressed=False) self.assertEqual(data, data2) def testUnicodeData(self): data = u"euro\u20aclowbytes\u0000\u0001\u007f\u0080\u00ff" ser, compressed = self.serializer.serializeData(data) data2 = self.serializer.deserializeData(ser, compressed=compressed) self.assertEqual(data, data2) def testUUID(self): data = uuid.uuid1() ser, compressed = self.serializer.serializeData(data) data2 = self.serializer.deserializeData(ser, compressed=compressed) uuid_as_str = str(data) self.assertTrue(data2==data or data2==uuid_as_str) def testSet(self): data = {111, 222, 333} ser, compressed = self.serializer.serializeData(data) data2 = self.serializer.deserializeData(ser, compressed=compressed) self.assertEqual(data, data2) def testCircular(self): data = [42, "hello", Pyro4.core.Proxy("PYRO:dummy@dummy:4444")] data.append(data) ser, compressed = self.serializer.serializeData(data) data2 = self.serializer.deserializeData(ser, compressed) self.assertIs(data2, data2[3]) self.assertEqual(42, data2[0]) def testCallPlain(self): ser, compressed = self.serializer.serializeCall("object", "method", ("vargs1", "vargs2"), {"kwargs": 999}) self.assertFalse(compressed) obj, method, vargs, kwargs = self.serializer.deserializeCall(ser, compressed=False) self.assertEqual("object", obj) self.assertEqual("method", method) self.assertTrue(len(vargs) == 2) self.assertTrue(vargs[0] == "vargs1") self.assertTrue(vargs[1] == "vargs2") self.assertDictEqual({"kwargs": 999}, kwargs) def testCallPyroObjAsArg(self): if self.SERIALIZER == "marshal": self.skipTest("marshal can't serialize custom objects") uri = Pyro4.core.URI("PYRO:555@localhost:80") ser, compressed = self.serializer.serializeCall("object", "method", [uri], {"thing": uri}) self.assertFalse(compressed) obj, method, vargs, kwargs = self.serializer.deserializeCall(ser, compressed=False) self.assertEqual("object", obj) self.assertEqual("method", method) self.assertEqual([uri], vargs) self.assertEqual({"thing": uri}, kwargs) def testCallCustomObjAsArg(self): if self.SERIALIZER == "marshal": self.skipTest("marshal can't serialize custom objects") e = ZeroDivisionError("hello") ser, compressed = self.serializer.serializeCall("object", "method", [e], {"thing": e}) self.assertFalse(compressed) obj, method, vargs, kwargs = self.serializer.deserializeCall(ser, compressed=False) self.assertEqual("object", obj) self.assertEqual("method", method) self.assertIsInstance(vargs, list) self.assertIsInstance(vargs[0], ZeroDivisionError) self.assertEqual("hello", str(vargs[0])) self.assertIsInstance(kwargs["thing"], ZeroDivisionError) self.assertEqual("hello", str(kwargs["thing"])) def testSerializeException(self): e = ZeroDivisionError() d, c = self.serializer.serializeData(e) e2 = self.serializer.deserializeData(d, c) self.assertIsInstance(e2, ZeroDivisionError) self.assertEqual("", str(e2)) e = ZeroDivisionError("hello") d, c = self.serializer.serializeData(e) e2 = self.serializer.deserializeData(d, c) self.assertIsInstance(e2, ZeroDivisionError) self.assertEqual("hello", str(e2)) e = ZeroDivisionError("hello", 42) d, c = self.serializer.serializeData(e) e2 = self.serializer.deserializeData(d, c) self.assertIsInstance(e2, ZeroDivisionError) self.assertIn(str(e2), ("('hello', 42)", "(u'hello', 42)")) e.custom_attribute = 999 if sys.platform == "cli": Pyro4.util.fixIronPythonExceptionForPickle(e, True) ser, compressed = self.serializer.serializeData(e) e2 = self.serializer.deserializeData(ser, compressed) if sys.platform == "cli": Pyro4.util.fixIronPythonExceptionForPickle(e2, False) self.assertIsInstance(e2, ZeroDivisionError) self.assertIn(str(e2), ("('hello', 42)", "(u'hello', 42)")) self.assertEqual(999, e2.custom_attribute) def testSerializeSpecialException(self): self.assertIn("GeneratorExit", Pyro4.util.all_exceptions) e = GeneratorExit() d, c = self.serializer.serializeData(e) e2 = self.serializer.deserializeData(d, c) self.assertIsInstance(e2, GeneratorExit) def testRecreateClasses(self): self.assertEqual([1, 2, 3], self.serializer.recreate_classes([1, 2, 3])) d = {"__class__": "invalid"} try: self.serializer.recreate_classes(d) self.fail("error expected") except Pyro4.errors.ProtocolError: pass # ok d = {"__class__": "Pyro4.core.URI", "state": ['PYRO', '555', None, 'localhost', 80]} uri = self.serializer.recreate_classes(d) self.assertEqual(Pyro4.core.URI("PYRO:555@localhost:80"), uri) number, uri = self.serializer.recreate_classes([1, {"uri": d}]) self.assertEqual(1, number) self.assertEqual(Pyro4.core.URI("PYRO:555@localhost:80"), uri["uri"]) def testProtocolVersion(self): self.assertGreaterEqual(config.PICKLE_PROTOCOL_VERSION, 2) self.assertEqual(pickle.HIGHEST_PROTOCOL, config.PICKLE_PROTOCOL_VERSION) def testUriSerializationWithoutSlots(self): orig_protocol = config.PICKLE_PROTOCOL_VERSION config.PICKLE_PROTOCOL_VERSION = 2 try: u = Pyro4.core.URI("PYRO:obj@localhost:1234") d, compr = self.serializer.serializeData(u) self.assertFalse(compr) import pickletools d = pickletools.optimize(d) result1 = b'\x80\x02cPyro4.core\nURI\n)\x81(U\x04PYROU\x03objNU\tlocalhostM\xd2\x04tb.' result2 = b'\x80\x02cPyro4.core\nURI\n)\x81(X\x04\x00\x00\x00PYROX\x03\x00\x00\x00objNX\t\x00\x00\x00localhostM\xd2\x04tb.' self.assertTrue(d in (result1, result2)) finally: config.PICKLE_PROTOCOL_VERSION = orig_protocol def testFloatPrecision(self): f1 = 1482514078.54635912345 f2 = 9876543212345.12345678987654321 f3 = 11223344.556677889988776655e33 floats = [f1, f2, f3] d, compr = self.serializer.serializeData(floats) v = self.serializer.deserializeData(d, compr) self.assertEqual(floats, v, "float precision must not be compromised in any serializer") def testSourceByteTypes_deserialize(self): # uncompressed call_ser, _ = self.serializer.serializeCall("object", "method", [1, 2, 3], {"kwarg": 42}, False) ser, _ = self.serializer.serializeData([4, 5, 6], False) _, _, vargs, _ = self.serializer.deserializeCall(bytearray(call_ser), False) self.assertEqual([1, 2, 3], vargs) d = self.serializer.deserializeData(bytearray(ser), False) self.assertEqual([4, 5, 6], d) if sys.version_info < (3, 0): _, _, vargs, _ = self.serializer.deserializeCall(buffer(call_ser), False) self.assertEqual([1, 2, 3], vargs) d = self.serializer.deserializeData(buffer(ser), False) self.assertEqual([4, 5, 6], d) # compressed call_ser, _ = self.serializer.serializeCall("object", "method", [1, 2, 3] * 100, {"kwarg": 42}, True) ser, _ = self.serializer.serializeData([4, 5, 6] * 100, True) _, _, vargs, _ = self.serializer.deserializeCall(bytearray(call_ser), True) self.assertEqual(300, len(vargs)) d = self.serializer.deserializeData(bytearray(ser), True) self.assertEqual(300, len(d)) if sys.version_info < (3, 0): _, _, vargs, _ = self.serializer.deserializeCall(buffer(call_ser), True) self.assertEqual(300, len(vargs)) d = self.serializer.deserializeData(buffer(ser), True) self.assertEqual(300, len(d)) @unittest.skipIf(sys.platform == "cli", "ironpython can't properly create memoryviews from serialized data") def testSourceByteTypes_deserialize_memoryview(self): # uncompressed call_ser, _ = self.serializer.serializeCall("object", "method", [1, 2, 3], {"kwarg": 42}, False) ser, _ = self.serializer.serializeData([4, 5, 6], False) _, _, vargs, _ = self.serializer.deserializeCall(memoryview(call_ser), False) self.assertEqual([1, 2, 3], vargs) d = self.serializer.deserializeData(memoryview(ser), False) self.assertEqual([4, 5, 6], d) # compressed call_ser, _ = self.serializer.serializeCall("object", "method", [1, 2, 3] * 100, {"kwarg": 42}, True) ser, _ = self.serializer.serializeData([4, 5, 6] * 100, True) _, _, vargs, _ = self.serializer.deserializeCall(memoryview(call_ser), True) self.assertEqual(300, len(vargs)) d = self.serializer.deserializeData(memoryview(ser), True) self.assertEqual(300, len(d)) def testSourceByteTypes_loads(self): call_ser, _ = self.serializer.serializeCall("object", "method", [1, 2, 3], {"kwarg": 42}, False) ser, _ = self.serializer.serializeData([4, 5, 6], False) _, _, vargs, _ = self.serializer.loadsCall(bytearray(call_ser)) self.assertEqual([1, 2, 3], vargs) d = self.serializer.loads(bytearray(ser)) self.assertEqual([4, 5, 6], d) if sys.version_info < (3, 0): _, _, vargs, _ = self.serializer.loadsCall(buffer(call_ser)) self.assertEqual([1, 2, 3], vargs) d = self.serializer.loads(buffer(ser)) self.assertEqual([4, 5, 6], d) @unittest.skipIf(sys.platform == "cli", "ironpython can't properly create memoryviews from serialized data") def testSourceByteTypes_loads_memoryview(self): call_ser, _ = self.serializer.serializeCall("object", "method", [1, 2, 3], {"kwarg": 42}, False) ser, _ = self.serializer.serializeData([4, 5, 6], False) _, _, vargs, _ = self.serializer.loadsCall(memoryview(call_ser)) self.assertEqual([1, 2, 3], vargs) d = self.serializer.loads(memoryview(ser)) self.assertEqual([4, 5, 6], d) def testSerializeDumpsAndDumpsCall(self): self.serializer.dumps(uuid.uuid4()) self.serializer.dumps(Pyro4.URI("PYRO:test@test:4444")) self.serializer.dumps(Pyro4.Proxy("PYRONAME:foobar")) self.serializer.dumpsCall("obj", "method", (1, 2, 3), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, array.array('i', [1, 2, 3])), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, array.array('i', [1, 2, 3])), {"arg1": array.array('i', [1, 2, 3])}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro4.URI("PYRO:test@test:4444")), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro4.URI("PYRO:test@test:4444")), {"arg1": Pyro4.URI("PYRO:test@test:4444")}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro4.Proxy("PYRONAME:foobar")), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro4.Proxy("PYRONAME:foobar")), {"arg1": Pyro4.Proxy("PYRONAME:foobar")}) def testArrays(self): if sys.version_info < (3, 0): a1 = array.array('c', b"hello") ser = self.serializer.dumps(a1) a2 = self.serializer.loads(ser) if type(a2) is array.array: self.assertEqual(a1, a2) else: self.assertEqual(b"hello", a2) a1 = array.array('u', unicode("hello")) ser = self.serializer.dumps(a1) a2 = self.serializer.loads(ser) if type(a2) is array.array: self.assertEqual(a1, a2) else: self.assertEqual(unicode("hello"), a2) a1 = array.array('h', [222, 333, 444, 555]) ser = self.serializer.dumps(a1) a2 = self.serializer.loads(ser) if type(a2) is array.array: self.assertEqual(a1, a2) else: self.assertEqual([222, 333, 444, 555], a2) def testArrays2(self): if sys.version_info < (3, 0): a1 = array.array('c', b"hello") ser = self.serializer.dumpsCall("obj", "method", [a1], {}) a2 = self.serializer.loads(ser) a2 = a2["params"][0] if self.SERIALIZER == "json" else a2[2][0] if type(a2) is array.array: self.assertEqual(a1, a2) else: self.assertEqual(b"hello", a2) a1 = array.array('u', unicode("hello")) ser = self.serializer.dumpsCall("obj", "method", [a1], {}) a2 = self.serializer.loads(ser) a2 = a2["params"][0] if self.SERIALIZER == "json" else a2[2][0] if type(a2) is array.array: self.assertEqual(a1, a2) else: self.assertEqual(unicode("hello"), a2) a1 = array.array('h', [222, 333, 444, 555]) ser = self.serializer.dumpsCall("obj", "method", [a1], {}) a2 = self.serializer.loads(ser) a2 = a2["params"][0] if self.SERIALIZER == "json" else a2[2][0] if type(a2) is array.array: self.assertEqual(a1, a2) else: self.assertEqual([222, 333, 444, 555], a2) class SerializeTests_cloudpickle(SerializeTests_pickle): SERIALIZER = "cloudpickle" @unittest.skip('not implemented') def testUriSerializationWithoutSlots(self): pass def testSerializeLambda(self): l = lambda x: x * x ser, compressed = self.serializer.serializeData(l) l2 = self.serializer.deserializeData(ser, compressed=compressed) self.assertEqual(l2(3.), 9.) def testSerializeLocalFunction(self): def f(x): return x * x ser, compressed = self.serializer.serializeData(f) f2 = self.serializer.deserializeData(ser, compressed=compressed) self.assertEqual(f2(3.), 9.) is_ironpython_without_dill = False try: import dill except ImportError: if sys.platform == "cli": is_ironpython_without_dill = True @unittest.skipIf(is_ironpython_without_dill, "dill with ironpython has issues so it's fine if we don't test this") class SerializeTests_dill(SerializeTests_pickle): SERIALIZER = "dill" def testProtocolVersion(self): import dill self.assertEqual(dill.HIGHEST_PROTOCOL, config.DILL_PROTOCOL_VERSION) @unittest.skip('not implemented') def testUriSerializationWithoutSlots(self): pass def testSerializeLambda(self): l = lambda x: x * x ser, compressed = self.serializer.serializeData(l) l2 = self.serializer.deserializeData(ser, compressed=compressed) self.assertEqual(l2(3.), 9.) def testSerializeLocalFunction(self): def f(x): return x * x ser, compressed = self.serializer.serializeData(f) f2 = self.serializer.deserializeData(ser, compressed=compressed) self.assertEqual(f2(3.), 9.) class SerializeTests_serpent(SerializeTests_pickle): SERIALIZER = "serpent" def testCircular(self): # serpent doesn't support object graphs (since serpent 1.7 reports ValueError instead of crashing) with self.assertRaises(ValueError): super(SerializeTests_serpent, self).testCircular() def testSet(self): data = {111, 222, 333} ser, compressed = self.serializer.serializeData(data) data2 = self.serializer.deserializeData(ser, compressed=compressed) self.assertEqual(tuple(data), tuple(data2)) def testDeque(self): # serpent converts a deque into a primitive list deq = collections.deque([1, 2, 3, 4]) ser, compressed = self.serializer.serializeData(deq) data2 = self.serializer.deserializeData(ser, compressed=compressed) self.assertEqual([1, 2, 3, 4], data2) @unittest.skipIf(sys.version_info < (2, 7), "ordereddict is in Python 2.7+") def testOrderedDict(self): od = collections.OrderedDict() od["a"] = 1 od["b"] = 2 od["c"] = 3 def recreate_OrderedDict(name, values): self.assertEqual("collections.OrderedDict", name) return collections.OrderedDict(values["items"]) Pyro4.util.SerializerBase.register_dict_to_class("collections.OrderedDict", recreate_OrderedDict) ser, compressed = self.serializer.serializeData(od) self.assertIn(b"collections.OrderedDict", ser) self.assertIn(b"[('a',1),('b',2),('c',3)]", ser) data2 = self.serializer.deserializeData(ser, compressed=compressed) self.assertEqual(od, data2) def testUriSerializationWithoutSlots(self): u = Pyro4.core.URI("PYRO:obj@localhost:1234") d, compr = self.serializer.serializeData(u) self.assertFalse(compr) result1 = b"# serpent utf-8 python3.2\n{'__class__':'Pyro4.core.URI','state':('PYRO','obj',None,'localhost',1234)}" result2 = b"# serpent utf-8 python3.2\n{'state':('PYRO','obj',None,'localhost',1234),'__class__':'Pyro4.core.URI'}" result3 = b"# serpent utf-8 python2.6\n{'state':('PYRO','obj',None,'localhost',1234),'__class__':'Pyro4.core.URI'}" result4 = b"# serpent utf-8 python2.6\n{'__class__':'Pyro4.core.URI','state':('PYRO','obj',None,'localhost',1234)}" self.assertTrue(d in (result1, result2, result3, result4)) class SerializeTests_json(SerializeTests_pickle): SERIALIZER = "json" def testCircular(self): with self.assertRaises(ValueError): # json doesn't support object graphs super(SerializeTests_json, self).testCircular() def testSet(self): # json serializes a set into a list, so we override this data = {111, 222, 333} ser, compressed = self.serializer.serializeData(data) data2 = self.serializer.deserializeData(ser, compressed=compressed) self.assertEqual(list(data), data2) def testUriSerializationWithoutSlots(self): u = Pyro4.core.URI("PYRO:obj@localhost:1234") d, compr = self.serializer.serializeData(u) self.assertFalse(compr) result1 = b'{"__class__": "Pyro4.core.URI", "state": ["PYRO", "obj", null, "localhost", 1234]}' result2 = b'{"state": ["PYRO", "obj", null, "localhost", 1234], "__class__": "Pyro4.core.URI"}' self.assertTrue(d in (result1, result2)) class SerializeTests_marshal(SerializeTests_pickle): SERIALIZER = "marshal" def testCircular(self): with self.assertRaises(ValueError): # marshal doesn't support object graphs super(SerializeTests_marshal, self).testCircular() @unittest.skip("marshaling is implementation dependent") def testUriSerializationWithoutSlots(self): pass class SerializeTests_msgpack(SerializeTests_pickle): SERIALIZER = "msgpack" @unittest.skip("circular will crash msgpack") def testCircular(self): pass def testSet(self): # msgpack serializes a set into a list, so we override this data = {111, 222, 333} ser, compressed = self.serializer.serializeData(data) data2 = self.serializer.deserializeData(ser, compressed=compressed) self.assertEqual(list(data), data2) @unittest.skip("msgpack is implementation dependent") def testUriSerializationWithoutSlots(self): pass class GenericTests(unittest.TestCase): def testSerializersAvailable(self): Pyro4.util.get_serializer("pickle") Pyro4.util.get_serializer("marshal") try: import json Pyro4.util.get_serializer("json") except ImportError: pass try: import serpent Pyro4.util.get_serializer("serpent") except ImportError: pass try: import cloudpickle Pyro4.util.get_serializer("cloudpickle") except ImportError: pass try: import dill Pyro4.util.get_serializer("dill") except ImportError: pass def testAssignedSerializerIds(self): self.assertEqual(1, Pyro4.util.SerpentSerializer.serializer_id) self.assertEqual(2, Pyro4.util.JsonSerializer.serializer_id) self.assertEqual(3, Pyro4.util.MarshalSerializer.serializer_id) self.assertEqual(4, Pyro4.util.PickleSerializer.serializer_id) self.assertEqual(5, Pyro4.util.DillSerializer.serializer_id) self.assertEqual(6, Pyro4.util.MsgpackSerializer.serializer_id) self.assertEqual(7, Pyro4.util.CloudpickleSerializer.serializer_id) def testSerializersAvailableById(self): Pyro4.util.get_serializer_by_id(1) # serpent Pyro4.util.get_serializer_by_id(2) # json Pyro4.util.get_serializer_by_id(3) # marshal Pyro4.util.get_serializer_by_id(4) # pickle # ids 5, 6 and 7 (dill, msgpack, cloudpickle) are not always available, so we skip those. self.assertRaises(Pyro4.errors.SerializeError, lambda: Pyro4.util.get_serializer_by_id(0)) self.assertRaises(Pyro4.errors.SerializeError, lambda: Pyro4.util.get_serializer_by_id(8)) def testDictClassFail(self): o = pprint.PrettyPrinter(stream="dummy", width=42) d = Pyro4.util.SerializerBase.class_to_dict(o) self.assertEqual(42, d["_width"]) self.assertEqual("pprint.PrettyPrinter", d["__class__"]) try: _ = Pyro4.util.SerializerBase.dict_to_class(d) self.fail("error expected") except Pyro4.errors.ProtocolError: pass def testDictException(self): x = ZeroDivisionError("hello", 42) expected = { "__class__": None, "__exception__": True, "args": ("hello", 42), "attributes": {} } if sys.version_info < (3, 0): expected["__class__"] = "exceptions.ZeroDivisionError" else: expected["__class__"] = "builtins.ZeroDivisionError" d = Pyro4.util.SerializerBase.class_to_dict(x) self.assertEqual(expected, d) x.custom_attribute = 999 expected["attributes"] = {"custom_attribute": 999} d = Pyro4.util.SerializerBase.class_to_dict(x) self.assertEqual(expected, d) def testDictClassOk(self): uri = Pyro4.core.URI("PYRO:object@host:4444") d = Pyro4.util.SerializerBase.class_to_dict(uri) self.assertEqual("Pyro4.core.URI", d["__class__"]) self.assertIn("state", d) x = Pyro4.util.SerializerBase.dict_to_class(d) self.assertIsInstance(x, Pyro4.core.URI) self.assertEqual(uri, x) self.assertEqual(4444, x.port) uri = Pyro4.core.URI("PYRO:12345@./u:/tmp/socketname") d = Pyro4.util.SerializerBase.class_to_dict(uri) self.assertEqual("Pyro4.core.URI", d["__class__"]) self.assertIn("state", d) x = Pyro4.util.SerializerBase.dict_to_class(d) self.assertIsInstance(x, Pyro4.core.URI) self.assertEqual(uri, x) self.assertEqual("/tmp/socketname", x.sockname) def testCustomDictClass(self): o = MyThingPartlyExposed("test") Pyro4.util.SerializerBase.register_class_to_dict(MyThingPartlyExposed, mything_dict) Pyro4.util.SerializerBase.register_dict_to_class("CUSTOM-Mythingymabob", mything_creator) d = Pyro4.util.SerializerBase.class_to_dict(o) self.assertEqual("CUSTOM-Mythingymabob", d["__class__"]) self.assertEqual("test", d["name"]) x = Pyro4.util.SerializerBase.dict_to_class(d) self.assertIsInstance(x, MyThingPartlyExposed) self.assertEqual("test", x.name) # unregister the conversion functions and try again Pyro4.util.SerializerBase.unregister_class_to_dict(MyThingPartlyExposed) Pyro4.util.SerializerBase.unregister_dict_to_class("CUSTOM-Mythingymabob") d_orig = Pyro4.util.SerializerBase.class_to_dict(o) clsname = d_orig["__class__"] self.assertTrue(clsname.endswith("testsupport.MyThingPartlyExposed")) try: _ = Pyro4.util.SerializerBase.dict_to_class(d) self.fail("should crash") except Pyro4.errors.ProtocolError: pass # ok def testExceptionNamespacePy2(self): data = {'__class__': 'exceptions.ZeroDivisionError', '__exception__': True, 'args': ('hello', 42), 'attributes': {"test_attribute": 99}} exc = Pyro4.util.SerializerBase.dict_to_class(data) self.assertIsInstance(exc, ZeroDivisionError) self.assertEqual("ZeroDivisionError('hello', 42)", repr(exc)) self.assertEqual(99, exc.test_attribute) def testExceptionNamespacePy3(self): data = {'__class__': 'builtins.ZeroDivisionError', '__exception__': True, 'args': ('hello', 42), 'attributes': {"test_attribute": 99}} exc = Pyro4.util.SerializerBase.dict_to_class(data) self.assertIsInstance(exc, ZeroDivisionError) self.assertEqual("ZeroDivisionError('hello', 42)", repr(exc)) self.assertEqual(99, exc.test_attribute) def testExceptionNotTagged(self): data = {'__class__': 'builtins.ZeroDivisionError', 'args': ('hello', 42), 'attributes': {}} with self.assertRaises(Pyro4.errors.SerializeError) as cm: _ = Pyro4.util.SerializerBase.dict_to_class(data) self.assertEqual("unsupported serialized class: builtins.ZeroDivisionError", str(cm.exception)) def testWeirdFloats(self): ser = Pyro4.util.get_serializer(config.SERIALIZER) p, _ = ser.serializeData([float("+inf"), float("-inf"), float("nan")]) s2 = ser.deserializeData(p) self.assertTrue(math.isinf(s2[0])) self.assertEqual(1.0, math.copysign(1, s2[0])) self.assertTrue(math.isinf(s2[1])) self.assertEqual(-1.0, math.copysign(1, s2[1])) self.assertTrue(math.isnan(s2[2])) def mything_dict(obj): return { "__class__": "CUSTOM-Mythingymabob", "name": obj.name } def mything_creator(classname, d): assert classname == "CUSTOM-Mythingymabob" assert d["__class__"] == "CUSTOM-Mythingymabob" return MyThingPartlyExposed(d["name"]) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() Pyro4-4.82/tests/PyroTests/test_server.py000066400000000000000000001420101416147301300205170ustar00rootroot00000000000000""" Tests for a running Pyro server, without timeouts. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import time import sys import threading import uuid import unittest import serpent import Pyro4.core import Pyro4.errors import Pyro4.util import Pyro4.message import Pyro4.socketutil from Pyro4.configuration import config from testsupport import * @Pyro4.core.expose class ServerTestObject(object): something = 99 dict_attr = {} def __init__(self): self._dictionary = {"number": 42} self.dict_attr = {"number2": 43} self._value = 12345 def getDict(self): return self._dictionary def getDictAttr(self): return self.dict_attr def multiply(self, x, y): return x * y def divide(self, x, y): return x // y def ping(self): pass def echo(self, obj): return obj def blob(self, blob): return blob.info, blob.deserialized() @Pyro4.core.oneway def oneway_delay(self, delay): time.sleep(delay) def delay(self, delay): time.sleep(delay) return "slept %d seconds" % delay def delayAndId(self, delay, id): time.sleep(delay) return "slept for " + str(id) def testargs(self, x, *args, **kwargs): return [x, list(args), kwargs] # don't return tuples, this enables us to test json serialization as well. def nonserializableException(self): raise NonserializableError(("xantippe", lambda x: 0)) @Pyro4.core.oneway def oneway_multiply(self, x, y): return x * y @property def value(self): return self._value @value.setter def value(self, newvalue): self._value = newvalue @property def dictionary(self): return self._dictionary def iterator(self): return iter(["one", "two", "three"]) def generator(self): yield "one" yield "two" yield "three" yield "four" yield "five" def response_annotation(self): # part of the annotations tests if "XYZZ" not in Pyro4.core.current_context.annotations: raise ValueError("XYZZ should be present in annotations in the daemon") if Pyro4.core.current_context.annotations["XYZZ"] != b"data from proxy via new api": raise ValueError("XYZZ annotation has wrong data") Pyro4.core.current_context.response_annotations["ANN2"] = b"daemon annotation via new api" return {"annotations_in_daemon": Pyro4.core.current_context.annotations} def new_test_object(self): return ServerTestObject() class NotEverythingExposedClass(object): def __init__(self, name): self.name = name @Pyro4.core.expose def getName(self): return self.name def unexposed(self): return "you should not see this" # .... only when REQUIRE_EXPOSE is set to True is this valid class DaemonLoopThread(threading.Thread): def __init__(self, pyrodaemon): super(DaemonLoopThread, self).__init__() self.setDaemon(True) self.pyrodaemon = pyrodaemon self.running = threading.Event() self.running.clear() def run(self): self.running.set() try: self.pyrodaemon.requestLoop() except Pyro4.errors.CommunicationError: pass # ignore pyro communication errors class DaemonWithSabotagedHandshake(Pyro4.core.Daemon): def _handshake(self, conn, denied_reason=None): # receive the client's handshake data msg = Pyro4.message.Message.recv(conn, [Pyro4.message.MSG_CONNECT], self._pyroHmacKey) # return a CONNECTFAIL always serializer = Pyro4.util.get_serializer_by_id(msg.serializer_id) data, _ = serializer.serializeData("rigged connection failure", compress=False) msg = Pyro4.message.Message(Pyro4.message.MSG_CONNECTFAIL, data, serializer.serializer_id, 0, 1, hmac_key=self._pyroHmacKey) conn.send(msg.to_bytes()) return False class ServerTestsBrokenHandshake(unittest.TestCase): def setUp(self): config.LOGWIRE = True config.SERIALIZERS_ACCEPTED.add("pickle") self.daemon = DaemonWithSabotagedHandshake(port=0) obj = ServerTestObject() uri = self.daemon.register(obj, "something") self.objectUri = uri self.daemonthread = DaemonLoopThread(self.daemon) self.daemonthread.start() self.daemonthread.running.wait() time.sleep(0.05) def tearDown(self): time.sleep(0.05) self.daemon.shutdown() self.daemonthread.join() config.SERIALIZERS_ACCEPTED.discard("pickle") def testDaemonConnectFail(self): # check what happens when the daemon responds with a failed connection msg with Pyro4.core.Proxy(self.objectUri) as p: try: p.ping() self.fail("expected CommunicationError") except Pyro4.errors.CommunicationError as x: message = str(x) self.assertIn("rejected:", message) self.assertIn("rigged connection failure", message) class ServerTestsOnce(unittest.TestCase): """tests that are fine to run with just a single server type""" def setUp(self): config.LOGWIRE = True config.SERIALIZERS_ACCEPTED.add("pickle") self.daemon = Pyro4.core.Daemon(port=0) obj = ServerTestObject() uri = self.daemon.register(obj, "something") self.objectUri = uri obj2 = NotEverythingExposedClass("hello") self.daemon.register(obj2, "unexposed") self.daemonthread = DaemonLoopThread(self.daemon) self.daemonthread.start() self.daemonthread.running.wait() time.sleep(0.05) def tearDown(self): time.sleep(0.05) if self.daemon is not None: self.daemon.shutdown() self.daemonthread.join() config.SERIALIZERS_ACCEPTED.discard("pickle") def testPingMessage(self): with Pyro4.core.Proxy(self.objectUri) as p: p._pyroBind() conn = p._pyroConnection msg = Pyro4.message.Message(Pyro4.message.MSG_PING, b"something", 42, 0, 999, hmac_key=p._pyroHmacKey) conn.send(msg.to_bytes()) msg = Pyro4.message.Message.recv(conn, [Pyro4.message.MSG_PING], hmac_key=p._pyroHmacKey) self.assertEqual(Pyro4.message.MSG_PING, msg.type) self.assertEqual(999, msg.seq) self.assertEqual(b"pong", msg.data) Pyro4.message.Message.ping(p._pyroConnection) # the convenience method that does the above def testSequence(self): with Pyro4.core.Proxy(self.objectUri) as p: p.echo(1) p.echo(2) p.echo(3) self.assertEqual(3, p._pyroSeq, "should have 3 method calls") p._pyroSeq = 999 # hacking the seq nr won't have any effect because it is the reply from the server that is checked self.assertEqual(42, p.echo(42)) def testMetaOffAttrs(self): try: old_meta = config.METADATA config.METADATA = False # should fail here, because there is no meta info about attributes with Pyro4.core.Proxy(self.objectUri) as p: self.assertEqual(55, p.multiply(5, 11)) x = p.getDict() self.assertEqual({"number": 42}, x) # property with self.assertRaises(AttributeError): p.dictionary.update({"more": 666}) # attribute with self.assertRaises(AttributeError): p.dict_attr.update({"more": 666}) x = p.getDict() self.assertEqual({"number": 42}, x) finally: config.METADATA = old_meta def testMetaOnAttrs(self): try: old_meta = config.METADATA config.METADATA = True with Pyro4.core.Proxy(self.objectUri) as p: self.assertEqual(55, p.multiply(5, 11)) # property x = p.getDict() self.assertEqual({"number": 42}, x) p.dictionary.update({"more": 666}) # should not fail because metadata is enabled and the dictionary property is retrieved as local copy x = p.getDict() self.assertEqual({"number": 42}, x) # not updated remotely because we had a local copy with Pyro4.core.Proxy(self.objectUri) as p: with self.assertRaises(AttributeError): # attribute should fail (meta only works for exposed properties) p.dict_attr.update({"more": 666}) finally: config.METADATA = old_meta def testSomeArgumentTypes(self): with Pyro4.core.Proxy(self.objectUri) as p: self.assertEqual([1, [], {}], p.testargs(1)) self.assertEqual([1, [2, 3], {'a': 4}], p.testargs(1, 2, 3, a=4)) self.assertEqual([1, [], {'a': 2}], p.testargs(1, **{'a': 2})) def testUnicodeKwargs(self): with Pyro4.core.Proxy(self.objectUri) as p: self.assertEqual([1, [], {unichr(65): 2}], p.testargs(1, **{unichr(65): 2})) result = p.testargs(unichr(0x20ac), **{unichr(0x20ac): 2}) self.assertEqual(result[0], unichr(0x20ac)) key = list(result[2].keys())[0] self.assertTrue(type(key) is unicode) self.assertEqual(key, unichr(0x20ac)) def testNormalProxy(self): with Pyro4.core.Proxy(self.objectUri) as p: self.assertEqual(42, p.multiply(7, 6)) def testExceptions(self): with Pyro4.core.Proxy(self.objectUri) as p: try: p.divide(1, 0) self.fail("should crash") except ZeroDivisionError: pass try: p.multiply("a", "b") self.fail("should crash") except TypeError: pass def testProxyMetadata(self): with Pyro4.core.Proxy(self.objectUri) as p: # unconnected proxies have empty metadata self.assertEqual(set(), p._pyroAttrs) self.assertEqual(set(), p._pyroMethods) self.assertEqual(set(), p._pyroOneway) # connecting it should obtain metadata (as long as METADATA is true) p._pyroBind() self.assertEqual({'value', 'dictionary'}, p._pyroAttrs) self.assertEqual({'echo', 'getDict', 'divide', 'nonserializableException', 'ping', 'oneway_delay', 'delayAndId', 'delay', 'testargs', 'multiply', 'oneway_multiply', 'getDictAttr', 'iterator', 'generator', 'response_annotation', 'blob', 'new_test_object'}, p._pyroMethods) self.assertEqual({'oneway_multiply', 'oneway_delay'}, p._pyroOneway) p._pyroAttrs = None p._pyroGetMetadata() self.assertEqual({'value', 'dictionary'}, p._pyroAttrs) p._pyroAttrs = None p._pyroGetMetadata(self.objectUri.object) self.assertEqual({'value', 'dictionary'}, p._pyroAttrs) p._pyroAttrs = None p._pyroGetMetadata(known_metadata={"attrs": set(), "oneway": set(), "methods": {"ping"}}) self.assertEqual(set(), p._pyroAttrs) def testProxyRepr(self): with Pyro4.core.Proxy(self.objectUri) as p: p._pyroBind() expected = "" % (id(p), Pyro4.socketutil.family_str(self.daemon.sock), self.objectUri) self.assertEqual(expected, str(p)) self.assertEqual(expected, repr(p)) def testProxyAttrsMetadataOff(self): try: config.METADATA = False # read attributes with Pyro4.core.Proxy(self.objectUri) as p: a = p.multiply self.assertIsInstance(a, Pyro4.core._RemoteMethod) a = p.value self.assertIsInstance(a, Pyro4.core._RemoteMethod) a = p.non_existing_attribute self.assertIsInstance(a, Pyro4.core._RemoteMethod) # set attributes with Pyro4.core.Proxy(self.objectUri) as p: p.some_weird_attribute = 42 self.assertEqual(42, p.some_weird_attribute) finally: config.METADATA = True def testProxyAttrsMetadataOn(self): try: config.METADATA = True # read attributes with Pyro4.core.Proxy(self.objectUri) as p: # unconnected proxy still has empty metadata. # but, as soon as an attribute is used, the metadata is obtained (as long as METADATA is true) a = p.value self.assertEqual(12345, a) a = p.multiply self.assertIsInstance(a, Pyro4.core._RemoteMethod) # multiply is still a regular method with self.assertRaises(AttributeError): _ = p.non_existing_attribute # set attributes, should also trigger getting metadata with Pyro4.core.Proxy(self.objectUri) as p: p.value = 42 self.assertEqual(42, p.value) self.assertTrue("value" in p._pyroAttrs) finally: config.METADATA = True def testProxyAnnotationsOldApi(self): class CustomAnnotationsProxy(Pyro4.core.Proxy): def __init__(self, uri, response): self.__dict__["response"] = response super(CustomAnnotationsProxy, self).__init__(uri) def _pyroAnnotations(self): ann = {"XYZZ": b"some data sent by old annotations api"} self.__dict__["response"]["annotations_sent"] = ann return ann def _pyroResponseAnnotations(self, annotations, msgtype): self.__dict__["response"]["annotations"] = annotations self.__dict__["response"]["msgtype"] = msgtype response = {} corr_id = Pyro4.core.current_context.correlation_id = uuid.uuid4() with CustomAnnotationsProxy(self.objectUri, response) as p: p.ping() self.assertDictEqual({"XYZZ": b"some data sent by old annotations api"}, p.__dict__["response"]["annotations_sent"]) self.assertEqual(Pyro4.message.MSG_RESULT, p.__dict__["response"]["msgtype"]) self.assertDictEqual({"CORR": corr_id.bytes}, p.__dict__["response"]["annotations"]) def testProxyAnnotations(self): class AnnotationsProxy(Pyro4.core.Proxy): def _pyroAnnotations(self): return {"QWER": b"data via old api"} with AnnotationsProxy(self.objectUri) as p: Pyro4.core.current_context.annotations = {"XYZZ": b"invalid test data"} try: p.response_annotation() self.fail("should fail") except ValueError: pass Pyro4.core.current_context.annotations = {"XYZZ": b"data from proxy via new api"} response = p.response_annotation() self.assertEqual(b"daemon annotation via new api", Pyro4.core.current_context.response_annotations["ANN2"]) # check that the daemon received both the old and the new annotation api data: daemon_annotations = response["annotations_in_daemon"] if sys.version_info < (3, 0) and sys.platform != "cli": self.assertEqual("data from proxy via new api", daemon_annotations["XYZZ"]) self.assertEqual("data via old api", daemon_annotations["QWER"]) else: self.assertEqual(b"data from proxy via new api", serpent.tobytes(daemon_annotations["XYZZ"])) self.assertEqual(b"data via old api", serpent.tobytes(daemon_annotations["QWER"])) def testExposedNotRequired(self): try: old_require = config.REQUIRE_EXPOSE config.REQUIRE_EXPOSE = False with self.daemon.proxyFor("unexposed") as p: self.assertEqual({"unexposed", "getName"}, p._pyroMethods) self.assertEqual("hello", p.getName()) self.assertEqual("you should not see this", p.unexposed()) # you *should* see it when REQUIRE_EXPOSE is False :) finally: config.REQUIRE_EXPOSE = old_require def testExposedRequired(self): try: old_require = config.REQUIRE_EXPOSE config.REQUIRE_EXPOSE = True with self.daemon.proxyFor("unexposed") as p: self.assertEqual({"getName"}, p._pyroMethods) self.assertEqual("hello", p.getName()) with self.assertRaises(AttributeError) as e: p.unexposed() expected_msg = "remote object '%s' has no exposed attribute or method 'unexposed'" % p._pyroUri self.assertEqual(expected_msg, str(e.exception)) with self.assertRaises(AttributeError) as e: p.unexposed_set = 999 expected_msg = "remote object '%s' has no exposed attribute 'unexposed_set'" % p._pyroUri self.assertEqual(expected_msg, str(e.exception)) finally: config.REQUIRE_EXPOSE = old_require def testProperties(self): with Pyro4.core.Proxy(self.objectUri) as p: _ = p.value # metadata should be loaded now self.assertEqual({"value", "dictionary"}, p._pyroAttrs) with self.assertRaises(AttributeError): _ = p.something with self.assertRaises(AttributeError): _ = p._dictionary with self.assertRaises(AttributeError): _ = p._value self.assertEqual(12345, p.value) self.assertEqual({"number": 42}, p.dictionary) def testHasAttr(self): try: config.METADATA = False with Pyro4.core.Proxy(self.objectUri) as p: # with metadata off, all attributes are considered valid (and return a RemoteMethod object) self.assertTrue(hasattr(p, "multiply")) self.assertTrue(hasattr(p, "oneway_multiply")) self.assertTrue(hasattr(p, "value")) self.assertTrue(hasattr(p, "_value")) self.assertTrue(hasattr(p, "_dictionary")) self.assertTrue(hasattr(p, "non_existing_attribute")) config.METADATA = True with Pyro4.core.Proxy(self.objectUri) as p: # with metadata on, hasattr actually gives proper results self.assertTrue(hasattr(p, "multiply")) self.assertTrue(hasattr(p, "oneway_multiply")) self.assertTrue(hasattr(p, "value")) self.assertFalse(hasattr(p, "_value")) self.assertFalse(hasattr(p, "_dictionary")) self.assertFalse(hasattr(p, "non_existing_attribute")) finally: config.METADATA = True def testProxyMetadataKnown(self): with Pyro4.core.Proxy(self.objectUri) as p: # unconnected proxies have empty metadata self.assertEqual(set(), p._pyroAttrs) self.assertEqual(set(), p._pyroMethods) self.assertEqual(set(), p._pyroOneway) # set some metadata manually, they should be overwritten at connection time p._pyroMethods = set("abc") p._pyroAttrs = set("xyz") p._pyroBind() self.assertNotEqual(set("xyz"), p._pyroAttrs) self.assertNotEqual(set("abc"), p._pyroMethods) self.assertNotEqual(set(), p._pyroOneway) def testNonserializableException_other(self): with Pyro4.core.Proxy(self.objectUri) as p: try: p.nonserializableException() self.fail("should crash") except Exception as x: self.assertIsInstance(x, Pyro4.errors.PyroError) tblines = "\n".join(Pyro4.util.getPyroTraceback()) self.assertTrue("unsupported serialized class" in tblines) def testNonserializableException_pickle(self): with Pyro4.core.Proxy(self.objectUri) as p: config.SERIALIZER = "pickle" try: p.nonserializableException() self.fail("should crash") except Exception as x: self.assertIsInstance(x, Pyro4.errors.PyroError) tblines = "\n".join(Pyro4.util.getPyroTraceback()) self.assertTrue("PyroError: Error serializing exception" in tblines) s1 = "Original exception: :" s2 = "Original exception: :" self.assertTrue(s1 in tblines or s2 in tblines) self.assertTrue("raise NonserializableError((\"xantippe" in tblines) finally: config.SERIALIZER = "serpent" def testBatchProxy(self): with Pyro4.core.Proxy(self.objectUri) as p: batch = Pyro4.core.batch(p) self.assertIsNone(batch.multiply(7, 6)) self.assertIsNone(batch.divide(999, 3)) self.assertIsNone(batch.ping()) self.assertIsNone(batch.divide(999, 0)) # force an exception here self.assertIsNone(batch.multiply(3, 4)) # this call should not be performed after the error results = batch() self.assertEqual(42, next(results)) self.assertEqual(333, next(results)) self.assertIsNone(next(results)) self.assertRaises(ZeroDivisionError, next, results) # 999//0 should raise this error self.assertRaises(StopIteration, next, results) # no more results should be available after the error def testAsyncProxy(self): with Pyro4.core.Proxy(self.objectUri) as p: Pyro4.core.asyncproxy(p) p._pyroBind() # force that any metadata is processed begin = time.time() result = p.delayAndId(1, 42) duration = time.time() - begin self.assertTrue(duration < 0.1) self.assertFalse(result.ready) self.assertFalse(result.wait(0.5)) # not available within 0.5 sec self.assertEqual("slept for 42", result.value) self.assertTrue(result.ready) self.assertTrue(result.wait()) def testAsyncProxyCallchain(self): class FuncHolder(object): count = AtomicCounter() def function(self, value, increase=1): self.count.incr() return value + increase with Pyro4.core.Proxy(self.objectUri) as p: Pyro4.core.asyncproxy(p) p._pyroBind() # force that any metadata is processed holder = FuncHolder() begin = time.time() result = p.multiply(2, 3) result.then(holder.function, increase=10) \ .then(holder.function, increase=5) \ .then(holder.function) duration = time.time() - begin self.assertTrue(duration < 0.1) value = result.value self.assertTrue(result.ready) self.assertEqual(22, value) self.assertEqual(3, holder.count.value) def testBatchOneway(self): with Pyro4.core.Proxy(self.objectUri) as p: batch = Pyro4.core.batch(p) self.assertIsNone(batch.multiply(7, 6)) self.assertIsNone(batch.delay(1)) # a delay shouldn't matter with oneway self.assertIsNone(batch.multiply(3, 4)) begin = time.time() results = batch(oneway=True) duration = time.time() - begin self.assertTrue(duration < 0.1, "oneway batch with delay should return almost immediately") self.assertIsNone(results) def testBatchAsync(self): with Pyro4.core.Proxy(self.objectUri) as p: batch = Pyro4.core.batch(p) self.assertIsNone(batch.multiply(7, 6)) self.assertIsNone(batch.delay(1)) # a delay shouldn't matter with asynchronous self.assertIsNone(batch.multiply(3, 4)) begin = time.time() asyncresult = batch(asynchronous=True) duration = time.time() - begin self.assertTrue(duration < 0.1, "async batch with delay should return almost immediately") results = asyncresult.value self.assertEqual(42, next(results)) self.assertEqual("slept 1 seconds", next(results)) self.assertEqual(12, next(results)) self.assertRaises(StopIteration, next, results) # no more results should be available def testBatchAsyncCallchain(self): class FuncHolder(object): count = AtomicCounter() def function(self, values): result = [value + 1 for value in values] self.count.incr() return result with Pyro4.core.Proxy(self.objectUri) as p: batch = Pyro4.core.batch(p) self.assertIsNone(batch.multiply(7, 6)) self.assertIsNone(batch.multiply(3, 4)) result = batch(asynchronous=True) holder = FuncHolder() result.then(holder.function).then(holder.function) value = result.value self.assertTrue(result.ready) self.assertEqual([44, 14], value) self.assertEqual(2, holder.count.value) def testPyroTracebackNormal(self): with Pyro4.core.Proxy(self.objectUri) as p: try: p.divide(999, 0) # force error here self.fail("expected error") except ZeroDivisionError: # going to check if the magic pyro traceback attribute is available for batch methods too tb = "".join(Pyro4.util.getPyroTraceback()) self.assertIn("Remote traceback:", tb) # validate if remote tb is present self.assertIn("ZeroDivisionError", tb) # the error self.assertIn("return x // y", tb) # the statement def testPyroTracebackBatch(self): with Pyro4.core.Proxy(self.objectUri) as p: batch = Pyro4.core.batch(p) self.assertIsNone(batch.divide(999, 0)) # force an exception here results = batch() try: next(results) self.fail("expected error") except ZeroDivisionError: # going to check if the magic pyro traceback attribute is available for batch methods too tb = "".join(Pyro4.util.getPyroTraceback()) self.assertIn("Remote traceback:", tb) # validate if remote tb is present self.assertIn("ZeroDivisionError", tb) # the error self.assertIn("return x // y", tb) # the statement self.assertRaises(StopIteration, next, results) # no more results should be available after the error def testAutoProxy(self): obj = ServerTestObject() config.SERIALIZER = "pickle" try: with Pyro4.core.Proxy(self.objectUri) as p: config.AUTOPROXY = False # make sure autoproxy is disabled result = p.echo(obj) self.assertIsInstance(result, ServerTestObject) self.daemon.register(obj) result = p.echo(obj) self.assertIsInstance(result, ServerTestObject, "with autoproxy off the object should be an instance of the class") self.daemon.unregister(obj) result = p.echo(obj) self.assertIsInstance(result, ServerTestObject, "serialized object must still be normal object") self.daemon.register(ServerTestObject) new_result = result.new_test_object() self.assertIsInstance(new_result, ServerTestObject, "serialized pyro object must be a normal object") self.daemon.unregister(ServerTestObject) config.AUTOPROXY = True # make sure autoproxying is enabled result = p.echo(obj) self.assertIsInstance(result, ServerTestObject, "non-pyro object must be returned as normal class") self.daemon.register(obj) result = p.echo(obj) self.assertIsInstance(result, Pyro4.core.Proxy, "serialized pyro object must be a proxy") self.daemon.register(ServerTestObject) new_result = result.new_test_object() self.assertIsInstance(new_result, Pyro4.core.Proxy, "serialized pyro object must be a proxy") self.daemon.unregister(ServerTestObject) self.daemon.unregister(obj) result = p.echo(obj) self.assertIsInstance(result, ServerTestObject, "unregistered pyro object must be normal class again") # note: the custom serializer may still be active but it should be smart enough to see # that the object is no longer a pyro object, and therefore, no proxy should be created. finally: config.AUTOPROXY = True config.SERIALIZER = "serpent" def testConnectOnce(self): with Pyro4.core.Proxy(self.objectUri) as proxy: self.assertTrue(proxy._pyroBind(), "first bind should always connect") self.assertFalse(proxy._pyroBind(), "second bind should not connect again") def testConnectingThreads(self): class ConnectingThread(threading.Thread): new_connections = AtomicCounter() def __init__(self, proxy, event): threading.Thread.__init__(self) self.proxy = proxy self.event = event self.setDaemon(True) self.new_connections.reset() def run(self): self.event.wait() if self.proxy._pyroBind(): ConnectingThread.new_connections.incr() # 1 more new connection done with Pyro4.core.Proxy(self.objectUri) as proxy: event = threading.Event() threads = [ConnectingThread(proxy, event) for _ in range(20)] for t in threads: t.start() event.set() for t in threads: t.join() self.assertEqual(1, ConnectingThread.new_connections.value) # proxy shared among threads must still have only 1 connect done def testMaxMsgSize(self): with Pyro4.core.Proxy(self.objectUri) as p: bigobject = [42] * 1000 result = p.echo(bigobject) self.assertEqual(result, bigobject) config.MAX_MESSAGE_SIZE = 999 try: _ = p.echo(bigobject) self.fail("should fail with ProtocolError msg too large") except Pyro4.errors.ProtocolError: pass config.MAX_MESSAGE_SIZE = 0 def testIterator(self): with Pyro4.core.Proxy(self.objectUri) as p: iterator = p.iterator() self.assertIsInstance(iterator, Pyro4.core._StreamResultIterator) self.assertEqual("one", next(iterator)) self.assertEqual("two", next(iterator)) self.assertEqual("three", next(iterator)) with self.assertRaises(StopIteration): next(iterator) iterator.close() def testGenerator(self): with Pyro4.core.Proxy(self.objectUri) as p: generator = p.generator() self.assertIsInstance(generator, Pyro4.core._StreamResultIterator) self.assertEqual("one", next(generator)) self.assertEqual("two", next(generator)) self.assertEqual("three", next(generator)) self.assertEqual("four", next(generator)) self.assertEqual("five", next(generator)) with self.assertRaises(StopIteration): next(generator) with self.assertRaises(StopIteration): next(generator) generator.close() generator = p.generator() [v for v in generator] with self.assertRaises(StopIteration): next(generator) generator.close() def testCleanup(self): p1 = Pyro4.core.Proxy(self.objectUri) p2 = Pyro4.core.Proxy(self.objectUri) p3 = Pyro4.core.Proxy(self.objectUri) p1.echo(42) p2.echo(42) p3.echo(42) # we have several active connections still up, see if we can cleanly shutdown the daemon # (it should interrupt the worker's socket connections) time.sleep(0.1) self.daemon.shutdown() self.daemon = None p1._pyroRelease() p2._pyroRelease() p3._pyroRelease() def testSerializedBlob(self): sb = Pyro4.core.SerializedBlob("blobname", [1, 2, 3]) self.assertEqual("blobname", sb.info) self.assertEqual([1, 2, 3], sb.deserialized()) def testSerializedBlobMessage(self): serializer = Pyro4.util.get_serializer("serpent") data, _ = serializer.serializeCall("object", "method", ([1, 2, 3],), False) msg = Pyro4.message.Message(Pyro4.message.MSG_INVOKE, data, serializer.serializer_id, 0, 42) sb = Pyro4.core.SerializedBlob("blobname", msg, is_blob=True) self.assertEqual("blobname", sb.info) self.assertEqual(([1, 2, 3],), sb.deserialized()) def testProxySerializedBlobArg(self): with Pyro4.core.Proxy(self.objectUri) as p: blobinfo, blobdata = p.blob(Pyro4.core.SerializedBlob("blobname", [1, 2, 3])) self.assertEqual("blobname", blobinfo) self.assertEqual([1, 2, 3], blobdata) def testResourceFreeing(self): rsvc = ResourceService() uri = self.daemon.register(rsvc) with Pyro4.core.Proxy(uri) as p: p.allocate("r1") p.allocate("r2") resources = {r.name: r for r in rsvc.resources} p.free("r1") rsc = p.list() self.assertEqual(["r2"], rsc) self.assertTrue(resources["r1"].close_called) self.assertFalse(resources["r2"].close_called) time.sleep(0.02) self.assertTrue(resources["r1"].close_called) self.assertTrue(resources["r2"].close_called) with Pyro4.core.Proxy(uri) as p: rsc = p.list() self.assertEqual([], rsc, "r2 must now be freed due to connection loss earlier") class ServerTestsThreadNoTimeout(unittest.TestCase): SERVERTYPE = "thread" COMMTIMEOUT = None def setUp(self): config.LOGWIRE = True config.POLLTIMEOUT = 0.1 config.SERVERTYPE = self.SERVERTYPE config.COMMTIMEOUT = self.COMMTIMEOUT config.SERIALIZERS_ACCEPTED.add("pickle") self.daemon = Pyro4.core.Daemon(port=0) obj = ServerTestObject() uri = self.daemon.register(obj, "something") self.objectUri = uri self.daemonthread = DaemonLoopThread(self.daemon) self.daemonthread.start() self.daemonthread.running.wait() time.sleep(0.05) def tearDown(self): time.sleep(0.05) self.daemon.shutdown() self.daemonthread.join() config.SERVERTYPE = "thread" config.COMMTIMEOUT = None config.SERIALIZERS_ACCEPTED.discard("pickle") def testConnectionStuff(self): p1 = Pyro4.core.Proxy(self.objectUri) p2 = Pyro4.core.Proxy(self.objectUri) self.assertIsNone(p1._pyroConnection) self.assertIsNone(p2._pyroConnection) p1.ping() p2.ping() _ = p1.multiply(11, 5) _ = p2.multiply(11, 5) self.assertIsNotNone(p1._pyroConnection) self.assertIsNotNone(p2._pyroConnection) p1._pyroRelease() p1._pyroRelease() p2._pyroRelease() p2._pyroRelease() self.assertIsNone(p1._pyroConnection) self.assertIsNone(p2._pyroConnection) p1._pyroBind() _ = p1.multiply(11, 5) _ = p2.multiply(11, 5) self.assertIsNotNone(p1._pyroConnection) self.assertIsNotNone(p2._pyroConnection) self.assertEqual("PYRO", p1._pyroUri.protocol) self.assertEqual("PYRO", p2._pyroUri.protocol) p1._pyroRelease() p2._pyroRelease() def testReconnectAndCompression(self): # try reconnects with Pyro4.core.Proxy(self.objectUri) as p: self.assertIsNone(p._pyroConnection) p._pyroReconnect(tries=100) self.assertIsNotNone(p._pyroConnection) self.assertIsNone(p._pyroConnection) # test compression: try: with Pyro4.core.Proxy(self.objectUri) as p: config.COMPRESSION = True self.assertEqual(55, p.multiply(5, 11)) self.assertEqual("*" * 1000, p.multiply("*" * 500, 2)) finally: config.COMPRESSION = False def testOnewayMetaOn(self): config.METADATA = True with Pyro4.core.Proxy(self.objectUri) as p: self.assertEqual(set(), p._pyroOneway) # when not bound, no meta info exchange has been done p._pyroBind() self.assertIn("oneway_multiply", p._pyroOneway) # after binding, meta info has been processed self.assertEqual(55, p.multiply(5, 11)) # not tagged as @Pyro4.oneway self.assertIsNone(p.oneway_multiply(5, 11)) # tagged as @Pyro4.oneway p._pyroOneway = set() self.assertEqual(55, p.multiply(5, 11)) self.assertEqual(55, p.oneway_multiply(5, 11)) # check nonexisting method behavoir for oneway methods with self.assertRaises(AttributeError): p.nonexisting_method() p._pyroOneway.add("nonexisting_method") # now it should still fail because of metadata telling Pyro what methods actually exist with self.assertRaises(AttributeError): p.nonexisting_method() def testOnewayMetaOff(self): config.METADATA = False with Pyro4.core.Proxy(self.objectUri) as p: self.assertEqual(set(), p._pyroOneway) # when not bound, no meta info exchange has been done p._pyroBind() self.assertEqual(set(), p._pyroOneway) # after binding, no meta info exchange has been done because disabled self.assertEqual(55, p.multiply(5, 11)) self.assertEqual(55, p.oneway_multiply(5, 11)) # check nonexisting method behavoir for oneway methods with self.assertRaises(AttributeError): p.nonexisting_method() p._pyroOneway.add("nonexisting_method") # now it shouldn't fail because of oneway semantics (!) (and becaue there's no metadata to tell Pyro that the method doesn't exist) p.nonexisting_method() config.METADATA = True def testOnewayWithProxySubclass(self): config.METADATA = False class ProxyWithOneway(Pyro4.core.Proxy): def __init__(self, arg): super(ProxyWithOneway, self).__init__(arg) self._pyroOneway = {"oneway_multiply", "multiply"} with ProxyWithOneway(self.objectUri) as p: self.assertIsNone(p.oneway_multiply(5, 11)) self.assertIsNone(p.multiply(5, 11)) p._pyroOneway = set() self.assertEqual(55, p.oneway_multiply(5, 11)) self.assertEqual(55, p.multiply(5, 11)) config.METADATA = True def testOnewayDelayed(self): try: with Pyro4.core.Proxy(self.objectUri) as p: p.ping() config.ONEWAY_THREADED = True # the default now = time.time() p.oneway_delay(1) # oneway so we should continue right away time.sleep(0.01) self.assertTrue(time.time() - now < 0.2, "delay should be running as oneway") now = time.time() self.assertEqual(55, p.multiply(5, 11), "expected a normal result from a non-oneway call") self.assertTrue(time.time() - now < 0.2, "delay should be running in its own thread") # make oneway calls run in the server thread # we can change the config here and the server will pick it up on the fly config.ONEWAY_THREADED = False now = time.time() p.oneway_delay(1) # oneway so we should continue right away time.sleep(0.01) self.assertTrue(time.time() - now < 0.2, "delay should be running as oneway") now = time.time() self.assertEqual(55, p.multiply(5, 11), "expected a normal result from a non-oneway call") self.assertFalse(time.time() - now < 0.2, "delay should be running in the server thread") finally: config.ONEWAY_THREADED = True # back to normal def testSerializeConnected(self): # online serialization tests ser = Pyro4.util.get_serializer(config.SERIALIZER) proxy = Pyro4.core.Proxy(self.objectUri) proxy._pyroBind() self.assertIsNotNone(proxy._pyroConnection) p, _ = ser.serializeData(proxy) proxy2 = ser.deserializeData(p) self.assertIsNone(proxy2._pyroConnection) self.assertIsNotNone(proxy._pyroConnection) self.assertEqual(proxy2._pyroUri, proxy._pyroUri) proxy2._pyroBind() self.assertIsNotNone(proxy2._pyroConnection) self.assertIsNot(proxy2._pyroConnection, proxy._pyroConnection) proxy._pyroRelease() proxy2._pyroRelease() self.assertIsNone(proxy._pyroConnection) self.assertIsNone(proxy2._pyroConnection) proxy.ping() proxy2.ping() # try copying a connected proxy import copy proxy3 = copy.copy(proxy) self.assertIsNone(proxy3._pyroConnection) self.assertIsNotNone(proxy._pyroConnection) self.assertEqual(proxy3._pyroUri, proxy._pyroUri) self.assertIsNot(proxy3._pyroUri, proxy._pyroUri) proxy._pyroRelease() proxy2._pyroRelease() proxy3._pyroRelease() def testException(self): with Pyro4.core.Proxy(self.objectUri) as p: try: p.divide(1, 0) except: et, ev, tb = sys.exc_info() self.assertEqual(ZeroDivisionError, et) pyrotb = "".join(Pyro4.util.getPyroTraceback(et, ev, tb)) self.assertIn("Remote traceback", pyrotb) self.assertIn("ZeroDivisionError", pyrotb) del tb def testTimeoutCall(self): config.COMMTIMEOUT = None with Pyro4.core.Proxy(self.objectUri) as p: p.ping() start = time.time() p.delay(0.5) duration = time.time() - start self.assertTrue(0.4 < duration < 0.6) p._pyroTimeout = 0.1 start = time.time() self.assertRaises(Pyro4.errors.TimeoutError, p.delay, 1) duration = time.time() - start if sys.platform != "cli": self.assertLess(duration, 0.3) else: # ironpython's time is weird self.assertTrue(0.0 < duration < 0.7) def testTimeoutConnect(self): # set up a unresponsive daemon with Pyro4.core.Daemon(port=0) as d: time.sleep(0.5) obj = ServerTestObject() uri = d.register(obj) # we're not going to start the daemon's event loop p = Pyro4.core.Proxy(uri) p._pyroTimeout = 0.2 start = time.time() with self.assertRaises(Pyro4.errors.TimeoutError) as e: p.ping() self.assertEqual("receiving: timeout", str(e.exception)) def testProxySharing(self): class SharedProxyThread(threading.Thread): def __init__(self, proxy): super(SharedProxyThread, self).__init__() self.proxy = proxy self.terminate = False self.error = True self.setDaemon(True) def run(self): try: while not self.terminate: reply = self.proxy.multiply(5, 11) assert reply == 55 time.sleep(0.001) self.error = False except: print("Something went wrong in the thread (SharedProxyThread):") print("".join(Pyro4.util.getPyroTraceback())) with Pyro4.core.Proxy(self.objectUri) as p: threads = [] for i in range(5): t = SharedProxyThread(p) threads.append(t) t.start() time.sleep(1) for t in threads: t.terminate = True t.join() for t in threads: self.assertFalse(t.error, "all threads should report no errors") def testServerConnections(self): # check if the server allows to grow the number of connections proxies = [Pyro4.core.Proxy(self.objectUri) for _ in range(10)] try: for p in proxies: p._pyroTimeout = 0.5 p._pyroBind() for p in proxies: p.ping() finally: for p in proxies: p._pyroRelease() def testServerParallelism(self): class ClientThread(threading.Thread): def __init__(self, uri, name): super(ClientThread, self).__init__() self.setDaemon(True) self.proxy = Pyro4.core.Proxy(uri) self.name = name self.error = True self.proxy._pyroTimeout = 5.0 self.proxy._pyroBind() def run(self): try: reply = self.proxy.delayAndId(0.5, self.name) assert reply == "slept for " + self.name self.error = False finally: self.proxy._pyroRelease() threads = [] start = time.time() try: for i in range(6): t = ClientThread(self.objectUri, "t%d" % i) threads.append(t) except: # some exception (probably timeout) while creating clients # try to clean up some connections first for t in threads: t.proxy._pyroRelease() raise # re-raise the exception for t in threads: t.start() for t in threads: t.join() self.assertFalse(t.error, "all threads should report no errors") del threads duration = time.time() - start if config.SERVERTYPE == "multiplex": # multiplex based server doesn't execute calls in parallel, # so 6 threads times 0.5 seconds =~ 3 seconds self.assertTrue(2.5 < duration < 3.5) else: # thread based server does execute calls in parallel, # so 6 threads taking 0.5 seconds =~ 0.5 seconds passed self.assertTrue(0.4 < duration < 0.9) def testGeneratorProxyClose(self): p = Pyro4.core.Proxy(self.objectUri) generator = p.generator() p._pyroRelease() with self.assertRaises(Pyro4.errors.ConnectionClosedError): next(generator) def testGeneratorLinger(self): orig_linger = config.ITER_STREAM_LINGER orig_commt = config.COMMTIMEOUT orig_pollt = config.POLLTIMEOUT try: config.ITER_STREAM_LINGER = 0.5 config.COMMTIMEOUT = 0.2 config.POLLTIMEOUT = 0.2 p = Pyro4.core.Proxy(self.objectUri) generator = p.generator() self.assertEqual("one", next(generator)) p._pyroRelease() with self.assertRaises(Pyro4.errors.ConnectionClosedError): next(generator) p._pyroReconnect() self.assertEqual("two", next(generator), "generator should resume after reconnect") # check that after the linger time passes, the generator *is* gone p._pyroRelease() time.sleep(2) p._pyroReconnect() with self.assertRaises(Pyro4.errors.PyroError): # should not be resumable anymore next(generator) finally: config.ITER_STREAM_LINGER = orig_linger config.COMMTIMEOUT = orig_commt config.POLLTIMEOUT = orig_pollt def testGeneratorNoLinger(self): orig_linger = config.ITER_STREAM_LINGER try: p = Pyro4.core.Proxy(self.objectUri) config.ITER_STREAM_LINGER = 0 # disable linger generator = p.generator() self.assertEqual("one", next(generator)) p._pyroRelease() time.sleep(0.2) with self.assertRaises(Pyro4.errors.ConnectionClosedError): next(generator) p._pyroReconnect() with self.assertRaises(Pyro4.errors.PyroError): # should not be resumable after reconnect next(generator) generator.close() finally: config.ITER_STREAM_LINGER = orig_linger class ServerTestsMultiplexNoTimeout(ServerTestsThreadNoTimeout): SERVERTYPE = "multiplex" COMMTIMEOUT = None def testProxySharing(self): pass def testException(self): pass if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() Pyro4-4.82/tests/PyroTests/test_server_timeout.py000066400000000000000000000016501416147301300222710ustar00rootroot00000000000000""" Tests for a running Pyro server, with timeouts. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import unittest import test_server class ServerTestsThreadTimeout(test_server.ServerTestsThreadNoTimeout): SERVERTYPE = "thread" COMMTIMEOUT = 2.0 def testServerParallelism(self): # this test is not suitable on a server with timeout set pass def testProxySharing(self): pass def testException(self): pass class ServerTestsMultiplexTimeout(test_server.ServerTestsMultiplexNoTimeout): SERVERTYPE = "multiplex" COMMTIMEOUT = 2.0 def testServerParallelism(self): # this test is not suitable on a server with timeout set pass def testProxySharing(self): pass def testException(self): pass if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() Pyro4-4.82/tests/PyroTests/test_socket.py000066400000000000000000000563451416147301300205200ustar00rootroot00000000000000""" Tests for the low level socket functions. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import socket import os import sys import platform import threading import time import unittest try: import ssl except ImportError: ssl = None import Pyro4.socketutil as SU import Pyro4.util import Pyro4.constants from Pyro4.configuration import config from Pyro4 import errors from Pyro4.socketserver.multiplexserver import SocketServer_Multiplex from Pyro4.socketserver.threadpoolserver import SocketServer_Threadpool from Pyro4.core import Daemon from testsupport import * # determine ipv6 capability has_ipv6 = socket.has_ipv6 if has_ipv6: s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) try: s.connect(("::1", 53)) s.close() socket.getaddrinfo("localhost", 53, socket.AF_INET6) except socket.error: has_ipv6 = False class TestSocketStuff(unittest.TestCase): def testSockname(self): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("", 0)) s.listen(5) host, port = s.getsockname() self.assertNotEqual(0, port) self.assertEqual("0.0.0.0", host) # ipv4 support only at this time s.close() class TestSocketutil(unittest.TestCase): def setUp(self): config.POLLTIMEOUT = 0.1 def testGetIP(self): config.PREFER_IP_VERSION = 4 myip = SU.getIpAddress("") self.assertTrue(len(myip) > 4) myip = SU.getIpAddress("", workaround127=True) self.assertTrue(len(myip) > 4) self.assertFalse(myip.startswith("127.")) self.assertEqual("127.0.0.1", SU.getIpAddress("127.0.0.1", workaround127=False)) self.assertNotEqual("127.0.0.1", SU.getIpAddress("127.0.0.1", workaround127=True)) @unittest.skipUnless(has_ipv6, "ipv6 testcase") def testGetIP6(self): self.assertIn(":", SU.getIpAddress("::1", ipVersion=6)) # self.assertTrue(":" in SU.getIpAddress("", ipVersion=6)) self.assertIn(":", SU.getIpAddress("localhost", ipVersion=6)) def testGetIpVersion4(self): version = config.PREFER_IP_VERSION try: config.PREFER_IP_VERSION = 4 self.assertEqual(4, SU.getIpVersion("127.0.0.1")) self.assertEqual(4, SU.getIpVersion("localhost")) config.PREFER_IP_VERSION = 0 self.assertEqual(4, SU.getIpVersion("127.0.0.1")) finally: config.PREFER_IP_VERSION = version @unittest.skipUnless(has_ipv6, "ipv6 testcase") def testGetIpVersion6(self): version = config.PREFER_IP_VERSION try: config.PREFER_IP_VERSION = 6 self.assertEqual(6, SU.getIpVersion("::1")) self.assertEqual(6, SU.getIpVersion("localhost")) config.PREFER_IP_VERSION = 4 self.assertEqual(4, SU.getIpVersion("127.0.0.1")) self.assertEqual(6, SU.getIpVersion("::1")) config.PREFER_IP_VERSION = 0 self.assertEqual(4, SU.getIpVersion("127.0.0.1")) self.assertEqual(6, SU.getIpVersion("::1")) finally: config.PREFER_IP_VERSION = version def testGetInterfaceAddress(self): self.assertTrue(SU.getInterfaceAddress("localhost").startswith("127.")) if has_ipv6: self.assertIn(":", SU.getInterfaceAddress("::1")) def testUnusedPort(self): port1 = SU.findProbablyUnusedPort() port2 = SU.findProbablyUnusedPort() self.assertTrue(port1 > 0) self.assertNotEqual(port1, port2) port1 = SU.findProbablyUnusedPort(socktype=socket.SOCK_DGRAM) port2 = SU.findProbablyUnusedPort(socktype=socket.SOCK_DGRAM) self.assertTrue(port1 > 0) self.assertNotEqual(port1, port2) @unittest.skipUnless(has_ipv6, "ipv6 testcase") def testUnusedPort6(self): port1 = SU.findProbablyUnusedPort(family=socket.AF_INET6) port2 = SU.findProbablyUnusedPort(family=socket.AF_INET6) self.assertTrue(port1 > 0) self.assertNotEqual(port1, port2) port1 = SU.findProbablyUnusedPort(family=socket.AF_INET6, socktype=socket.SOCK_DGRAM) port2 = SU.findProbablyUnusedPort(family=socket.AF_INET6, socktype=socket.SOCK_DGRAM) self.assertTrue(port1 > 0) self.assertNotEqual(port1, port2) def testBindUnusedPort(self): sock1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) port1 = SU.bindOnUnusedPort(sock1) port2 = SU.bindOnUnusedPort(sock2) self.assertTrue(port1 > 0) self.assertNotEqual(port1, port2) sockname = sock1.getsockname() self.assertEqual(("127.0.0.1", port1), sockname) sock1.close() sock2.close() @unittest.skipUnless(has_ipv6, "ipv6 testcase") def testBindUnusedPort6(self): sock1 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) sock2 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) port1 = SU.bindOnUnusedPort(sock1) port2 = SU.bindOnUnusedPort(sock2) self.assertTrue(port1 > 0) self.assertNotEqual(port1, port2) host, port, _, _ = sock1.getsockname() self.assertIn(":", host) self.assertEqual(port1, port) sock1.close() sock2.close() def testCreateUnboundSockets(self): s = SU.createSocket() self.assertEqual(socket.AF_INET, s.family) bs = SU.createBroadcastSocket() self.assertEqual(socket.AF_INET, bs.family) try: host, port = s.getsockname() # can either fail with socket.error or return (host,0) self.assertEqual(0, port) except socket.error: pass try: host, port = bs.getsockname() # can either fail with socket.error or return (host,0) self.assertEqual(0, port) except socket.error: pass s.close() bs.close() @unittest.skipUnless(has_ipv6, "ipv6 testcase") def testCreateUnboundSockets6(self): s = SU.createSocket(ipv6=True) self.assertEqual(socket.AF_INET6, s.family) bs = SU.createBroadcastSocket(ipv6=True) self.assertEqual(socket.AF_INET6, bs.family) try: host, port, _, _ = s.getsockname() # can either fail with socket.error or return (host,0) self.assertEqual(0, port) except socket.error: pass try: host, port, _, _ = bs.getsockname() # can either fail with socket.error or return (host,0) self.assertEqual(0, port) except socket.error: pass s.close() bs.close() def testCreateBoundSockets(self): s = SU.createSocket(bind=('127.0.0.1', 0)) self.assertEqual(socket.AF_INET, s.family) bs = SU.createBroadcastSocket(bind=('127.0.0.1', 0)) self.assertEqual('127.0.0.1', s.getsockname()[0]) self.assertEqual('127.0.0.1', bs.getsockname()[0]) s.close() bs.close() self.assertRaises(ValueError, SU.createSocket, bind=('localhost', 12345), connect=('localhost', 1234)) @unittest.skipUnless(has_ipv6, "ipv6 testcase") def testCreateBoundSockets6(self): s = SU.createSocket(bind=('::1', 0)) self.assertEqual(socket.AF_INET6, s.family) bs = SU.createBroadcastSocket(bind=('::1', 0)) self.assertIn(':', s.getsockname()[0]) self.assertIn(':', bs.getsockname()[0]) s.close() bs.close() self.assertRaises(ValueError, SU.createSocket, bind=('::1', 12345), connect=('::1', 1234)) @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "unix domain sockets required") def testCreateBoundUnixSockets(self): SOCKNAME = "test_unixsocket" if os.path.exists(SOCKNAME): os.remove(SOCKNAME) s = SU.createSocket(bind=SOCKNAME) self.assertEqual(socket.AF_UNIX, s.family) self.assertEqual(SOCKNAME, s.getsockname()) s.close() if os.path.exists(SOCKNAME): os.remove(SOCKNAME) # unicode arg SOCKNAME = unicode(SOCKNAME) s = SU.createSocket(bind=SOCKNAME) self.assertEqual(socket.AF_UNIX, s.family) self.assertEqual(SOCKNAME, s.getsockname()) s.close() if os.path.exists(SOCKNAME): os.remove(SOCKNAME) self.assertRaises(ValueError, SU.createSocket, bind=SOCKNAME, connect=SOCKNAME) @unittest.skipUnless(hasattr(socket, "AF_UNIX") and sys.platform.startswith("linux"), "linux and unix domain sockets required") def testAbstractNamespace(self): SOCKNAME = "\0test_unixsocket_abstract_ns" # mind the \0 at the start s = SU.createSocket(bind=SOCKNAME) sn_bytes = tobytes(SOCKNAME) self.assertEqual(sn_bytes, s.getsockname()) s.close() def testSend(self): ss = SU.createSocket(bind=("localhost", 0)) port = ss.getsockname()[1] cs = SU.createSocket(connect=("localhost", port)) SU.sendData(cs, tobytes("foobar!") * 10) cs.shutdown(socket.SHUT_WR) a = ss.accept() data = SU.receiveData(a[0], 5) self.assertEqual(tobytes("fooba"), data) data = SU.receiveData(a[0], 5) self.assertEqual(tobytes("r!foo"), data) a[0].close() ss.close() cs.close() @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "unix domain sockets required") def testSendUnix(self): SOCKNAME = "test_unixsocket" ss = SU.createSocket(bind=SOCKNAME) cs = SU.createSocket(connect=SOCKNAME) SU.sendData(cs, tobytes("foobar!") * 10) cs.shutdown(socket.SHUT_WR) a = ss.accept() data = SU.receiveData(a[0], 5) self.assertEqual(tobytes("fooba"), data) data = SU.receiveData(a[0], 5) self.assertEqual(tobytes("r!foo"), data) a[0].close() ss.close() cs.close() if os.path.exists(SOCKNAME): os.remove(SOCKNAME) def testBroadcast(self): ss = SU.createBroadcastSocket((None, 0)) port = ss.getsockname()[1] cs = SU.createBroadcastSocket() for bcaddr in config.parseAddressesString(config.BROADCAST_ADDRS): try: cs.sendto(tobytes("monkey"), 0, (bcaddr, port)) except socket.error as x: err = getattr(x, "errno", x.args[0]) # handle some errno that some platforms like to throw if err not in Pyro4.socketutil.ERRNO_EADDRNOTAVAIL and err not in Pyro4.socketutil.ERRNO_EADDRINUSE: raise data, _ = ss.recvfrom(500) self.assertEqual(tobytes("monkey"), data) cs.close() ss.close() def testMsgWaitallProblems(self): ss = SU.createSocket(bind=("localhost", 0), timeout=2) port = ss.getsockname()[1] cs = SU.createSocket(connect=("localhost", port), timeout=2) a = ss.accept() # test some sizes that might be problematic with MSG_WAITALL and check that they work fine for size in [1000, 10000, 32000, 32768, 32780, 41950, 41952, 42000, 65000, 65535, 65600, 80000]: SU.sendData(cs, tobytes("x") * size) data = SU.receiveData(a[0], size) SU.sendData(a[0], data) data = SU.receiveData(cs, size) self.assertEqual(size, len(data)) a[0].close() ss.close() cs.close() def testMsgWaitallProblems2(self): class ReceiveThread(threading.Thread): def __init__(self, sock, sizes): super(ReceiveThread, self).__init__() self.sock = sock self.sizes = sizes def run(self): cs, _ = self.sock.accept() for size in self.sizes: data = SU.receiveData(cs, size) SU.sendData(cs, data) cs.close() ss = SU.createSocket(bind=("localhost", 0)) SIZES = [1000, 10000, 32000, 32768, 32780, 41950, 41952, 42000, 65000, 65535, 65600, 80000, 999999] serverthread = ReceiveThread(ss, SIZES) serverthread.setDaemon(True) serverthread.start() port = ss.getsockname()[1] cs = SU.createSocket(connect=("localhost", port), timeout=2) # test some sizes that might be problematic with MSG_WAITALL and check that they work fine for size in SIZES: SU.sendData(cs, tobytes("x") * size) data = SU.receiveData(cs, size) self.assertEqual(size, len(data)) serverthread.join() ss.close() cs.close() def testMsgWaitAllConfig(self): if platform.system() == "Windows": # default config should be False on these platforms even though socket.MSG_WAITALL might exist self.assertFalse(config.USE_MSG_WAITALL) else: # on all other platforms, default config should be True (as long as socket.MSG_WAITALL exists) if hasattr(socket, "MSG_WAITALL"): self.assertTrue(config.USE_MSG_WAITALL) else: self.assertFalse(config.USE_MSG_WAITALL) class ServerCallback(object): def _handshake(self, connection, denied_reason=None): raise RuntimeError("this handshake method should never be called") def handleRequest(self, connection): if not isinstance(connection, SU.SocketConnection): raise TypeError("handleRequest expected SocketConnection parameter") msg = Pyro4.message.Message.recv(connection, [Pyro4.message.MSG_PING]) if msg.type == Pyro4.message.MSG_PING: msg = Pyro4.message.Message(Pyro4.message.MSG_PING, b"ping", msg.serializer_id, 0, msg.seq) connection.send(msg.to_bytes()) else: print("unhandled message type", msg.type) connection.close() def _housekeeping(self): pass class ServerCallback_BrokenHandshake(ServerCallback): def _handshake(self, connection, denied_reason=None): raise ZeroDivisionError("handshake crashed (on purpose)") class TestDaemon(Daemon): def __init__(self): super(TestDaemon, self).__init__() class TestSocketServer(unittest.TestCase): def testServer_thread(self): daemon = ServerCallback() port = SU.findProbablyUnusedPort() serv = SocketServer_Threadpool() serv.init(daemon, "localhost", port) self.assertEqual("localhost:" + str(port), serv.locationStr) self.assertIsNotNone(serv.sock) conn = SU.SocketConnection(serv.sock, "ID12345") self.assertEqual("ID12345", conn.objectId) self.assertIsNotNone(conn.sock) conn.close() conn.close() self.assertIsNotNone(conn.sock, "connections keep their socket object even if it's closed") serv.close() serv.close() self.assertIsNone(serv.sock) def testServer_multiplex(self): daemon = ServerCallback() port = SU.findProbablyUnusedPort() serv = SocketServer_Multiplex() serv.init(daemon, "localhost", port) self.assertEqual("localhost:" + str(port), serv.locationStr) self.assertIsNotNone(serv.sock) conn = SU.SocketConnection(serv.sock, "ID12345") self.assertEqual("ID12345", conn.objectId) self.assertIsNotNone(conn.sock) conn.close() conn.close() self.assertIsNotNone(conn.sock, "connections keep their socket object even if it's closed") serv.close() serv.close() self.assertIsNone(serv.sock) class TestServerDOS_multiplex(unittest.TestCase): def setUp(self): self.orig_poll_timeout = config.POLLTIMEOUT self.orig_comm_timeout = config.COMMTIMEOUT config.POLLTIMEOUT = 0.5 config.COMMTIMEOUT = 0.5 self.socket_server = SocketServer_Multiplex def tearDown(self): config.POLLTIMEOUT = self.orig_poll_timeout config.COMMTIMEOUT = self.orig_comm_timeout class ServerThread(threading.Thread): def __init__(self, server, daemon): threading.Thread.__init__(self) self.serv = server() self.serv.init(daemon(), "localhost", 0) self.locationStr = self.serv.locationStr self.stop_loop = threading.Event() def run(self): self.serv.loop(loopCondition=lambda: not self.stop_loop.is_set()) self.serv.close() def testConnectCrash(self): serv_thread = TestServerDOS_multiplex.ServerThread(self.socket_server, ServerCallback_BrokenHandshake) serv_thread.start() time.sleep(0.2) self.assertTrue(serv_thread.is_alive(), "server thread failed to start") threadpool = getattr(serv_thread.serv, "pool", None) if threadpool: self.assertEqual(1, len(threadpool.idle)) self.assertEqual(0, len(threadpool.busy)) try: host, port = serv_thread.locationStr.split(':') port = int(port) try: # first connection attempt (will fail because server daemon _handshake crashes) csock = SU.createSocket(connect=(host, port)) conn = SU.SocketConnection(csock, "uri") Pyro4.message.Message.recv(conn, [Pyro4.message.MSG_CONNECTOK]) except errors.ConnectionClosedError: pass conn.close() time.sleep(0.1) if threadpool: self.assertEqual(1, len(threadpool.idle)) self.assertEqual(0, len(threadpool.busy)) try: # second connection attempt, should still work (i.e. server should still be running) csock = SU.createSocket(connect=(host, port)) conn = SU.SocketConnection(csock, "uri") Pyro4.message.Message.recv(conn, [Pyro4.message.MSG_CONNECTOK]) except errors.ConnectionClosedError: pass finally: if conn: conn.close() serv_thread.stop_loop.set() serv_thread.join() def testInvalidMessageCrash(self): serv_thread = TestServerDOS_multiplex.ServerThread(self.socket_server, TestDaemon) serv_thread.start() time.sleep(0.2) self.assertTrue(serv_thread.is_alive(), "server thread failed to start") threadpool = getattr(serv_thread.serv, "pool", None) if threadpool: self.assertEqual(1, len(threadpool.idle)) self.assertEqual(0, len(threadpool.busy)) def connect(host, port): # connect to the server csock = SU.createSocket(connect=(host, port)) conn = SU.SocketConnection(csock, "uri") # send the handshake/connect data ser = Pyro4.util.get_serializer_by_id(Pyro4.util.MarshalSerializer.serializer_id) data, _ = ser.serializeData({"handshake": "hello", "object": Pyro4.constants.DAEMON_NAME}, False) msg = Pyro4.message.Message(Pyro4.message.MSG_CONNECT, data, Pyro4.util.MarshalSerializer.serializer_id, 0, 0) conn.send(msg.to_bytes()) # get the handshake/connect response Pyro4.message.Message.recv(conn, [Pyro4.message.MSG_CONNECTOK]) return conn conn = None try: host, port = serv_thread.locationStr.split(':') port = int(port) conn = connect(host, port) # invoke something, but screw up the message (in this case, mess with the protocol version) orig_protocol_version = Pyro4.constants.PROTOCOL_VERSION Pyro4.constants.PROTOCOL_VERSION = 9999 msgbytes = Pyro4.message.Message(Pyro4.message.MSG_PING, b"something", 42, 0, 0).to_bytes() Pyro4.constants.PROTOCOL_VERSION = orig_protocol_version conn.send(msgbytes) # this should cause an error in the server because of invalid msg try: msg = Pyro4.message.Message.recv(conn, [Pyro4.message.MSG_RESULT]) data = msg.data if sys.version_info >= (2, 7): data = msg.data.decode("ascii", errors="ignore") # convert raw message to string to check some stuff self.assertIn("Traceback", data) self.assertIn("ProtocolError", data) self.assertIn("version", data) except errors.ConnectionClosedError: # invalid message can cause the connection to be closed, this is fine pass # invoke something again, this should still work (server must still be running, but our client connection was terminated) conn.close() time.sleep(0.1) if threadpool: self.assertEqual(1, len(threadpool.idle)) self.assertEqual(0, len(threadpool.busy)) try: conn = connect(host, port) except errors.ProtocolError as px: # @todo this is strange, it sometimes occurs in Travis. self.fail("unexpected ProtocolError in testInvalidMessageCrash: \n" + "".join(Pyro4.util.getPyroTraceback())) msg = Pyro4.message.Message(Pyro4.message.MSG_PING, b"something", 42, 0, 999) # a valid message this time conn.send(msg.to_bytes()) msg = Pyro4.message.Message.recv(conn, [Pyro4.message.MSG_PING]) self.assertEqual(Pyro4.message.MSG_PING, msg.type) self.assertEqual(999, msg.seq) self.assertEqual(b"pong", msg.data) finally: if conn: conn.close() serv_thread.stop_loop.set() serv_thread.join() class TestServerDOS_threading(TestServerDOS_multiplex): def setUp(self): super(TestServerDOS_threading, self).setUp() self.socket_server = SocketServer_Threadpool self.orig_numthreads = config.THREADPOOL_SIZE self.orig_numthreads_min = config.THREADPOOL_SIZE_MIN config.THREADPOOL_SIZE = 1 config.THREADPOOL_SIZE_MIN = 1 def tearDown(self): config.THREADPOOL_SIZE = self.orig_numthreads config.THREADPOOL_SIZE_MIN = self.orig_numthreads_min @unittest.skipIf(not ssl, "ssl tests requires ssl module") class TestSSL(unittest.TestCase): def testContextAndSock(self): cert_dir = "../../certs" if not os.path.isdir(cert_dir): cert_dir = "../certs" if not os.path.isdir(cert_dir): cert_dir = "./certs" if not os.path.isdir(cert_dir): self.fail("cannot locate test certs directory") try: config.SSL = True config.SSL_REQUIRECLIENTCERT = True server_ctx = SU.getSSLcontext(cert_dir+"/server_cert.pem", cert_dir+"/server_key.pem") client_ctx = SU.getSSLcontext(clientcert=cert_dir+"/client_cert.pem", clientkey=cert_dir+"/client_key.pem") self.assertEqual(ssl.CERT_REQUIRED, server_ctx.verify_mode) self.assertEqual(ssl.CERT_REQUIRED, client_ctx.verify_mode) self.assertTrue(client_ctx.check_hostname) sock = SU.createSocket(sslContext=server_ctx) try: self.assertTrue(hasattr(sock, "getpeercert")) finally: sock.close() finally: config.SSL = False if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() Pyro4-4.82/tests/PyroTests/test_threadpool.py000066400000000000000000000110121416147301300213470ustar00rootroot00000000000000""" Tests for the thread pool. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import time import random import unittest from Pyro4 import socketutil, core from Pyro4.socketserver.threadpool import Pool, PoolError, NoFreeWorkersError from Pyro4.socketserver.threadpoolserver import SocketServer_Threadpool from Pyro4.configuration import config JOB_TIME = 0.2 class Job(object): def __init__(self, name="unnamed"): self.name = name def __call__(self): time.sleep(JOB_TIME - random.random() / 10.0) class SlowJob(object): def __init__(self, name="unnamed"): self.name = name def __call__(self): time.sleep(5*JOB_TIME - random.random() / 10.0) class PoolTests(unittest.TestCase): def setUp(self): config.THREADPOOL_SIZE_MIN = 2 config.THREADPOOL_SIZE = 4 def tearDown(self): config.reset() def testCreate(self): with Pool() as jq: _ = repr(jq) self.assertTrue(jq.closed) def testSingle(self): with Pool() as p: job = Job() p.process(job) time.sleep(0.02) # let it pick up the job self.assertEqual(1, len(p.busy)) def testAllBusy(self): try: config.COMMTIMEOUT = 0.2 with Pool() as p: for i in range(config.THREADPOOL_SIZE): p.process(SlowJob(str(i+1))) # putting one more than the number of workers should raise an error: with self.assertRaises(NoFreeWorkersError): p.process(SlowJob("toomuch")) finally: config.COMMTIMEOUT = 0.0 def testClose(self): with Pool() as p: for i in range(config.THREADPOOL_SIZE): p.process(Job(str(i + 1))) with self.assertRaises(PoolError): p.process(Job(1)) # must not allow new jobs after closing self.assertEqual(0, len(p.busy)) self.assertEqual(0, len(p.idle)) def testScaling(self): with Pool() as p: for i in range(config.THREADPOOL_SIZE_MIN-1): p.process(Job("x")) self.assertEqual(1, len(p.idle)) self.assertEqual(config.THREADPOOL_SIZE_MIN-1, len(p.busy)) p.process(Job("x")) self.assertEqual(0, len(p.idle)) self.assertEqual(config.THREADPOOL_SIZE_MIN, len(p.busy)) # grow until no more free workers while True: try: p.process(Job("x")) except NoFreeWorkersError: break self.assertEqual(0, len(p.idle)) self.assertEqual(config.THREADPOOL_SIZE, len(p.busy)) # wait till jobs are done and check ending situation time.sleep(JOB_TIME*1.5) self.assertEqual(0, len(p.busy)) self.assertEqual(config.THREADPOOL_SIZE_MIN, len(p.idle)) class ServerCallback(core.Daemon): def __init__(self): self.received_denied_reasons = [] def _handshake(self, connection, denied_reason=None): self.received_denied_reasons.append(denied_reason) # store the denied reason return True def handleRequest(self, connection): time.sleep(0.05) def _housekeeping(self): pass class ThreadPoolServerTests(unittest.TestCase): def setUp(self): config.THREADPOOL_SIZE_MIN = 1 config.THREADPOOL_SIZE = 1 config.POLLTIMEOUT = 0.5 config.COMMTIMEOUT = 0.5 def tearDown(self): config.reset() def testServerPoolFull(self): port = socketutil.findProbablyUnusedPort() serv = SocketServer_Threadpool() daemon = ServerCallback() serv.init(daemon, "localhost", port) serversock = serv.sock.getsockname() csock1 = socketutil.createSocket(connect=serversock) csock2 = socketutil.createSocket(connect=serversock) try: serv.events([serv.sock]) time.sleep(0.2) self.assertEqual([None], daemon.received_denied_reasons) serv.events([serv.sock]) time.sleep(0.2) self.assertEqual(2, len(daemon.received_denied_reasons)) self.assertIn("no free workers, increase server threadpool size", daemon.received_denied_reasons) finally: csock1.close() csock2.close() serv.shutdown() if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() Pyro4-4.82/tests/PyroTests/test_util.py000066400000000000000000000550351416147301300202000ustar00rootroot00000000000000""" Tests for the utility functions. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import sys import os import unittest import Pyro4.util from Pyro4.configuration import config from testsupport import * # noinspection PyUnusedLocal def crash(arg=100): pre1 = "black" pre2 = 999 # noinspection PyUnusedLocal def nest(p1, p2): q = "white" + pre1 x = pre2 y = arg // 2 p3 = p1 // p2 return p3 a = 10 b = 0 s = "hello" c = nest(a, b) return c class TestUtils(unittest.TestCase): def testFormatTracebackNormal(self): try: crash() self.fail("must crash with ZeroDivisionError") except ZeroDivisionError: tb = "".join(Pyro4.util.formatTraceback(detailed=False)) self.assertIn("p3 = p1 // p2", tb) self.assertIn("ZeroDivisionError", tb) self.assertNotIn(" a = 10", tb) self.assertNotIn(" s = 'whiteblack'", tb) self.assertNotIn(" pre2 = 999", tb) self.assertNotIn(" x = 999", tb) def testFormatTracebackDetail(self): try: crash() self.fail("must crash with ZeroDivisionError") except ZeroDivisionError: tb = "".join(Pyro4.util.formatTraceback(detailed=True)) self.assertIn("p3 = p1 // p2", tb) self.assertIn("ZeroDivisionError", tb) if sys.platform != "cli": self.assertIn(" a = 10", tb) self.assertIn(" q = 'whiteblack'", tb) self.assertIn(" pre2 = 999", tb) self.assertIn(" x = 999", tb) def testPyroTraceback(self): try: crash() self.fail("must crash with ZeroDivisionError") except ZeroDivisionError: pyro_tb = Pyro4.util.formatTraceback(detailed=True) if sys.platform != "cli": self.assertIn(" Extended stacktrace follows (most recent call last)\n", pyro_tb) try: crash("stringvalue") self.fail("must crash with TypeError") except TypeError as x: x._pyroTraceback = pyro_tb # set the remote traceback info pyrotb = "".join(Pyro4.util.getPyroTraceback()) self.assertIn("Remote traceback", pyrotb) self.assertIn("crash(\"stringvalue\")", pyrotb) self.assertIn("TypeError:", pyrotb) self.assertIn("ZeroDivisionError", pyrotb) del x._pyroTraceback pyrotb = "".join(Pyro4.util.getPyroTraceback()) self.assertNotIn("Remote traceback", pyrotb) self.assertNotIn("ZeroDivisionError", pyrotb) self.assertIn("crash(\"stringvalue\")", pyrotb) self.assertIn("TypeError:", pyrotb) def testPyroTracebackArgs(self): try: crash() self.fail("must crash with ZeroDivisionError") except ZeroDivisionError: ex_type, ex_value, ex_tb = sys.exc_info() x = ex_value tb1 = Pyro4.util.getPyroTraceback() tb2 = Pyro4.util.getPyroTraceback(ex_type, ex_value, ex_tb) self.assertEqual(tb1, tb2) tb1 = Pyro4.util.formatTraceback() tb2 = Pyro4.util.formatTraceback(ex_type, ex_value, ex_tb) self.assertEqual(tb1, tb2) tb2 = Pyro4.util.formatTraceback(detailed=True) if sys.platform != "cli": self.assertNotEqual(tb1, tb2) # old call syntax, should get an error now: self.assertRaises(TypeError, Pyro4.util.getPyroTraceback, x) self.assertRaises(TypeError, Pyro4.util.formatTraceback, x) def testExcepthook(self): # simply test the excepthook by calling it the way Python would try: crash() self.fail("must crash with ZeroDivisionError") except ZeroDivisionError: pyro_tb = Pyro4.util.formatTraceback() try: crash("stringvalue") self.fail("must crash with TypeError") except TypeError: ex_type, ex_value, ex_tb = sys.exc_info() ex_value._pyroTraceback = pyro_tb # set the remote traceback info oldstderr = sys.stderr try: sys.stderr = StringIO() Pyro4.util.excepthook(ex_type, ex_value, ex_tb) output = sys.stderr.getvalue() self.assertIn("Remote traceback", output) self.assertIn("crash(\"stringvalue\")", output) self.assertIn("TypeError:", output) self.assertIn("ZeroDivisionError", output) finally: sys.stderr = oldstderr def clearEnv(self): if "PYRO_HOST" in os.environ: del os.environ["PYRO_HOST"] if "PYRO_NS_PORT" in os.environ: del os.environ["PYRO_NS_PORT"] if "PYRO_COMPRESSION" in os.environ: del os.environ["PYRO_COMPRESSION"] config.reset() def testConfig(self): self.clearEnv() try: self.assertEqual(9090, config.NS_PORT) self.assertEqual("localhost", config.HOST) self.assertEqual(False, config.COMPRESSION) os.environ["NS_PORT"] = "4444" config.reset() self.assertEqual(9090, config.NS_PORT) os.environ["PYRO_NS_PORT"] = "4444" os.environ["PYRO_HOST"] = "something.com" os.environ["PYRO_COMPRESSION"] = "OFF" config.reset() self.assertEqual(4444, config.NS_PORT) self.assertEqual("something.com", config.HOST) self.assertEqual(False, config.COMPRESSION) finally: self.clearEnv() self.assertEqual(9090, config.NS_PORT) self.assertEqual("localhost", config.HOST) self.assertEqual(False, config.COMPRESSION) def testConfigReset(self): try: config.reset() self.assertEqual("localhost", config.HOST) config.HOST = "foobar" self.assertEqual("foobar", config.HOST) config.reset() self.assertEqual("localhost", config.HOST) os.environ["PYRO_HOST"] = "foobar" config.reset() self.assertEqual("foobar", config.HOST) del os.environ["PYRO_HOST"] config.reset() self.assertEqual("localhost", config.HOST) finally: self.clearEnv() def testResolveAttr(self): config.REQUIRE_EXPOSE = True @Pyro4.core.expose class Exposed(object): def __init__(self, value): self.propvalue = value self.__value__ = value # is not affected by the @expose def __str__(self): return "<%s>" % self.value def _p(self): return "should not be allowed" def __p(self): return "should not be allowed" def __p__(self): return "should be allowed (dunder)" @property def value(self): return self.propvalue class Unexposed(object): def __init__(self): self.value = 42 def __value__(self): return self.value obj = Exposed("hello") obj.a = Exposed("a") obj.a.b = Exposed("b") obj.a.b.c = Exposed("c") obj.a._p = Exposed("p1") obj.a._p.q = Exposed("q1") obj.a.__p = Exposed("p2") obj.a.__p.q = Exposed("q2") obj.u = Unexposed() obj.u.v = Unexposed() # check the accessible attributes self.assertEqual("", str(Pyro4.util.getAttribute(obj, "a"))) dunder = str(Pyro4.util.getAttribute(obj, "__p__")) self.assertTrue(dunder.startswith("", str(Pyro4.util.getAttribute(obj, "a"))) self.assertRaises(AttributeError, Pyro4.util.getAttribute, obj, "u.v") # still not allowed to follow the dots self.assertRaises(AttributeError, Pyro4.util.getAttribute, obj, "u.v.value") # still not allowed to follow the dots config.REQUIRE_EXPOSE = True def testUnicodeKwargs(self): # test the way the interpreter deals with unicode function kwargs def function(*args, **kwargs): return args, kwargs processed_args = function(*(1, 2, 3), **{unichr(65): 42}) self.assertEqual(((1, 2, 3), {unichr(65): 42}), processed_args) processed_args = function(*(1, 2, 3), **{unichr(0x20ac): 42}) key = list(processed_args[1].keys())[0] self.assertEqual(key, unichr(0x20ac)) self.assertEqual(((1, 2, 3), {unichr(0x20ac): 42}), processed_args) class TestMetaAndExpose(unittest.TestCase): def setUp(self): config.REQUIRE_EXPOSE = True def testBasic(self): o = MyThingFullExposed("irmen") m1 = Pyro4.util.get_exposed_members(o) m2 = Pyro4.util.get_exposed_members(MyThingFullExposed) self.assertEqual(m1, m2) keys = m1.keys() self.assertEqual(3, len(keys)) self.assertIn("methods", keys) self.assertIn("attrs", keys) self.assertIn("oneway", keys) def testResetExposedCache(self): o = MyThingFullExposed("irmen") m1 = Pyro4.util.get_exposed_members(o) self.assertNotIn("newly_added_method", m1["methods"]) MyThingFullExposed.newly_added_method = Pyro4.core.expose(lambda self: None) m2 = Pyro4.util.get_exposed_members(o) self.assertNotIn("newly_added_method", m2["methods"]) Pyro4.util.reset_exposed_members(o) m3 = Pyro4.util.get_exposed_members(o) self.assertIn("newly_added_method", m3["methods"]) def testGetExposedCacheWorks(self): class Thingy(object): def method1(self): pass @property def prop(self): return 1 def notexposed(self): pass m1 = Pyro4.util.get_exposed_members(Thingy, only_exposed=False) def new_method(self, arg): return arg Thingy.new_method = new_method m2 = Pyro4.util.get_exposed_members(Thingy, only_exposed=False) self.assertEqual(m1, m2, "should still be equal because result from cache") m2 = Pyro4.util.get_exposed_members(Thingy, only_exposed=False, use_cache=False) self.assertNotEqual(m1, m2, "should not be equal because new result not from cache") def testPrivateNotExposed(self): o = MyThingFullExposed("irmen") m = Pyro4.util.get_exposed_members(o) self.assertEqual({"classmethod", "staticmethod", "method", "__dunder__", "oneway", "exposed"}, m["methods"]) self.assertEqual({"prop1", "readonly_prop1", "prop2"}, m["attrs"]) self.assertEqual({"oneway"}, m["oneway"]) o = MyThingPartlyExposed("irmen") m = Pyro4.util.get_exposed_members(o) self.assertEqual({"oneway", "exposed"}, m["methods"]) self.assertEqual({"prop1", "readonly_prop1"}, m["attrs"]) self.assertEqual({"oneway"}, m["oneway"]) def testNotOnlyExposed(self): o = MyThingPartlyExposed("irmen") m = Pyro4.util.get_exposed_members(o, only_exposed=False) self.assertEqual({"classmethod", "staticmethod", "method", "__dunder__", "oneway", "exposed"}, m["methods"]) self.assertEqual({"prop1", "readonly_prop1", "prop2"}, m["attrs"]) self.assertEqual({"oneway"}, m["oneway"]) def testPartlyExposedSubclass(self): o = MyThingPartlyExposedSub("irmen") m = Pyro4.util.get_exposed_members(o) self.assertEqual({"prop1", "readonly_prop1"}, m["attrs"]) self.assertEqual({"oneway"}, m["oneway"]) self.assertEqual({"sub_exposed", "exposed", "oneway"}, m["methods"]) def testExposedSubclass(self): o = MyThingExposedSub("irmen") m = Pyro4.util.get_exposed_members(o) self.assertEqual({"readonly_prop1", "prop1", "prop2"}, m["attrs"]) self.assertEqual({"oneway", "oneway2"}, m["oneway"]) self.assertEqual({"classmethod", "staticmethod", "oneway", "__dunder__", "method", "exposed", "oneway2", "sub_exposed", "sub_unexposed"}, m["methods"]) def testExposePrivateFails(self): with self.assertRaises(AttributeError): class Test1(object): @Pyro4.core.expose def _private(self): pass with self.assertRaises(AttributeError): class Test3(object): @Pyro4.core.expose def __private(self): pass with self.assertRaises(AttributeError): @Pyro4.core.expose class _Test4(object): pass with self.assertRaises(AttributeError): @Pyro4.core.expose class __Test5(object): pass def testExposeDunderOk(self): class Test1(object): @Pyro4.core.expose def __dunder__(self): pass self.assertTrue(Test1.__dunder__._pyroExposed) @Pyro4.core.expose class Test2(object): def __dunder__(self): pass self.assertTrue(Test2._pyroExposed) self.assertTrue(Test2.__dunder__._pyroExposed) def testClassmethodExposeWrongOrderFail(self): with self.assertRaises(AttributeError) as ax: class TestClass: @Pyro4.core.expose @classmethod def cmethod(cls): pass self.assertTrue("must be done after" in str(ax.exception)) with self.assertRaises(AttributeError) as ax: class TestClass: @Pyro4.core.expose @staticmethod def smethod(cls): pass self.assertTrue("must be done after" in str(ax.exception)) def testClassmethodExposeCorrectOrderOkay(self): class TestClass: @classmethod @Pyro4.core.expose def cmethod(cls): pass @staticmethod @Pyro4.core.expose def smethod(cls): pass self.assertTrue(TestClass.cmethod._pyroExposed) self.assertTrue(TestClass.smethod._pyroExposed) def testGetExposedProperty(self): o = MyThingFullExposed("irmen") with self.assertRaises(AttributeError): Pyro4.util.get_exposed_property_value(o, "name") with self.assertRaises(AttributeError): Pyro4.util.get_exposed_property_value(o, "c_attr") with self.assertRaises(AttributeError): Pyro4.util.get_exposed_property_value(o, "propvalue") with self.assertRaises(AttributeError): Pyro4.util.get_exposed_property_value(o, "unexisting_attribute") self.assertEqual(42, Pyro4.util.get_exposed_property_value(o, "prop1")) self.assertEqual(42, Pyro4.util.get_exposed_property_value(o, "prop2")) def testGetExposedPropertyFromPartiallyExposed(self): o = MyThingPartlyExposed("irmen") with self.assertRaises(AttributeError): Pyro4.util.get_exposed_property_value(o, "name") with self.assertRaises(AttributeError): Pyro4.util.get_exposed_property_value(o, "c_attr") with self.assertRaises(AttributeError): Pyro4.util.get_exposed_property_value(o, "propvalue") with self.assertRaises(AttributeError): Pyro4.util.get_exposed_property_value(o, "unexisting_attribute") self.assertEqual(42, Pyro4.util.get_exposed_property_value(o, "prop1")) with self.assertRaises(AttributeError): Pyro4.util.get_exposed_property_value(o, "prop2") def testSetExposedProperty(self): o = MyThingFullExposed("irmen") with self.assertRaises(AttributeError): Pyro4.util.set_exposed_property_value(o, "name", "erorr") with self.assertRaises(AttributeError): Pyro4.util.set_exposed_property_value(o, "unexisting_attribute", 42) with self.assertRaises(AttributeError): Pyro4.util.set_exposed_property_value(o, "readonly_prop1", 42) with self.assertRaises(AttributeError): Pyro4.util.set_exposed_property_value(o, "propvalue", 999) self.assertEqual(42, o.prop1) self.assertEqual(42, o.prop2) Pyro4.util.set_exposed_property_value(o, "prop1", 999) self.assertEqual(999, o.propvalue) Pyro4.util.set_exposed_property_value(o, "prop2", 8888) self.assertEqual(8888, o.propvalue) def testSetExposedPropertyFromPartiallyExposed(self): o = MyThingPartlyExposed("irmen") with self.assertRaises(AttributeError): Pyro4.util.set_exposed_property_value(o, "name", "erorr") with self.assertRaises(AttributeError): Pyro4.util.set_exposed_property_value(o, "unexisting_attribute", 42) with self.assertRaises(AttributeError): Pyro4.util.set_exposed_property_value(o, "readonly_prop1", 42) with self.assertRaises(AttributeError): Pyro4.util.set_exposed_property_value(o, "propvalue", 999) self.assertEqual(42, o.prop1) self.assertEqual(42, o.prop2) Pyro4.util.set_exposed_property_value(o, "prop1", 999) self.assertEqual(999, o.propvalue) with self.assertRaises(AttributeError): Pyro4.util.set_exposed_property_value(o, "prop2", 8888) def testIsPrivateName(self): self.assertTrue(Pyro4.util.is_private_attribute("_")) self.assertTrue(Pyro4.util.is_private_attribute("__")) self.assertTrue(Pyro4.util.is_private_attribute("___")) self.assertTrue(Pyro4.util.is_private_attribute("_p")) self.assertTrue(Pyro4.util.is_private_attribute("_pp")) self.assertTrue(Pyro4.util.is_private_attribute("_p_")) self.assertTrue(Pyro4.util.is_private_attribute("_p__")) self.assertTrue(Pyro4.util.is_private_attribute("__p")) self.assertTrue(Pyro4.util.is_private_attribute("___p")) self.assertFalse(Pyro4.util.is_private_attribute("__dunder__")) # dunder methods should not be private except a list of exceptions as tested below self.assertTrue(Pyro4.util.is_private_attribute("__init__")) self.assertTrue(Pyro4.util.is_private_attribute("__call__")) self.assertTrue(Pyro4.util.is_private_attribute("__new__")) self.assertTrue(Pyro4.util.is_private_attribute("__del__")) self.assertTrue(Pyro4.util.is_private_attribute("__repr__")) self.assertTrue(Pyro4.util.is_private_attribute("__unicode__")) self.assertTrue(Pyro4.util.is_private_attribute("__str__")) self.assertTrue(Pyro4.util.is_private_attribute("__format__")) self.assertTrue(Pyro4.util.is_private_attribute("__nonzero__")) self.assertTrue(Pyro4.util.is_private_attribute("__bool__")) self.assertTrue(Pyro4.util.is_private_attribute("__coerce__")) self.assertTrue(Pyro4.util.is_private_attribute("__cmp__")) self.assertTrue(Pyro4.util.is_private_attribute("__eq__")) self.assertTrue(Pyro4.util.is_private_attribute("__ne__")) self.assertTrue(Pyro4.util.is_private_attribute("__lt__")) self.assertTrue(Pyro4.util.is_private_attribute("__gt__")) self.assertTrue(Pyro4.util.is_private_attribute("__le__")) self.assertTrue(Pyro4.util.is_private_attribute("__ge__")) self.assertTrue(Pyro4.util.is_private_attribute("__hash__")) self.assertTrue(Pyro4.util.is_private_attribute("__dir__")) self.assertTrue(Pyro4.util.is_private_attribute("__enter__")) self.assertTrue(Pyro4.util.is_private_attribute("__exit__")) self.assertTrue(Pyro4.util.is_private_attribute("__copy__")) self.assertTrue(Pyro4.util.is_private_attribute("__deepcopy__")) self.assertTrue(Pyro4.util.is_private_attribute("__sizeof__")) self.assertTrue(Pyro4.util.is_private_attribute("__getattr__")) self.assertTrue(Pyro4.util.is_private_attribute("__setattr__")) self.assertTrue(Pyro4.util.is_private_attribute("__hasattr__")) self.assertTrue(Pyro4.util.is_private_attribute("__delattr__")) self.assertTrue(Pyro4.util.is_private_attribute("__getattribute__")) self.assertTrue(Pyro4.util.is_private_attribute("__instancecheck__")) self.assertTrue(Pyro4.util.is_private_attribute("__subclasscheck__")) self.assertTrue(Pyro4.util.is_private_attribute("__subclasshook__")) self.assertTrue(Pyro4.util.is_private_attribute("__getinitargs__")) self.assertTrue(Pyro4.util.is_private_attribute("__getnewargs__")) self.assertTrue(Pyro4.util.is_private_attribute("__getstate__")) self.assertTrue(Pyro4.util.is_private_attribute("__setstate__")) self.assertTrue(Pyro4.util.is_private_attribute("__reduce__")) self.assertTrue(Pyro4.util.is_private_attribute("__reduce_ex__")) self.assertTrue(Pyro4.util.is_private_attribute("__getstate_for_dict__")) self.assertTrue(Pyro4.util.is_private_attribute("__setstate_from_dict__")) if __name__ == "__main__": # import sys; sys.argv = ['', 'Test.testName'] unittest.main() Pyro4-4.82/tests/PyroTests/testsupport.py000066400000000000000000000146311416147301300205750ustar00rootroot00000000000000""" Support code for the test suite. There's some Python 2.x <-> 3.x compatibility code here. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import sys import pickle import threading from Pyro4 import errors, core, expose, behavior, current_context from Pyro4.configuration import config __all__ = ["tobytes", "tostring", "unicode", "unichr", "basestring", "StringIO", "NonserializableError", "MyThingPartlyExposed", "MyThingFullExposed", "MyThingExposedSub", "MyThingPartlyExposedSub", "ConnectionMock", "AtomicCounter", "ResourceService", "Resource"] config.reset(False) # reset the config to default if sys.version_info < (3, 0): # noinspection PyUnresolvedReferences from StringIO import StringIO def tobytes(string, encoding=None): return string def tostring(bytes): return bytes unicode = unicode unichr = unichr basestring = basestring else: from io import StringIO def tobytes(string, encoding="iso-8859-1"): return bytes(string, encoding) def tostring(bytes, encoding="utf-8"): return str(bytes, encoding) unicode = str unichr = chr basestring = str class NonserializableError(Exception): def __reduce__(self): raise pickle.PicklingError("to make this error non-serializable") class MyThingPartlyExposed(object): c_attr = "hi" propvalue = 42 _private_attr1 = "hi" __private_attr2 = "hi" name = "" def __init__(self, name="dummy"): self.name = name def __eq__(self, other): if type(other) is MyThingPartlyExposed: return self.name == other.name return False def method(self, arg, default=99, **kwargs): pass @staticmethod def staticmethod(arg): pass @classmethod def classmethod(cls, arg): pass def __dunder__(self): pass def __private(self): pass def _private(self): pass @core.expose @property def prop1(self): return self.propvalue @core.expose @prop1.setter def prop1(self, value): self.propvalue = value @core.expose @property def readonly_prop1(self): return self.propvalue @property def prop2(self): return self.propvalue @prop2.setter def prop2(self, value): self.propvalue = value @core.oneway @core.expose def oneway(self, arg): pass @core.expose def exposed(self): pass __hash__ = object.__hash__ @core.expose class MyThingFullExposed(object): """this is the same as MyThingPartlyExposed but the whole class should be exposed""" c_attr = "hi" propvalue = 42 _private_attr1 = "hi" __private_attr2 = "hi" name = "" def __init__(self, name="dummy"): self.name = name # note: not affected by @expose, only real properties are def __eq__(self, other): if type(other) is MyThingFullExposed: return self.name == other.name return False def method(self, arg, default=99, **kwargs): pass @staticmethod def staticmethod(arg): pass @classmethod def classmethod(cls, arg): pass def __dunder__(self): pass def __private(self): pass def _private(self): pass @property def prop1(self): return self.propvalue @prop1.setter def prop1(self, value): self.propvalue = value @property def readonly_prop1(self): return self.propvalue @property def prop2(self): return self.propvalue @prop2.setter def prop2(self, value): self.propvalue = value @core.oneway def oneway(self, arg): pass def exposed(self): pass __hash__ = object.__hash__ @core.expose class MyThingExposedSub(MyThingFullExposed): def sub_exposed(self): pass def sub_unexposed(self): pass @core.oneway def oneway2(self): pass class MyThingPartlyExposedSub(MyThingPartlyExposed): @core.expose def sub_exposed(self): pass def sub_unexposed(self): pass @core.oneway def oneway2(self): pass class ConnectionMock(object): def __init__(self, initial_msg=None): self.keep_open = False if not initial_msg: self.received = b"" elif isinstance(initial_msg, (str, bytes)): self.received = initial_msg else: self.received = initial_msg.to_bytes() # it's probably a Message object def send(self, data): self.received += data def recv(self, datasize): chunk = self.received[:datasize] self.received = self.received[datasize:] if len(chunk) < datasize: raise errors.ConnectionClosedError("receiving: not enough data") return chunk class AtomicCounter(object): def __init__(self, value=0): self.__initial = value self.__value = value self.__lock = threading.Lock() def reset(self): self.__value = self.__initial def incr(self, amount=1): with self.__lock: self.__value += amount return self.__value def decr(self, amount=1): with self.__lock: self.__value -= amount return self.__value @property def value(self): return self.__value class Resource(object): # a fictional resource that gets allocated and must be freed again later. def __init__(self, name, collection): self.name = name self.collection = collection self.close_called = False def close(self): # Pyro will call this on a tracked resource once the client's connection gets closed! self.collection.discard(self) self.close_called = True @expose @behavior(instance_mode="single") class ResourceService(object): def __init__(self): self.resources = set() # the allocated resources def allocate(self, name): resource = Resource(name, self.resources) self.resources.add(resource) current_context.track_resource(resource) def free(self, name): resources = {r for r in self.resources if r.name == name} self.resources -= resources for r in resources: r.close() current_context.untrack_resource(r) def list(self): return [r.name for r in self.resources] Pyro4-4.82/tests/run_ser_performance.py000066400000000000000000000122471416147301300202440ustar00rootroot00000000000000""" Prints a comparison between different serializers. Compares results based on size of the output, and time taken to (de)serialize. """ from __future__ import print_function from timeit import default_timer as perf_timer import sys import datetime import decimal import uuid import Pyro4.util import Pyro4.errors import Pyro4.core data = { "bytes": b"0123456789abcdefghijklmnopqrstuvwxyz" * 2000, "bytearray": bytearray(b"0123456789abcdefghijklmnopqrstuvwxyz") * 2000, "str": "\"0123456789\"\n'abcdefghijklmnopqrstuvwxyz'\t" * 2000, "unicode": u"abcdefghijklmnopqrstuvwxyz\u20ac\u20ac\u20ac\u20ac\u20ac" * 2000, "int": [123456789] * 1000, "double": [12345.987654321] * 1000, "long": [123456789123456789123456789123456789] * 1000, "tuple": [(x * x, "tuple", (300, 400, (500, 600, (x * x, x * x)))) for x in range(200)], "list": [[x * x, "list", [300, 400, [500, 600, [x * x]]]] for x in range(200)], "set": set(x * x for x in range(1000)), "dict": {str(i * i): {str(1000 + j): chr(j + 65) for j in range(5)} for i in range(100)}, "exception": [ZeroDivisionError("test exeception", x * x) for x in range(1000)], "class": [Pyro4.core.URI("PYRO:obj@addr:9999") for x in range(1000)], "datetime": [datetime.datetime.now() for x in range(1000)], "complex": [complex(x + x, x * x) for x in range(1000)], "decimal": [decimal.Decimal("1122334455667788998877665544332211.9876543212345678987654321123456789") for x in range(1000)], "uuid": uuid.uuid4() } no_result = 9999999999 def run(): results = {} number = 10 repeat = 3 for serializername, ser in Pyro4.util._serializers.items(): print("\nserializer:", serializername) results[serializername] = {"sizes": {}, "ser-times": {}, "deser-times": {}} for key in sorted(data): print(key, end="; ") sys.stdout.flush() try: serialized = ser.dumps(data[key]) except (TypeError, ValueError, OverflowError, Pyro4.errors.SerializeError) as x: print("error!") print(x, key) results[serializername]["sizes"][key] = no_result results[serializername]["ser-times"][key] = no_result results[serializername]["deser-times"][key] = no_result else: results[serializername]["sizes"][key] = len(serialized) durations_ser = [] durations_deser = [] serialized_data = ser.dumps(data[key]) for _ in range(repeat): start = perf_timer() for _ in range(number): ser.dumps(data[key]) durations_ser.append(perf_timer() - start) for _ in range(repeat): start = perf_timer() for _ in range(number): ser.loads(serialized_data) durations_deser.append(perf_timer() - start) duration_ser = min(durations_ser) duration_deser = min(durations_deser) results[serializername]["ser-times"][key] = round(duration_ser * 1e6 / number, 2) results[serializername]["deser-times"][key] = round(duration_deser * 1e6 / number, 2) print() return results def tables_size(results): print("\nSIZE RESULTS\n") sizes_per_datatype = {} for ser in results: for datatype in results[ser]["sizes"]: size = results[ser]["sizes"][datatype] if datatype not in sizes_per_datatype: sizes_per_datatype[datatype] = [] sizes_per_datatype[datatype].append((size, ser)) sizes_per_datatype = {datatype: sorted(sizes) for datatype, sizes in sizes_per_datatype.items()} for dt in sorted(sizes_per_datatype): print(dt) for pos, (size, serializer) in enumerate(sizes_per_datatype[dt]): if size == no_result: size = "unsupported" else: size = "%8d" % size print(" %2d: %-8s %s" % (pos + 1, serializer, size)) print() def tables_speed(results, what_times, header): print("\n%s\n" % header) durations_per_datatype = {} for ser in results: for datatype in results[ser]["sizes"]: duration = results[ser][what_times][datatype] if datatype not in durations_per_datatype: durations_per_datatype[datatype] = [] durations_per_datatype[datatype].append((duration, ser)) durations_per_datatype = {datatype: sorted(durations) for datatype, durations in durations_per_datatype.items()} for dt in sorted(durations_per_datatype): print(dt) for pos, (duration, serializer) in enumerate(durations_per_datatype[dt]): if duration == no_result: duration = "unsupported" else: duration = "%8d" % duration print(" %2d: %-8s %s" % (pos + 1, serializer, duration)) print() if __name__ == "__main__": results = run() tables_size(results) tables_speed(results, "ser-times", "SPEED RESULTS (SERIALIZATION)") tables_speed(results, "deser-times", "SPEED RESULTS (DESERIALIZATION)") Pyro4-4.82/tests/run_syntaxcheck.py000066400000000000000000000016121416147301300174100ustar00rootroot00000000000000""" Run some syntax checks. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import os import sys sys.path.insert(0, "../src") sys.path.insert(1, "PyroTests") def Pyflakes(path, modules): try: from pyflakes.scripts.pyflakes import checkPath except ImportError: print("PYFLAKES not installed. Skipping.") return warnings = 0 for m in modules: warnings += checkPath(os.path.join(path, m)) print("%d warnings occurred in pyflakes check" % warnings) def main(args): pyropath = "../src/Pyro4" pyromodules = [module for module in os.listdir(pyropath) if module.endswith(".py")] checkers = args or ["flakes"] if "flakes" in checkers: print("-" * 20 + "PYFLAKES") Pyflakes(pyropath, pyromodules) if __name__ == "__main__": main(sys.argv[1:]) Pyro4-4.82/tests/run_testsuite.py000066400000000000000000000030551416147301300171200ustar00rootroot00000000000000""" Run the complete test suite. use --tox to make this work from Tox. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import unittest import sys import os from Pyro4.configuration import config config.reset() from_tox = "--tox" in sys.argv xml_report = "--xml" in sys.argv if from_tox: # running from Tox, don't screw with paths otherwise it screws up the virtualenv pass else: # running from normal shell invocation dirname = os.path.dirname(__file__) if dirname: print("chdir to " + dirname) os.chdir(dirname) sys.path.insert(0, "../src") # add Pyro source directory sys.path.insert(1, "PyroTests") if __name__ == "__main__": # add test modules here modules = sorted(module[:-3] for module in os.listdir("PyroTests") if module.endswith(".py") and not module.startswith("__")) print("gathering testcases from %s" % modules) suite = unittest.TestSuite() for module in modules: m = __import__("PyroTests." + module) m = getattr(m, module) testcases = unittest.defaultTestLoader.loadTestsFromModule(m) suite.addTest(testcases) if xml_report: print("\nRUNNING UNIT TESTS (XML reporting)...") import xmlrunner result = xmlrunner.XMLTestRunner(verbosity=1, output="test-reports").run(suite) else: print("\nRUNNING UNIT TESTS...") result = unittest.TextTestRunner(verbosity=1, failfast=False).run(suite) if not result.wasSuccessful(): sys.exit(10) Pyro4-4.82/tox.ini000066400000000000000000000004121416147301300137740ustar00rootroot00000000000000[tox] envlist=py27,py35,py36,py37,py38,py39,py310 [testenv] deps=-rtest_requirements.txt changedir=tests commands=python -bb -tt -E -Wall run_testsuite.py --tox [testenv:pypy3] # pypy3 doesn't have the -tt option commands=pypy3 -E -Wall -bb run_testsuite.py --tox