Hello community, here is the log from the commit of package python-zeroconf for openSUSE:Factory checked in at 2020-11-26 23:15:13 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-zeroconf (Old) and /work/SRC/openSUSE:Factory/.python-zeroconf.new.5913 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-zeroconf" Thu Nov 26 23:15:13 2020 rev:16 rq:850926 version:0.28.6 Changes: -------- --- /work/SRC/openSUSE:Factory/python-zeroconf/python-zeroconf.changes 2020-09-16 19:41:59.654978397 +0200 +++ /work/SRC/openSUSE:Factory/.python-zeroconf.new.5913/python-zeroconf.changes 2020-11-26 23:16:09.977073587 +0100 @@ -1,0 +2,10 @@ +Thu Nov 26 08:50:58 UTC 2020 - Dirk Mueller <dmuel...@suse.com> + +- update to 0.28.6: + * Loosened service name validation when receiving from the network this lets us handle + some real world devices previously causing errors + * Enabled ignoring duplicated messages which decreases CPU usage + * Fixed spurious AttributeError: module 'unittest' has no attribute 'mock' + * Improved cache reaper performance significantly + +------------------------------------------------------------------- Old: ---- python-zeroconf-0.28.3.tar.gz New: ---- python-zeroconf-0.28.6.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-zeroconf.spec ++++++ --- /var/tmp/diff_new_pack.JpBhew/_old 2020-11-26 23:16:10.641074103 +0100 +++ /var/tmp/diff_new_pack.JpBhew/_new 2020-11-26 23:16:10.645074106 +0100 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-zeroconf -Version: 0.28.3 +Version: 0.28.6 Release: 0 Summary: Pure Python Multicast DNS Service Discovery Library (Bonjour/Avahi compatible) License: LGPL-2.0-only ++++++ python-zeroconf-0.28.3.tar.gz -> python-zeroconf-0.28.6.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.28.3/.travis.yml new/python-zeroconf-0.28.6/.travis.yml --- old/python-zeroconf-0.28.3/.travis.yml 2020-08-31 12:57:18.000000000 +0200 +++ new/python-zeroconf-0.28.6/.travis.yml 2020-10-13 20:09:25.000000000 +0200 @@ -4,8 +4,12 @@ - "3.6" - "3.7" - "3.8" + - "3.9-dev" - "pypy3.5" - "pypy3" +matrix: + allow_failures: + - python: "3.9-dev" install: - pip install --upgrade -r requirements-dev.txt # mypy can't be installed on pypy diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.28.3/README.rst new/python-zeroconf-0.28.6/README.rst --- old/python-zeroconf-0.28.3/README.rst 2020-08-31 12:57:18.000000000 +0200 +++ new/python-zeroconf-0.28.6/README.rst 2020-10-13 20:09:25.000000000 +0200 @@ -134,10 +134,29 @@ Changelog ========= +0.28.6 +====== + +* Loosened service name validation when receiving from the network this lets us handle + some real world devices previously causing errors, thanks to J. Nick Koston. + +0.28.5 +====== + +* Enabled ignoring duplicated messages which decreases CPU usage, thanks to J. Nick Koston. +* Fixed spurious AttributeError: module 'unittest' has no attribute 'mock' in tests. + +0.28.4 +====== + +* Improved cache reaper performance significantly, thanks to J. Nick Koston. +* Added ServiceListener to __all__ as it's part of the public API, thanks to Justin Nesselrotte. + 0.28.3 ====== -* Reduced a time an internal lock is held which should eliminate deadlocks in high-traffic networks. +* Reduced a time an internal lock is held which should eliminate deadlocks in high-traffic networks, + thanks to J. Nick Koston. 0.28.2 ====== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.28.3/zeroconf/__init__.py new/python-zeroconf-0.28.6/zeroconf/__init__.py --- old/python-zeroconf-0.28.3/zeroconf/__init__.py 2020-08-31 12:57:18.000000000 +0200 +++ new/python-zeroconf-0.28.6/zeroconf/__init__.py 2020-10-13 20:09:25.000000000 +0200 @@ -35,14 +35,14 @@ import time import warnings from collections import OrderedDict -from typing import Dict, List, Optional, Sequence, Union, cast +from typing import Dict, Iterable, List, Optional, Sequence, Union, cast from typing import Any, Callable, Set, Tuple # noqa # used in type hints import ifaddr __author__ = 'Paul Scott-Murphy, William McBrine' __maintainer__ = 'Jakub Stasiak <ja...@stasiak.at>' -__version__ = '0.28.3' +__version__ = '0.28.6' __license__ = 'LGPL' @@ -51,6 +51,7 @@ "Zeroconf", "ServiceInfo", "ServiceBrowser", + "ServiceListener", "Error", "InterfaceChoice", "ServiceStateChange", @@ -178,6 +179,10 @@ _EXPIRE_STALE_TIME_PERCENT = 50 _EXPIRE_REFRESH_TIME_PERCENT = 75 +_LOCAL_TRAILER = '.local.' +_TCP_PROTOCOL_LOCAL_TRAILER = '._tcp.local.' +_NONTCP_PROTOCOL_LOCAL_TRAILER = '._udp.local.' + try: _IPPROTO_IPV6 = socket.IPPROTO_IPV6 except AttributeError: @@ -228,7 +233,7 @@ return socket.inet_pton(address_family, address) -def service_type_name(type_: str, *, allow_underscores: bool = False) -> str: +def service_type_name(type_: str, *, strict: bool = True) -> str: """ Validate a fully qualified service name, instance or subtype. [rfc6763] @@ -245,9 +250,11 @@ This is true because we are implementing mDNS and since the 'm' means multi-cast, the 'local.' domain is mandatory. - 2) local is preceded with either '_udp.' or '_tcp.' + 2) local is preceded with either '_udp.' or '_tcp.' unless + strict is False - 3) service name <sn> precedes <_tcp|_udp> + 3) service name <sn> precedes <_tcp|_udp> unless + strict is False The rules for Service Names [RFC6335] state that they may be no more than fifteen characters long (not counting the mandatory underscore), @@ -268,45 +275,64 @@ :param type_: Type, SubType or service name to validate :return: fully qualified service name (eg: _http._tcp.local.) """ - if not (type_.endswith('._tcp.local.') or type_.endswith('._udp.local.')): - raise BadTypeInNameException("Type '%s' must end with '._tcp.local.' or '._udp.local.'" % type_) - remaining = type_[: -len('._tcp.local.')].split('.') - name = remaining.pop() - if not name: - raise BadTypeInNameException("No Service name found") + if type_.endswith(_TCP_PROTOCOL_LOCAL_TRAILER) or type_.endswith(_NONTCP_PROTOCOL_LOCAL_TRAILER): + remaining = type_[: -len(_TCP_PROTOCOL_LOCAL_TRAILER)].split('.') + trailer = type_[-len(_TCP_PROTOCOL_LOCAL_TRAILER) :] + has_protocol = True + elif strict: + raise BadTypeInNameException( + "Type '%s' must end with '%s' or '%s'" + % (type_, _TCP_PROTOCOL_LOCAL_TRAILER, _NONTCP_PROTOCOL_LOCAL_TRAILER) + ) + elif type_.endswith(_LOCAL_TRAILER): + remaining = type_[: -len(_LOCAL_TRAILER)].split('.') + trailer = type_[-len(_LOCAL_TRAILER) + 1 :] + has_protocol = False + else: + raise BadTypeInNameException("Type '%s' must end with '%s'" % (type_, _LOCAL_TRAILER)) - if len(remaining) == 1 and len(remaining[0]) == 0: - raise BadTypeInNameException("Type '%s' must not start with '.'" % type_) + if strict or has_protocol: + service_name = remaining.pop() + if not service_name: + raise BadTypeInNameException("No Service name found") - if name[0] != '_': - raise BadTypeInNameException("Service name (%s) must start with '_'" % name) + if len(remaining) == 1 and len(remaining[0]) == 0: + raise BadTypeInNameException("Type '%s' must not start with '.'" % type_) - # remove leading underscore - name = name[1:] + if service_name[0] != '_': + raise BadTypeInNameException("Service name (%s) must start with '_'" % service_name) - if len(name) > 15: - raise BadTypeInNameException("Service name (%s) must be <= 15 bytes" % name) + test_service_name = service_name[1:] - if '--' in name: - raise BadTypeInNameException("Service name (%s) must not contain '--'" % name) + if len(test_service_name) > 15: + raise BadTypeInNameException("Service name (%s) must be <= 15 bytes" % test_service_name) - if '-' in (name[0], name[-1]): - raise BadTypeInNameException("Service name (%s) may not start or end with '-'" % name) + if '--' in test_service_name: + raise BadTypeInNameException("Service name (%s) must not contain '--'" % test_service_name) - if not _HAS_A_TO_Z.search(name): - raise BadTypeInNameException("Service name (%s) must contain at least one letter (eg: 'A-Z')" % name) + if '-' in (test_service_name[0], test_service_name[-1]): + raise BadTypeInNameException( + "Service name (%s) may not start or end with '-'" % test_service_name + ) - allowed_characters_re = ( - _HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE if allow_underscores else _HAS_ONLY_A_TO_Z_NUM_HYPHEN - ) + if not _HAS_A_TO_Z.search(test_service_name): + raise BadTypeInNameException( + "Service name (%s) must contain at least one letter (eg: 'A-Z')" % test_service_name + ) - if not allowed_characters_re.search(name): - raise BadTypeInNameException( - "Service name (%s) must contain only these characters: " - "A-Z, a-z, 0-9, hyphen ('-')%s" % (name, ", underscore ('_')" if allow_underscores else "") + allowed_characters_re = ( + _HAS_ONLY_A_TO_Z_NUM_HYPHEN if strict else _HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE ) + if not allowed_characters_re.search(test_service_name): + raise BadTypeInNameException( + "Service name (%s) must contain only these characters: " + "A-Z, a-z, 0-9, hyphen ('-')%s" % (test_service_name, "" if strict else ", underscore ('_')") + ) + else: + service_name = '' + if remaining and remaining[-1] == '_sub': remaining.pop() if len(remaining) == 0 or len(remaining[0]) == 0: @@ -325,7 +351,7 @@ "Ascii control character 0x00-0x1F and 0x7F illegal in '%s'" % remaining[0] ) - return '_' + name + type_[-len('._tcp.local.') :] + return service_name + trailer # Exceptions @@ -1268,10 +1294,21 @@ """Returns a list of all entries""" if not self.cache: return [] - else: - # avoid size change during iteration by copying the cache - values = list(self.cache.values()) - return list(itertools.chain.from_iterable(values)) + + # avoid size change during iteration by copying the cache + return list(itertools.chain.from_iterable(list(self.cache.values()))) + + def iterable_entries(self) -> Iterable[DNSRecord]: + """Returns an iterable of all entries. + + This function is provided to avoid copying + the entries but is not threadsafe as the + contents of the cache can change during iteration. + + Callers should trap RuntimeError and fallback + to calling entries. + """ + return itertools.chain.from_iterable(self.cache.values()) class Engine(threading.Thread): @@ -1369,6 +1406,17 @@ self.log_exception_warning('Error reading from socket %d', socket_.fileno()) return + if self.data == data: + log.debug( + 'Ignoring duplicate message received from %r:%r (socket %d) (%d bytes) as [%r]', + addr, + port, + socket_.fileno(), + len(data), + data, + ) + return + self.data = data msg = DNSIncoming(data) if msg.valid: @@ -1422,15 +1470,29 @@ self.name = "zeroconf-Reaper_%s" % (getattr(self, 'native_id', self.ident),) def run(self) -> None: + """Perodic removal of expired entries from the cache.""" while True: - self.zc.wait(10 * 1000) + with self.zc.reaper_condition: + self.zc.reaper_condition.wait(10) + if self.zc.done: return - now = current_time_millis() - for record in self.zc.cache.entries(): - if record.is_expired(now): - self.zc.update_record(now, record) - self.zc.cache.remove(record) + try: + # We try to iterate the cache without copying the whole + # cache as this can be quite an expensive operation. + self._cleanup_cache(self.zc.cache.iterable_entries()) + except RuntimeError: + # If the cache changes during iteration, we fallback + # to making a copy before iteraiton. + self._cleanup_cache(self.zc.cache.entries()) + + def _cleanup_cache(self, entries: Iterable[DNSRecord]) -> None: + """Remove expired entries from the cache.""" + now = current_time_millis() + for record in entries: + if record.is_expired(now): + self.zc.update_record(now, record) + self.zc.cache.remove(record) class Signal: @@ -1501,7 +1563,7 @@ assert handlers or listener, 'You need to specify at least one handler' self.types = set(type_ if isinstance(type_, list) else [type_]) for check_type_ in self.types: - if not check_type_.endswith(service_type_name(check_type_, allow_underscores=True)): + if not check_type_.endswith(service_type_name(check_type_, strict=False)): raise BadTypeInNameException threading.Thread.__init__(self) self.daemon = True @@ -1733,7 +1795,7 @@ # Accept both none, or one, but not both. if addresses is not None and parsed_addresses is not None: raise TypeError("addresses and parsed_addresses cannot be provided together") - if not type_.endswith(service_type_name(name, allow_underscores=True)): + if not type_.endswith(service_type_name(name, strict=False)): raise BadTypeInNameException self.type = type_ self.name = name @@ -2342,6 +2404,7 @@ self.cache = DNSCache() self.condition = threading.Condition() + self.reaper_condition = threading.Condition() # Ensure we create the lock before # we add the listener as we could get @@ -2373,6 +2436,11 @@ with self.condition: self.condition.notify_all() + def notify_reaper(self) -> None: + """Notifies reaper""" + with self.reaper_condition: + self.reaper_condition.notify_all() + def get_service_info(self, type_: str, name: str, timeout: int = 3000) -> Optional[ServiceInfo]: """Returns network's service information for a particular name and type, or None if no service matches by the timeout, @@ -2878,6 +2946,7 @@ # shutdown the rest self.notify_all() + self.notify_reaper() self.reaper.join() for s in self._respond_sockets: s.close() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.28.3/zeroconf/test.py new/python-zeroconf-0.28.6/zeroconf/test.py --- old/python-zeroconf-0.28.3/zeroconf/test.py 2020-08-31 12:57:18.000000000 +0200 +++ new/python-zeroconf-0.28.6/zeroconf/test.py 2020-10-13 20:09:25.000000000 +0200 @@ -12,6 +12,7 @@ import threading import time import unittest +import unittest.mock from threading import Event from typing import Dict, Optional # noqa # used in type hints from typing import cast @@ -656,14 +657,43 @@ for name in bad_names_to_try: self.assertRaises(r.BadTypeInNameException, self.browser.get_service_info, name, 'x.' + name) + def test_bad_local_names_for_get_service_info(self): + bad_names_to_try = ( + 'homekitdev._nothttp._tcp.local.', + 'homekitdev._http._udp.local.', + ) + for name in bad_names_to_try: + self.assertRaises( + r.BadTypeInNameException, self.browser.get_service_info, '_http._tcp.local.', name + ) + def test_good_instance_names(self): + assert r.service_type_name('.._x._tcp.local.') == '_x._tcp.local.' + assert r.service_type_name('x.sub._http._tcp.local.') == '_http._tcp.local.' + assert ( + r.service_type_name('6d86f882b90facee9170ad3439d72a4d6ee9f511._zget._http._tcp.local.') + == '_http._tcp.local.' + ) + + def test_good_instance_names_without_protocol(self): good_names_to_try = ( - '.._x._tcp.local.', - 'x.sub._http._tcp.local.', - '6d86f882b90facee9170ad3439d72a4d6ee9f511._zget._http._tcp.local.', + "Rachio-C73233.local.", + 'YeelightColorBulb-3AFD.local.', + 'YeelightTunableBulb-7220.local.', + "AlexanderHomeAssistant 74651D.local.", + 'iSmartGate-152.local.', + 'MyQ-FGA.local.', + 'lutron-02c4392a.local.', + 'WICED-hap-3E2734.local.', + 'MyHost.local.', + 'MyHost.sub.local.', ) for name in good_names_to_try: - r.service_type_name(name) + assert r.service_type_name(name, strict=False) == 'local.' + + for name in good_names_to_try: + # Raises without strict=False + self.assertRaises(r.BadTypeInNameException, r.service_type_name, name) def test_bad_types(self): bad_names_to_try = ( @@ -686,17 +716,18 @@ def test_good_service_names(self): good_names_to_try = ( - '_x._tcp.local.', - '_x._udp.local.', - '_12345-67890-abc._udp.local.', - 'x._sub._http._tcp.local.', - 'a' * 63 + '._sub._http._tcp.local.', - 'a' * 61 + u'â._sub._http._tcp.local.', + ('_x._tcp.local.', '_x._tcp.local.'), + ('_x._udp.local.', '_x._udp.local.'), + ('_12345-67890-abc._udp.local.', '_12345-67890-abc._udp.local.'), + ('x._sub._http._tcp.local.', '_http._tcp.local.'), + ('a' * 63 + '._sub._http._tcp.local.', '_http._tcp.local.'), + ('a' * 61 + u'â._sub._http._tcp.local.', '_http._tcp.local.'), ) - for name in good_names_to_try: - r.service_type_name(name) - r.service_type_name('_one_two._tcp.local.', allow_underscores=True) + for name, result in good_names_to_try: + assert r.service_type_name(name) == result + + assert r.service_type_name('_one_two._tcp.local.', strict=False) == '_one_two._tcp.local.' def test_invalid_addresses(self): type_ = "_test-srvc-type._tcp.local." @@ -884,6 +915,54 @@ assert 'a' not in cache.cache +class TestReaper(unittest.TestCase): + def test_reaper(self): + zeroconf = Zeroconf(interfaces=['127.0.0.1']) + original_entries = zeroconf.cache.entries() + record_with_10s_ttl = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 10, b'a') + record_with_1s_ttl = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 1, b'b') + zeroconf.cache.add(record_with_10s_ttl) + zeroconf.cache.add(record_with_1s_ttl) + entries_with_cache = zeroconf.cache.entries() + time.sleep(1.05) + zeroconf.notify_reaper() + time.sleep(0.05) + entries = zeroconf.cache.entries() + + try: + iterable_entries = list(zeroconf.cache.iterable_entries()) + finally: + zeroconf.close() + + assert entries != original_entries + assert entries_with_cache != original_entries + assert record_with_10s_ttl in entries + assert record_with_1s_ttl not in entries + assert record_with_10s_ttl in iterable_entries + assert record_with_1s_ttl not in iterable_entries + + def test_reaper_with_dict_change_during_iteration(self): + zeroconf = Zeroconf(interfaces=['127.0.0.1']) + original_entries = zeroconf.cache.entries() + record_with_10s_ttl = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 10, b'a') + record_with_1s_ttl = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 1, b'b') + zeroconf.cache.add(record_with_10s_ttl) + zeroconf.cache.add(record_with_1s_ttl) + entries_with_cache = zeroconf.cache.entries() + with unittest.mock.patch("zeroconf.DNSCache.iterable_entries", side_effect=RuntimeError): + time.sleep(1.05) + zeroconf.notify_reaper() + time.sleep(0.05) + + entries = zeroconf.cache.entries() + zeroconf.close() + + assert entries != original_entries + assert entries_with_cache != original_entries + assert record_with_10s_ttl in entries + assert record_with_1s_ttl not in entries + + class ServiceTypesQuery(unittest.TestCase): def test_integration_with_listener(self): _______________________________________________ openSUSE Commits mailing list -- commit@lists.opensuse.org To unsubscribe, email commit-le...@lists.opensuse.org List Netiquette: https://en.opensuse.org/openSUSE:Mailing_list_netiquette List Archives: https://lists.opensuse.org/archives/list/commit@lists.opensuse.org