[Python-modules-commits] [python-zeroconf] 01/05: Import python-zeroconf_0.17.6.orig.tar.gz

Ruben Undheim rubund-guest at moszumanska.debian.org
Thu Oct 6 17:11:19 UTC 2016


This is an automated email from the git hooks/post-receive script.

rubund-guest pushed a commit to branch master
in repository python-zeroconf.

commit e694928afacaf099988e8b2c6f40df196df18245
Author: Ruben Undheim <ruben.undheim at gmail.com>
Date:   Thu Oct 6 19:02:19 2016 +0200

    Import python-zeroconf_0.17.6.orig.tar.gz
---
 .gitignore       |  10 +
 .travis.yml      |   1 +
 README.rst       |  24 +++
 setup.cfg        |   1 +
 test_zeroconf.py | 226 ++++++++++++++++++--
 zeroconf.py      | 639 ++++++++++++++++++++++++++++++++++++-------------------
 6 files changed, 661 insertions(+), 240 deletions(-)

diff --git a/.gitignore b/.gitignore
index dc84959..264bddc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,12 @@
 build/
+*.pyc
+*.pyo
+Thumbs.db
+.DS_Store
+.project
+.pydevproject
+.settings
+.idea
+.vslick
+.cache
 
diff --git a/.travis.yml b/.travis.yml
index 3c09140..94f8d95 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,7 @@ python:
     - "2.7"
     - "3.3"
     - "3.4"
+    - "3.5"
     - "pypy"
     - "pypy3"
 matrix:
diff --git a/README.rst b/README.rst
index 3e812c8..314dba4 100644
--- a/README.rst
+++ b/README.rst
@@ -110,11 +110,33 @@ Here's an example:
     If you want to customize that you need to specify ``interfaces`` argument when
     constructing ``Zeroconf`` object (see the code for details).
 
+If you don't know the name of the service you need to browse for, try:
+
+.. code-block:: python
+
+    from zeroconf import ZeroconfServiceTypes
+    print('\n'.join(ZeroconfServiceTypes.find()))
+
 See examples directory for more.
 
 Changelog
 =========
 
+0.17.6
+------
+
+* Many improvements to address race conditions and exceptions during ZC()
+  startup and shutdown, thanks to: morpav, veawor, justingiorgi, herczy,
+  stephenrauch
+* Added more test coverage: strahlex, stephenrauch
+* Stephen Rauch contributed:
+
+  - Speed up browser startup
+  - Add ZeroconfServiceTypes() query class to discover all advertised service types
+  - Add full validation for service names, types and subtypes
+  - Fix for subtype browsing
+  - Fix DNSHInfo support
+
 0.17.5
 ------
 
@@ -223,6 +245,7 @@ Changelog
 ----
 
 * Jonathon Paisley contributed these corrections:
+
   - always multicast replies, even when query is unicast
   - correct a pointer encoding problem
   - can now write records in any order
@@ -230,6 +253,7 @@ Changelog
   - better TXT record parsing
   - server is now separate from name
   - can cancel a service browser
+  
 * modified some unit tests to accommodate these changes
 
 0.09
diff --git a/setup.cfg b/setup.cfg
index 51017e1..24b129b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,3 +5,4 @@ universal = 1
 show-source = 1
 import-order-style=google
 application-import-names=zeroconf
+max-line-length=110
diff --git a/test_zeroconf.py b/test_zeroconf.py
index 6ecf672..e0e368a 100644
--- a/test_zeroconf.py
+++ b/test_zeroconf.py
@@ -7,21 +7,22 @@
 import logging
 import socket
 import struct
+import time
 import unittest
 from threading import Event
 
-from mock import Mock
 from six import indexbytes
 from six.moves import xrange
 
 import zeroconf as r
 from zeroconf import (
+    DNSHinfo,
     DNSText,
-    Listener,
     ServiceBrowser,
     ServiceInfo,
     ServiceStateChange,
     Zeroconf,
+    ZeroconfServiceTypes,
 )
 
 log = logging.getLogger('zeroconf')
@@ -66,6 +67,19 @@ class PacketGeneration(unittest.TestCase):
         self.assertEqual(len(generated.questions), len(parsed.questions))
         self.assertEqual(question, parsed.questions[0])
 
