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


Reply via email to