Hello community, here is the log from the commit of package python-rpyc for openSUSE:Factory checked in at 2020-03-19 19:50:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-rpyc (Old) and /work/SRC/openSUSE:Factory/.python-rpyc.new.3160 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-rpyc" Thu Mar 19 19:50:48 2020 rev:7 rq:786356 version:4.1.4 Changes: -------- --- /work/SRC/openSUSE:Factory/python-rpyc/python-rpyc.changes 2019-09-11 10:35:46.955289445 +0200 +++ /work/SRC/openSUSE:Factory/.python-rpyc.new.3160/python-rpyc.changes 2020-03-19 19:54:13.904277536 +0100 @@ -1,0 +2,14 @@ +Thu Mar 19 07:57:20 UTC 2020 - pgaj...@suse.com + +- version update to 4.1.4 + - Merged 3.7 and 3.8 teleportatio compat enhancement `#371`_ + - Fixed connection hanging due to namepack cursor `#369`_ + - Fixed test dependencies and is_py_* for 3.9 + - Performance improvements: `#366`_ and `#351`_ + - Merged fix for propagate_KeyboardInterrupt_locally `#364`_ + - Fixed handling of exceptions for request callbacks `#365`_ + - Partially fixed return value for netref.__class__ `#355`_ + - Fixed `CVE-2019-16328`_ which was caused by a missing protocol security check + - Fixed RPyC over RPyC for mutable parameters and extended unit testing for `#346`_ + +------------------------------------------------------------------- Old: ---- 4.1.1.tar.gz New: ---- 4.1.4.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-rpyc.spec ++++++ --- /var/tmp/diff_new_pack.rePmNz/_old 2020-03-19 19:54:14.320277550 +0100 +++ /var/tmp/diff_new_pack.rePmNz/_new 2020-03-19 19:54:14.324277551 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-rpyc # -# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2020 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -26,7 +26,7 @@ %bcond_with test %endif Name: python-rpyc%{psuffix} -Version: 4.1.1 +Version: 4.1.4 Release: 0 Summary: Remote Python Call (RPyC), a RPC library License: MIT @@ -36,14 +36,14 @@ BuildRequires: %{python_module setuptools} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-plumbum +Requires: python-plumbum >= 1.2 Requires(post): update-alternatives Requires(postun): update-alternatives BuildArch: noarch %if %{with test} BuildRequires: %{python_module gevent} BuildRequires: %{python_module nose} -BuildRequires: %{python_module plumbum} +BuildRequires: %{python_module plumbum >= 1.2} BuildRequires: %{python_module rpyc = %{version}} %endif %python_subpackages @@ -76,7 +76,7 @@ %if %{with test} %check -%python_expand nosetests-%{$python_bin_suffix} -v -I test_deploy -I test_gevent_server -I test_ssh -I test_registry +%python_expand nosetests-%{$python_bin_suffix} -v -I test_deploy -I test_gevent_server -I test_ssh -I test_registry -I test_win32pipes %endif %if !%{with test} ++++++ 4.1.1.tar.gz -> 4.1.4.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/.travis.yml new/rpyc-4.1.4/.travis.yml --- old/rpyc-4.1.1/.travis.yml 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/.travis.yml 2020-01-30 07:26:06.000000000 +0100 @@ -1,26 +1,23 @@ language: python -dist: trusty +dist: bionic # See: https://docs.travis-ci.com/user/languages/python/ # and: https://docs.travis-ci.com/user/customizing-the-build/ matrix: include: - - {python: "2.6", env: SKIP_GEVENT=true SKIP_DEPLOY=true} - - {python: "2.7", env: SKIP_GEVENT=true } - - {python: "3.3", env: SKIP_GEVENT=true SKIP_DEPLOY=true} - - {python: "3.4", env: } - - {python: "3.5", env: SKIP_DEPLOY=true} - - {python: "3.6", env: SKIP_DEPLOY=true} - - {python: "3.7", env: SKIP_GEVENT=true SKIP_DEPLOY=true, - dist: xenial, sudo: true} - - {python: "nightly", env: SKIP_GEVENT=true SKIP_DEPLOY=true, - dist: xenial, sudo: true} + - {python: "2.7"} + - {python: "3.6"} + - {python: "3.7"} + - {python: "3.8"} + - {python: "3.8-dev"} + - {python: "nightly"} install: - python setup.py install - - "pip install gevent || [[ -n $SKIP_GEVENT ]]" - - "pip install paramiko || [[ -n $SKIP_DEPLOY ]]" + # Install fails and historically has been skipped anyway + # - pip install gevent + # - pip install paramiko before_script: - "echo NoHostAuthenticationForLocalhost yes >> ~/.ssh/config" @@ -32,11 +29,7 @@ - "cd tests" script: - - nosetests -vv -I test_deploy -I test_gevent_server - # need to run in its own python interpreter (`monkey.patch_all()`): - - "python test_gevent_server.py || [[ -n $SKIP_GEVENT ]]" - # it currently fails on all but the native python versions on travis: - - "python test_deploy.py || [[ -n $SKIP_DEPLOY ]]" + - python -m unittest discover notifications: email: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/CHANGELOG.rst new/rpyc-4.1.4/CHANGELOG.rst --- old/rpyc-4.1.1/CHANGELOG.rst 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/CHANGELOG.rst 2020-01-30 07:26:06.000000000 +0100 @@ -1,3 +1,41 @@ +4.1.4 +----- +Date: 1.30.2020 + +- Merged 3.7 and 3.8 teleportatio compat enhancement `#371`_ +- Fixed connection hanging due to namepack cursor `#369`_ +- Fixed test dependencies and is_py_* for 3.9 + +.. _#371: https://github.com/tomerfiliba/rpyc/issues/371 +.. _#369: https://github.com/tomerfiliba/rpyc/issues/369 + +4.1.3 +----- +Date: 1.25.2020 + +- Performance improvements: `#366`_ and `#351`_ +- Merged fix for propagate_KeyboardInterrupt_locally `#364`_ +- Fixed handling of exceptions for request callbacks `#365`_ +- Partially fixed return value for netref.__class__ `#355`_ + +.. _#366: https://github.com/tomerfiliba/rpyc/issues/366 +.. _#351: https://github.com/tomerfiliba/rpyc/pull/351 +.. _#364: https://github.com/tomerfiliba/rpyc/pull/364 +.. _#365: https://github.com/tomerfiliba/rpyc/issues/365 +.. _#355: https://github.com/tomerfiliba/rpyc/issues/355 + + +4.1.2 +----- +Date: 10.03.2019 + +- Fixed `CVE-2019-16328`_ which was caused by a missing protocol security check +- Fixed RPyC over RPyC for mutable parameters and extended unit testing for `#346`_ + +.. _CVE-2019-16328: https://rpyc.readthedocs.io/en/latest/docs/security.html +.. _#346: https://github.com/tomerfiliba/rpyc/issues/346 + + 4.1.1 ----- Date: 07.27.2019 Binary files old/rpyc-4.1.1/docs/docs/_static/advanced-debugging-chained-connection-w-wireshark.png and new/rpyc-4.1.4/docs/docs/_static/advanced-debugging-chained-connection-w-wireshark.png differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/docs/docs/advanced-debugging.rst new/rpyc-4.1.4/docs/docs/advanced-debugging.rst --- old/rpyc-4.1.1/docs/docs/advanced-debugging.rst 1970-01-01 01:00:00.000000000 +0100 +++ new/rpyc-4.1.4/docs/docs/advanced-debugging.rst 2020-01-30 07:26:06.000000000 +0100 @@ -0,0 +1,37 @@ +.. _advdebugging: + +Advanced Debugging +================== + +A guide to using Wireshark when debugging complex use such as chained-connections. + +Tips and Tricks +--------------- +Setting up for a specific version :: + + pkgver=4.0.2 + myvenv=rpyc${pkgver} + virtualenv2 /tmp/$myvenv + source /tmp/$myvenv/bin/activate + pip install rpyc==$pkgver + +Display filtering for Wireshark :: + + tcp.port == 18878 || tcp.port == 18879 + (tcp.port == 18878 || tcp.port == 18879) && tcp.segment_data contains "rpyc.core.service.SlaveService" + +Running the chained-connection unit test :: + + cd tests + python -m unittest test_get_id_pack.Test_get_id_pack.test_chained_connect + + +After stopping Wireshark, export specified packets, and open the PCAP. If not already configured, add a custom display column: :: + + Title, Type, Fields, Field Occurrence + Stream Index, Custom, tcp.stream, 0 + +The stream index column makes it easier to decide which TCP stream to follow. Following a TCP provides a more human readable overview +of requests and replies that can be printed as a PDF. + +.. figure:: _static/advanced-debugging-chained-connection-w-wireshark.png diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/docs/docs/security.rst new/rpyc-4.1.4/docs/docs/security.rst --- old/rpyc-4.1.1/docs/docs/security.rst 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/docs/docs/security.rst 2020-01-30 07:26:06.000000000 +0100 @@ -3,26 +3,34 @@ Security ======== Operating over a network always involve a certain security risk, and requires some awareness. -RPyC is believed to be a secure protocol -- no incidents have been reported since version 3 was -released in 2008. Version 3 was a rewrite of the library, specifically targeting security and -service-orientation. Unlike v2.6, RPyC no longer makes use of unsecure protocols like ``pickle``, +Version 3 of RPyC was a rewrite of the library, specifically targeting security and +service-orientation. Unlike version 2.6, RPyC no longer makes use of unsecure protocols like ``pickle``, supports :data:`security-related configuration parameters <rpyc.core.protocol.DEFAULT_CONFIG>`, -comes with strict defaults, and encourages the use of a capability-based security model. -I daresay RPyC itself is secure. +comes with strict defaults, and encourages the use of a capability-based security model. Even so, it behooves you to +take a layered to secure programming and not let RPyC be a single point of failure. -However, if not used properly, RPyC is also the perfect back-door... The general recommendation -is not to use RPyC openly exposed over the Internet. It's wiser to use it only over secure local +`CVE-2019-16328`_ is the first vulnerability since 2008, which made it possible for a remote attacker to +bypass standard protocol security checks and modify the behavior of a service. The latent flaw was committed +to master from September 2018 to October 2019 and affected versions `4.1.0` and `4.1.1`. As of version +`4.1.2`, the vulnerability has been fixed. + +RPyC is intuitive and secure when used properly. However, if not used properly, RPyC is also the perfect back-door... +The general recommendation is not to use RPyC openly exposed over the Internet. It's wiser to use it only over secure local networks, where you trust your peers. This does not imply that there's anything wrong with the -mechanism -- but the implementation details are sometimes too subtle to be sure of. -Of course you can use RPyC over a :ref:`secure connection <ssl>`, to mitigate these risks +mechanism--but the implementation details are sometimes too subtle to be sure of. +Of course, you can use RPyC over a :ref:`secure connection <ssl>`, to mitigate these risks. RPyC works by exposing a root object, which in turn may expose other objects (and so on). For instance, if you expose a module or an object that has a reference to the ``sys`` module, a user may be able to reach it. After reaching ``sys``, the user can traverse ``sys.modules`` and -gain access to all of the modules that the server imports. This of course depends on RPyC -being configured to allow attribute access (by default, this parameter is set to false). -But if you enable ``allow_public_attrs``, such things are likely to slip under the radar -(though it's possible to prevent this -- see below). +gain access to all of the modules that the server imports. More complex methodologies, similiar to those used in ``CVE-2019-16328``, +could leverage access to ``builtins.str``, ``builtins.type``, ``builtins.object``, and ``builtins.dict`` and gain access to +``sys`` modules. The default configurations for RPyC are intended to mitigate access to dangerous objects. But if you enable +``allow_public_attrs``, return uninitialized classes or override ``_rpyc_getattr`` such things are likely to slip under the radar +(it's possible to prevent this -- see below). + +.. _CVE-2019-16328: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16328 + Wrapping -------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/docs/docs.rst new/rpyc-4.1.4/docs/docs.rst --- old/rpyc-4.1.1/docs/docs.rst 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/docs/docs.rst 2020-01-30 07:26:06.000000000 +0100 @@ -41,6 +41,7 @@ docs/security docs/secure-connection docs/zerodeploy + docs/advanced-debugging * :ref:`Servers <servers>` - using the built-in servers and writing custom ones @@ -62,3 +63,5 @@ * :ref:`Zero-Deploy <zerodeploy>` - spawn temporary, short-lived RPyC server on remote machine with nothing more than SSH and a Python interpreter + +* :ref:`Advanced Debugging <advdebugging>` - debugging at the packet level diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/core/brine.py new/rpyc-4.1.4/rpyc/core/brine.py --- old/rpyc-4.1.1/rpyc/core/brine.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/core/brine.py 2020-01-30 07:26:06.000000000 +0100 @@ -17,7 +17,7 @@ >>> x == z True """ -from rpyc.lib.compat import Struct, BytesIO, is_py3k, BYTES_LITERAL +from rpyc.lib.compat import Struct, BytesIO, is_py_3k, BYTES_LITERAL # singletons @@ -49,7 +49,7 @@ TAG_SLICE = b"\x19" TAG_FSET = b"\x1a" TAG_COMPLEX = b"\x1b" -if is_py3k: +if is_py_3k: IMM_INTS = dict((i, bytes([i + 0x50])) for i in range(-0x30, 0xa0)) else: IMM_INTS = dict((i, chr(i + 0x50)) for i in range(-0x30, 0xa0)) @@ -156,7 +156,7 @@ _dump_bytes(obj.encode("utf8"), stream) -if not is_py3k: +if not is_py_3k: @register(_dump_registry, long) # noqa: F821 def _dump_long(obj, stream): stream.append(TAG_LONG) @@ -229,7 +229,7 @@ return b"" -if is_py3k: +if is_py_3k: @register(_load_registry, TAG_LONG) def _load_long(stream): obj = _load(stream) @@ -316,7 +316,7 @@ return tuple(_load(stream) for i in range(l)) -if is_py3k: +if is_py_3k: @register(_load_registry, TAG_TUP_L4) def _load_tup_l4(stream): l, = I4.unpack(stream.read(4)) @@ -385,7 +385,7 @@ return _load(stream) -if is_py3k: +if is_py_3k: simple_types = frozenset([type(None), int, bool, float, bytes, str, complex, type(NotImplemented), type(Ellipsis)]) else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/core/channel.py new/rpyc-4.1.4/rpyc/core/channel.py --- old/rpyc-4.1.1/rpyc/core/channel.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/core/channel.py 2020-01-30 07:26:06.000000000 +0100 @@ -70,6 +70,6 @@ data = zlib.compress(data, self.COMPRESSION_LEVEL) else: compressed = 0 - header = self.FRAME_HEADER.pack(len(data), compressed) - buf = header + data + self.FLUSHER - self.stream.write(buf) + self.stream.write(self.FRAME_HEADER.pack(len(data), compressed)) + self.stream.write(data) + self.stream.write(self.FLUSHER) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/core/netref.py new/rpyc-4.1.4/rpyc/core/netref.py --- old/rpyc-4.1.1/rpyc/core/netref.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/core/netref.py 2020-01-30 07:26:06.000000000 +0100 @@ -4,7 +4,7 @@ import sys import types from rpyc.lib import get_methods, get_id_pack -from rpyc.lib.compat import pickle, is_py3k, maxint, with_metaclass +from rpyc.lib.compat import pickle, is_py_3k, maxint, with_metaclass from rpyc.core import consts @@ -47,7 +47,7 @@ else: _builtin_types.append(BaseException) -if is_py3k: +if is_py_3k: _builtin_types.extend([ bytes, bytearray, type(iter(range(10))), memoryview, ]) @@ -229,9 +229,15 @@ elif other.____id_pack__[2] != 0: return True else: + # seems dubious if each netref proxies to a different address spaces return syncreq(self, consts.HANDLE_INSTANCECHECK, other.____id_pack__) else: - return isinstance(other, self.__class__) + if self.____id_pack__[2] == 0: + # outside the context of `__instancecheck__`, `__class__` is expected to be type(self) + # within the context of `__instancecheck__`, `other` should be compared to the proxied class + return isinstance(other, type(self).__dict__['__class__'].instance) + else: + raise TypeError("isinstance() arg 2 must be a class, type, or tuple of classes and types") def _make_method(name, doc): @@ -271,6 +277,33 @@ return method +class NetrefClass(object): + """a descriptor of the class being proxied + + Future considerations: + + there may be a cleaner alternative but lib.compat.with_metaclass prevented using __new__ + + consider using __slot__ for this class + + revisit the design choice to use properties here + """ + + def __init__(self, class_obj): + self._class_obj = class_obj + + @property + def instance(self): + """accessor to class object for the instance being proxied""" + return self._class_obj + + @property + def owner(self): + """accessor to the class object for the instance owner being proxied""" + return self._class_obj.__class__ + + def __get__(self, netref_instance, netref_owner): + """the value returned when accessing the netref class is dictated by whether or not an instance is proxied""" + return self.owner if netref_instance.____id_pack__[2] == 0 else self.instance + + def class_factory(id_pack, methods): """Creates a netref class proxying the given class @@ -281,30 +314,30 @@ """ ns = {"__slots__": (), "__class__": None} name_pack = id_pack[0] - if name_pack is not None: # attempt to resolve against builtins and sys.modules - ns["__class__"] = _normalized_builtin_types.get(name_pack) - if ns["__class__"] is None: - _module = None - didx = name_pack.rfind('.') - if didx != -1: - _module = sys.modules.get(name_pack[:didx]) - if _module is not None: - _module = getattr(_module, name_pack[didx + 1:], None) - else: - _module = sys.modules.get(name_pack) - else: - _module = sys.modules.get(name_pack) - if _module: - if id_pack[2] == 0: - ns["__class__"] = _module - else: - ns["__class__"] = getattr(_module, "__class__", None) - + class_descriptor = None + if name_pack is not None: + # attempt to resolve __class__ using sys.modules (i.e. builtins and imported modules) + _module = None + cursor = len(name_pack) + while cursor != -1: + _module = sys.modules.get(name_pack[:cursor]) + if _module is None: + cursor = name_pack[:cursor].rfind('.') + continue + _class_name = name_pack[cursor + 1:] + _class = getattr(_module, _class_name, None) + if _class is not None and hasattr(_class, '__class__'): + class_descriptor = NetrefClass(_class) + break + ns['__class__'] = class_descriptor + netref_name = class_descriptor.owner.__name__ if class_descriptor is not None else name_pack + # create methods that must perform a syncreq for name, doc in methods: name = str(name) # IronPython issue #10 + # only create methods that wont shadow BaseNetref during merge for mro if name not in LOCAL_ATTRS: # i.e. `name != __class__` ns[name] = _make_method(name, doc) - return type(name_pack, (BaseNetref,), ns) + return type(netref_name, (BaseNetref,), ns) for _builtin in _builtin_types: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/core/protocol.py new/rpyc-4.1.4/rpyc/core/protocol.py --- old/rpyc-4.1.1/rpyc/core/protocol.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/core/protocol.py 2020-01-30 07:26:06.000000000 +0100 @@ -8,7 +8,7 @@ from threading import Lock, Condition from rpyc.lib import spawn, Timeout, get_methods, get_id_pack -from rpyc.lib.compat import pickle, next, is_py3k, maxint, select_error, acquire_lock # noqa: F401 +from rpyc.lib.compat import pickle, next, is_py_3k, maxint, select_error, acquire_lock # noqa: F401 from rpyc.lib.colls import WeakValueDict, RefCountingColl from rpyc.core import consts, brine, vinegar, netref from rpyc.core.async_ import AsyncResult @@ -272,13 +272,6 @@ else: id_pack = get_id_pack(obj) self._local_objects.add(id_pack, obj) - try: - cls = obj.__class__ - except Exception: - # see issue #16 - cls = type(obj) - if not isinstance(cls, type): - cls = type(obj) return consts.LABEL_REMOTE_REF, id_pack def _unbox(self, package): # boxing @@ -305,15 +298,19 @@ def _netref_factory(self, id_pack): # boxing """id_pack is for remote, so when class id fails to directly match """ - if id_pack[0] in netref.builtin_classes_cache: + cls = None + if id_pack[2] == 0 and id_pack in self._netref_classes_cache: + cls = self._netref_classes_cache[id_pack] + elif id_pack[0] in netref.builtin_classes_cache: cls = netref.builtin_classes_cache[id_pack[0]] - elif id_pack[1] in self._netref_classes_cache: - cls = self._netref_classes_cache[id_pack[1]] - else: + if cls is None: # in the future, it could see if a sys.module cache/lookup hits first cls_methods = self.sync_request(consts.HANDLE_INSPECT, id_pack) cls = netref.class_factory(id_pack, cls_methods) - self._netref_classes_cache[id_pack[1]] = cls + if id_pack[2] == 0: + # only use cached netrefs for classes + # ... instance caching after gc of a proxy will take some mental gymnastics + self._netref_classes_cache[id_pack] = cls return cls(self, id_pack) def _dispatch_request(self, seq, raw_args): # dispatch @@ -321,7 +318,7 @@ handler, args = raw_args args = self._unbox(args) res = self._HANDLERS[handler](self, *args) - except Exception: + except: # TODO: revist how to catch handle locally, this should simplify when py2 is dropped # need to catch old style exceptions too t, v, tb = sys.exc_info() self._last_traceback = tb @@ -347,16 +344,24 @@ instantiate_custom_exceptions=self._config["instantiate_custom_exceptions"], instantiate_oldstyle_exceptions=self._config["instantiate_oldstyle_exceptions"]) + def _seq_request_callback(self, msg, seq, is_exc, obj): + _callback = self._request_callbacks.pop(seq, None) + if _callback is not None: + _callback(is_exc, obj) + elif self._config["logger"] is not None: + debug_msg = 'Recieved {} seq {} and a related request callback did not exist' + self._config["logger"].debug(debug_msg.format(msg, seq)) + def _dispatch(self, data): # serving---dispatch? msg, seq, args = brine.load(data) if msg == consts.MSG_REQUEST: self._dispatch_request(seq, args) elif msg == consts.MSG_REPLY: obj = self._unbox(args) - self._request_callbacks.pop(seq)(False, obj) + self._seq_request_callback(msg, seq, False, obj) elif msg == consts.MSG_EXCEPTION: obj = self._unbox_exc(args) - self._request_callbacks.pop(seq)(True, obj) + self._seq_request_callback(msg, seq, True, obj) else: raise ValueError("invalid message type: %r" % (msg,)) @@ -410,8 +415,14 @@ self.close() def serve_threaded(self, thread_count=10): # serving - """Serves all requests and replies for as long as the connection is - alive.""" + """Serves all requests and replies for as long as the connection is alive. + + CAVEAT: using non-immutable types that require a netref to be constructed to serve a request, + or invoking anything else that performs a sync_request, may timeout due to the sync_request reply being + received by another thread serving the connection. A more conventional approach where each client thread + opens a new connection would allow `ThreadedServer` to naturally avoid such multiplexing issues and + is the preferred approach for threading procedures that invoke sync_request. See issue #345 + """ def _thread_target(): try: while True: @@ -463,6 +474,9 @@ try: self._send(consts.MSG_REQUEST, seq, (handler, self._box(args))) except Exception: + # TODO: review test_remote_exception, logging exceptions show attempt to write on closed stream + # depending on the case, the MSG_REQUEST may or may not have been sent completely + # so, pop the callback and raise to keep response integrity is consistent self._request_callbacks.pop(seq, None) raise @@ -507,7 +521,7 @@ raise AttributeError("cannot access %r" % (name,)) def _access_attr(self, obj, name, args, overrider, param, default): # attribute access - if is_py3k: + if is_py_3k: if type(name) is bytes: name = str(name, "utf8") elif type(name) is not str: @@ -566,12 +580,11 @@ return str(obj) def _handle_cmp(self, obj, other, op='__cmp__'): # request handler - # cmp() might enter recursive resonance... yet another workaround - # return cmp(obj, other) + # cmp() might enter recursive resonance... so use the underlying type and return cmp(obj, other) try: - return getattr(type(obj), op)(obj, other) - except (AttributeError, TypeError): - return NotImplemented + return self._access_attr(type(obj), op, (), "_rpyc_getattr", "allow_getattr", getattr)(obj, other) + except Exception: + raise def _handle_hash(self, obj): # request handler return hash(obj) @@ -583,7 +596,14 @@ return tuple(dir(obj)) def _handle_inspect(self, id_pack): # request handler - return tuple(get_methods(netref.LOCAL_ATTRS, self._local_objects[id_pack])) + if hasattr(self._local_objects[id_pack], '____conn__'): + # When RPyC is chained (RPyC over RPyC), id_pack is cached in local objects as a netref + # since __mro__ is not a safe attribute the request is forwarded using the proxy connection + # see issue #346 or tests.test_rpyc_over_rpyc.Test_rpyc_over_rpyc + conn = self._local_objects[id_pack].____conn__ + return conn.sync_request(consts.HANDLE_INSPECT, id_pack) + else: + return tuple(get_methods(netref.LOCAL_ATTRS, self._local_objects[id_pack])) def _handle_getattr(self, obj, name): # request handler return self._access_attr(obj, name, (), "_rpyc_getattr", "allow_getattr", getattr) @@ -609,14 +629,27 @@ return self._handle_getattr(obj, "__exit__")(exc, typ, tb) def _handle_instancecheck(self, obj, other_id_pack): + # TODOs: + # + refactor cache instancecheck/inspect/class_factory + # + improve cache docs + + if hasattr(obj, '____conn__'): # keep unwrapping! + # When RPyC is chained (RPyC over RPyC), id_pack is cached in local objects as a netref + # since __mro__ is not a safe attribute the request is forwarded using the proxy connection + # relates to issue #346 or tests.test_netref_hierachy.Test_Netref_Hierarchy.test_StandardError + conn = obj.____conn__ + return conn.sync_request(consts.HANDLE_INSPECT, id_pack) # Create a name pack which would be familiar here and see if there is a hit other_id_pack2 = (other_id_pack[0], other_id_pack[1], 0) - if other_id_pack2 in self._netref_classes_cache: + if other_id_pack[0] in netref.builtin_classes_cache: + cls = netref.builtin_classes_cache[other_id_pack[0]] + other = cls(self, other_id_pack) + elif other_id_pack2 in self._netref_classes_cache: cls = self._netref_classes_cache[other_id_pack2] other = cls(self, other_id_pack2) - return isinstance(other, obj) else: # might just have missed cache, FIX ME return False + return isinstance(other, obj) def _handle_pickle(self, obj, proto): # request handler if not self._config["allow_pickle"]: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/core/service.py new/rpyc-4.1.4/rpyc/core/service.py --- old/rpyc-4.1.1/rpyc/core/service.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/core/service.py 2020-01-30 07:26:06.000000000 +0100 @@ -9,7 +9,7 @@ from functools import partial from rpyc.lib import hybridmethod -from rpyc.lib.compat import execute, is_py3k +from rpyc.lib.compat import execute, is_py_3k from rpyc.core.protocol import Connection @@ -215,7 +215,7 @@ @staticmethod def _install(conn, slave): modules = ModuleNamespace(slave.getmodule) - builtin = modules.builtins if is_py3k else modules.__builtin__ + builtin = modules.builtins if is_py_3k else modules.__builtin__ conn.modules = modules conn.eval = slave.eval conn.execute = slave.execute diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/core/vinegar.py new/rpyc-4.1.4/rpyc/core/vinegar.py --- old/rpyc-4.1.1/rpyc/core/vinegar.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/core/vinegar.py 2020-01-30 07:26:06.000000000 +0100 @@ -22,7 +22,7 @@ from rpyc.core import brine from rpyc.core import consts from rpyc import version -from rpyc.lib.compat import is_py3k +from rpyc.lib.compat import is_py_3k REMOTE_LINE_START = "\n\n========= Remote Traceback " @@ -135,7 +135,7 @@ else: cls = None - if is_py3k: + if is_py_3k: if not isinstance(cls, type) or not issubclass(cls, BaseException): cls = None else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/lib/__init__.py new/rpyc-4.1.4/rpyc/lib/__init__.py --- old/rpyc-4.1.1/rpyc/lib/__init__.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/lib/__init__.py 2020-01-30 07:26:06.000000000 +0100 @@ -151,8 +151,39 @@ def get_id_pack(obj): - """introspects the given (local) object, returns id_pack as expected by BaseNetref""" - if not inspect.isclass(obj): + """introspects the given "local" object, returns id_pack as expected by BaseNetref + + The given object is "local" in the sense that it is from the local cache. Any object in the local cache exists + in the current address space or is a netref. A netref in the local cache could be from a chained-connection. + To handle type related behavior properly, the attribute `__class__` is a descriptor for netrefs. + + So, check thy assumptions regarding the given object when creating `id_pack`. + """ + if hasattr(obj, '____id_pack__'): + # netrefs are handled first since __class__ is a descriptor + return obj.____id_pack__ + elif inspect.ismodule(obj) or getattr(obj, '__name__', None) == 'module': + # TODO: not sure about this, need to enumerate cases in units + if isinstance(obj, type): # module + obj_cls = type(obj) + name_pack = '{0}.{1}'.format(obj_cls.__module__, obj_cls.__name__) + return (name_pack, id(type(obj)), id(obj)) + else: + if inspect.ismodule(obj) and obj.__name__ != 'module': + if obj.__name__ in sys.modules: + name_pack = obj.__name__ + else: + name_pack = '{0}.{1}'.format(obj.__class__.__module__, obj.__name__) + elif inspect.ismodule(obj): + name_pack = '{0}.{1}'.format(obj__module__, obj.__name__) + print(name_pack) + elif hasattr(obj, '__module__'): + name_pack = '{0}.{1}'.format(obj.__module__, obj.__name__) + else: + obj_cls = type(obj) + name_pack = '{0}'.format(obj.__name__) + return (name_pack, id(type(obj)), id(obj)) + elif not inspect.isclass(obj): name_pack = '{0}.{1}'.format(obj.__class__.__module__, obj.__class__.__name__) return (name_pack, id(type(obj)), id(obj)) else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/lib/compat.py new/rpyc-4.1.4/rpyc/lib/compat.py --- old/rpyc-4.1.1/rpyc/lib/compat.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/lib/compat.py 2020-01-30 07:26:06.000000000 +0100 @@ -5,9 +5,11 @@ import sys import time -is_py3k = (sys.version_info[0] >= 3) +is_py_3k = (sys.version_info[0] >= 3) +is_py_gte38 = is_py_3k and (sys.version_info[1] >= 8) -if is_py3k: + +if is_py_3k: exec("execute = exec") def BYTES_LITERAL(text): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/utils/classic.py new/rpyc-4.1.4/rpyc/utils/classic.py --- old/rpyc-4.1.1/rpyc/utils/classic.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/utils/classic.py 2020-01-30 07:26:06.000000000 +0100 @@ -2,7 +2,7 @@ import sys import os import inspect -from rpyc.lib.compat import pickle, execute, is_py3k # noqa: F401 +from rpyc.lib.compat import pickle, execute, is_py_3k # noqa: F401 from rpyc.core.service import ClassicService, Slave from rpyc.utils import factory from rpyc.core.service import ModuleNamespace # noqa: F401 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/utils/teleportation.py new/rpyc-4.1.4/rpyc/utils/teleportation.py --- old/rpyc-4.1.1/rpyc/utils/teleportation.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/utils/teleportation.py 2020-01-30 07:26:06.000000000 +0100 @@ -4,7 +4,7 @@ import __builtin__ except ImportError: import builtins as __builtin__ # noqa: F401 -from rpyc.lib.compat import is_py3k +from rpyc.lib.compat import is_py_3k, is_py_gte38 from types import CodeType, FunctionType from rpyc.core import brine from rpyc.core import netref @@ -39,7 +39,7 @@ def decode_codeobj(codeobj): # adapted from dis.dis - if is_py3k: + if is_py_3k: codestr = codeobj.co_code else: codestr = [ord(ch) for ch in codeobj.co_code] @@ -75,7 +75,13 @@ else: raise TypeError("Cannot export a function with non-brinable constants: %r" % (const,)) - if is_py3k: + if is_py_gte38: + # Constructor was changed in 3.8 to support "advanced" programming styles + exported = (cobj.co_argcount, cobj.co_posonlyargcount, cobj.co_kwonlyargcount, cobj.co_nlocals, + cobj.co_stacksize, cobj.co_flags, cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, + cobj.co_filename, cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, + cobj.co_cellvars) + elif is_py_3k: exported = (cobj.co_argcount, cobj.co_kwonlyargcount, cobj.co_nlocals, cobj.co_stacksize, cobj.co_flags, cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, cobj.co_filename, cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, cobj.co_cellvars) @@ -89,7 +95,7 @@ def export_function(func): - if is_py3k: + if is_py_3k: func_closure = func.__closure__ func_code = func.__code__ func_defaults = func.__defaults__ @@ -107,12 +113,18 @@ def _import_codetup(codetup): - if is_py3k: - (argcnt, kwargcnt, nloc, stk, flg, codestr, consts, names, varnames, filename, name, - firstlineno, lnotab, freevars, cellvars) = codetup + if is_py_3k: + # Handle tuples sent from 3.8 as well as 3 < version < 3.8. + if len(codetup) == 16: + (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, + filename, name, firstlineno, lnotab, freevars, cellvars) = codetup + else: + (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, + filename, name, firstlineno, lnotab, freevars, cellvars) = codetup + posonlyargcount = 0 else: - (argcnt, nloc, stk, flg, codestr, consts, names, varnames, filename, name, - firstlineno, lnotab, freevars, cellvars) = codetup + (argcount, nlocals, stacksize, flags, code, consts, names, varnames, + filename, name, firstlineno, lnotab, freevars, cellvars) = codetup consts2 = [] for const in consts: @@ -120,13 +132,17 @@ consts2.append(_import_codetup(const[1])) else: consts2.append(const) - - if is_py3k: - return CodeType(argcnt, kwargcnt, nloc, stk, flg, codestr, tuple(consts2), names, varnames, filename, name, - firstlineno, lnotab, freevars, cellvars) + consts = tuple(consts2) + if is_py_gte38: + codetup = (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, + filename, name, firstlineno, lnotab, freevars, cellvars) + elif is_py_3k: + codetup = (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, filename, name, + firstlineno, lnotab, freevars, cellvars) else: - return CodeType(argcnt, nloc, stk, flg, codestr, tuple(consts2), names, varnames, filename, name, - firstlineno, lnotab, freevars, cellvars) + codetup = (argcount, nlocals, stacksize, flags, code, consts, names, varnames, filename, name, firstlineno, + lnotab, freevars, cellvars) + return CodeType(*codetup) def import_function(functup, globals=None, def_=True): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/utils/zerodeploy.py new/rpyc-4.1.4/rpyc/utils/zerodeploy.py --- old/rpyc-4.1.1/rpyc/utils/zerodeploy.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/utils/zerodeploy.py 2020-01-30 07:26:06.000000000 +0100 @@ -13,6 +13,7 @@ import rpyc.utils.classic try: from plumbum import local, ProcessExecutionError, CommandNotFound + from plumbum.commands.base import BoundCommand from plumbum.path import copy except ImportError: import inspect @@ -102,7 +103,9 @@ modname, clsname = server_class.rsplit(".", 1) script.write(SERVER_SCRIPT.replace("$MODULE$", modname).replace( "$SERVER$", clsname).replace("$EXTRA_SETUP$", extra_setup)) - if python_executable: + if isinstance(python_executable, BoundCommand): + cmd = python_executable + elif python_executable: cmd = remote_machine[python_executable] else: major = sys.version_info[0] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/rpyc/version.py new/rpyc-4.1.4/rpyc/version.py --- old/rpyc-4.1.1/rpyc/version.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/rpyc/version.py 2020-01-30 07:26:06.000000000 +0100 @@ -1,3 +1,3 @@ -version = (4, 1, 1) +version = (4, 1, 4) version_string = ".".join(map(str, version)) -release_date = "2019.05.25" +release_date = "2020.1.30" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/setup.py new/rpyc-4.1.4/setup.py --- old/rpyc-4.1.1/setup.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/setup.py 2020-01-30 07:26:06.000000000 +0100 @@ -33,7 +33,7 @@ os.path.join("bin", "rpyc_classic.py"), os.path.join("bin", "rpyc_registry.py"), ], - tests_require=['nose'], + tests_require=[], test_suite='nose.collector', install_requires=["plumbum"], # entry_points = dict( @@ -52,14 +52,12 @@ "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "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", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Object Brokering", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_attr_access.py new/rpyc-4.1.4/tests/test_attr_access.py --- old/rpyc-4.1.1/tests/test_attr_access.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_attr_access.py 2020-01-30 07:26:06.000000000 +0100 @@ -89,7 +89,7 @@ class TestRestricted(unittest.TestCase): def setUp(self): - self.server = ThreadedServer(MyService, port=0) + self.server = ThreadedServer(MyService) self.thd = self.server._start_in_thread() self.conn = rpyc.connect("localhost", self.server.port) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_brine.py new/rpyc-4.1.4/tests/test_brine.py --- old/rpyc-4.1.1/tests/test_brine.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_brine.py 2020-01-30 07:26:06.000000000 +0100 @@ -1,11 +1,11 @@ from rpyc.core import brine -from rpyc.lib.compat import is_py3k +from rpyc.lib.compat import is_py_3k import unittest class BrineTest(unittest.TestCase): def test_brine_2(self): - if is_py3k: + if is_py_3k: exec('''x = (b"he", 7, "llo", 8, (), 900, None, True, Ellipsis, 18.2, 18.2j + 13, slice(1, 2, 3), frozenset([5, 6, 7]), NotImplemented, (1,2))''', globals()) else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_classic.py new/rpyc-4.1.4/tests/test_classic.py --- old/rpyc-4.1.1/tests/test_classic.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_classic.py 2020-01-30 07:26:06.000000000 +0100 @@ -37,20 +37,10 @@ self.assertEqual(list(bi), list(range(10000))) def test_classic(self): - print(self.conn.modules.sys) - print(self.conn.modules["xml.dom.minidom"].parseString("<a/>")) self.conn.execute("x = 5") self.assertEqual(self.conn.namespace["x"], 5) self.assertEqual(self.conn.eval("1+x"), 6) - def test_isinstance(self): - x = self.conn.builtin.list((1, 2, 3, 4)) - print(self.conn.builtin.list, type(self.conn.builtin.list)) # <class 'list'> <netref class 'builtins.type'> - print(x, type(x)) # [1, 2, 3, 4] <netref class 'builtins.list'> - print(x.__class__, type(x.__class__)) # <class 'list'> <class 'type'> - self.assertTrue(isinstance(x, list)) - self.assertTrue(isinstance(x, rpyc.BaseNetref)) - def test_mock_connection(self): from rpyc.utils.classic import MockClassicConnection import sys diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_deploy.py new/rpyc-4.1.4/tests/test_deploy.py --- old/rpyc-4.1.1/tests/test_deploy.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_deploy.py 2020-01-30 07:26:06.000000000 +0100 @@ -2,9 +2,16 @@ import unittest import sys from plumbum import SshMachine +from plumbum.machines.paramiko_machine import ParamikoMachine from rpyc.utils.zerodeploy import DeployedServer +try: + import paramiko # noqa + _paramiko_import_failed = False +except Exception: + _paramiko_import_failed = True +@unittest.skipIf(_paramiko_import_failed, "Paramiko is not available") class TestDeploy(unittest.TestCase): def test_deploy(self): rem = SshMachine("localhost") @@ -23,12 +30,6 @@ self.fail("expected an EOFError") def test_deploy_paramiko(self): - try: - import paramiko # @UnusedImport - except Exception: - self.skipTest("Paramiko is not available") - from plumbum.machines.paramiko_machine import ParamikoMachine - rem = ParamikoMachine("localhost", missing_host_policy=paramiko.AutoAddPolicy()) with DeployedServer(rem) as dep: conn = dep.classic_connect() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_get_id_pack.py new/rpyc-4.1.4/tests/test_get_id_pack.py --- old/rpyc-4.1.1/tests/test_get_id_pack.py 1970-01-01 01:00:00.000000000 +0100 +++ new/rpyc-4.1.4/tests/test_get_id_pack.py 2020-01-30 07:26:06.000000000 +0100 @@ -0,0 +1,44 @@ +import rpyc +from rpyc.utils.server import ThreadedServer +from rpyc import SlaveService +import unittest + + +class Test_get_id_pack(unittest.TestCase): + + def setUp(self): + self.port = 18878 + self.port2 = 18879 + self.server = ThreadedServer(SlaveService, port=self.port, auto_register=False) + self.server2 = ThreadedServer(SlaveService, port=self.port2, auto_register=False) + self.server._start_in_thread() + self.server2._start_in_thread() + self.conn = rpyc.classic.connect("localhost", port=self.port) + self.conn_rpyc = self.conn.root.getmodule('rpyc') + self.chained_conn = self.conn_rpyc.connect('localhost', self.port2) + + def tearDown(self): + self.chained_conn.close() + self.conn.close() + self.server.close() + self.server2.close() + + def test_netref(self): + self.assertEquals(self.conn.root.____id_pack__, rpyc.lib.get_id_pack(self.conn.root)) + + def test_chained_connect(self): + self.chained_conn.root.getmodule('os') + + def test_class_instance_wo_name(self): + ss = rpyc.SlaveService() + id_pack = rpyc.lib.get_id_pack(ss) + self.assertEqual('rpyc.core.service.SlaveService', id_pack[0]) + + def test_class_wo_name(self): + ss = rpyc.SlaveService + id_pack = rpyc.lib.get_id_pack(ss) + self.assertEqual('rpyc.core.service.SlaveService', id_pack[0]) + + +if __name__ == "__main__": + unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_gevent_server.py new/rpyc-4.1.4/tests/test_gevent_server.py --- old/rpyc-4.1.1/tests/test_gevent_server.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_gevent_server.py 2020-01-30 07:26:06.000000000 +0100 @@ -3,14 +3,19 @@ from rpyc.utils.server import GeventServer import time import rpyc -import gevent -from gevent import monkey -monkey.patch_all() +try: + import gevent + _gevent_import_failed = False +except Exception: + _gevent_import_failed = True +@unittest.skipIf(_gevent_import_failed, "Gevent is not available") class Test_GeventServer(unittest.TestCase): def setUp(self): + from gevent import monkey + monkey.patch_all() self.server = GeventServer(SlaveService, port=18878, auto_register=False) self.server.logger.quiet = False self.server._listen() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_ipv6.py new/rpyc-4.1.4/tests/test_ipv6.py --- old/rpyc-4.1.1/tests/test_ipv6.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_ipv6.py 2020-01-30 07:26:06.000000000 +0100 @@ -2,12 +2,10 @@ import unittest from rpyc.utils.server import ThreadedServer from rpyc import SlaveService -from nose import SkipTest - -# travis: "Network is unreachable", https://travis-ci.org/tomerfiliba/rpyc/jobs/108231239#L450 -raise SkipTest("requires IPv6") +# travis: "Network is unreachable", https://travis-ci.org/tomerfiliba/rpyc/jobs/108231239#L450 +@unittest.skip("requires IPv6") class Test_IPv6(unittest.TestCase): def setUp(self): self.server = ThreadedServer(SlaveService, port=0, ipv6=True) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_netref_hierachy.py new/rpyc-4.1.4/tests/test_netref_hierachy.py --- old/rpyc-4.1.1/tests/test_netref_hierachy.py 1970-01-01 01:00:00.000000000 +0100 +++ new/rpyc-4.1.4/tests/test_netref_hierachy.py 2020-01-30 07:26:06.000000000 +0100 @@ -0,0 +1,126 @@ +import os +import rpyc +import tempfile +from rpyc.utils.server import ThreadedServer, ThreadPoolServer +from rpyc import SlaveService +import unittest + + +class MyMeta(type): + + def spam(self): + return self.__name__ * 5 + + +class MyClass(object): + __metaclass__ = MyMeta + + +class MyService(rpyc.Service): + on_connect_called = False + on_disconnect_called = False + + def on_connect(self, conn): + self.on_connect_called = True + + def on_disconnect(self, conn): + self.on_disconnect_called = True + + def exposed_distance(self, p1, p2): + x1, y1 = p1 + x2, y2 = p2 + return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) + + def exposed_getlist(self): + return [ + 1, 2, 3] + + def foobar(self): + assert False + + def exposed_getmeta(self): + return MyClass() + + def exposed_instance(self, inst, cls): + return isinstance(inst, cls) + + +class Test_Netref_Hierarchy(unittest.TestCase): + + def setUp(self): + self.server = ThreadedServer(SlaveService, port=18878, auto_register=False) + self.server.logger.quiet = False + self.server._start_in_thread() + + def tearDown(self): + self.server.close() + + def test_instancecheck_across_connections(self): + conn = rpyc.classic.connect('localhost', port=18878) + conn2 = rpyc.classic.connect('localhost', port=18878) + conn.execute('import test_magic') + conn2.execute('import test_magic') + foo = conn.modules.test_magic.Foo() + bar = conn.modules.test_magic.Bar() + self.assertTrue(isinstance(foo, conn.modules.test_magic.Foo)) + self.assertTrue(isinstance(bar, conn2.modules.test_magic.Bar)) + self.assertFalse(isinstance(bar, conn.modules.test_magic.Foo)) + with self.assertRaises(TypeError): + isinstance(conn.modules.test_magic.Foo, bar) + conn.close() + conn2.close() + + def test_classic(self): + conn = rpyc.classic.connect_thread() + x = conn.builtin.list((1, 2, 3, 4)) + self.assertTrue(isinstance(x, list)) + self.assertTrue(isinstance(x, rpyc.BaseNetref)) + with self.assertRaises(TypeError): + isinstance([], x) + i = 0 + self.assertTrue(type(x).__getitem__(x, i) == x.__getitem__(i)) + _builtins = conn.modules.builtins if rpyc.lib.compat.is_py_3k else conn.modules.__builtin__ + self.assertEqual(repr(_builtins.float.__class__), repr(type)) + self.assertEqual(repr(type(_builtins.float)), repr(type(_builtins.type))) + + def test_instancecheck_list(self): + service = MyService() + conn = rpyc.connect_thread(remote_service=service) + conn.root + remote_list = conn.root.getlist() + self.assertTrue(conn.root.instance(remote_list, list)) + conn.close() + + def test_StandardError(self): + conn = rpyc.classic.connect_thread() + _builtins = conn.modules.builtins if rpyc.lib.compat.is_py_3k else conn.modules.__builtin__ + self.assertTrue(isinstance(_builtins.Exception(), _builtins.BaseException)) + self.assertTrue(isinstance(_builtins.Exception(), _builtins.Exception)) + self.assertTrue(isinstance(_builtins.Exception(), BaseException)) + self.assertTrue(isinstance(_builtins.Exception(), Exception)) + + def test_modules(self): + """ + >>> type(sys) + <type 'module'> # base case + >>> type(conn.modules.sys) + <netref class 'rpyc.core.netref.__builtin__.module'> # matches base case + >>> sys.__class__ + <type 'module'> # base case + >>> conn.modules.sys.__class__ + <type 'module'> # matches base case + >>> type(sys.__class__) + <type 'type'> # base case + >>> type(conn.modules.sys.__class__) + <netref class 'rpyc.core.netref.__builtin__.module'> # doesn't match. Should be a netref class of "type" (or maybe just <type 'type'> itself?) + """ + import sys + conn = rpyc.classic.connect_thread() + self.assertEqual(repr(sys.__class__), repr(conn.modules.sys.__class__)) + # _builtin = sys.modules['builtins' if rpyc.lib.compat.is_py_3k else '__builtins__'].__name__ + # self.assertEqual(repr(type(conn.modules.sys)), "<netref class 'rpyc.core.netref.{}.module'>".format(_builtin)) + # self.assertEqual(repr(type(conn.modules.sys.__class__)), "<netref class 'rpyc.core.netref.{}.type'>".format(_builtin)) + + +if __name__ == '__main__': + unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_remoting.py new/rpyc-4.1.4/tests/test_remoting.py --- old/rpyc-4.1.1/tests/test_remoting.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_remoting.py 2020-01-30 07:26:06.000000000 +0100 @@ -2,7 +2,6 @@ import tempfile import shutil import unittest -from nose import SkipTest import rpyc @@ -32,16 +31,17 @@ shutil.rmtree(base) + @unittest.skip("TODO: upload a package and a module") def test_distribution(self): - raise SkipTest("TODO: upload a package and a module") + pass + @unittest.skip("Requires manual testing atm") def test_interactive(self): - raise SkipTest("Need to be manually") print("type Ctrl+D to exit (Ctrl+Z on Windows)") rpyc.classic.interact(self.conn) + @unittest.skip("Requires manual testing atm") def test_post_mortem(self): - raise SkipTest("Need to be manually") try: self.conn.modules.sys.path[100000] except IndexError: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_rpyc_over_rpyc.py new/rpyc-4.1.4/tests/test_rpyc_over_rpyc.py --- old/rpyc-4.1.1/tests/test_rpyc_over_rpyc.py 1970-01-01 01:00:00.000000000 +0100 +++ new/rpyc-4.1.4/tests/test_rpyc_over_rpyc.py 2020-01-30 07:26:06.000000000 +0100 @@ -0,0 +1,102 @@ +import rpyc +from rpyc.utils.server import ThreadedServer +import unittest + +CONNECT_CONFIG = {"allow_setattr": True} + + +class Fee(object): + + def __init__(self, msg="Fee"): + self._msg = msg + + @property + def exposed_msg(self): + return self._msg + + @exposed_msg.setter + def exposed_msg(self, value): + self._msg = value + + def __str__(self): + return str(self._msg) + + def __add__(self, rhs): + return self.__str__() + str(rhs) + + +class Service(rpyc.Service): + + PORT = 18878 + + def exposed_fee(self, arg): + return arg + + def exposed_fee_str(self, arg): + return str(arg) + + def exposed_foe_update(self, arg, msg): + arg.msg = arg.msg + " foe" + msg + return arg + + +class Intermediate(rpyc.Service): + + PORT = 18879 + + def exposed_fee(self, arg): + with rpyc.connect("localhost", port=Service.PORT, config=CONNECT_CONFIG) as conn: + return conn.root.fee(arg) + + def exposed_fee_str(self, arg): + with rpyc.connect("localhost", port=Service.PORT) as conn: + return conn.root.fee_str(arg) + + def exposed_fie_update(self, arg): + arg.msg = arg.msg + " fie" + with rpyc.connect("localhost", port=Service.PORT, config=CONNECT_CONFIG) as conn: + return conn.root.foe_update(arg, " foo bar") + + +class Test_rpyc_over_rpyc(unittest.TestCase): + """Issue #346 shows that exceptions are being raised when an RPyC service method + calls another RPyC service, forwarding a non-trivial (and thus given as a proxy) argument. + """ + + def setUp(self): + self.server = ThreadedServer(Service, port=Service.PORT, auto_register=False) + self.i_server = ThreadedServer(Intermediate, port=Intermediate.PORT, + auto_register=False, protocol_config=CONNECT_CONFIG) + self.server._start_in_thread() + self.i_server._start_in_thread() + self.conn = rpyc.connect("localhost", port=Intermediate.PORT, config=CONNECT_CONFIG) + + def tearDown(self): + self.conn.close() + self.server.close() + self.i_server.close() + + def test_immutable_object_return(self): + """Tests using rpyc over rpyc---issue #346 reported traceback for this use case""" + obj = Fee() + result = self.conn.root.fee_str(obj) + self.assertEqual(str(obj), "Fee", "String representation of obj should not have changed") + self.assertEqual(str(result), "Fee", "String representation of result should be the same as obj") + + def test_return_of_unmodified_parameter(self): + obj = Fee() + original_obj_id = id(obj) + result = self.conn.root.fee(obj) + self.assertEqual(str(obj), "Fee", "String representation of obj should not have changed") + self.assertEqual(id(result), original_obj_id, "Unboxing of result should be bound to the same object as obj") + + def test_return_of_modified_parameter(self): + obj = Fee() + original_obj_id = id(obj) + result = self.conn.root.fie_update(obj) + self.assertEqual(str(obj), "Fee fie foe foo bar", "String representation of obj should have changed") + self.assertEqual(id(result), original_obj_id, "Unboxing of result should be bound to the same object as obj") + + +if __name__ == "__main__": + unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_service_pickle.py new/rpyc-4.1.4/tests/test_service_pickle.py --- old/rpyc-4.1.1/tests/test_service_pickle.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_service_pickle.py 2020-01-30 07:26:06.000000000 +0100 @@ -4,13 +4,15 @@ import timeit import rpyc import unittest -from nose import SkipTest import cfg_tests try: import pandas as pd import numpy as np + _pampy_import_failed = False except Exception: - raise SkipTest("Requires pandas, numpy, and tables") + pd = None + np = None + _pampy_import_failed = True DF_ROWS = 2000 @@ -30,10 +32,14 @@ def exposed_write_data(self, dataframe): rpyc.classic.obtain(dataframe) + def exposed_get(self): + return np.random.rand(3, 3) + def exposed_ping(self): return "pong" +@unittest.skipIf(_pampy_import_failed, "Pandas & numpy are not available") class TestServicePickle(unittest.TestCase): """Issues #323 and #329 showed for large objects there is an excessive number of round trips. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_ssl.py new/rpyc-4.1.4/tests/test_ssl.py --- old/rpyc-4.1.1/tests/test_ssl.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_ssl.py 2020-01-30 07:26:06.000000000 +0100 @@ -1,17 +1,18 @@ import rpyc import os import unittest -import nose from rpyc.utils.authenticators import SSLAuthenticator from rpyc.utils.server import ThreadedServer from rpyc import SlaveService try: import ssl # noqa + _ssl_import_failed = False except ImportError: - raise nose.SkipTest("requires ssl") + _ssl_import_failed = True +@unittest.skipIf(_ssl_import_failed, "Ssl not available") class Test_SSL(unittest.TestCase): ''' created key like that diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_teleportation.py new/rpyc-4.1.4/tests/test_teleportation.py --- old/rpyc-4.1.1/tests/test_teleportation.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_teleportation.py 2020-01-30 07:26:06.000000000 +0100 @@ -3,8 +3,10 @@ import sys import os import rpyc +import types import unittest from rpyc.utils.teleportation import export_function, import_function +from rpyc.lib.compat import is_py_3k, is_py_gte38 from rpyc.utils.classic import teleport_function @@ -76,6 +78,40 @@ self.assertEqual(foo_(), 43) self.assertEqual(bar_(), 42) + def test_compat(self): # assumes func has only brineable types + + def get37_schema(cobj): + return (cobj.co_argcount, 0, cobj.co_nlocals, cobj.co_stacksize, + cobj.co_flags, cobj.co_code, cobj.co_consts, cobj.co_names, cobj.co_varnames, + cobj.co_filename, cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, + cobj.co_freevars, cobj.co_cellvars) + + def get38_schema(cobj): + return (cobj.co_argcount, 2, cobj.co_kwonlyargcount, cobj.co_nlocals, + cobj.co_stacksize, cobj.co_flags, cobj.co_code, cobj.co_consts, cobj.co_names, + cobj.co_varnames, cobj.co_filename, cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, + cobj.co_freevars, cobj.co_cellvars) + + if is_py_3k: + pow37 = lambda x, y : x ** y # noqa + pow38 = lambda x, y : x ** y # noqa + export37 = get37_schema(pow37.__code__) + export38 = get38_schema(pow38.__code__) + schema37 = (pow37.__name__, pow37.__module__, pow37.__defaults__, export37) + schema38 = (pow38.__name__, pow38.__module__, pow38.__defaults__, export38) + pow37_netref = self.conn.modules["rpyc.utils.teleportation"].import_function(schema37) + pow38_netref = self.conn.modules["rpyc.utils.teleportation"].import_function(schema38) + self.assertEquals(pow37_netref(2, 3), pow37(2, 3)) + self.assertEquals(pow38_netref(2, 3), pow38(2, 3)) + self.assertEquals(pow37_netref(x=2, y=3), pow37(x=2, y=3)) + if not is_py_gte38: + return # skip remained of tests for 3.7 + pow38.__code__ = types.CodeType(*export38) # pow38 = lambda x, y, /: x ** y + with self.assertRaises(TypeError): # show local behavior + pow38(x=2, y=3) + with self.assertRaises(TypeError): + pow38_netref(x=2, y=3) + if __name__ == "__main__": unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_threaded_server.py new/rpyc-4.1.4/tests/test_threaded_server.py --- old/rpyc-4.1.1/tests/test_threaded_server.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_threaded_server.py 2020-01-30 07:26:06.000000000 +0100 @@ -25,21 +25,6 @@ self.assertEqual(conn.eval("1+x"), 6) conn.close() - def test_instancecheck_across_connections(self): - conn = rpyc.classic.connect("localhost", port=18878) - conn2 = rpyc.classic.connect("localhost", port=18878) - conn.execute("import test_magic") - conn2.execute("import test_magic") - foo = conn.modules.test_magic.Foo() - bar = conn.modules.test_magic.Bar() - self.assertTrue(isinstance(foo, conn.modules.test_magic.Foo)) - self.assertTrue(isinstance(bar, conn2.modules.test_magic.Bar)) - self.assertFalse(isinstance(bar, conn.modules.test_magic.Foo)) - with self.assertRaises(TypeError): - isinstance(conn.modules.test_magic.Foo, bar) - conn.close() - conn2.close() - class Test_ThreadedServerOverUnixSocket(unittest.TestCase): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpyc-4.1.1/tests/test_win32pipes.py new/rpyc-4.1.4/tests/test_win32pipes.py --- old/rpyc-4.1.1/tests/test_win32pipes.py 2019-07-27 08:52:40.000000000 +0200 +++ new/rpyc-4.1.4/tests/test_win32pipes.py 2020-01-30 07:26:06.000000000 +0100 @@ -5,11 +5,8 @@ import time import unittest -from nose import SkipTest -if sys.platform != "win32": - raise SkipTest("Requires windows") - +@unittest.skipIf(sys.platform != "win32", "Requires windows") class Test_Pipes(unittest.TestCase): def test_basic_io(self): p1, p2 = PipeStream.create_pair() @@ -38,6 +35,7 @@ server_thread.join() +@unittest.skipIf(sys.platform != "win32", "Requires windows") class Test_NamedPipe(object): def setUp(self): self.pipe_server_thread = rpyc.spawn(self.pipe_server)