+    def test_dns_hinfo(self):
+        generated = r.DNSOutgoing(0)
+        generated.add_additional_answer(
+            DNSHinfo('irrelevant', r._TYPE_HINFO, 0, 0, 'cpu', 'os'))
+        parsed = r.DNSIncoming(generated.packet())
+        self.assertEqual(parsed.answers[0].cpu, u'cpu')
+        self.assertEqual(parsed.answers[0].os, u'os')
+
+        generated = r.DNSOutgoing(0)
+        generated.add_additional_answer(
+            DNSHinfo('irrelevant', r._TYPE_HINFO, 0, 0, 'cpu', 'x' * 257))
+        self.assertRaises(r.NamePartTooLongException, generated.packet)
+
 
 class PacketForm(unittest.TestCase):
 
@@ -151,6 +165,198 @@ class Framework(unittest.TestCase):
         rv.close()
 
 
+class Exceptions(unittest.TestCase):
+
+    browser = None
+
+    @classmethod
+    def setUpClass(cls):
+        cls.browser = Zeroconf()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.browser.close()
+        cls.browser = None
+
+    def test_bad_service_info_name(self):
+        self.assertRaises(
+            r.BadTypeInNameException,
+            self.browser.get_service_info, "type", "type_not")
+
+    def test_bad_service_names(self):
+        bad_names_to_try = (
+            '',
+            'local',
+            '_tcp.local.',
+            '_udp.local.',
+            '._udp.local.',
+            '_ at ._tcp.local.',
+            '_A at ._tcp.local.',
+            '_x--x._tcp.local.',
+            '_-x._udp.local.',
+            '_x-._tcp.local.',
+            '_22._udp.local.',
+            '_2-2._tcp.local.',
+            '_1234567890-abcde._udp.local.',
+            '._x._udp.local.',
+        )
+        for name in bad_names_to_try:
+            self.assertRaises(
+                r.BadTypeInNameException,
+                self.browser.get_service_info, name, 'x.' + name)
+
+    def test_bad_sub_types(self):
+        bad_names_to_try = (
+            '_sub._http._tcp.local.',
+            'x.sub._http._tcp.local.',
+            'a' * 64 + '._sub._http._tcp.local.',
+            'a' * 62 + u'â._sub._http._tcp.local.',
+        )
+        for name in bad_names_to_try:
+            self.assertRaises(
+                r.BadTypeInNameException, r.service_type_name, name)
+
+    def test_good_service_names(self):
+        good_names_to_try = (
+            '_x._tcp.local.',
+            '_x._udp.local.',
+            '_12345-67890-abc._udp.local.',
+            'x._sub._http._tcp.local.',
+            'a' * 63 + '._sub._http._tcp.local.',
+            'a' * 61 + u'â._sub._http._tcp.local.',
+        )
+        for name in good_names_to_try:
+            r.service_type_name(name)
+
+
+class ServiceTypesQuery(unittest.TestCase):
+
+    def test_integration_with_listener(self):
+
+        type_ = "_test-srvc-type._tcp.local."
+        name = "xxxyyy"
+        registration_name = "%s.%s" % (name, type_)
+
+        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.")
+        zeroconf_registrar.register_service(info)
+
+        try:
+            service_types = ZeroconfServiceTypes.find(timeout=0.5)
+            assert type_ in service_types
+            service_types = ZeroconfServiceTypes.find(
+                zc=zeroconf_registrar, timeout=0.5)
+            assert type_ in service_types
+
+        finally:
+            zeroconf_registrar.close()
+
+    def test_integration_with_subtype_and_listener(self):
+        subtype_ = "_subtype._sub"
+        type_ = "_type._tcp.local."
+        name = "xxxyyy"
+        # Note: discovery returns only DNS-SD type not subtype
+        discovery_type = "%s.%s" % (subtype_, type_)
+        registration_name = "%s.%s" % (name, type_)
+
+        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.")
+        zeroconf_registrar.register_service(info)
+
+        try:
+            service_types = ZeroconfServiceTypes.find(timeout=0.5)
+            print(service_types)
+            assert discovery_type in service_types
+            service_types = ZeroconfServiceTypes.find(
+                zc=zeroconf_registrar, timeout=0.5)
+            assert discovery_type in service_types
+
+        finally:
+            zeroconf_registrar.close()
+
+
+class ListenerTest(unittest.TestCase):
+
+    def test_integration_with_listener_class(self):
+
+        service_added = Event()
+        service_removed = Event()
+
+        subtype_name = "My special Subtype"
+        type_ = "_http._tcp.local."
+        subtype = subtype_name + "._sub." + type_
+        name = "xxxyyy"
+        registration_name = "%s.%s" % (name, type_)
+
+        class MyListener(object):
+            def add_service(self, zeroconf, type, name):
+                zeroconf.get_service_info(type, name)
+                service_added.set()
+
+            def remove_service(self, zeroconf, type, name):
+                service_removed.set()
+
+        zeroconf_browser = Zeroconf()
+        zeroconf_browser.add_service_listener(subtype, MyListener())
+
+        properties = dict(
+            prop_none=None,
+            prop_string=b'a_prop',
+            prop_float=1.0,
+            prop_blank=b'a blanked string',
+            prop_true=1,
+            prop_false=0,
+        )
+
+        zeroconf_registrar = Zeroconf()
+        desc = {'path': '/~paulsm/'}
+        desc.update(properties)
+        info_service = ServiceInfo(
+            subtype, registration_name,
+            socket.inet_aton("10.0.1.2"), 80, 0, 0,
+            desc, "ash-2.local.")
+        zeroconf_registrar.register_service(info_service)
+
+        try:
+            service_added.wait(1)
+            assert service_added.is_set()
+
+            # short pause to allow multicast timers to expire
+            time.sleep(2)
+
+            # clear the answer cache to force query
+            for record in zeroconf_browser.cache.entries():
+                zeroconf_browser.cache.remove(record)
+
+            # get service info without answer cache
+            info = zeroconf_browser.get_service_info(type_, registration_name)
+
+            assert info.properties[b'prop_none'] is False
+            assert info.properties[b'prop_string'] == properties['prop_string']
+            assert info.properties[b'prop_float'] is False
+            assert info.properties[b'prop_blank'] == properties['prop_blank']
+            assert info.properties[b'prop_true'] is True
+            assert info.properties[b'prop_false'] is False
+
+            info = zeroconf_browser.get_service_info(subtype, registration_name)
+            assert info.properties[b'prop_none'] is False
+
+            zeroconf_registrar.unregister_service(info_service)
+            service_removed.wait(1)
+            assert service_removed.is_set()
+        finally:
+            zeroconf_registrar.close()
+            zeroconf_browser.close()
+
+
 def test_integration():
     service_added = Event()
     service_removed = Event()
