Hello community,
here is the log from the commit of package python-zeroconf for openSUSE:Factory
checked in at 2020-09-16 19:41:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-zeroconf (Old)
and /work/SRC/openSUSE:Factory/.python-zeroconf.new.4249 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-zeroconf"
Wed Sep 16 19:41:40 2020 rev:15 rq:834888 version:0.28.3
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-zeroconf/python-zeroconf.changes
2020-07-21 15:54:22.188586543 +0200
+++
/work/SRC/openSUSE:Factory/.python-zeroconf.new.4249/python-zeroconf.changes
2020-09-16 19:41:59.654978397 +0200
@@ -1,0 +2,9 @@
+Wed Sep 16 11:22:18 UTC 2020 - Dirk Mueller <[email protected]>
+
+- update to 0.28.3:
+ * Reduced a time an internal lock is held which should eliminate deadlocks
in high-traffic networks.
+ * Stopped asking questions we already have answers for in cache, thanks to
Paul Daumlechner.
+ * Removed initial delay before querying for service info, thanks to Erik
Montnemery.
+ * Fixed a resource leak connected to using ServiceBrowser with multiple types
+
+-------------------------------------------------------------------
Old:
----
python-zeroconf-0.28.0.tar.gz
New:
----
python-zeroconf-0.28.3.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-zeroconf.spec ++++++
--- /var/tmp/diff_new_pack.rqssYz/_old 2020-09-16 19:42:00.306979130 +0200
+++ /var/tmp/diff_new_pack.rqssYz/_new 2020-09-16 19:42:00.310979135 +0200
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%define skip_python2 1
Name: python-zeroconf
-Version: 0.28.0
+Version: 0.28.3
Release: 0
Summary: Pure Python Multicast DNS Service Discovery Library
(Bonjour/Avahi compatible)
License: LGPL-2.0-only
++++++ python-zeroconf-0.28.0.tar.gz -> python-zeroconf-0.28.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.28.0/.gitignore
new/python-zeroconf-0.28.3/.gitignore
--- old/python-zeroconf-0.28.0/.gitignore 2020-07-07 13:22:12.000000000
+0200
+++ new/python-zeroconf-0.28.3/.gitignore 2020-08-31 12:57:18.000000000
+0200
@@ -12,3 +12,5 @@
.mypy_cache/
docs/_build/
.vscode
+/dist/
+/zeroconf.egg-info/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.28.0/README.rst
new/python-zeroconf-0.28.3/README.rst
--- old/python-zeroconf-0.28.0/README.rst 2020-07-07 13:22:12.000000000
+0200
+++ new/python-zeroconf-0.28.3/README.rst 2020-08-31 12:57:18.000000000
+0200
@@ -134,6 +134,23 @@
Changelog
=========
+0.28.3
+======
+
+* Reduced a time an internal lock is held which should eliminate deadlocks in
high-traffic networks.
+
+0.28.2
+======
+
+* Stopped asking questions we already have answers for in cache, thanks to
Paul Daumlechner.
+* Removed initial delay before querying for service info, thanks to Erik
Montnemery.
+
+0.28.1
+======
+
+* Fixed a resource leak connected to using ServiceBrowser with multiple types,
thanks to
+ J. Nick Koston.
+
0.28.0
======
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.28.0/zeroconf/__init__.py
new/python-zeroconf-0.28.3/zeroconf/__init__.py
--- old/python-zeroconf-0.28.0/zeroconf/__init__.py 2020-07-07
13:22:12.000000000 +0200
+++ new/python-zeroconf-0.28.3/zeroconf/__init__.py 2020-08-31
12:57:18.000000000 +0200
@@ -42,7 +42,7 @@
__author__ = 'Paul Scott-Murphy, William McBrine'
__maintainer__ = 'Jakub Stasiak <[email protected]>'
-__version__ = '0.28.0'
+__version__ = '0.28.3'
__license__ = 'LGPL'
@@ -174,6 +174,10 @@
_HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE = re.compile(r'^[A-Za-z0-9\-\_]+$')
_HAS_ASCII_CONTROL_CHARS = re.compile(r'[\x00-\x1f\x7f]')
+_EXPIRE_FULL_TIME_PERCENT = 100
+_EXPIRE_STALE_TIME_PERCENT = 50
+_EXPIRE_REFRESH_TIME_PERCENT = 75
+
try:
_IPPROTO_IPV6 = socket.IPPROTO_IPV6
except AttributeError:
@@ -459,8 +463,8 @@
DNSEntry.__init__(self, name, type_, class_)
self.ttl = ttl
self.created = current_time_millis()
- self._expiration_time = self.get_expiration_time(100)
- self._stale_time = self.get_expiration_time(50)
+ self._expiration_time =
self.get_expiration_time(_EXPIRE_FULL_TIME_PERCENT)
+ self._stale_time = self.get_expiration_time(_EXPIRE_STALE_TIME_PERCENT)
def __eq__(self, other: Any) -> bool:
"""Abstract method"""
@@ -506,8 +510,8 @@
another record."""
self.created = other.created
self.ttl = other.ttl
- self._expiration_time = self.get_expiration_time(100)
- self._stale_time = self.get_expiration_time(50)
+ self._expiration_time =
self.get_expiration_time(_EXPIRE_FULL_TIME_PERCENT)
+ self._stale_time = self.get_expiration_time(_EXPIRE_STALE_TIME_PERCENT)
def write(self, out: 'DNSOutgoing') -> None:
"""Abstract method"""
@@ -934,7 +938,7 @@
self.authorities.append(record)
def add_additional_answer(self, record: DNSRecord) -> None:
- """ Adds an additional answer
+ """Adds an additional answer
From: RFC 6763, DNS-Based Service Discovery, February 2013
@@ -1132,7 +1136,7 @@
or less in length, except for the case of a single answer which
will be written out to a single oversized packet no more than
_MAX_MSG_ABSOLUTE in length (and hence will be subject to IP
- fragmentation potentially). """
+ fragmentation potentially)."""
if self.state == self.State.finished:
return self.packets_data
@@ -1609,7 +1613,7 @@
enqueue_callback(ServiceStateChange.Removed, record.name,
record.alias)
return
- expires = record.get_expiration_time(75)
+ expires = record.get_expiration_time(_EXPIRE_REFRESH_TIME_PERCENT)
if expires < self._next_time[record.name]:
self._next_time[record.name] = expires
@@ -1649,8 +1653,8 @@
self.join()
def run(self) -> None:
- for type_ in self.types:
- self.zc.add_listener(self, DNSQuestion(type_, _TYPE_PTR,
_CLASS_IN))
+ questions = [DNSQuestion(type_, _TYPE_PTR, _CLASS_IN) for type_ in
self.types]
+ self.zc.add_listener(self, questions)
while True:
now = current_time_millis()
@@ -1890,7 +1894,7 @@
"""
now = current_time_millis()
delay = _LISTENER_TIME
- next_ = now + delay
+ next_ = now
last = now + timeout
record_types_for_check_cache = [(_TYPE_SRV, _CLASS_IN), (_TYPE_TXT,
_CLASS_IN)]
@@ -1912,19 +1916,24 @@
return False
if next_ <= now:
out = DNSOutgoing(_FLAGS_QR_QUERY)
- out.add_question(DNSQuestion(self.name, _TYPE_SRV,
_CLASS_IN))
- out.add_answer_at_time(zc.cache.get_by_details(self.name,
_TYPE_SRV, _CLASS_IN), now)
-
- out.add_question(DNSQuestion(self.name, _TYPE_TXT,
_CLASS_IN))
- out.add_answer_at_time(zc.cache.get_by_details(self.name,
_TYPE_TXT, _CLASS_IN), now)
+ cached_entry = zc.cache.get_by_details(self.name,
_TYPE_SRV, _CLASS_IN)
+ if not cached_entry:
+ out.add_question(DNSQuestion(self.name, _TYPE_SRV,
_CLASS_IN))
+ out.add_answer_at_time(cached_entry, now)
+ cached_entry = zc.cache.get_by_details(self.name,
_TYPE_TXT, _CLASS_IN)
+ if not cached_entry:
+ out.add_question(DNSQuestion(self.name, _TYPE_TXT,
_CLASS_IN))
+ out.add_answer_at_time(cached_entry, now)
if self.server is not None:
- out.add_question(DNSQuestion(self.server, _TYPE_A,
_CLASS_IN))
-
out.add_answer_at_time(zc.cache.get_by_details(self.server, _TYPE_A,
_CLASS_IN), now)
- out.add_question(DNSQuestion(self.server, _TYPE_AAAA,
_CLASS_IN))
- out.add_answer_at_time(
- zc.cache.get_by_details(self.server, _TYPE_AAAA,
_CLASS_IN), now
- )
+ cached_entry = zc.cache.get_by_details(self.server,
_TYPE_A, _CLASS_IN)
+ if not cached_entry:
+ out.add_question(DNSQuestion(self.server, _TYPE_A,
_CLASS_IN))
+ out.add_answer_at_time(cached_entry, now)
+ cached_entry = zc.cache.get_by_details(self.name,
_TYPE_AAAA, _CLASS_IN)
+ if not cached_entry:
+ out.add_question(DNSQuestion(self.server,
_TYPE_AAAA, _CLASS_IN))
+ out.add_answer_at_time(cached_entry, now)
zc.send(out)
next_ = now + delay
delay *= 2
@@ -2595,16 +2604,20 @@
i += 1
next_time += _CHECK_TIME
- def add_listener(self, listener: RecordUpdateListener, question:
Optional[DNSQuestion]) -> None:
+ def add_listener(
+ self, listener: RecordUpdateListener, question:
Optional[Union[DNSQuestion, List[DNSQuestion]]]
+ ) -> None:
"""Adds a listener for a given question. The listener will have
its update_record method called when information is available to
- answer the question."""
+ answer the question(s)."""
now = current_time_millis()
self.listeners.append(listener)
if question is not None:
- for record in self.cache.entries_with_name(question.name):
- if question.answered_by(record) and not record.is_expired(now):
- listener.update_record(self, now, record)
+ questions = [question] if isinstance(question, DNSQuestion) else
question
+ for single_question in questions:
+ for record in
self.cache.entries_with_name(single_question.name):
+ if single_question.answered_by(record) and not
record.is_expired(now):
+ listener.update_record(self, now, record)
self.notify_all()
def remove_listener(self, listener: RecordUpdateListener) -> None:
@@ -2625,45 +2638,52 @@
def handle_response(self, msg: DNSIncoming) -> None:
"""Deal with incoming response packets. All answers
are held in the cache, and listeners are notified."""
+ updates = [] # type: List[Tuple[float, DNSRecord,
Optional[DNSRecord]]]
+ now = current_time_millis()
+ for record in msg.answers:
- with self._handlers_lock:
+ updated = True
- now = current_time_millis()
- for record in msg.answers:
+ if record.unique: #
https://tools.ietf.org/html/rfc6762#section-10.2
+ # Since the cache format is keyed on the lower case record name
+ # we can avoid iterating everything in the cache and
+ # only look though entries for the specific name.
+ # entries_with_name will take care of converting to lowercase
+ #
+ # We make a copy of the list that entries_with_name returns
+ # since we cannot iterate over something we might remove
+ for entry in self.cache.entries_with_name(record.name).copy():
+
+ if entry == record:
+ updated = False
+
+ # Check the time first because it is far cheaper
+ # than the __eq__
+ if (record.created - entry.created > 1000) and
DNSEntry.__eq__(entry, record):
+ self.cache.remove(entry)
- updated = True
+ expired = record.is_expired(now)
+ maybe_entry = self.cache.get(record)
+ if not expired:
+ if maybe_entry is not None:
+ maybe_entry.reset_ttl(record)
+ else:
+ self.cache.add(record)
+ if updated:
+ updates.append((now, record, None))
+ elif maybe_entry is not None:
+ updates.append((now, record, maybe_entry))
- if record.unique: #
https://tools.ietf.org/html/rfc6762#section-10.2
- # Since the cache format is keyed on the lower case record
name
- # we can avoid iterating everything in the cache and
- # only look though entries for the specific name.
- # entries_with_name will take care of converting to
lowercase
- #
- # We make a copy of the list that entries_with_name returns
- # since we cannot iterate over something we might remove
- for entry in
self.cache.entries_with_name(record.name).copy():
-
- if entry == record:
- updated = False
-
- # Check the time first because it is far cheaper
- # than the __eq__
- if (record.created - entry.created > 1000) and
DNSEntry.__eq__(entry, record):
- self.cache.remove(entry)
+ if not updates:
+ return
- expired = record.is_expired(now)
- maybe_entry = self.cache.get(record)
- if not expired:
- if maybe_entry is not None:
- maybe_entry.reset_ttl(record)
- else:
- self.cache.add(record)
- if updated:
- self.update_record(now, record)
- else:
- if maybe_entry is not None:
- self.update_record(now, record)
- self.cache.remove(maybe_entry)
+ # Only hold the lock if we have updates
+ with self._handlers_lock:
+ for update in updates:
+ now, record, entry_to_remove = update
+ self.update_record(update[0], update[1])
+ if entry_to_remove:
+ self.cache.remove(entry_to_remove)
def handle_query(self, msg: DNSIncoming, addr: Optional[str], port: int)
-> None:
"""Deal with incoming query packets. Provides a response if
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.28.0/zeroconf/test.py
new/python-zeroconf-0.28.3/zeroconf/test.py
--- old/python-zeroconf-0.28.0/zeroconf/test.py 2020-07-07 13:22:12.000000000
+0200
+++ new/python-zeroconf-0.28.3/zeroconf/test.py 2020-08-31 12:57:18.000000000
+0200
@@ -9,6 +9,7 @@
import os
import socket
import struct
+import threading
import time
import unittest
from threading import Event
@@ -24,6 +25,7 @@
ServiceStateChange,
Zeroconf,
ZeroconfServiceTypes,
+ _EXPIRE_REFRESH_TIME_PERCENT,
)
log = logging.getLogger('zeroconf')
@@ -108,7 +110,14 @@
name = "xxxyyy"
registration_name = "%s.%s" % (name, type_)
info = ServiceInfo(
- type_, registration_name, 80, 0, 0, b'', "ash-2.local.",
addresses=[socket.inet_aton("10.0.1.2")],
+ type_,
+ registration_name,
+ 80,
+ 0,
+ 0,
+ b'',
+ "ash-2.local.",
+ addresses=[socket.inet_aton("10.0.1.2")],
)
assert not info != info
@@ -860,6 +869,20 @@
cache.remove(record2)
assert 'a' not in cache.cache
+ def test_cache_empty_multiple_calls_does_not_throw(self):
+ record1 = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 1, b'a')
+ record2 = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 1, b'b')
+ cache = r.DNSCache()
+ cache.add(record1)
+ cache.add(record2)
+ assert 'a' in cache.cache
+ cache.remove(record1)
+ cache.remove(record2)
+ # Ensure multiple removes does not throw
+ cache.remove(record1)
+ cache.remove(record2)
+ assert 'a' not in cache.cache
+
class ServiceTypesQuery(unittest.TestCase):
def test_integration_with_listener(self):
@@ -1237,16 +1260,242 @@
assert service_removed_count == 1
finally:
+ assert len(zeroconf.listeners) == 1
service_browser.cancel()
+ assert len(zeroconf.listeners) == 0
zeroconf.remove_all_service_listeners()
zeroconf.close()
+class TestServiceInfo(unittest.TestCase):
+ def test_get_info_partial(self):
+
+ zc = r.Zeroconf(interfaces=['127.0.0.1'])
+
+ service_name = 'name._type._tcp.local.'
+ service_type = '_type._tcp.local.'
+ service_server = 'ash-1.local.'
+ service_text = b'path=/~matt1/'
+ service_address = '10.0.1.2'
+
+ service_info = None
+ send_event = Event()
+ service_info_event = Event()
+
+ last_sent = None # type: Optional[r.DNSOutgoing]
+
+ def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
+ """Sends an outgoing packet."""
+ nonlocal last_sent
+
+ last_sent = out
+ send_event.set()
+
+ # monkey patch the zeroconf send
+ setattr(zc, "send", send)
+
+ def mock_incoming_msg(records) -> r.DNSIncoming:
+
+ generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
+
+ for record in records:
+ generated.add_answer_at_time(record, 0)
+
+ return r.DNSIncoming(generated.packet())
+
+ def get_service_info_helper(zc, type, name):
+ nonlocal service_info
+ service_info = zc.get_service_info(type, name)
+ service_info_event.set()
+
+ try:
+ ttl = 120
+ helper_thread = threading.Thread(
+ target=get_service_info_helper, args=(zc, service_type,
service_name)
+ )
+ helper_thread.start()
+ wait_time = 1
+
+ # Expext query for SRV, TXT, A, AAAA
+ send_event.wait(wait_time)
+ assert last_sent is not None
+ assert len(last_sent.questions) == 4
+ assert r.DNSQuestion(service_name, r._TYPE_SRV, r._CLASS_IN) in
last_sent.questions
+ assert r.DNSQuestion(service_name, r._TYPE_TXT, r._CLASS_IN) in
last_sent.questions
+ assert r.DNSQuestion(service_name, r._TYPE_A, r._CLASS_IN) in
last_sent.questions
+ assert r.DNSQuestion(service_name, r._TYPE_AAAA, r._CLASS_IN) in
last_sent.questions
+ assert service_info is None
+
+ # Expext query for SRV, A, AAAA
+ last_sent = None
+ send_event.clear()
+ zc.handle_response(
+ mock_incoming_msg(
+ [r.DNSText(service_name, r._TYPE_TXT, r._CLASS_IN |
r._CLASS_UNIQUE, ttl, service_text)]
+ )
+ )
+ send_event.wait(wait_time)
+ assert last_sent is not None
+ assert len(last_sent.questions) == 3
+ assert r.DNSQuestion(service_name, r._TYPE_SRV, r._CLASS_IN) in
last_sent.questions
+ assert r.DNSQuestion(service_name, r._TYPE_A, r._CLASS_IN) in
last_sent.questions
+ assert r.DNSQuestion(service_name, r._TYPE_AAAA, r._CLASS_IN) in
last_sent.questions
+ assert service_info is None
+
+ # Expext query for A, AAAA
+ last_sent = None
+ send_event.clear()
+ zc.handle_response(
+ mock_incoming_msg(
+ [
+ r.DNSService(
+ service_name,
+ r._TYPE_SRV,
+ r._CLASS_IN | r._CLASS_UNIQUE,
+ ttl,
+ 0,
+ 0,
+ 80,
+ service_server,
+ )
+ ]
+ )
+ )
+ send_event.wait(wait_time)
+ assert last_sent is not None
+ assert len(last_sent.questions) == 2
+ assert r.DNSQuestion(service_server, r._TYPE_A, r._CLASS_IN) in
last_sent.questions
+ assert r.DNSQuestion(service_server, r._TYPE_AAAA, r._CLASS_IN) in
last_sent.questions
+ last_sent = None
+ assert service_info is None
+
+ # Expext no further queries
+ last_sent = None
+ send_event.clear()
+ zc.handle_response(
+ mock_incoming_msg(
+ [
+ r.DNSAddress(
+ service_server,
+ r._TYPE_A,
+ r._CLASS_IN | r._CLASS_UNIQUE,
+ ttl,
+ socket.inet_pton(socket.AF_INET, service_address),
+ )
+ ]
+ )
+ )
+ send_event.wait(wait_time)
+ assert last_sent is None
+ assert service_info is not None
+
+ finally:
+ helper_thread.join()
+ zc.remove_all_service_listeners()
+ zc.close()
+
+ def test_get_info_single(self):
+
+ zc = r.Zeroconf(interfaces=['127.0.0.1'])
+
+ service_name = 'name._type._tcp.local.'
+ service_type = '_type._tcp.local.'
+ service_server = 'ash-1.local.'
+ service_text = b'path=/~matt1/'
+ service_address = '10.0.1.2'
+
+ service_info = None
+ send_event = Event()
+ service_info_event = Event()
+
+ last_sent = None # type: Optional[r.DNSOutgoing]
+
+ def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
+ """Sends an outgoing packet."""
+ nonlocal last_sent
+
+ last_sent = out
+ send_event.set()
+
+ # monkey patch the zeroconf send
+ setattr(zc, "send", send)
+
+ def mock_incoming_msg(records) -> r.DNSIncoming:
+
+ generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
+
+ for record in records:
+ generated.add_answer_at_time(record, 0)
+
+ return r.DNSIncoming(generated.packet())
+
+ def get_service_info_helper(zc, type, name):
+ nonlocal service_info
+ service_info = zc.get_service_info(type, name)
+ service_info_event.set()
+
+ try:
+ ttl = 120
+ helper_thread = threading.Thread(
+ target=get_service_info_helper, args=(zc, service_type,
service_name)
+ )
+ helper_thread.start()
+ wait_time = 1
+
+ # Expext query for SRV, TXT, A, AAAA
+ send_event.wait(wait_time)
+ assert last_sent is not None
+ assert len(last_sent.questions) == 4
+ assert r.DNSQuestion(service_name, r._TYPE_SRV, r._CLASS_IN) in
last_sent.questions
+ assert r.DNSQuestion(service_name, r._TYPE_TXT, r._CLASS_IN) in
last_sent.questions
+ assert r.DNSQuestion(service_name, r._TYPE_A, r._CLASS_IN) in
last_sent.questions
+ assert r.DNSQuestion(service_name, r._TYPE_AAAA, r._CLASS_IN) in
last_sent.questions
+ assert service_info is None
+
+ # Expext no further queries
+ last_sent = None
+ send_event.clear()
+ zc.handle_response(
+ mock_incoming_msg(
+ [
+ r.DNSText(
+ service_name, r._TYPE_TXT, r._CLASS_IN |
r._CLASS_UNIQUE, ttl, service_text
+ ),
+ r.DNSService(
+ service_name,
+ r._TYPE_SRV,
+ r._CLASS_IN | r._CLASS_UNIQUE,
+ ttl,
+ 0,
+ 0,
+ 80,
+ service_server,
+ ),
+ r.DNSAddress(
+ service_server,
+ r._TYPE_A,
+ r._CLASS_IN | r._CLASS_UNIQUE,
+ ttl,
+ socket.inet_pton(socket.AF_INET, service_address),
+ ),
+ ]
+ )
+ )
+ send_event.wait(wait_time)
+ assert last_sent is None
+ assert service_info is not None
+
+ finally:
+ helper_thread.join()
+ zc.remove_all_service_listeners()
+ zc.close()
+
+
class TestServiceBrowserMultipleTypes(unittest.TestCase):
def test_update_record(self):
- service_names = ['name._type._tcp.local.', 'name._type._udp.local']
- service_types = ['_type._tcp.local.', '_type._udp.local.']
+ service_names = ['name2._type2._tcp.local.', 'name._type._tcp.local.',
'name._type._udp.local']
+ service_types = ['_type2._tcp.local.', '_type._tcp.local.',
'_type._udp.local.']
service_added_count = 0
service_removed_count = 0
@@ -1257,25 +1506,19 @@
def add_service(self, zc, type_, name) -> None:
nonlocal service_added_count
service_added_count += 1
- if service_added_count == 2:
+ if service_added_count == 3:
service_add_event.set()
def remove_service(self, zc, type_, name) -> None:
nonlocal service_removed_count
service_removed_count += 1
- if service_removed_count == 2:
+ if service_removed_count == 3:
service_removed_event.set()
def mock_incoming_msg(
- service_state_change: r.ServiceStateChange, service_type: str,
service_name: str
+ service_state_change: r.ServiceStateChange, service_type: str,
service_name: str, ttl: int
) -> r.DNSIncoming:
generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
-
- if service_state_change == r.ServiceStateChange.Removed:
- ttl = 0
- else:
- ttl = 120
-
generated.add_answer_at_time(
r.DNSPointer(service_type, r._TYPE_PTR, r._CLASS_IN, ttl,
service_name), 0
)
@@ -1287,30 +1530,54 @@
try:
wait_time = 3
- # both services added
+ # all three services added
zeroconf.handle_response(
- mock_incoming_msg(r.ServiceStateChange.Added,
service_types[0], service_names[0])
+ mock_incoming_msg(r.ServiceStateChange.Added,
service_types[0], service_names[0], 120)
)
zeroconf.handle_response(
- mock_incoming_msg(r.ServiceStateChange.Added,
service_types[1], service_names[1])
+ mock_incoming_msg(r.ServiceStateChange.Added,
service_types[1], service_names[1], 120)
)
+ zeroconf.handle_response(
+ mock_incoming_msg(r.ServiceStateChange.Added,
service_types[2], service_names[2], 120)
+ )
+
+ called_with_refresh_time_check = False
+
+ def _mock_get_expiration_time(self, percent):
+ nonlocal called_with_refresh_time_check
+ if percent == _EXPIRE_REFRESH_TIME_PERCENT:
+ called_with_refresh_time_check = True
+ return 0
+ return self.created + (percent * self.ttl * 10)
+
+ # Set an expire time that will force a refresh
+ with unittest.mock.patch("zeroconf.DNSRecord.get_expiration_time",
new=_mock_get_expiration_time):
+ zeroconf.handle_response(
+ mock_incoming_msg(r.ServiceStateChange.Added,
service_types[2], service_names[2], 120)
+ )
service_add_event.wait(wait_time)
- assert service_added_count == 2
+ assert called_with_refresh_time_check is True
+ assert service_added_count == 3
assert service_removed_count == 0
- # both services removed
+ # all three services removed
zeroconf.handle_response(
- mock_incoming_msg(r.ServiceStateChange.Removed,
service_types[0], service_names[0])
+ mock_incoming_msg(r.ServiceStateChange.Removed,
service_types[0], service_names[0], 0)
)
zeroconf.handle_response(
- mock_incoming_msg(r.ServiceStateChange.Removed,
service_types[1], service_names[1])
+ mock_incoming_msg(r.ServiceStateChange.Removed,
service_types[1], service_names[1], 0)
+ )
+ zeroconf.handle_response(
+ mock_incoming_msg(r.ServiceStateChange.Removed,
service_types[2], service_names[2], 0)
)
service_removed_event.wait(wait_time)
- assert service_added_count == 2
- assert service_removed_count == 2
+ assert service_added_count == 3
+ assert service_removed_count == 3
finally:
+ assert len(zeroconf.listeners) == 1
service_browser.cancel()
+ assert len(zeroconf.listeners) == 0
zeroconf.remove_all_service_listeners()
zeroconf.close()
@@ -1505,7 +1772,14 @@
address_v6 = socket.inet_pton(socket.AF_INET6, address_v6_parsed)
infos = [
ServiceInfo(
- type_, registration_name, 80, 0, 0, desc, "ash-2.local.",
addresses=[address, address_v6],
+ type_,
+ registration_name,
+ 80,
+ 0,
+ 0,
+ desc,
+ "ash-2.local.",
+ addresses=[address, address_v6],
),
ServiceInfo(
type_,