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 <[email protected]>
+
+- 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 <[email protected]>'
-__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 -- [email protected]
To unsubscribe, email [email protected]
List Netiquette: https://en.opensuse.org/openSUSE:Mailing_list_netiquette
List Archives:
https://lists.opensuse.org/archives/list/[email protected]