@@ -179,26 +385,14 @@ def test_integration():
     try:
         service_added.wait(1)
         assert service_added.is_set()
-        zeroconf_registrar.unregister_service(info)
-        service_removed.wait(1)
-        assert service_removed.is_set()
+        # Don't remove service, allow close() to cleanup
+
     finally:
         zeroconf_registrar.close()
         browser.cancel()
         zeroconf_browser.close()
 
 
-def test_listener_handles_closed_socket_situation_gracefully():
-    error = socket.error(socket.EBADF)
-    error.errno = socket.EBADF
-
-    zeroconf = Mock()
-    zeroconf.socket.recvfrom.side_effect = error
-
-    listener = Listener(zeroconf)
-    listener.handle_read(zeroconf.socket)
-
-
 def test_dnstext_repr_works():
     # There was an issue on Python 3 that prevented DNSText's repr
     # from working when the text was longer than 10 bytes
diff --git a/zeroconf.py b/zeroconf.py
index 8c00d2b..d1211f7 100644
--- a/zeroconf.py
+++ b/zeroconf.py
@@ -1,4 +1,5 @@
-from __future__ import absolute_import, division, print_function, unicode_literals
+from __future__ import (
+    absolute_import, division, print_function, unicode_literals)
 
 """ Multicast DNS Service Discovery for Python, v0.14-wmcbrine
     Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
@@ -25,6 +26,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 import enum
 import errno
 import logging
+import re
 import select
 import socket
 import struct
@@ -38,7 +40,7 @@ from six.moves import xrange
 
 __author__ = 'Paul Scott-Murphy, William McBrine'
 __maintainer__ = 'Jakub Stasiak <jakub at stasiak.at>'
-__version__ = '0.17.5'
+__version__ = '0.17.6'
 __license__ = 'LGPL'
 
 
@@ -64,10 +66,6 @@ log.addHandler(NullHandler())
 if log.level == logging.NOTSET:
     log.setLevel(logging.WARN)
 
-# hook for threads
-
-_GLOBAL_DONE = False
-
 # Some timing constants
 
 _UNREGISTER_TIME = 125
@@ -157,6 +155,9 @@ _TYPES = {_TYPE_A: "a",
           _TYPE_SRV: "srv",
           _TYPE_ANY: "any"}
 
+_HAS_A_TO_Z = re.compile(r'[A-Za-z]')
+_HAS_ONLY_A_TO_Z_NUM_HYPHEN = re.compile(r'^[A-Za-z0-9\-]+$')
+
 # utility functions
 
 
@@ -164,6 +165,94 @@ def current_time_millis():
     """Current system time in milliseconds"""
     return time.time() * 1000
 
+
+def service_type_name(type_):
+    """
+    Validate a fully qualified service name, instance or subtype. [rfc6763]
+
+    Returns fully qualified service name.
+
+    Domain names used by mDNS-SD take the following forms:
+
+                   <sn> . <_tcp|_udp> . local.
+      <Instance> . <sn> . <_tcp|_udp> . local.
+      <sub>._sub . <sn> . <_tcp|_udp> . local.
+
+    1) must end with 'local.'
+
+      This is true because we are implementing mDNS and since the 'm' means
+      multi-cast, the 'local.' domain is mandatory.
+
+    2) local is preceded with either '_udp.' or '_tcp.'
+
+    3) service name <sn> precedes <_tcp|_udp>
+
+      The rules for Service Names [RFC6335] state that they may be no more
+      than fifteen characters long (not counting the mandatory underscore),
+      consisting of only letters, digits, and hyphens, must begin and end
+      with a letter or digit, must not contain consecutive hyphens, and
+      must contain at least one letter.
+
+    The instance name <Instance> and sub type <sub> may be up to 63 bytes.
+
+    :param type_: Type, SubType or service name to validate
+    :return: fully qualified service name (eg: _http._tcp.local.)
+    """
+    if not (type_.endswith('._tcp.local.') or type_.endswith('._udp.local.')):
+        raise BadTypeInNameException(
+            "Type must end with '._tcp.local.' or '._udp.local.'")
+
+    if type_.startswith('.'):
+        raise BadTypeInNameException("Type must not start with '.'")
+
+    remaining = type_[:-len('._tcp.local.')].split('.')
+    name = remaining.pop()
+    if not name:
+        raise BadTypeInNameException("No Service name found")
+
+    if name[0] != '_':
+        raise BadTypeInNameException("Service name must start with '_'")
+
+    # remove leading underscore
+    name = name[1:]
+
+    if len(name) > 15:
+        raise BadTypeInNameException("Service name must be <= 15 bytes")
+
+    if '--' in name:
+        raise BadTypeInNameException("Service name must not contain '--'")
+
+    if '-' in (name[0], name[-1]):
+        raise BadTypeInNameException(
+            "Service name may not start or end with '-'")
+
+    if not _HAS_A_TO_Z.search(name):
+        raise BadTypeInNameException(
+            "Service name must contain at least one letter (eg: 'A-Z')")
+
+    if not _HAS_ONLY_A_TO_Z_NUM_HYPHEN.search(name):
+        raise BadTypeInNameException(
+            "Service name must contain only these characters: "
+            "A-Z, a-z, 0-9, hyphen ('-')")
+
+    if remaining and remaining[-1] == '_sub':
+        remaining.pop()
+        if len(remaining) == 0:
+            raise BadTypeInNameException(
+                "_sub requires a subtype name")
+
+    if len(remaining) > 1:
+        raise BadTypeInNameException(
+            "Unexpected characters '%s.'" % '.'.join(remaining[1:]))
+
+    if remaining:
+        length = len(remaining[0].encode('utf-8'))
+        if length > 63:
+            raise BadTypeInNameException("Too long: '%s'" % remaining[0])
+
+    return '_' + name + type_[-len('._tcp.local.'):]
+
+
 # Exceptions
 
 
@@ -197,10 +286,10 @@ class DNSEntry(object):
 
     """A DNS entry"""
 
-    def __init__(self, name, type, class_):
+    def __init__(self, name, type_, class_):
         self.key = name.lower()
         self.name = name
-        self.type = type
+        self.type = type_
         self.class_ = class_ & _CLASS_MASK
         self.unique = (class_ & _CLASS_UNIQUE) != 0
 
@@ -215,11 +304,13 @@ class DNSEntry(object):
         """Non-equality test"""
         return not self.__eq__(other)
 
-    def get_class_(self, class_):
+    @staticmethod
+    def get_class_(class_):
         """Class accessor"""
         return _CLASSES.get(class_, "?(%s)" % class_)
 
-    def get_type(self, t):
+    @staticmethod
+    def get_type(t):
         """Type accessor"""
         return _TYPES.get(t, "?(%s)" % t)
 
@@ -233,7 +324,7 @@ class DNSEntry(object):
             result += ","
         result += self.name
         if other is not None:
-            result += ",%s]" % (other)
+            result += ",%s]" % other
         else:
             result += "]"
         return result
@@ -243,10 +334,8 @@ class DNSQuestion(DNSEntry):
 
     """A DNS question entry"""
 
-    def __init__(self, name, type, class_):
-        # if not name.endswith(".local."):
-        #    raise NonLocalNameException
-        DNSEntry.__init__(self, name, type, class_)
+    def __init__(self, name, type_, class_):
+        DNSEntry.__init__(self, name, type_, class_)
 
     def answered_by(self, rec):
         """Returns true if the question is answered by the record"""
@@ -263,8 +352,8 @@ class DNSRecord(DNSEntry):
 
     """A DNS record - like a DNS entry, but has a TTL"""
 
-    def __init__(self, name, type, class_, ttl):
-        DNSEntry.__init__(self, name, type, class_)
+    def __init__(self, name, type_, class_, ttl):
+        DNSEntry.__init__(self, name, type_, class_)
         self.ttl = ttl
         self.created = current_time_millis()
 
@@ -292,7 +381,7 @@ class DNSRecord(DNSEntry):
 
     def get_remaining_ttl(self, now):
         """Returns the remaining TTL in seconds."""
-        return max(0, (self.get_expiration_time(100) - now) / 1000)
+        return max(0, (self.get_expiration_time(100) - now) / 1000.0)
 
     def is_expired(self, now):
         """Returns true if this record has expired."""
@@ -313,9 +402,9 @@ class DNSRecord(DNSEntry):
         raise AbstractMethodException
 
     def to_string(self, other):
-        """String representation with addtional information"""
-        arg = "%s/%s,%s" % (self.ttl,
-                            self.get_remaining_ttl(current_time_millis()), other)
+        """String representation with additional information"""
+        arg = "%s/%s,%s" % (
+            self.ttl, self.get_remaining_ttl(current_time_millis()), other)
         return DNSEntry.to_string(self, "record", arg)
 
 
@@ -323,8 +412,8 @@ class DNSAddress(DNSRecord):
 
     """A DNS address record"""
 
-    def __init__(self, name, type, class_, ttl, address):
-        DNSRecord.__init__(self, name, type, class_, ttl)
+    def __init__(self, name, type_, class_, ttl, address):
+        DNSRecord.__init__(self, name, type_, class_, ttl)
         self.address = address
 
     def write(self, out):
@@ -348,15 +437,21 @@ class DNSHinfo(DNSRecord):
 
     """A DNS host information record"""
 
-    def __init__(self, name, type, class_, ttl, cpu, os):
-        DNSRecord.__init__(self, name, type, class_, ttl)
-        self.cpu = cpu
-        self.os = os
+    def __init__(self, name, type_, class_, ttl, cpu, os):
+        DNSRecord.__init__(self, name, type_, class_, ttl)
+        try:
+            self.cpu = cpu.decode('utf-8')
+        except AttributeError:
+            self.cpu = cpu
+        try:
+            self.os = os.decode('utf-8')
+        except AttributeError:
+            self.os = os
 
     def write(self, out):
         """Used in constructing an outgoing packet"""
-        out.write_string(self.cpu)
-        out.write_string(self.oso)
+        out.write_character_string(self.cpu.encode('utf-8'))
+        out.write_character_string(self.os.encode('utf-8'))
 
     def __eq__(self, other):
         """Tests equality on cpu and os"""
@@ -372,8 +467,8 @@ class DNSPointer(DNSRecord):
 
     """A DNS pointer record"""
 
-    def __init__(self, name, type, class_, ttl, alias):
-        DNSRecord.__init__(self, name, type, class_, ttl)
+    def __init__(self, name, type_, class_, ttl, alias):
+        DNSRecord.__init__(self, name, type_, class_, ttl)
         self.alias = alias
 
     def write(self, out):
@@ -418,8 +513,9 @@ class DNSService(DNSRecord):
 
     """A DNS service record"""
 
-    def __init__(self, name, type, class_, ttl, priority, weight, port, server):
-        DNSRecord.__init__(self, name, type, class_, ttl)
+    def __init__(self, name, type_, class_, ttl,
+                 priority, weight, port, server):
+        DNSRecord.__init__(self, name, type_, class_, ttl)
         self.priority = priority
         self.weight = weight
         self.port = port
@@ -455,6 +551,8 @@ class DNSIncoming(object):
         self.data = data
         self.questions = []
         self.answers = []
+        self.id = 0
+        self.flags = 0
         self.num_questions = 0
         self.num_answers = 0
         self.num_authorities = 0
@@ -464,24 +562,25 @@ class DNSIncoming(object):
         self.read_questions()
         self.read_others()
 
-    def unpack(self, format):
-        length = struct.calcsize(format)
-        info = struct.unpack(format, self.data[self.offset:self.offset + length])
+    def unpack(self, format_):
+        length = struct.calcsize(format_)
+        info = struct.unpack(
+            format_, self.data[self.offset:self.offset + length])
         self.offset += length
         return info
 
     def read_header(self):
         """Reads header portion of packet"""
         (self.id, self.flags, self.num_questions, self.num_answers,
-         self.num_quthorities, self.num_additionals) = self.unpack(b'!6H')
+         self.num_authorities, self.num_additionals) = self.unpack(b'!6H')
 
     def read_questions(self):
         """Reads questions section of packet"""
         for i in xrange(self.num_questions):
             name = self.read_name()
-            type, class_ = self.unpack(b'!HH')
+            type_, class_ = self.unpack(b'!HH')
 
-            question = DNSQuestion(name, type, class_)
+            question = DNSQuestion(name, type_, class_)
             self.questions.append(question)
 
     def read_int(self):
@@ -510,24 +609,30 @@ class DNSIncoming(object):
         n = self.num_answers + self.num_authorities + self.num_additionals
         for i in xrange(n):
             domain = self.read_name()
-            type, class_, ttl, length = self.unpack(b'!HHiH')
+            type_, class_, ttl, length = self.unpack(b'!HHiH')
 
             rec = None
-            if type == _TYPE_A:
-                rec = DNSAddress(domain, type, class_, ttl, self.read_string(4))
-            elif type == _TYPE_CNAME or type == _TYPE_PTR:
-                rec = DNSPointer(domain, type, class_, ttl, self.read_name())
-            elif type == _TYPE_TXT:
-                rec = DNSText(domain, type, class_, ttl, self.read_string(length))
-            elif type == _TYPE_SRV:
-                rec = DNSService(domain, type, class_, ttl,
-                                 self.read_unsigned_short(), self.read_unsigned_short(),
-                                 self.read_unsigned_short(), self.read_name())
-            elif type == _TYPE_HINFO:
-                rec = DNSHinfo(domain, type, class_, ttl,
-                               self.read_character_string(), self.read_character_string())
-            elif type == _TYPE_AAAA:
-                rec = DNSAddress(domain, type, class_, ttl, self.read_string(16))
+            if type_ == _TYPE_A:
+                rec = DNSAddress(
+                    domain, type_, class_, ttl, self.read_string(4))
+            elif type_ == _TYPE_CNAME or type_ == _TYPE_PTR:
+                rec = DNSPointer(
+                    domain, type_, class_, ttl, self.read_name())
+            elif type_ == _TYPE_TXT:
+                rec = DNSText(
+                    domain, type_, class_, ttl, self.read_string(length))
+            elif type_ == _TYPE_SRV:
+                rec = DNSService(
+                    domain, type_, class_, ttl,
+                    self.read_unsigned_short(), self.read_unsigned_short(),
+                    self.read_unsigned_short(), self.read_name())
+            elif type_ == _TYPE_HINFO:
+                rec = DNSHinfo(
+                    domain, type_, class_, ttl,
+                    self.read_character_string(), self.read_character_string())
+            elif type_ == _TYPE_AAAA:
+                rec = DNSAddress(
+                    domain, type_, class_, ttl, self.read_string(16))
             else:
                 # Try to ignore types we don't know about
                 # Skip the payload for the resource record so the next
@@ -553,7 +658,7 @@ class DNSIncoming(object):
         """Reads a domain name from the packet"""
         result = ''
         off = self.offset
-        next = -1
+        next_ = -1
         first = off
 
         while True:
@@ -566,8 +671,8 @@ class DNSIncoming(object):
                 result = ''.join((result, self.read_utf(off, length) + '.'))
                 off += length
             elif t == 0xC0:
-                if next < 0:
-                    next = off + 1
+                if next_ < 0:
+                    next_ = off + 1
                 off = ((length & 0x3F) << 8) | indexbytes(self.data, off)
                 if off >= first:
                     # TODO raise more specific exception
@@ -577,8 +682,8 @@ class DNSIncoming(object):
                 # TODO raise more specific exception
                 raise Exception("Bad domain name at %s" % (off,))
 
-        if next >= 0:
-            self.offset = next
+        if next_ >= 0:
+            self.offset = next_
         else:
             self.offset = off
 
@@ -626,9 +731,9 @@ class DNSOutgoing(object):
         """Adds an additional answer"""
         self.additionals.append(record)
 
-    def pack(self, format, value):
-        self.data.append(struct.pack(format, value))
-        self.size += struct.calcsize(format)
+    def pack(self, format_, value):
+        self.data.append(struct.pack(format_, value))
+        self.size += struct.calcsize(format_)
 
     def write_byte(self, value):
         """Writes a single byte to the packet"""
@@ -662,6 +767,14 @@ class DNSOutgoing(object):
         self.write_byte(length)
         self.write_string(utfstr)
 
+    def write_character_string(self, value):
+        assert isinstance(value, bytes)
+        length = len(value)
+        if length > 256:
+            raise NamePartTooLongException
+        self.write_byte(length)
+        self.write_string(value)
+
     def write_name(self, name):
         """Writes a domain name to the packet"""
 
@@ -768,14 +881,16 @@ class DNSCache(object):
         matching entry."""
         try:
             list_ = self.cache[entry.key]
