Hello community,
here is the log from the commit of package python-zeroconf for openSUSE:Factory
checked in at 2020-06-10 00:48:05
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-zeroconf (Old)
and /work/SRC/openSUSE:Factory/.python-zeroconf.new.3606 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-zeroconf"
Wed Jun 10 00:48:05 2020 rev:13 rq:812731 version:0.27.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-zeroconf/python-zeroconf.changes
2020-04-16 23:04:33.635744596 +0200
+++
/work/SRC/openSUSE:Factory/.python-zeroconf.new.3606/python-zeroconf.changes
2020-06-10 00:48:08.758902574 +0200
@@ -1,0 +2,24 @@
+Mon Jun 8 19:36:00 UTC 2020 - Dirk Mueller <[email protected]>
+
+- update to 0.27.1
+ * Improved the logging situation
+ * Large multi-resource responses are now split into separate packets which
fixes a bad
+ mdns-repeater/ChromeCast Audio interaction ending with ChromeCast Audio
crash (and possibly
+ some others) and improves RFC 6762 compliance, thanks to Greg Badros
+ * Added a warning presented when the listener passed to ServiceBrowser lacks
update_service()
+ callback
+ * Added support for finding all services available in the browser example,
thanks to Perry Kunder
+ * Removed previously deprecated ServiceInfo address constructor parameter
and property
+ * Improved readability of logged incoming data, thanks to Erik Montnemery
+ * Threads are given unique names now to aid debugging, thanks to Erik
Montnemery
+ * Fixed a regression where get_service_info() called within a listener
add_service method
+ would deadlock, timeout and incorrectly return None, fix thanks to Erik
Montnemery, but
+ Matt Saxon and Hmmbob were also involved in debugging it.
+ * Added support for multiple types to ServiceBrowser, thanks to J. Nick
Koston
+ * Fixed a race condition where a listener gets a message before the lock is
created
+ * Fixed a performance regression introduced in 0.26.0, thanks to J. Nick
Koston (this is close in
+ spirit to an optimization made in 0.24.5 by the same author)
+ * Fixed a regression where service update listener wasn't called on IP
address change (it's called
+ on SRV/A/AAAA record changes now), thanks to Matt Saxon
+
+-------------------------------------------------------------------
Old:
----
python-zeroconf-0.25.1.tar.gz
New:
----
python-zeroconf-0.27.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-zeroconf.spec ++++++
--- /var/tmp/diff_new_pack.GFXKd9/_old 2020-06-10 00:48:09.442904364 +0200
+++ /var/tmp/diff_new_pack.GFXKd9/_new 2020-06-10 00:48:09.442904364 +0200
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%define skip_python2 1
Name: python-zeroconf
-Version: 0.25.1
+Version: 0.27.1
Release: 0
Summary: Pure Python Multicast DNS Service Discovery Library
(Bonjour/Avahi compatible)
License: LGPL-2.0-only
++++++ python-zeroconf-0.25.1.tar.gz -> python-zeroconf-0.27.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.25.1/.gitignore
new/python-zeroconf-0.27.1/.gitignore
--- old/python-zeroconf-0.25.1/.gitignore 2020-04-14 21:01:53.000000000
+0200
+++ new/python-zeroconf-0.27.1/.gitignore 2020-06-05 11:09:58.000000000
+0200
@@ -11,3 +11,4 @@
.cache
.mypy_cache/
docs/_build/
+.vscode
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.25.1/README.rst
new/python-zeroconf-0.27.1/README.rst
--- old/python-zeroconf-0.25.1/README.rst 2020-04-14 21:01:53.000000000
+0200
+++ new/python-zeroconf-0.27.1/README.rst 2020-06-05 11:09:58.000000000
+0200
@@ -134,6 +134,59 @@
Changelog
=========
+0.27.1
+------
+
+* Improved the logging situation (includes fixing a false-positive "packets()
made no progress
+ adding records", thanks to Greg Badros)
+
+0.27.0
+------
+
+* Large multi-resource responses are now split into separate packets which
fixes a bad
+ mdns-repeater/ChromeCast Audio interaction ending with ChromeCast Audio
crash (and possibly
+ some others) and improves RFC 6762 compliance, thanks to Greg Badros
+* Added a warning presented when the listener passed to ServiceBrowser lacks
update_service()
+ callback
+* Added support for finding all services available in the browser example,
thanks to Perry Kunder
+
+Backwards incompatible:
+
+* Removed previously deprecated ServiceInfo address constructor parameter and
property
+
+0.26.3
+------
+
+* Improved readability of logged incoming data, thanks to Erik Montnemery
+* Threads are given unique names now to aid debugging, thanks to Erik
Montnemery
+* Fixed a regression where get_service_info() called within a listener
add_service method
+ would deadlock, timeout and incorrectly return None, fix thanks to Erik
Montnemery, but
+ Matt Saxon and Hmmbob were also involved in debugging it.
+
+0.26.2
+------
+
+* Added support for multiple types to ServiceBrowser, thanks to J. Nick Koston
+* Fixed a race condition where a listener gets a message before the lock is
created, thanks to
+ J. Nick Koston
+
+0.26.1
+------
+
+* Fixed a performance regression introduced in 0.26.0, thanks to J. Nick
Koston (this is close in
+ spirit to an optimization made in 0.24.5 by the same author)
+
+0.26.0
+------
+
+* Fixed a regression where service update listener wasn't called on IP address
change (it's called
+ on SRV/A/AAAA record changes now), thanks to Matt Saxon
+
+Technically backwards incompatible:
+
+* Service update hook is no longer called on service addition (service added
hook is still called),
+ this is related to the fix above
+
0.25.1
------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.25.1/examples/browser.py
new/python-zeroconf-0.27.1/examples/browser.py
--- old/python-zeroconf-0.25.1/examples/browser.py 2020-04-14
21:01:53.000000000 +0200
+++ new/python-zeroconf-0.27.1/examples/browser.py 2020-06-05
11:09:58.000000000 +0200
@@ -1,6 +1,9 @@
#!/usr/bin/env python3
-""" Example of browsing for a service (in this case, HTTP) """
+""" Example of browsing for a service.
+
+The default is HTTP and HAP; use --find to search for all available services
in the network
+"""
import argparse
import logging
@@ -8,7 +11,7 @@
from time import sleep
from typing import cast
-from zeroconf import IPVersion, ServiceBrowser, ServiceStateChange, Zeroconf
+from zeroconf import IPVersion, ServiceBrowser, ServiceStateChange, Zeroconf,
ZeroconfServiceTypes
def on_service_state_change(
@@ -18,6 +21,7 @@
if state_change is ServiceStateChange.Added:
info = zeroconf.get_service_info(service_type, name)
+ print("Info from zeroconf.get_service_info: %r" % (info))
if info:
addresses = ["%s:%d" % (socket.inet_ntoa(addr), cast(int,
info.port)) for addr in info.addresses]
print(" Addresses: %s" % ", ".join(addresses))
@@ -39,6 +43,7 @@
parser = argparse.ArgumentParser()
parser.add_argument('--debug', action='store_true')
+ parser.add_argument('--find', action='store_true', help='Browse all
available services')
version_group = parser.add_mutually_exclusive_group()
version_group.add_argument('--v6', action='store_true')
version_group.add_argument('--v6-only', action='store_true')
@@ -54,8 +59,13 @@
ip_version = IPVersion.V4Only
zeroconf = Zeroconf(ip_version=ip_version)
- print("\nBrowsing services, press Ctrl-C to exit...\n")
- browser = ServiceBrowser(zeroconf, "_http._tcp.local.",
handlers=[on_service_state_change])
+
+ services = ["_http._tcp.local.", "_hap._tcp.local."]
+ if args.find:
+ services = list(ZeroconfServiceTypes.find(zc=zeroconf))
+
+ print("\nBrowsing %d service(s), press Ctrl-C to exit...\n" %
len(services))
+ browser = ServiceBrowser(zeroconf, services,
handlers=[on_service_state_change])
try:
while True:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.25.1/setup.py
new/python-zeroconf-0.27.1/setup.py
--- old/python-zeroconf-0.25.1/setup.py 2020-04-14 21:01:53.000000000 +0200
+++ new/python-zeroconf-0.27.1/setup.py 2020-06-05 11:09:58.000000000 +0200
@@ -9,7 +9,7 @@
readme = f.read()
version = (
- [l for l in open(join(PROJECT_ROOT, 'zeroconf', '__init__.py')) if
'__version__' in l][0]
+ [ln for ln in open(join(PROJECT_ROOT, 'zeroconf', '__init__.py')) if
'__version__' in ln][0]
.split('=')[-1]
.strip()
.strip('\'"')
@@ -43,5 +43,5 @@
'Programming Language :: Python :: Implementation :: PyPy',
],
keywords=['Bonjour', 'Avahi', 'Zeroconf', 'Multicast DNS', 'Service
Discovery', 'mDNS'],
- install_requires=['ifaddr', 'typing;python_version<"3.5"'],
+ install_requires=['ifaddr'],
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.25.1/zeroconf/__init__.py
new/python-zeroconf-0.27.1/zeroconf/__init__.py
--- old/python-zeroconf-0.25.1/zeroconf/__init__.py 2020-04-14
21:01:53.000000000 +0200
+++ new/python-zeroconf-0.27.1/zeroconf/__init__.py 2020-06-05
11:09:58.000000000 +0200
@@ -35,6 +35,7 @@
import threading
import time
import warnings
+from collections import OrderedDict
from typing import Dict, List, Optional, Sequence, Union, cast
from typing import Any, Callable, Set, Tuple # noqa # used in type hints
@@ -42,7 +43,7 @@
__author__ = 'Paul Scott-Murphy, William McBrine'
__maintainer__ = 'Jakub Stasiak <[email protected]>'
-__version__ = '0.25.1'
+__version__ = '0.27.1'
__license__ = 'LGPL'
@@ -57,11 +58,11 @@
"IPVersion",
]
-if sys.version_info <= (3, 3):
+if sys.version_info <= (3, 4):
raise ImportError(
'''
-Python version > 3.3 required for python-zeroconf.
-If you need support for Python 2 or Python 3.3 please use version 19.1
+Python version > 3.4 required for python-zeroconf.
+If you need support for Python 2 or Python 3.3-3.4 please use version 19.1
'''
)
@@ -352,7 +353,7 @@
_seen_logs = {} # type: Dict[str, Union[int, tuple]]
@classmethod
- def log_exception_warning(cls, logger_data: Optional[Tuple] = None) ->
None:
+ def log_exception_warning(cls, *logger_data: Any) -> None:
exc_info = sys.exc_info()
exc_str = str(exc_info[1])
if exc_str not in cls._seen_logs:
@@ -361,9 +362,7 @@
logger = log.warning
else:
logger = log.debug
- if logger_data is not None:
- logger(*logger_data)
- logger('Exception occurred:', exc_info=True)
+ logger(*(logger_data or ['Exception occurred']), exc_info=True)
@classmethod
def log_warning_once(cls, *args: Any) -> None:
@@ -553,18 +552,10 @@
"""A DNS host information record"""
- def __init__(
- self, name: str, type_: int, class_: int, ttl: int, cpu: Union[bytes,
str], os: Union[bytes, str]
- ) -> None:
+ def __init__(self, name: str, type_: int, class_: int, ttl: int, cpu: str,
os: str) -> None:
DNSRecord.__init__(self, name, type_, class_, ttl)
- try:
- self.cpu = cast(bytes, cpu).decode('utf-8')
- except AttributeError:
- self.cpu = cast(str, cpu)
- try:
- self.os = cast(bytes, os).decode('utf-8')
- except AttributeError:
- self.os = cast(str, os)
+ self.cpu = cpu
+ self.os = os
def write(self, out: 'DNSOutgoing') -> None:
"""Used in constructing an outgoing packet"""
@@ -716,7 +707,21 @@
self.valid = True
except (IndexError, struct.error, IncomingDecodeError):
- self.log_exception_warning(('Choked at offset %d while unpacking
%r', self.offset, data))
+ self.log_exception_warning('Choked at offset %d while unpacking
%r', self.offset, data)
+
+ def __repr__(self) -> str:
+ return '<DNSIncoming:{%s}>' % ', '.join(
+ [
+ 'id=%s' % self.id,
+ 'flags=%s' % self.flags,
+ 'n_q=%s' % self.num_questions,
+ 'n_ans=%s' % self.num_answers,
+ 'n_auth=%s' % self.num_authorities,
+ 'n_add=%s' % self.num_additionals,
+ 'questions=%s' % self.questions,
+ 'answers=%s' % self.answers,
+ ]
+ )
def unpack(self, format_: bytes) -> tuple:
length = struct.calcsize(format_)
@@ -792,7 +797,12 @@
)
elif type_ == _TYPE_HINFO:
rec = DNSHinfo(
- domain, type_, class_, ttl, self.read_character_string(),
self.read_character_string()
+ domain,
+ type_,
+ class_,
+ ttl,
+ self.read_character_string().decode('utf-8'),
+ self.read_character_string().decode('utf-8'),
)
elif type_ == _TYPE_AAAA:
rec = DNSAddress(domain, type_, class_, ttl,
self.read_string(16))
@@ -860,9 +870,13 @@
self.id = 0
self.multicast = multicast
self.flags = flags
+ self.packets_data = [] # type: List[bytes]
+
+ # these 3 are per-packet -- see also reset_for_next_packet()
self.names = {} # type: Dict[str, int]
self.data = [] # type: List[bytes]
self.size = 12
+
self.state = self.State.init
self.questions = [] # type: List[DNSQuestion]
@@ -870,6 +884,11 @@
self.authorities = [] # type: List[DNSPointer]
self.additionals = [] # type: List[DNSRecord]
+ def reset_for_next_packet(self) -> None:
+ self.names = {}
+ self.data = []
+ self.size = 12
+
def __repr__(self) -> str:
return '<DNSOutgoing:{%s}>' % ', '.join(
[
@@ -1044,11 +1063,13 @@
self.write_short(question.type)
self.write_short(question.class_)
- def write_record(self, record: DNSRecord, now: float) -> int:
+ def write_record(self, record: DNSRecord, now: float, allow_long: bool =
False) -> bool:
"""Writes a record (answer, authoritative answer, additional) to
- the packet"""
+ the packet. Returns True on success, or False if we did not (either
+ because the packet was already finished or because the record does
+ not fit."""
if self.state == self.State.finished:
- return 1
+ return False
start_data_length, start_size = len(self.data), self.size
self.write_name(record.name)
@@ -1072,44 +1093,104 @@
# Here is the short we adjusted for
self.insert_short(index, length)
+ len_limit = _MAX_MSG_ABSOLUTE if allow_long else _MAX_MSG_TYPICAL
+
# if we go over, then rollback and quit
- if self.size > _MAX_MSG_ABSOLUTE:
+ if self.size > len_limit:
while len(self.data) > start_data_length:
self.data.pop()
self.size = start_size
- self.state = self.State.finished
- return 1
- return 0
+ return False
+ return True
def packet(self) -> bytes:
- """Returns a string containing the packet's bytes
+ """Returns a bytestring containing the first packet's bytes.
+
+ Generally, you want to use packets() in case the response
+ does not fit in a single packet, but this exists for
+ backward compatibility."""
+ packets = self.packets()
+ if len(packets) > 0:
+ if len(packets[0]) > _MAX_MSG_ABSOLUTE:
+ QuietLogger.log_warning_once(
+ "Created over-sized packet (%d bytes) %r",
len(packets[0]), packets[0]
+ )
+ return packets[0]
+ else:
+ return b''
+
+ def packets(self) -> List[bytes]:
+ """Returns a list of bytestrings containing the packets' bytes
No further parts should be added to the packet once this
- is done."""
+ is done. The packets are each restricted to _MAX_MSG_TYPICAL
+ 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). """
- overrun_answers, overrun_authorities, overrun_additionals = 0, 0, 0
+ if self.state == self.State.finished:
+ return self.packets_data
- if self.state != self.State.finished:
+ answer_offset = 0
+ authority_offset = 0
+ additional_offset = 0
+
+ # we have to at least write out the question
+ first_time = True
+
+ while (
+ first_time
+ or answer_offset < len(self.answers)
+ or authority_offset < len(self.authorities)
+ or additional_offset < len(self.additionals)
+ ):
+ first_time = False
+ log.debug("offsets = %d, %d, %d", answer_offset, authority_offset,
additional_offset)
+ log.debug("lengths = %d, %d, %d", len(self.answers),
len(self.authorities), len(self.additionals))
+
+ additionals_written = 0
+ authorities_written = 0
+ answers_written = 0
+ questions_written = 0
for question in self.questions:
self.write_question(question)
- for answer, time_ in self.answers:
- overrun_answers += self.write_record(answer, time_)
- for authority in self.authorities:
- overrun_authorities += self.write_record(authority, 0)
- for additional in self.additionals:
- overrun_additionals += self.write_record(additional, 0)
- self.state = self.State.finished
-
- self.insert_short(0, len(self.additionals) - overrun_additionals)
- self.insert_short(0, len(self.authorities) - overrun_authorities)
- self.insert_short(0, len(self.answers) - overrun_answers)
- self.insert_short(0, len(self.questions))
+ questions_written += 1
+ allow_long = True # at most one answer is allowed to be a long
packet
+ for answer, time_ in self.answers[answer_offset:]:
+ if self.write_record(answer, time_, allow_long):
+ answers_written += 1
+ allow_long = False
+ for authority in self.authorities[authority_offset:]:
+ if self.write_record(authority, 0):
+ authorities_written += 1
+ for additional in self.additionals[additional_offset:]:
+ if self.write_record(additional, 0):
+ additionals_written += 1
+
+ self.insert_short(0, additionals_written)
+ self.insert_short(0, authorities_written)
+ self.insert_short(0, answers_written)
+ self.insert_short(0, questions_written)
self.insert_short(0, self.flags)
if self.multicast:
self.insert_short(0, 0)
else:
self.insert_short(0, self.id)
- return b''.join(self.data)
+ self.packets_data.append(b''.join(self.data))
+ self.reset_for_next_packet()
+
+ answer_offset += answers_written
+ authority_offset += authorities_written
+ additional_offset += additionals_written
+ log.debug("now offsets = %d, %d, %d", answer_offset,
authority_offset, additional_offset)
+ if (answers_written + authorities_written + additionals_written)
== 0 and (
+ len(self.answers) + len(self.authorities) +
len(self.additionals)
+ ) > 0:
+ log.warning("packets() made no progress adding records;
returning")
+ break
+ self.state = self.State.finished
+ return self.packets_data
class DNSCache:
@@ -1121,8 +1202,9 @@
def add(self, entry: DNSRecord) -> None:
"""Adds an entry"""
- # Insert first in list so get returns newest entry
- self.cache.setdefault(entry.key, []).insert(0, entry)
+ # Insert last in list, get will return newest entry
+ # iteration will result in last update winning
+ self.cache.setdefault(entry.key, []).append(entry)
def remove(self, entry: DNSRecord) -> None:
"""Removes an entry"""
@@ -1142,7 +1224,7 @@
matching entry."""
try:
list_ = self.cache[entry.key]
- for cached_entry in list_:
+ for cached_entry in reversed(list_):
if entry.__eq__(cached_entry):
return cached_entry
return None
@@ -1164,7 +1246,7 @@
def current_entry_with_name_and_alias(self, name: str, alias: str) ->
Optional[DNSRecord]:
now = current_time_millis()
- for record in self.entries_with_name(name):
+ for record in reversed(self.entries_with_name(name)):
if (
record.type == _TYPE_PTR
and not record.is_expired(now)
@@ -1197,7 +1279,7 @@
"""
def __init__(self, zc: 'Zeroconf') -> None:
- threading.Thread.__init__(self, name='zeroconf-Engine')
+ threading.Thread.__init__(self)
self.daemon = True
self.zc = zc
self.readers = {} # type: Dict[socket.socket, Listener]
@@ -1205,6 +1287,7 @@
self.condition = threading.Condition()
self.socketpair = socket.socketpair()
self.start()
+ self.name = "zeroconf-Engine-%s" % (getattr(self, 'native_id',
self.ident),)
def run(self) -> None:
while not self.zc.done:
@@ -1274,13 +1357,31 @@
try:
data, (addr, port, *_v6) = socket_.recvfrom(_MAX_MSG_ABSOLUTE)
except Exception:
- self.log_exception_warning()
+ self.log_exception_warning('Error reading from socket %d',
socket_.fileno())
return
- log.debug('Received from %r:%r: %r ', addr, port, data)
-
self.data = data
msg = DNSIncoming(data)
+ if msg.valid:
+ log.debug(
+ 'Received from %r:%r (socket %d): %r (%d bytes) as [%r]',
+ addr,
+ port,
+ socket_.fileno(),
+ msg,
+ len(data),
+ data,
+ )
+ else:
+ log.debug(
+ 'Received from %r:%r (socket %d): (%d bytes) [%r]',
+ addr,
+ port,
+ socket_.fileno(),
+ len(data),
+ data,
+ )
+
if not msg.valid:
pass
@@ -1305,10 +1406,11 @@
have expired."""
def __init__(self, zc: 'Zeroconf') -> None:
- threading.Thread.__init__(self, name='zeroconf-Reaper')
+ threading.Thread.__init__(self)
self.daemon = True
self.zc = zc
self.start()
+ self.name = "zeroconf-Reaper_%s" % (getattr(self, 'native_id',
self.ident),)
def run(self) -> None:
while True:
@@ -1377,7 +1479,7 @@
def __init__(
self,
zc: 'Zeroconf',
- type_: str,
+ type_: Union[str, list],
# NOTE: Callable quoting needed on Python 3.5.2, see
# https://github.com/jstasiak/python-zeroconf/issues/208 for details.
handlers: Optional[Union[ServiceListener, List['Callable[...,
None]']]] = None,
@@ -1388,19 +1490,23 @@
) -> None:
"""Creates a browser for a specific type"""
assert handlers or listener, 'You need to specify at least one handler'
- if not type_.endswith(service_type_name(type_,
allow_underscores=True)):
- raise BadTypeInNameException
- threading.Thread.__init__(self, name='zeroconf-ServiceBrowser_' +
type_)
+ 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)):
+ raise BadTypeInNameException
+ threading.Thread.__init__(self)
self.daemon = True
self.zc = zc
- self.type = type_
self.addr = addr
self.port = port
self.multicast = self.addr in (None, _MDNS_ADDR, _MDNS_ADDR6)
- self.services = {} # type: Dict[str, DNSRecord]
- self.next_time = current_time_millis()
- self.delay = delay
- self._handlers_to_call = [] # type: List[Callable[[Zeroconf], None]]
+ self._services = {
+ check_type_: {} for check_type_ in self.types
+ } # type: Dict[str, Dict[str, DNSRecord]]
+ current_time = current_time_millis()
+ self._next_time = {check_type_: current_time for check_type_ in
self.types}
+ self._delay = {check_type_: delay for check_type_ in self.types}
+ self._handlers_to_call = OrderedDict() # type: OrderedDict[str,
Tuple[str, ServiceStateChange]]
self._service_state_changed = Signal()
@@ -1428,6 +1534,12 @@
elif state_change is ServiceStateChange.Updated:
if hasattr(listener, 'update_service'):
listener.update_service(*args)
+ else:
+ warnings.warn(
+ "%r has no update_service method. Provide one (it
can be empty if you "
+ "don't care about the updates), it'll become
mandatory." % (listener,),
+ FutureWarning,
+ )
else:
raise NotImplementedError(state_change)
@@ -1437,6 +1549,10 @@
self.service_state_changed.register_handler(h)
self.start()
+ self.name = "zeroconf-ServiceBrowser_%s_%s" % (
+ '-'.join(self.types),
+ getattr(self, 'native_id', self.ident),
+ )
@property
def service_state_changed(self) -> SignalRegistrationInterface:
@@ -1445,42 +1561,82 @@
def update_record(self, zc: 'Zeroconf', now: float, record: DNSRecord) ->
None:
"""Callback invoked by Zeroconf when new information arrives.
- Updates information required by browser in the Zeroconf cache."""
+ Updates information required by browser in the Zeroconf cache.
+
+ Ensures that there is are no unecessary duplicates in the list
+
+ """
+
+ def enqueue_callback(state_change: ServiceStateChange, type_: str,
name: str) -> None:
+
+ # Code to ensure we only do a single update message
+ # Precedence is; Added, Remove, Update
- def enqueue_callback(state_change: ServiceStateChange, name: str) ->
None:
- self._handlers_to_call.append(
- lambda zeroconf: self._service_state_changed.fire(
- zeroconf=zeroconf, service_type=self.type, name=name,
state_change=state_change
+ if (
+ state_change is ServiceStateChange.Added
+ or (
+ state_change is ServiceStateChange.Removed
+ and (
+ self._handlers_to_call.get(name) is
ServiceStateChange.Updated
+ or self._handlers_to_call.get(name) is
ServiceStateChange.Added
+ or self._handlers_to_call.get(name) is None
+ )
)
- )
+ or (state_change is ServiceStateChange.Updated and name not in
self._handlers_to_call)
+ ):
+ self._handlers_to_call[name] = (type_, state_change)
- if record.type == _TYPE_PTR and record.name == self.type:
+ if record.type == _TYPE_PTR and record.name in self.types:
assert isinstance(record, DNSPointer)
expired = record.is_expired(now)
service_key = record.alias.lower()
try:
- old_record = self.services[service_key]
+ old_record = self._services[record.name][service_key]
except KeyError:
if not expired:
- self.services[service_key] = record
- enqueue_callback(ServiceStateChange.Added, record.alias)
+ self._services[record.name][service_key] = record
+ enqueue_callback(ServiceStateChange.Added, record.name,
record.alias)
else:
if not expired:
old_record.reset_ttl(record)
else:
- del self.services[service_key]
- enqueue_callback(ServiceStateChange.Removed, record.alias)
+ del self._services[record.name][service_key]
+ enqueue_callback(ServiceStateChange.Removed, record.name,
record.alias)
return
expires = record.get_expiration_time(75)
- if expires < self.next_time:
- self.next_time = expires
+ if expires < self._next_time[record.name]:
+ self._next_time[record.name] = expires
- elif record.type == _TYPE_TXT and record.name.endswith(self.type):
- assert isinstance(record, DNSText)
- expired = record.is_expired(now)
- if not expired:
- enqueue_callback(ServiceStateChange.Updated, record.name)
+ elif record.type == _TYPE_A or record.type == _TYPE_AAAA:
+ assert isinstance(record, DNSAddress)
+ if record.is_expired(now):
+ return
+
+ address_changed = False
+ for service in zc.cache.entries_with_name(record.name):
+ if isinstance(service, DNSAddress) and service.address !=
record.address:
+ address_changed = True
+ break
+
+ # Avoid iterating the entire DNSCache if the address has not
changed
+ # as this is an expensive operation when there many hosts
+ # generating zeroconf traffic.
+ if not address_changed:
+ return
+
+ # Iterate through the DNSCache and callback any services that use
this address
+ for service in zc.cache.entries():
+ if not isinstance(service, DNSService) or not service.server
== record.name:
+ continue
+ for type_ in self.types:
+ if service.name.endswith(type_):
+ enqueue_callback(ServiceStateChange.Updated, type_,
service.name)
+
+ elif not record.is_expired(now):
+ for type_ in self.types:
+ if record.name.endswith(type_):
+ enqueue_callback(ServiceStateChange.Updated, type_,
record.name)
def cancel(self) -> None:
self.done = True
@@ -1488,29 +1644,40 @@
self.join()
def run(self) -> None:
- self.zc.add_listener(self, DNSQuestion(self.type, _TYPE_PTR,
_CLASS_IN))
+ for type_ in self.types:
+ self.zc.add_listener(self, DNSQuestion(type_, _TYPE_PTR,
_CLASS_IN))
while True:
now = current_time_millis()
- if len(self._handlers_to_call) == 0 and self.next_time > now:
- self.zc.wait(self.next_time - now)
+ # Wait for the type has the smallest next time
+ next_time = min(self._next_time.values())
+ if len(self._handlers_to_call) == 0 and next_time > now:
+ self.zc.wait(next_time - now)
if self.zc.done or self.done:
return
now = current_time_millis()
- if self.next_time <= now:
+ for type_ in self.types:
+ if self._next_time[type_] > now:
+ continue
out = DNSOutgoing(_FLAGS_QR_QUERY, multicast=self.multicast)
- out.add_question(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
- for record in self.services.values():
+ out.add_question(DNSQuestion(type_, _TYPE_PTR, _CLASS_IN))
+ for record in self._services[type_].values():
if not record.is_stale(now):
out.add_answer_at_time(record, now)
self.zc.send(out, addr=self.addr, port=self.port)
- self.next_time = now + self.delay
- self.delay = min(_BROWSER_BACKOFF_LIMIT * 1000, self.delay * 2)
+ self._next_time[type_] = now + self._delay[type_]
+ self._delay[type_] = min(_BROWSER_BACKOFF_LIMIT * 1000,
self._delay[type_] * 2)
if len(self._handlers_to_call) > 0 and not self.zc.done:
- handler = self._handlers_to_call.pop(0)
- handler(self.zc)
+ with self.zc._handlers_lock:
+ (name, service_type_state_change) =
self._handlers_to_call.popitem(False)
+ self._service_state_changed.fire(
+ zeroconf=self.zc,
+ service_type=service_type_state_change[0],
+ name=name,
+ state_change=service_type_state_change[1],
+ )
class ServiceInfo(RecordUpdateListener):
@@ -1520,7 +1687,6 @@
* type_: fully qualified service type name
* name: fully qualified service name
- * address: IP address as unsigned short, network byte order (deprecated,
use addresses)
* port: port that the service runs on
* weight: weight of the service
* priority: priority of the service
@@ -1544,7 +1710,6 @@
self,
type_: str,
name: str,
- address: Optional[Union[bytes, List[bytes]]] = None,
port: Optional[int] = None,
weight: int = 0,
priority: int = 0,
@@ -1555,22 +1720,12 @@
*,
addresses: Optional[List[bytes]] = None
) -> None:
- # Accept both none, or one, but not both.
- if address is not None and addresses is not None:
- raise TypeError("address and addresses cannot be provided
together")
-
if not type_.endswith(service_type_name(name, allow_underscores=True)):
raise BadTypeInNameException
self.type = type_
self.name = name
if addresses is not None:
self._addresses = addresses
- elif address is not None:
- warnings.warn("address is deprecated, use addresses instead",
DeprecationWarning)
- if isinstance(address, list):
- self._addresses = address
- else:
- self._addresses = [address]
else:
self._addresses = []
# This results in an ugly error when registering, better check now
@@ -1593,23 +1748,6 @@
# fmt: on
@property
- def address(self) -> Optional[bytes]:
- warnings.warn("ServiceInfo.address is deprecated, use addresses
instead", DeprecationWarning)
- try:
- # Return the first V4 address for compatibility
- return self.addresses[0]
- except IndexError:
- return None
-
- @address.setter
- def address(self, value: bytes) -> None:
- warnings.warn("ServiceInfo.address is deprecated, use addresses
instead", DeprecationWarning)
- if value is None:
- self._addresses = []
- else:
- self._addresses = [value]
-
- @property
def addresses(self) -> List[bytes]:
"""IPv4 addresses of this service.
@@ -1985,12 +2123,11 @@
else:
try:
s.setsockopt(socket.SOL_SOCKET, reuseport, 1)
- except (OSError, socket.error) as err:
- # OSError on python 3, socket.error on python 2
+ except OSError as err:
if not err.errno == errno.ENOPROTOOPT:
raise
- if port is _MDNS_PORT:
+ if port == _MDNS_PORT:
ttl = struct.pack(b'B', 255)
loop = struct.pack(b'B', 1)
if ip_version != IPVersion.V6Only:
@@ -2017,7 +2154,7 @@
) -> Optional[socket.socket]:
# This is based on assumptions in normalize_interface_choice
is_v6 = isinstance(interface, int)
- log.debug('Adding %r to multicast group', interface)
+ log.debug('Adding %r (socket %d) to multicast group', interface,
listen_socket.fileno())
try:
if is_v6:
iface_bin = struct.pack('@I', cast(int, interface))
@@ -2051,7 +2188,7 @@
respond_socket = new_socket(
ip_version=(IPVersion.V6Only if is_v6 else IPVersion.V4Only),
apple_p2p=apple_p2p
)
- log.debug('Configuring %s with multicast interface %s', respond_socket,
interface)
+ log.debug('Configuring socket %d with multicast interface %s',
respond_socket, interface)
if is_v6:
respond_socket.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_IF,
iface_bin)
else:
@@ -2162,6 +2299,11 @@
self.condition = threading.Condition()
+ # Ensure we create the lock before
+ # we add the listener as we could get
+ # a message before the lock is created.
+ self._handlers_lock = threading.Lock() # ensure we process a full
message in one go
+
self.engine = Engine(self)
self.listener = Listener(self)
if not unicast:
@@ -2449,42 +2591,45 @@
def handle_response(self, msg: DNSIncoming) -> None:
"""Deal with incoming response packets. All answers
are held in the cache, and listeners are notified."""
- now = current_time_millis()
- for record in msg.answers:
- updated = True
+ with self._handlers_lock:
- 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)
+ now = current_time_millis()
+ for record in msg.answers:
- 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)
+ updated = True
+
+ 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)
+
+ 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:
- 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)
+ if maybe_entry is not None:
+ self.update_record(now, record)
+ self.cache.remove(maybe_entry)
def handle_query(self, msg: DNSIncoming, addr: Optional[str], port: int)
-> None:
"""Deal with incoming query packets. Provides a response if
@@ -2626,36 +2771,39 @@
def send(self, out: DNSOutgoing, addr: Optional[str] = None, port: int =
_MDNS_PORT) -> None:
"""Sends an outgoing packet."""
- packet = out.packet()
- if len(packet) > _MAX_MSG_ABSOLUTE:
- self.log_warning_once("Dropping %r over-sized packet (%d bytes)
%r", out, len(packet), packet)
- return
- log.debug('Sending %r (%d bytes) as %r...', out, len(packet), packet)
- for s in self._respond_sockets:
- if self._GLOBAL_DONE:
+ packets = out.packets()
+ packet_num = 0
+ for packet in packets:
+ packet_num += 1
+ if len(packet) > _MAX_MSG_ABSOLUTE:
+ self.log_warning_once("Dropping %r over-sized packet (%d
bytes) %r", out, len(packet), packet)
return
- try:
- if addr is None:
- real_addr = _MDNS_ADDR6 if s.family == socket.AF_INET6
else _MDNS_ADDR
- elif not can_send_to(s, addr):
- continue
+ log.debug('Sending (%d bytes #%d) %r as %r...', len(packet),
packet_num, out, packet)
+ for s in self._respond_sockets:
+ if self._GLOBAL_DONE:
+ return
+ try:
+ if addr is None:
+ real_addr = _MDNS_ADDR6 if s.family == socket.AF_INET6
else _MDNS_ADDR
+ elif not can_send_to(s, addr):
+ continue
+ else:
+ real_addr = addr
+ bytes_sent = s.sendto(packet, 0, (real_addr, port))
+ except Exception as exc: # TODO stop catching all Exceptions
+ if (
+ isinstance(exc, OSError)
+ and exc.errno == errno.ENETUNREACH
+ and s.family == socket.AF_INET6
+ ):
+ # with IPv6 we don't have a reliable way to determine
if an interface actually has
+ # IPV6 support, so we have to try and ignore errors.
+ continue
+ # on send errors, log the exception and keep going
+ self.log_exception_warning('Error sending through socket
%d', s.fileno())
else:
- real_addr = addr
- bytes_sent = s.sendto(packet, 0, (real_addr, port))
- except Exception as exc: # TODO stop catching all Exceptions
- if (
- isinstance(exc, OSError)
- and exc.errno == errno.ENETUNREACH
- and s.family == socket.AF_INET6
- ):
- # with IPv6 we don't have a reliable way to determine if
an interface actually has IPv6
- # support, so we have to try and ignore errors.
- continue
- # on send errors, log the exception and keep going
- self.log_exception_warning()
- else:
- if bytes_sent != len(packet):
- self.log_warning_once('!!! sent %d out of %d bytes to %r'
% (bytes_sent, len(packet), s))
+ if bytes_sent != len(packet):
+ self.log_warning_once('!!! sent %d of %d bytes to %r'
% (bytes_sent, len(packet), s))
def close(self) -> None:
"""Ends the background threads, and prevent this instance from
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.25.1/zeroconf/test.py
new/python-zeroconf-0.27.1/zeroconf/test.py
--- old/python-zeroconf-0.25.1/zeroconf/test.py 2020-04-14 21:01:53.000000000
+0200
+++ new/python-zeroconf-0.27.1/zeroconf/test.py 2020-06-05 11:09:58.000000000
+0200
@@ -108,7 +108,7 @@
name = "xxxyyy"
registration_name = "%s.%s" % (name, type_)
info = ServiceInfo(
- type_, registration_name, socket.inet_aton("10.0.1.2"), 80, 0, 0,
b'', "ash-2.local."
+ type_, registration_name, 80, 0, 0, b'', "ash-2.local.",
addresses=[socket.inet_aton("10.0.1.2")],
)
assert not info != info
@@ -121,7 +121,7 @@
info = ServiceInfo(
type_=type_,
name=registration_name,
- address=socket.inet_aton("10.0.1.2"),
+ addresses=[socket.inet_aton("10.0.1.2")],
port=80,
server="ash-2.local.",
)
@@ -318,6 +318,13 @@
generated.add_question(question)
r.DNSIncoming(generated.packet())
+ def test_extra_exceedingly_long_name(self):
+ generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
+ name = "%slocal." % ("part." * 4000)
+ question = r.DNSQuestion(name, r._TYPE_SRV, r._CLASS_IN)
+ generated.add_question(question)
+ r.DNSIncoming(generated.packet())
+
def test_exceedingly_long_name_part(self):
name = "%s.local." % ("a" * 1000)
generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
@@ -355,12 +362,12 @@
def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
"""Sends an outgoing packet."""
- packet = out.packet()
- nonlocal longest_packet_len, longest_packet
- if longest_packet_len < len(packet):
- longest_packet_len = len(packet)
- longest_packet = out
- old_send(out, addr=addr, port=port)
+ for packet in out.packets():
+ nonlocal longest_packet_len, longest_packet
+ if longest_packet_len < len(packet):
+ longest_packet_len = len(packet)
+ longest_packet = out
+ old_send(out, addr=addr, port=port)
# monkey patch the zeroconf send
setattr(zc, "send", send)
@@ -374,6 +381,9 @@
# wait until the browse request packet has maxed out in size
sleep_count = 0
+ # we will never get to this large of a packet given the
application-layer
+ # splitting of packets, but we still want to track the
longest_packet_len
+ # for the debug message below
while sleep_count < 100 and longest_packet_len < r._MAX_MSG_ABSOLUTE -
100:
sleep_count += 1
time.sleep(0.1)
@@ -386,8 +396,8 @@
zeroconf.log.debug('sleep_count %d, sized %d', sleep_count,
longest_packet_len)
# now the browser has sent at least one request, verify the size
- assert longest_packet_len <= r._MAX_MSG_ABSOLUTE
- assert longest_packet_len >= r._MAX_MSG_ABSOLUTE - 100
+ assert longest_packet_len <= r._MAX_MSG_TYPICAL
+ assert longest_packet_len >= r._MAX_MSG_TYPICAL - 100
# mock zeroconf's logger warning() and debug()
from unittest.mock import patch
@@ -407,13 +417,11 @@
call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count
# try to send an oversized packet
zc.send(out)
- assert mocked_log_warn.call_count == call_counts[0] + 1
- assert mocked_log_debug.call_count == call_counts[0]
+ assert mocked_log_warn.call_count == call_counts[0]
zc.send(out)
- assert mocked_log_warn.call_count == call_counts[0] + 1
- assert mocked_log_debug.call_count == call_counts[0] + 1
+ assert mocked_log_warn.call_count == call_counts[0]
- # force a receive of an oversized packet
+ # force a receive of a packet
packet = out.packet()
s = zc._respond_sockets[0]
@@ -456,7 +464,14 @@
def verify_name_change(self, zc, type_, name, number_hosts):
desc = {'path': '/~paulsm/'}
info_service = ServiceInfo(
- type_, '%s.%s' % (name, type_), socket.inet_aton("10.0.1.2"), 80,
0, 0, desc, "ash-2.local."
+ type_,
+ '%s.%s' % (name, type_),
+ 80,
+ 0,
+ 0,
+ desc,
+ "ash-2.local.",
+ addresses=[socket.inet_aton("10.0.1.2")],
)
# verify name conflict
@@ -736,7 +751,14 @@
desc = {'path': '/~paulsm/'}
info = ServiceInfo(
- type_, registration_name, socket.inet_aton("10.0.1.2"), 80, 0, 0,
desc, "ash-2.local."
+ type_,
+ registration_name,
+ 80,
+ 0,
+ 0,
+ desc,
+ "ash-2.local.",
+ addresses=[socket.inet_aton("10.0.1.2")],
)
# we are going to monkey patch the zeroconf send to check packet sizes
@@ -849,7 +871,14 @@
zeroconf_registrar = Zeroconf(interfaces=['127.0.0.1'])
desc = {'path': '/~paulsm/'}
info = ServiceInfo(
- type_, registration_name, socket.inet_aton("10.0.1.2"), 80, 0, 0,
desc, "ash-2.local."
+ type_,
+ registration_name,
+ 80,
+ 0,
+ 0,
+ desc,
+ "ash-2.local.",
+ addresses=[socket.inet_aton("10.0.1.2")],
)
zeroconf_registrar.register_service(info)
@@ -874,7 +903,14 @@
zeroconf_registrar = Zeroconf(interfaces=['127.0.0.1'])
desc = {'path': '/~paulsm/'}
info = ServiceInfo(
- type_, registration_name, socket.inet_pton(socket.AF_INET6, addr),
80, 0, 0, desc, "ash-2.local."
+ type_,
+ registration_name,
+ 80,
+ 0,
+ 0,
+ desc,
+ "ash-2.local.",
+ addresses=[socket.inet_pton(socket.AF_INET6, addr)],
)
zeroconf_registrar.register_service(info)
@@ -898,7 +934,14 @@
zeroconf_registrar = Zeroconf(ip_version=r.IPVersion.V6Only)
desc = {'path': '/~paulsm/'}
info = ServiceInfo(
- type_, registration_name, socket.inet_aton("10.0.1.2"), 80, 0, 0,
desc, "ash-2.local."
+ type_,
+ registration_name,
+ 80,
+ 0,
+ 0,
+ desc,
+ "ash-2.local.",
+ addresses=[socket.inet_aton("10.0.1.2")],
)
zeroconf_registrar.register_service(info)
@@ -922,7 +965,14 @@
zeroconf_registrar = Zeroconf(interfaces=['127.0.0.1'])
desc = {'path': '/~paulsm/'}
info = ServiceInfo(
- discovery_type, registration_name, socket.inet_aton("10.0.1.2"),
80, 0, 0, desc, "ash-2.local."
+ discovery_type,
+ registration_name,
+ 80,
+ 0,
+ 0,
+ desc,
+ "ash-2.local.",
+ addresses=[socket.inet_aton("10.0.1.2")],
)
zeroconf_registrar.register_service(info)
@@ -1028,7 +1078,14 @@
properties['prop_blank'] = b'an updated string'
desc.update(properties)
info_service = ServiceInfo(
- subtype, registration_name, socket.inet_aton("10.0.1.2"), 80,
0, 0, desc, "ash-2.local."
+ subtype,
+ registration_name,
+ 80,
+ 0,
+ 0,
+ desc,
+ "ash-2.local.",
+ addresses=[socket.inet_aton("10.0.1.2")],
)
zeroconf_registrar.update_service(info_service)
service_updated.wait(1)
@@ -1053,12 +1110,12 @@
service_name = 'name._type._tcp.local.'
service_type = '_type._tcp.local.'
- service_server = 'ash-2.local.'
- service_text = b'path=/~paulsm/'
+ service_server = 'ash-1.local.'
+ service_text = b'path=/~matt1/'
service_address = '10.0.1.2'
- service_added = False
- service_removed = False
+ service_added_count = 0
+ service_removed_count = 0
service_updated_count = 0
service_add_event = Event()
service_removed_event = Event()
@@ -1066,49 +1123,44 @@
class MyServiceListener(r.ServiceListener):
def add_service(self, zc, type_, name) -> None:
- nonlocal service_added
- service_added = True
+ nonlocal service_added_count
+ service_added_count += 1
service_add_event.set()
def remove_service(self, zc, type_, name) -> None:
- nonlocal service_added, service_removed
- service_added = False
- service_removed = True
+ nonlocal service_removed_count
+ service_removed_count += 1
service_removed_event.set()
def update_service(self, zc, type_, name) -> None:
nonlocal service_updated_count
service_updated_count += 1
-
service_info = zc.get_service_info(type_, name)
+ assert service_info.addresses[0] ==
socket.inet_aton(service_address)
assert service_info.text == service_text
+ assert service_info.server == service_server
service_updated_event.set()
def mock_incoming_msg(service_state_change: r.ServiceStateChange) ->
r.DNSIncoming:
- ttl = 120
- generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
- if service_state_change == r.ServiceStateChange.Updated:
- generated.add_answer_at_time(
- r.DNSText(service_name, r._TYPE_TXT, r._CLASS_IN |
r._CLASS_UNIQUE, ttl, service_text), 0
- )
- return r.DNSIncoming(generated.packet())
+ 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
+ r.DNSText(service_name, r._TYPE_TXT, r._CLASS_IN |
r._CLASS_UNIQUE, ttl, service_text), 0
)
+
generated.add_answer_at_time(
r.DNSService(
service_name, r._TYPE_SRV, r._CLASS_IN | r._CLASS_UNIQUE,
ttl, 0, 0, 80, service_server
),
0,
)
- generated.add_answer_at_time(
- r.DNSText(service_name, r._TYPE_TXT, r._CLASS_IN |
r._CLASS_UNIQUE, ttl, service_text), 0
- )
+
generated.add_answer_at_time(
r.DNSAddress(
service_server,
@@ -1120,36 +1172,142 @@
0,
)
+ generated.add_answer_at_time(
+ r.DNSPointer(service_type, r._TYPE_PTR, r._CLASS_IN, ttl,
service_name), 0
+ )
+
return r.DNSIncoming(generated.packet())
zeroconf = r.Zeroconf(interfaces=['127.0.0.1'])
service_browser = r.ServiceBrowser(zeroconf, service_type,
listener=MyServiceListener())
try:
+ wait_time = 3
+
# service added
zeroconf.handle_response(mock_incoming_msg(r.ServiceStateChange.Added))
- service_add_event.wait(1)
- service_updated_event.wait(1)
- assert service_added is True
- assert service_updated_count == 1
- assert service_removed is False
+ service_add_event.wait(wait_time)
+ assert service_added_count == 1
+ assert service_updated_count == 0
+ assert service_removed_count == 0
- # service updated. currently only text record can be updated
+ # service SRV updated
service_updated_event.clear()
- service_text = b'path=/~humingchun/'
+ service_server = 'ash-2.local.'
zeroconf.handle_response(mock_incoming_msg(r.ServiceStateChange.Updated))
+ service_updated_event.wait(wait_time)
+ assert service_added_count == 1
+ assert service_updated_count == 1
+ assert service_removed_count == 0
+
+ # service TXT updated
+ service_updated_event.clear()
+ service_text = b'path=/~matt2/'
zeroconf.handle_response(mock_incoming_msg(r.ServiceStateChange.Updated))
- service_updated_event.wait(1)
- assert service_added is True
+ service_updated_event.wait(wait_time)
+ assert service_added_count == 1
assert service_updated_count == 2
- assert service_removed is False
+ assert service_removed_count == 0
+
+ # service A updated
+ service_updated_event.clear()
+ service_address = '10.0.1.3'
+
zeroconf.handle_response(mock_incoming_msg(r.ServiceStateChange.Updated))
+ service_updated_event.wait(wait_time)
+ assert service_added_count == 1
+ assert service_updated_count == 3
+ assert service_removed_count == 0
+
+ # service all updated
+ service_updated_event.clear()
+ service_server = 'ash-3.local.'
+ service_text = b'path=/~matt3/'
+ service_address = '10.0.1.3'
+
zeroconf.handle_response(mock_incoming_msg(r.ServiceStateChange.Updated))
+ service_updated_event.wait(wait_time)
+ assert service_added_count == 1
+ assert service_updated_count == 4
+ assert service_removed_count == 0
# service removed
zeroconf.handle_response(mock_incoming_msg(r.ServiceStateChange.Removed))
- service_removed_event.wait(1)
- assert service_added is False
- assert service_updated_count == 2
- assert service_removed is True
+ service_removed_event.wait(wait_time)
+ assert service_added_count == 1
+ assert service_updated_count == 4
+ assert service_removed_count == 1
+
+ finally:
+ service_browser.cancel()
+ zeroconf.remove_all_service_listeners()
+ zeroconf.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_added_count = 0
+ service_removed_count = 0
+ service_add_event = Event()
+ service_removed_event = Event()
+
+ class MyServiceListener(r.ServiceListener):
+ def add_service(self, zc, type_, name) -> None:
+ nonlocal service_added_count
+ service_added_count += 1
+ if service_added_count == 2:
+ 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:
+ service_removed_event.set()
+
+ def mock_incoming_msg(
+ service_state_change: r.ServiceStateChange, service_type: str,
service_name: str
+ ) -> 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
+ )
+ return r.DNSIncoming(generated.packet())
+
+ zeroconf = r.Zeroconf(interfaces=['127.0.0.1'])
+ service_browser = r.ServiceBrowser(zeroconf, service_types,
listener=MyServiceListener())
+
+ try:
+ wait_time = 3
+
+ # both services added
+ zeroconf.handle_response(
+ mock_incoming_msg(r.ServiceStateChange.Added,
service_types[0], service_names[0])
+ )
+ zeroconf.handle_response(
+ mock_incoming_msg(r.ServiceStateChange.Added,
service_types[1], service_names[1])
+ )
+ service_add_event.wait(wait_time)
+ assert service_added_count == 2
+ assert service_removed_count == 0
+
+ # both services removed
+ zeroconf.handle_response(
+ mock_incoming_msg(r.ServiceStateChange.Removed,
service_types[0], service_names[0])
+ )
+ zeroconf.handle_response(
+ mock_incoming_msg(r.ServiceStateChange.Removed,
service_types[1], service_names[1])
+ )
+ service_removed_event.wait(wait_time)
+ assert service_added_count == 2
+ assert service_removed_count == 2
finally:
service_browser.cancel()
@@ -1284,7 +1442,9 @@
zeroconf_registrar = Zeroconf(interfaces=['127.0.0.1'])
desc = {'path': '/~paulsm/'}
- info = ServiceInfo(type_, registration_name, socket.inet_aton("10.0.1.2"),
80, 0, 0, desc, "ash-2.local.")
+ info = ServiceInfo(
+ type_, registration_name, 80, 0, 0, desc, "ash-2.local.",
addresses=[socket.inet_aton("10.0.1.2")]
+ )
zeroconf_registrar.register_service(info)
try:
@@ -1323,45 +1483,17 @@
address_parsed = "10.0.1.2"
address = socket.inet_aton(address_parsed)
- # Old way
- info = ServiceInfo(type_, registration_name, address, 80, 0, 0, desc,
"ash-2.local.")
-
- assert info.address == address
- assert info.addresses == [address]
-
- # Updating works
- address2 = socket.inet_aton("10.0.1.3")
- info.address = address2
-
- assert info.address == address2
- assert info.addresses == [address2]
-
- info.address = None
-
- assert info.address is None
- assert info.addresses == []
-
- info.addresses = [address2]
-
- assert info.address == address2
- assert info.addresses == [address2]
-
- # Compatibility way
- info = ServiceInfo(type_, registration_name, [address, address], 80, 0, 0,
desc, "ash-2.local.")
-
- assert info.addresses == [address, address]
-
# New kwarg way
- info = ServiceInfo(
- type_, registration_name, None, 80, 0, 0, desc, "ash-2.local.",
addresses=[address, address]
- )
+ info = ServiceInfo(type_, registration_name, 80, 0, 0, desc,
"ash-2.local.", addresses=[address, address])
assert info.addresses == [address, address]
if socket.has_ipv6 and not os.environ.get('SKIP_IPV6'):
address_v6_parsed = "2001:db8::1"
address_v6 = socket.inet_pton(socket.AF_INET6, address_v6_parsed)
- info = ServiceInfo(type_, registration_name, [address, address_v6],
80, 0, 0, desc, "ash-2.local.")
+ info = ServiceInfo(
+ type_, registration_name, 80, 0, 0, desc, "ash-2.local.",
addresses=[address, address_v6],
+ )
assert info.addresses == [address]
assert info.addresses_by_version(r.IPVersion.All) == [address,
address_v6]
assert info.addresses_by_version(r.IPVersion.V4Only) == [address]
@@ -1382,7 +1514,9 @@
registration_name = "%s.%s" % (name, type_)
desc = {'path': '/~paulsm/'}
- info = ServiceInfo(type_, registration_name, socket.inet_aton("10.0.1.2"),
80, 0, 0, desc, "ash-2.local.")
+ info = ServiceInfo(
+ type_, registration_name, 80, 0, 0, desc, "ash-2.local.",
addresses=[socket.inet_aton("10.0.1.2")]
+ )
# we are going to monkey patch the zeroconf send to check packet sizes
old_send = zc.send