-            return list_[list_.index(entry)]
+            for cached_entry in list_:
+                if entry.__eq__(cached_entry):
+                    return cached_entry
         except (KeyError, ValueError):
             return None
 
-    def get_by_details(self, name, type, class_):
+    def get_by_details(self, name, type_, class_):
         """Gets an entry by details.  Will return None if there is
         no matching entry."""
-        entry = DNSEntry(name, type, class_)
+        entry = DNSEntry(name, type_, class_)
         return self.get(entry)
 
     def entries_with_name(self, name):
@@ -790,7 +905,7 @@ class DNSCache(object):
         if not self.cache:
             return []
         else:
-            # copy the cache before running the reduce, to avoid size change during iteration
+            # avoid size change during iteration by copying the cache
             values = list(self.cache.values())
             return reduce(lambda a, b: a + b, values)
 
@@ -809,7 +924,7 @@ class Engine(threading.Thread):
     """
 
     def __init__(self, zc):
-        threading.Thread.__init__(self)
+        threading.Thread.__init__(self, name='zeroconf-Engine')
         self.daemon = True
         self.zc = zc
         self.readers = {}  # maps socket to reader
@@ -818,43 +933,37 @@ class Engine(threading.Thread):
         self.start()
 
     def run(self):
-        while not _GLOBAL_DONE:
-            rs = self.get_readers()
-            if len(rs) == 0:
-                # No sockets to manage, but we wait for the timeout
-                # or addition of a socket
-                #
-                with self.condition:
+        while not self.zc.done:
+            with self.condition:
+                rs = self.readers.keys()
+                if len(rs) == 0:
+                    # No sockets to manage, but we wait for the timeout
+                    # or addition of a socket
                     self.condition.wait(self.timeout)
-            else:
+
+            if len(rs) != 0:
                 try:
                     rr, wr, er = select.select(rs, [], [], self.timeout)
-                    for socket_ in rr:
-                        try:
-                            self.readers[socket_].handle_read(socket_)
-                        except Exception as e:  # TODO stop catching all Exceptions
-                            log.exception('Unknown error, possibly benign: %r', e)
-                except Exception as e:  # TODO stop catching all Exceptions
-                    log.exception('Unknown error, possibly benign: %r', e)
-
-    def get_readers(self):
-        result = []
-        with self.condition:
-            result = self.readers.keys()
-        return result
-
-    def add_reader(self, reader, socket):
+                    if not self.zc.done:
+                        for socket_ in rr:
+                            reader = self.readers.get(socket_)
+                            if reader:
+                                reader.handle_read(socket_)
+
+                except socket.error as e:
+                    # If the socket was closed by another thread, during
+                    # shutdown, ignore it and exit
+                    if e.errno != socket.EBADF or not self.zc.done:
+                        raise
+
+    def add_reader(self, reader, socket_):
         with self.condition:
-            self.readers[socket] = reader
+            self.readers[socket_] = reader
             self.condition.notify()
 
-    def del_reader(self, socket):
-        with self.condition:
-            del self.readers[socket]
-            self.condition.notify()
-
-    def notify(self):
+    def del_reader(self, socket_):
         with self.condition:
+            del self.readers[socket_]
             self.condition.notify()
 
 
@@ -865,24 +974,15 @@ class Listener(object):
     to cache information as it arrives.
 
     It requires registration with an Engine object in order to have
-    the read() method called when a socket is availble for reading."""
+    the read() method called when a socket is available for reading."""
 
     def __init__(self, zc):
         self.zc = zc
+        self.data = None
 
     def handle_read(self, socket_):
-        try:
-            data, (addr, port) = socket_.recvfrom(_MAX_MSG_ABSOLUTE)
-        except socket.error as e:
-            # If the socket was closed by another thread -- which happens
-            # regularly on shutdown -- an EBADF exception is thrown here.
-            # Ignore it.
-            if e.errno == socket.EBADF:
-                return
-            else:
-                raise e
-        else:
-            log.debug('Received %r from %r:%r', data, addr, port)
+        data, (addr, port) = socket_.recvfrom(_MAX_MSG_ABSOLUTE)
+        log.debug('Received %r from %r:%r', data, addr, port)
 
         self.data = data
         msg = DNSIncoming(data)
@@ -907,7 +1007,7 @@ class Reaper(threading.Thread):
     have expired."""
 
     def __init__(self, zc):
-        threading.Thread.__init__(self)
+        threading.Thread.__init__(self, name='zeroconf-Reaper')
         self.daemon = True
         self.zc = zc
         self.start()
@@ -915,7 +1015,7 @@ class Reaper(threading.Thread):
     def run(self):
         while True:
             self.zc.wait(10 * 1000)
-            if _GLOBAL_DONE:
+            if self.zc.done:
                 return
             now = current_time_millis()
             for record in self.zc.cache.entries():
@@ -962,7 +1062,10 @@ class ServiceBrowser(threading.Thread):
     def __init__(self, zc, type_, handlers=None, listener=None):
         """Creates a browser for a specific type"""
         assert handlers or listener, 'You need to specify at least one handler'
-        threading.Thread.__init__(self)
+        if not type_.endswith(service_type_name(type_)):
+            raise BadTypeInNameException
+        threading.Thread.__init__(
+            self, name='zeroconf-ServiceBrowser_' + type_)
         self.daemon = True
         self.zc = zc
         self.type = type_
@@ -971,12 +1074,10 @@ class ServiceBrowser(threading.Thread):
         self.delay = _BROWSER_TIME
         self._handlers_to_call = []
 
-        self.done = False
-
-        self.zc.add_listener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
-
         self._service_state_changed = Signal()
 
+        self.done = False
+
         if hasattr(handlers, 'add_service'):
             listener = handlers
             handlers = None
... 541 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-zeroconf.git



More information about the Python-modules-commits mailing list