allow 2 channel AAC audio to transfer (without) reencoding while
[pyTivo/wmcbrine/lucasnz.git] / Zeroconf.py
bloba6b3a031a6a3c7469e441d2b6a057760ddf195bb
1 """ Multicast DNS Service Discovery for Python, v0.12
2 Copyright (C) 2003, Paul Scott-Murphy
4 This module provides a framework for the use of DNS Service Discovery
5 using IP multicast. It has been tested against the JRendezvous
6 implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
7 and against the mDNSResponder from Mac OS X 10.3.8.
9 This library is free software; you can redistribute it and/or
10 modify it under the terms of the GNU Lesser General Public
11 License as published by the Free Software Foundation; either
12 version 2.1 of the License, or (at your option) any later version.
14 This library is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 Lesser General Public License for more details.
19 You should have received a copy of the GNU Lesser General Public
20 License along with this library; if not, write to the Free Software
21 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 """
25 """0.12 update - allow selection of binding interface
26 typo fix - Thanks A. M. Kuchlingi
27 removed all use of word 'Rendezvous' - this is an API change"""
29 """0.11 update - correction to comments for addListener method
30 support for new record types seen from OS X
31 - IPv6 address
32 - hostinfo
33 ignore unknown DNS record types
34 fixes to name decoding
35 works alongside other processes using port 5353
36 (e.g. on Mac OS X)
37 tested against Mac OS X 10.3.2's mDNSResponder
38 corrections to removal of list entries for service browser"""
40 """0.10 update - Jonathon Paisley contributed these corrections:
41 always multicast replies, even when query is unicast
42 correct a pointer encoding problem
43 can now write records in any order
44 traceback shown on failure
45 better TXT record parsing
46 server is now separate from name
47 can cancel a service browser
49 modified some unit tests to accommodate these changes"""
51 """0.09 update - remove all records on service unregistration
52 fix DOS security problem with readName"""
54 """0.08 update - changed licensing to LGPL"""
56 """0.07 update - faster shutdown on engine
57 pointer encoding of outgoing names
58 ServiceBrowser now works
59 new unit tests"""
61 """0.06 update - small improvements with unit tests
62 added defined exception types
63 new style objects
64 fixed hostname/interface problem
65 fixed socket timeout problem
66 fixed addServiceListener() typo bug
67 using select() for socket reads
68 tested on Debian unstable with Python 2.2.2"""
70 """0.05 update - ensure case insensitivty on domain names
71 support for unicast DNS queries"""
73 """0.04 update - added some unit tests
74 added __ne__ adjuncts where required
75 ensure names end in '.local.'
76 timeout on receiving socket for clean shutdown"""
78 __author__ = "Paul Scott-Murphy"
79 __email__ = "paul at scott dash murphy dot com"
80 __version__ = "0.12"
82 import time
83 import struct
84 import socket
85 import threading
86 import select
87 import traceback
89 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
91 # hook for threads
93 _GLOBAL_DONE = False
95 # Some timing constants
97 _UNREGISTER_TIME = 125
98 _CHECK_TIME = 175
99 _REGISTER_TIME = 225
100 _LISTENER_TIME = 200
101 _BROWSER_TIME = 500
103 # Some DNS constants
105 _MDNS_ADDR = '224.0.0.251'
106 _MDNS_PORT = 5353;
107 _DNS_PORT = 53;
108 _DNS_TTL = 60 * 60; # one hour default TTL
110 _MAX_MSG_TYPICAL = 1460 # unused
111 _MAX_MSG_ABSOLUTE = 8972
113 _FLAGS_QR_MASK = 0x8000 # query response mask
114 _FLAGS_QR_QUERY = 0x0000 # query
115 _FLAGS_QR_RESPONSE = 0x8000 # response
117 _FLAGS_AA = 0x0400 # Authorative answer
118 _FLAGS_TC = 0x0200 # Truncated
119 _FLAGS_RD = 0x0100 # Recursion desired
120 _FLAGS_RA = 0x8000 # Recursion available
122 _FLAGS_Z = 0x0040 # Zero
123 _FLAGS_AD = 0x0020 # Authentic data
124 _FLAGS_CD = 0x0010 # Checking disabled
126 _CLASS_IN = 1
127 _CLASS_CS = 2
128 _CLASS_CH = 3
129 _CLASS_HS = 4
130 _CLASS_NONE = 254
131 _CLASS_ANY = 255
132 _CLASS_MASK = 0x7FFF
133 _CLASS_UNIQUE = 0x8000
135 _TYPE_A = 1
136 _TYPE_NS = 2
137 _TYPE_MD = 3
138 _TYPE_MF = 4
139 _TYPE_CNAME = 5
140 _TYPE_SOA = 6
141 _TYPE_MB = 7
142 _TYPE_MG = 8
143 _TYPE_MR = 9
144 _TYPE_NULL = 10
145 _TYPE_WKS = 11
146 _TYPE_PTR = 12
147 _TYPE_HINFO = 13
148 _TYPE_MINFO = 14
149 _TYPE_MX = 15
150 _TYPE_TXT = 16
151 _TYPE_AAAA = 28
152 _TYPE_SRV = 33
153 _TYPE_ANY = 255
155 # Mapping constants to names
157 _CLASSES = { _CLASS_IN : "in",
158 _CLASS_CS : "cs",
159 _CLASS_CH : "ch",
160 _CLASS_HS : "hs",
161 _CLASS_NONE : "none",
162 _CLASS_ANY : "any" }
164 _TYPES = { _TYPE_A : "a",
165 _TYPE_NS : "ns",
166 _TYPE_MD : "md",
167 _TYPE_MF : "mf",
168 _TYPE_CNAME : "cname",
169 _TYPE_SOA : "soa",
170 _TYPE_MB : "mb",
171 _TYPE_MG : "mg",
172 _TYPE_MR : "mr",
173 _TYPE_NULL : "null",
174 _TYPE_WKS : "wks",
175 _TYPE_PTR : "ptr",
176 _TYPE_HINFO : "hinfo",
177 _TYPE_MINFO : "minfo",
178 _TYPE_MX : "mx",
179 _TYPE_TXT : "txt",
180 _TYPE_AAAA : "quada",
181 _TYPE_SRV : "srv",
182 _TYPE_ANY : "any" }
184 # utility functions
186 def currentTimeMillis():
187 """Current system time in milliseconds"""
188 return time.time() * 1000
190 # Exceptions
192 class NonLocalNameException(Exception):
193 pass
195 class NonUniqueNameException(Exception):
196 pass
198 class NamePartTooLongException(Exception):
199 pass
201 class AbstractMethodException(Exception):
202 pass
204 class BadTypeInNameException(Exception):
205 pass
207 # implementation classes
209 class DNSEntry(object):
210 """A DNS entry"""
212 def __init__(self, name, type, clazz):
213 self.key = name.lower()
214 self.name = name
215 self.type = type
216 self.clazz = clazz & _CLASS_MASK
217 self.unique = (clazz & _CLASS_UNIQUE) != 0
219 def __eq__(self, other):
220 """Equality test on name, type, and class"""
221 return (isinstance(other, DNSEntry) and
222 self.name == other.name and
223 self.type == other.type and
224 self.clazz == other.clazz)
226 def __ne__(self, other):
227 """Non-equality test"""
228 return not self.__eq__(other)
230 def getClazz(self, clazz):
231 """Class accessor"""
232 try:
233 return _CLASSES[clazz]
234 except:
235 return "?(%s)" % (clazz)
237 def getType(self, type):
238 """Type accessor"""
239 try:
240 return _TYPES[type]
241 except:
242 return "?(%s)" % (type)
244 def toString(self, hdr, other):
245 """String representation with additional information"""
246 result = "%s[%s,%s" % (hdr, self.getType(self.type),
247 self.getClazz(self.clazz))
248 if self.unique:
249 result += "-unique,"
250 else:
251 result += ","
252 result += self.name
253 if other is not None:
254 result += ",%s]" % (other)
255 else:
256 result += "]"
257 return result
259 class DNSQuestion(DNSEntry):
260 """A DNS question entry"""
262 def __init__(self, name, type, clazz):
263 #if not name.endswith(".local."):
264 # raise NonLocalNameException
265 DNSEntry.__init__(self, name, type, clazz)
267 def answeredBy(self, rec):
268 """Returns true if the question is answered by the record"""
269 return (self.clazz == rec.clazz and
270 (self.type == rec.type or self.type == _TYPE_ANY) and
271 self.name == rec.name)
273 def __repr__(self):
274 """String representation"""
275 return DNSEntry.toString(self, "question", None)
278 class DNSRecord(DNSEntry):
279 """A DNS record - like a DNS entry, but has a TTL"""
281 def __init__(self, name, type, clazz, ttl):
282 DNSEntry.__init__(self, name, type, clazz)
283 self.ttl = ttl
284 self.created = currentTimeMillis()
286 def __eq__(self, other):
287 """Tests equality as per DNSRecord"""
288 return isinstance(other, DNSRecord) and DNSEntry.__eq__(self, other)
290 def suppressedBy(self, msg):
291 """Returns true if any answer in a message can suffice for the
292 information held in this record."""
293 for record in msg.answers:
294 if self.suppressedByAnswer(record):
295 return True
296 return False
298 def suppressedByAnswer(self, other):
299 """Returns true if another record has same name, type and class,
300 and if its TTL is at least half of this record's."""
301 return self == other and other.ttl > (self.ttl / 2)
303 def getExpirationTime(self, percent):
304 """Returns the time at which this record will have expired
305 by a certain percentage."""
306 return self.created + (percent * self.ttl * 10)
308 def getRemainingTTL(self, now):
309 """Returns the remaining TTL in seconds."""
310 return max(0, (self.getExpirationTime(100) - now) / 1000)
312 def isExpired(self, now):
313 """Returns true if this record has expired."""
314 return self.getExpirationTime(100) <= now
316 def isStale(self, now):
317 """Returns true if this record is at least half way expired."""
318 return self.getExpirationTime(50) <= now
320 def resetTTL(self, other):
321 """Sets this record's TTL and created time to that of
322 another record."""
323 self.created = other.created
324 self.ttl = other.ttl
326 def write(self, out):
327 """Abstract method"""
328 raise AbstractMethodException
330 def toString(self, other):
331 """String representation with addtional information"""
332 arg = "%s/%s,%s" % (self.ttl,
333 self.getRemainingTTL(currentTimeMillis()), other)
334 return DNSEntry.toString(self, "record", arg)
336 class DNSAddress(DNSRecord):
337 """A DNS address record"""
339 def __init__(self, name, type, clazz, ttl, address):
340 DNSRecord.__init__(self, name, type, clazz, ttl)
341 self.address = address
343 def write(self, out):
344 """Used in constructing an outgoing packet"""
345 out.writeString(self.address)
347 def __eq__(self, other):
348 """Tests equality on address"""
349 return isinstance(other, DNSAddress) and self.address == other.address
351 def __repr__(self):
352 """String representation"""
353 try:
354 return socket.inet_ntoa(self.address)
355 except:
356 return self.address
358 class DNSHinfo(DNSRecord):
359 """A DNS host information record"""
361 def __init__(self, name, type, clazz, ttl, cpu, os):
362 DNSRecord.__init__(self, name, type, clazz, ttl)
363 self.cpu = cpu
364 self.os = os
366 def write(self, out):
367 """Used in constructing an outgoing packet"""
368 out.writeString(self.cpu)
369 out.writeString(self.oso)
371 def __eq__(self, other):
372 """Tests equality on cpu and os"""
373 return (isinstance(other, DNSHinfo) and
374 self.cpu == other.cpu and self.os == other.os)
376 def __repr__(self):
377 """String representation"""
378 return self.cpu + " " + self.os
380 class DNSPointer(DNSRecord):
381 """A DNS pointer record"""
383 def __init__(self, name, type, clazz, ttl, alias):
384 DNSRecord.__init__(self, name, type, clazz, ttl)
385 self.alias = alias
387 def write(self, out):
388 """Used in constructing an outgoing packet"""
389 out.writeName(self.alias)
391 def __eq__(self, other):
392 """Tests equality on alias"""
393 return isinstance(other, DNSPointer) and self.alias == other.alias
395 def __repr__(self):
396 """String representation"""
397 return self.toString(self.alias)
399 class DNSText(DNSRecord):
400 """A DNS text record"""
402 def __init__(self, name, type, clazz, ttl, text):
403 DNSRecord.__init__(self, name, type, clazz, ttl)
404 self.text = text
406 def write(self, out):
407 """Used in constructing an outgoing packet"""
408 out.writeString(self.text)
410 def __eq__(self, other):
411 """Tests equality on text"""
412 return isinstance(other, DNSText) and self.text == other.text
414 def __repr__(self):
415 """String representation"""
416 if len(self.text) > 10:
417 return self.toString(self.text[:7] + "...")
418 else:
419 return self.toString(self.text)
421 class DNSService(DNSRecord):
422 """A DNS service record"""
424 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
425 DNSRecord.__init__(self, name, type, clazz, ttl)
426 self.priority = priority
427 self.weight = weight
428 self.port = port
429 self.server = server
431 def write(self, out):
432 """Used in constructing an outgoing packet"""
433 out.writeShort(self.priority)
434 out.writeShort(self.weight)
435 out.writeShort(self.port)
436 out.writeName(self.server)
438 def __eq__(self, other):
439 """Tests equality on priority, weight, port and server"""
440 return (isinstance(other, DNSService) and
441 self.priority == other.priority and
442 self.weight == other.weight and
443 self.port == other.port and
444 self.server == other.server)
446 def __repr__(self):
447 """String representation"""
448 return self.toString("%s:%s" % (self.server, self.port))
450 class DNSIncoming(object):
451 """Object representation of an incoming DNS packet"""
453 def __init__(self, data):
454 """Constructor from string holding bytes of packet"""
455 self.offset = 0
456 self.data = data
457 self.questions = []
458 self.answers = []
459 self.numQuestions = 0
460 self.numAnswers = 0
461 self.numAuthorities = 0
462 self.numAdditionals = 0
464 self.readHeader()
465 self.readQuestions()
466 self.readOthers()
468 def unpack(self, format):
469 length = struct.calcsize(format)
470 info = struct.unpack(format, self.data[self.offset:self.offset+length])
471 self.offset += length
472 return info
474 def readHeader(self):
475 """Reads header portion of packet"""
476 (self.id, self.flags, self.numQuestions, self.numAnswers,
477 self.numAuthorities, self.numAdditionals) = self.unpack('!HHHHHH')
479 def readQuestions(self):
480 """Reads questions section of packet"""
481 for i in xrange(self.numQuestions):
482 name = self.readName()
483 type, clazz = self.unpack('!HH')
485 question = DNSQuestion(name, type, clazz)
486 self.questions.append(question)
488 def readInt(self):
489 """Reads an integer from the packet"""
490 return self.unpack('!I')[0]
492 def readCharacterString(self):
493 """Reads a character string from the packet"""
494 length = ord(self.data[self.offset])
495 self.offset += 1
496 return self.readString(length)
498 def readString(self, length):
499 """Reads a string of a given length from the packet"""
500 info = self.data[self.offset:self.offset+length]
501 self.offset += length
502 return info
504 def readUnsignedShort(self):
505 """Reads an unsigned short from the packet"""
506 return self.unpack('!H')[0]
508 def readOthers(self):
509 """Reads the answers, authorities and additionals section of the
510 packet"""
511 n = self.numAnswers + self.numAuthorities + self.numAdditionals
512 for i in xrange(n):
513 domain = self.readName()
514 type, clazz, ttl, length = self.unpack('!HHiH')
516 rec = None
517 if type == _TYPE_A:
518 rec = DNSAddress(domain, type, clazz, ttl, self.readString(4))
519 elif type == _TYPE_CNAME or type == _TYPE_PTR:
520 rec = DNSPointer(domain, type, clazz, ttl, self.readName())
521 elif type == _TYPE_TXT:
522 rec = DNSText(domain, type, clazz, ttl, self.readString(length))
523 elif type == _TYPE_SRV:
524 rec = DNSService(domain, type, clazz, ttl,
525 self.readUnsignedShort(), self.readUnsignedShort(),
526 self.readUnsignedShort(), self.readName())
527 elif type == _TYPE_HINFO:
528 rec = DNSHinfo(domain, type, clazz, ttl,
529 self.readCharacterString(), self.readCharacterString())
530 elif type == _TYPE_AAAA:
531 rec = DNSAddress(domain, type, clazz, ttl, self.readString(16))
532 else:
533 # Try to ignore types we don't know about
534 # Skip the payload for the resource record so the next
535 # records can be parsed correctly
536 self.offset += length
538 if rec is not None:
539 self.answers.append(rec)
541 def isQuery(self):
542 """Returns true if this is a query"""
543 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
545 def isResponse(self):
546 """Returns true if this is a response"""
547 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
549 def readUTF(self, offset, len):
550 """Reads a UTF-8 string of a given length from the packet"""
551 return unicode(self.data[offset:offset+len], 'utf-8', 'replace')
553 def readName(self):
554 """Reads a domain name from the packet"""
555 result = ''
556 off = self.offset
557 next = -1
558 first = off
560 while True:
561 len = ord(self.data[off])
562 off += 1
563 if len == 0:
564 break
565 t = len & 0xC0
566 if t == 0x00:
567 result = ''.join((result, self.readUTF(off, len) + '.'))
568 off += len
569 elif t == 0xC0:
570 if next < 0:
571 next = off + 1
572 off = ((len & 0x3F) << 8) | ord(self.data[off])
573 if off >= first:
574 raise "Bad domain name (circular) at " + str(off)
575 first = off
576 else:
577 raise "Bad domain name at " + str(off)
579 if next >= 0:
580 self.offset = next
581 else:
582 self.offset = off
584 return result
587 class DNSOutgoing(object):
588 """Object representation of an outgoing packet"""
590 def __init__(self, flags, multicast=True):
591 self.finished = False
592 self.id = 0
593 self.multicast = multicast
594 self.flags = flags
595 self.names = {}
596 self.data = []
597 self.size = 12
599 self.questions = []
600 self.answers = []
601 self.authorities = []
602 self.additionals = []
604 def addQuestion(self, record):
605 """Adds a question"""
606 self.questions.append(record)
608 def addAnswer(self, inp, record):
609 """Adds an answer"""
610 if not record.suppressedBy(inp):
611 self.addAnswerAtTime(record, 0)
613 def addAnswerAtTime(self, record, now):
614 """Adds an answer if if does not expire by a certain time"""
615 if record is not None:
616 if now == 0 or not record.isExpired(now):
617 self.answers.append((record, now))
619 def addAuthorativeAnswer(self, record):
620 """Adds an authoritative answer"""
621 self.authorities.append(record)
623 def addAdditionalAnswer(self, record):
624 """Adds an additional answer"""
625 self.additionals.append(record)
627 def pack(self, format, value):
628 self.data.append(struct.pack(format, value))
629 self.size += struct.calcsize(format)
631 def writeByte(self, value):
632 """Writes a single byte to the packet"""
633 self.pack('!c', chr(value))
635 def insertShort(self, index, value):
636 """Inserts an unsigned short in a certain position in the packet"""
637 self.data.insert(index, struct.pack('!H', value))
638 self.size += 2
640 def writeShort(self, value):
641 """Writes an unsigned short to the packet"""
642 self.pack('!H', value)
644 def writeInt(self, value):
645 """Writes an unsigned integer to the packet"""
646 self.pack('!I', int(value))
648 def writeString(self, value):
649 """Writes a string to the packet"""
650 self.data.append(value)
651 self.size += len(value)
653 def writeUTF(self, s):
654 """Writes a UTF-8 string of a given length to the packet"""
655 utfstr = s.encode('utf-8')
656 length = len(utfstr)
657 if length > 64:
658 raise NamePartTooLongException
659 self.writeByte(length)
660 self.writeString(utfstr)
662 def writeName(self, name):
663 """Writes a domain name to the packet"""
665 try:
666 # Find existing instance of this name in packet
668 index = self.names[name]
669 except KeyError:
670 # No record of this name already, so write it
671 # out as normal, recording the location of the name
672 # for future pointers to it.
674 self.names[name] = self.size
675 parts = name.split('.')
676 if parts[-1] == '':
677 parts = parts[:-1]
678 for part in parts:
679 self.writeUTF(part)
680 self.writeByte(0)
681 return
683 # An index was found, so write a pointer to it
685 self.writeByte((index >> 8) | 0xC0)
686 self.writeByte(index)
688 def writeQuestion(self, question):
689 """Writes a question to the packet"""
690 self.writeName(question.name)
691 self.writeShort(question.type)
692 self.writeShort(question.clazz)
694 def writeRecord(self, record, now):
695 """Writes a record (answer, authoritative answer, additional) to
696 the packet"""
697 self.writeName(record.name)
698 self.writeShort(record.type)
699 if record.unique and self.multicast:
700 self.writeShort(record.clazz | _CLASS_UNIQUE)
701 else:
702 self.writeShort(record.clazz)
703 if now == 0:
704 self.writeInt(record.ttl)
705 else:
706 self.writeInt(record.getRemainingTTL(now))
707 index = len(self.data)
708 # Adjust size for the short we will write before this record
710 self.size += 2
711 record.write(self)
712 self.size -= 2
714 length = len(''.join(self.data[index:]))
715 self.insertShort(index, length) # Here is the short we adjusted for
717 def packet(self):
718 """Returns a string containing the packet's bytes
720 No further parts should be added to the packet once this
721 is done."""
722 if not self.finished:
723 self.finished = True
724 for question in self.questions:
725 self.writeQuestion(question)
726 for answer, time in self.answers:
727 self.writeRecord(answer, time)
728 for authority in self.authorities:
729 self.writeRecord(authority, 0)
730 for additional in self.additionals:
731 self.writeRecord(additional, 0)
733 self.insertShort(0, len(self.additionals))
734 self.insertShort(0, len(self.authorities))
735 self.insertShort(0, len(self.answers))
736 self.insertShort(0, len(self.questions))
737 self.insertShort(0, self.flags)
738 if self.multicast:
739 self.insertShort(0, 0)
740 else:
741 self.insertShort(0, self.id)
742 return ''.join(self.data)
745 class DNSCache(object):
746 """A cache of DNS entries"""
748 def __init__(self):
749 self.cache = {}
751 def add(self, entry):
752 """Adds an entry"""
753 try:
754 list = self.cache[entry.key]
755 except:
756 list = self.cache[entry.key] = []
757 list.append(entry)
759 def remove(self, entry):
760 """Removes an entry"""
761 try:
762 list = self.cache[entry.key]
763 list.remove(entry)
764 except:
765 pass
767 def get(self, entry):
768 """Gets an entry by key. Will return None if there is no
769 matching entry."""
770 try:
771 list = self.cache[entry.key]
772 return list[list.index(entry)]
773 except:
774 return None
776 def getByDetails(self, name, type, clazz):
777 """Gets an entry by details. Will return None if there is
778 no matching entry."""
779 entry = DNSEntry(name, type, clazz)
780 return self.get(entry)
782 def entriesWithName(self, name):
783 """Returns a list of entries whose key matches the name."""
784 try:
785 return self.cache[name]
786 except:
787 return []
789 def entries(self):
790 """Returns a list of all entries"""
791 def add(x, y): return x+y
792 try:
793 return reduce(add, self.cache.values())
794 except:
795 return []
798 class Engine(threading.Thread):
799 """An engine wraps read access to sockets, allowing objects that
800 need to receive data from sockets to be called back when the
801 sockets are ready.
803 A reader needs a handle_read() method, which is called when the socket
804 it is interested in is ready for reading.
806 Writers are not implemented here, because we only send short
807 packets.
810 def __init__(self, zc):
811 threading.Thread.__init__(self)
812 self.zc = zc
813 self.readers = {} # maps socket to reader
814 self.timeout = 5
815 self.condition = threading.Condition()
816 self.start()
818 def run(self):
819 while not _GLOBAL_DONE:
820 rs = self.getReaders()
821 if len(rs) == 0:
822 # No sockets to manage, but we wait for the timeout
823 # or addition of a socket
825 self.condition.acquire()
826 self.condition.wait(self.timeout)
827 self.condition.release()
828 else:
829 try:
830 rr, wr, er = select.select(rs, [], [], self.timeout)
831 for socket in rr:
832 try:
833 self.readers[socket].handle_read()
834 except:
835 traceback.print_exc()
836 except:
837 pass
839 def getReaders(self):
840 result = []
841 self.condition.acquire()
842 result = self.readers.keys()
843 self.condition.release()
844 return result
846 def addReader(self, reader, socket):
847 self.condition.acquire()
848 self.readers[socket] = reader
849 self.condition.notify()
850 self.condition.release()
852 def delReader(self, socket):
853 self.condition.acquire()
854 del(self.readers[socket])
855 self.condition.notify()
856 self.condition.release()
858 def notify(self):
859 self.condition.acquire()
860 self.condition.notify()
861 self.condition.release()
863 class Listener(object):
864 """A Listener is used by this module to listen on the multicast
865 group to which DNS messages are sent, allowing the implementation
866 to cache information as it arrives.
868 It requires registration with an Engine object in order to have
869 the read() method called when a socket is availble for reading."""
871 def __init__(self, zc):
872 self.zc = zc
873 self.zc.engine.addReader(self, self.zc.socket)
875 def handle_read(self):
876 try:
877 data, (addr, port) = self.zc.socket.recvfrom(_MAX_MSG_ABSOLUTE)
878 except socket.error, e:
879 # If the socket was closed by another thread -- which happens
880 # regularly on shutdown -- an EBADF exception is thrown here.
881 # Ignore it.
882 if e[0] == socket.EBADF:
883 return
884 else:
885 raise e
886 self.data = data
887 msg = DNSIncoming(data)
888 if msg.isQuery():
889 # Always multicast responses
891 if port == _MDNS_PORT:
892 self.zc.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
893 # If it's not a multicast query, reply via unicast
894 # and multicast
896 elif port == _DNS_PORT:
897 self.zc.handleQuery(msg, addr, port)
898 self.zc.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
899 else:
900 self.zc.handleResponse(msg)
903 class Reaper(threading.Thread):
904 """A Reaper is used by this module to remove cache entries that
905 have expired."""
907 def __init__(self, zc):
908 threading.Thread.__init__(self)
909 self.zc = zc
910 self.start()
912 def run(self):
913 while True:
914 self.zc.wait(10 * 1000)
915 if _GLOBAL_DONE:
916 return
917 now = currentTimeMillis()
918 for record in self.zc.cache.entries():
919 if record.isExpired(now):
920 self.zc.updateRecord(now, record)
921 self.zc.cache.remove(record)
924 class ServiceBrowser(threading.Thread):
925 """Used to browse for a service of a specific type.
927 The listener object will have its addService() and
928 removeService() methods called when this browser
929 discovers changes in the services availability."""
931 def __init__(self, zc, type, listener):
932 """Creates a browser for a specific type"""
933 threading.Thread.__init__(self)
934 self.zc = zc
935 self.type = type
936 self.listener = listener
937 self.services = {}
938 self.nextTime = currentTimeMillis()
939 self.delay = _BROWSER_TIME
940 self.list = []
942 self.done = False
944 self.zc.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
945 self.start()
947 def updateRecord(self, zc, now, record):
948 """Callback invoked by Zeroconf when new information arrives.
950 Updates information required by browser in the Zeroconf cache."""
951 if record.type == _TYPE_PTR and record.name == self.type:
952 expired = record.isExpired(now)
953 try:
954 oldrecord = self.services[record.alias.lower()]
955 if not expired:
956 oldrecord.resetTTL(record)
957 else:
958 del(self.services[record.alias.lower()])
959 callback = lambda x: self.listener.removeService(x,
960 self.type, record.alias)
961 self.list.append(callback)
962 return
963 except:
964 if not expired:
965 self.services[record.alias.lower()] = record
966 callback = lambda x: self.listener.addService(x,
967 self.type, record.alias)
968 self.list.append(callback)
970 expires = record.getExpirationTime(75)
971 if expires < self.nextTime:
972 self.nextTime = expires
974 def cancel(self):
975 self.done = True
976 self.zc.notifyAll()
978 def run(self):
979 while True:
980 event = None
981 now = currentTimeMillis()
982 if len(self.list) == 0 and self.nextTime > now:
983 self.zc.wait(self.nextTime - now)
984 if _GLOBAL_DONE or self.done:
985 return
986 now = currentTimeMillis()
988 if self.nextTime <= now:
989 out = DNSOutgoing(_FLAGS_QR_QUERY)
990 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
991 for record in self.services.values():
992 if not record.isExpired(now):
993 out.addAnswerAtTime(record, now)
994 self.zc.send(out)
995 self.nextTime = now + self.delay
996 self.delay = min(20 * 1000, self.delay * 2)
998 if len(self.list) > 0:
999 event = self.list.pop(0)
1001 if event is not None:
1002 event(self.zc)
1005 class ServiceInfo(object):
1006 """Service information"""
1008 def __init__(self, type, name, address=None, port=None, weight=0,
1009 priority=0, properties=None, server=None):
1010 """Create a service description.
1012 type: fully qualified service type name
1013 name: fully qualified service name
1014 address: IP address as unsigned short, network byte order
1015 port: port that the service runs on
1016 weight: weight of the service
1017 priority: priority of the service
1018 properties: dictionary of properties (or a string holding the
1019 bytes for the text field)
1020 server: fully qualified name for service host (defaults to name)"""
1022 if not name.endswith(type):
1023 raise BadTypeInNameException
1024 self.type = type
1025 self.name = name
1026 self.address = address
1027 self.port = port
1028 self.weight = weight
1029 self.priority = priority
1030 if server:
1031 self.server = server
1032 else:
1033 self.server = name
1034 self.setProperties(properties)
1036 def setProperties(self, properties):
1037 """Sets properties and text of this info from a dictionary"""
1038 if isinstance(properties, dict):
1039 self.properties = properties
1040 list = []
1041 result = ''
1042 for key in properties:
1043 value = properties[key]
1044 if value is None:
1045 suffix = ''.encode('utf-8')
1046 elif isinstance(value, str):
1047 suffix = value.encode('utf-8')
1048 elif isinstance(value, int):
1049 if value:
1050 suffix = 'true'
1051 else:
1052 suffix = 'false'
1053 else:
1054 suffix = ''.encode('utf-8')
1055 list.append('='.join((key, suffix)))
1056 for item in list:
1057 result = ''.join((result, chr(len(item)), item))
1058 self.text = result
1059 else:
1060 self.text = properties
1062 def setText(self, text):
1063 """Sets properties and text given a text field"""
1064 self.text = text
1065 try:
1066 result = {}
1067 end = len(text)
1068 index = 0
1069 strs = []
1070 while index < end:
1071 length = ord(text[index])
1072 index += 1
1073 strs.append(text[index:index+length])
1074 index += length
1076 for s in strs:
1077 try:
1078 key, value = s.split('=', 1)
1079 if value == 'true':
1080 value = True
1081 elif value == 'false' or not value:
1082 value = False
1083 except:
1084 # No equals sign at all
1085 key = s
1086 value = False
1088 # Only update non-existent properties
1089 if key and result.get(key) == None:
1090 result[key] = value
1092 self.properties = result
1093 except:
1094 traceback.print_exc()
1095 self.properties = None
1097 def getType(self):
1098 """Type accessor"""
1099 return self.type
1101 def getName(self):
1102 """Name accessor"""
1103 if self.type is not None and self.name.endswith("." + self.type):
1104 return self.name[:len(self.name) - len(self.type) - 1]
1105 return self.name
1107 def getAddress(self):
1108 """Address accessor"""
1109 return self.address
1111 def getPort(self):
1112 """Port accessor"""
1113 return self.port
1115 def getPriority(self):
1116 """Pirority accessor"""
1117 return self.priority
1119 def getWeight(self):
1120 """Weight accessor"""
1121 return self.weight
1123 def getProperties(self):
1124 """Properties accessor"""
1125 return self.properties
1127 def getText(self):
1128 """Text accessor"""
1129 return self.text
1131 def getServer(self):
1132 """Server accessor"""
1133 return self.server
1135 def updateRecord(self, zc, now, record):
1136 """Updates service information from a DNS record"""
1137 if record is not None and not record.isExpired(now):
1138 if record.type == _TYPE_A:
1139 #if record.name == self.name:
1140 if record.name == self.server:
1141 self.address = record.address
1142 elif record.type == _TYPE_SRV:
1143 if record.name == self.name:
1144 self.server = record.server
1145 self.port = record.port
1146 self.weight = record.weight
1147 self.priority = record.priority
1148 #self.address = None
1149 self.updateRecord(zc, now,
1150 zc.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1151 elif record.type == _TYPE_TXT:
1152 if record.name == self.name:
1153 self.setText(record.text)
1155 def request(self, zc, timeout):
1156 """Returns true if the service could be discovered on the
1157 network, and updates this object with details discovered.
1159 now = currentTimeMillis()
1160 delay = _LISTENER_TIME
1161 next = now + delay
1162 last = now + timeout
1163 result = False
1164 try:
1165 zc.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1166 while (self.server is None or self.address is None or
1167 self.text is None):
1168 if last <= now:
1169 return False
1170 if next <= now:
1171 out = DNSOutgoing(_FLAGS_QR_QUERY)
1172 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV,
1173 _CLASS_IN))
1174 out.addAnswerAtTime(zc.cache.getByDetails(self.name,
1175 _TYPE_SRV, _CLASS_IN), now)
1176 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT,
1177 _CLASS_IN))
1178 out.addAnswerAtTime(zc.cache.getByDetails(self.name,
1179 _TYPE_TXT, _CLASS_IN), now)
1180 if self.server is not None:
1181 out.addQuestion(DNSQuestion(self.server,
1182 _TYPE_A, _CLASS_IN))
1183 out.addAnswerAtTime(zc.cache.getByDetails(self.server,
1184 _TYPE_A, _CLASS_IN), now)
1185 zc.send(out)
1186 next = now + delay
1187 delay = delay * 2
1189 zc.wait(min(next, last) - now)
1190 now = currentTimeMillis()
1191 result = True
1192 finally:
1193 zc.removeListener(self)
1195 return result
1197 def __eq__(self, other):
1198 """Tests equality of service name"""
1199 if isinstance(other, ServiceInfo):
1200 return other.name == self.name
1201 return False
1203 def __ne__(self, other):
1204 """Non-equality test"""
1205 return not self.__eq__(other)
1207 def __repr__(self):
1208 """String representation"""
1209 result = "service[%s,%s:%s," % (self.name,
1210 socket.inet_ntoa(self.getAddress()), self.port)
1211 if self.text is None:
1212 result += "None"
1213 else:
1214 if len(self.text) < 20:
1215 result += self.text
1216 else:
1217 result += self.text[:17] + "..."
1218 result += "]"
1219 return result
1222 class Zeroconf(object):
1223 """Implementation of Zeroconf Multicast DNS Service Discovery
1225 Supports registration, unregistration, queries and browsing.
1227 def __init__(self, bindaddress=None):
1228 """Creates an instance of the Zeroconf class, establishing
1229 multicast communications, listening and reaping threads."""
1230 global _GLOBAL_DONE
1231 _GLOBAL_DONE = False
1232 if bindaddress is None:
1233 self.intf = socket.gethostbyname(socket.gethostname())
1234 else:
1235 self.intf = bindaddress
1236 self.group = ('', _MDNS_PORT)
1237 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1238 try:
1239 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1240 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1241 except:
1242 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1243 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1244 # Volume 2"), but some BSD-derived systems require
1245 # SO_REUSEPORT to be specified explicity. Also, not all
1246 # versions of Python have SO_REUSEPORT available. So
1247 # if you're on a BSD-based system, and haven't upgraded
1248 # to Python 2.3 yet, you may find this library doesn't
1249 # work as expected.
1251 pass
1252 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
1253 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
1254 try:
1255 self.socket.bind(self.group)
1256 except:
1257 # Some versions of linux raise an exception even though
1258 # the SO_REUSE* options have been set, so ignore it
1260 pass
1261 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF,
1262 # socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1263 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP,
1264 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1266 self.listeners = []
1267 self.browsers = []
1268 self.services = {}
1269 self.servicetypes = {}
1271 self.cache = DNSCache()
1273 self.condition = threading.Condition()
1275 self.engine = Engine(self)
1276 self.listener = Listener(self)
1277 self.reaper = Reaper(self)
1279 def isLoopback(self):
1280 return self.intf.startswith("127.0.0.1")
1282 def isLinklocal(self):
1283 return self.intf.startswith("169.254.")
1285 def wait(self, timeout):
1286 """Calling thread waits for a given number of milliseconds or
1287 until notified."""
1288 self.condition.acquire()
1289 self.condition.wait(timeout/1000)
1290 self.condition.release()
1292 def notifyAll(self):
1293 """Notifies all waiting threads"""
1294 self.condition.acquire()
1295 self.condition.notifyAll()
1296 self.condition.release()
1298 def getServiceInfo(self, type, name, timeout=3000):
1299 """Returns network's service information for a particular
1300 name and type, or None if no service matches by the timeout,
1301 which defaults to 3 seconds."""
1302 info = ServiceInfo(type, name)
1303 if info.request(self, timeout):
1304 return info
1305 return None
1307 def addServiceListener(self, type, listener):
1308 """Adds a listener for a particular service type. This object
1309 will then have its updateRecord method called when information
1310 arrives for that type."""
1311 self.removeServiceListener(listener)
1312 self.browsers.append(ServiceBrowser(self, type, listener))
1314 def removeServiceListener(self, listener):
1315 """Removes a listener from the set that is currently listening."""
1316 for browser in self.browsers:
1317 if browser.listener == listener:
1318 browser.cancel()
1319 del(browser)
1321 def registerService(self, info, ttl=_DNS_TTL):
1322 """Registers service information to the network with a default TTL
1323 of 60 seconds. Zeroconf will then respond to requests for
1324 information for that service. The name of the service may be
1325 changed if needed to make it unique on the network."""
1326 self.checkService(info)
1327 self.services[info.name.lower()] = info
1328 if info.type in self.servicetypes:
1329 self.servicetypes[info.type]+=1
1330 else:
1331 self.servicetypes[info.type]=1
1332 now = currentTimeMillis()
1333 nextTime = now
1334 i = 0
1335 while i < 3:
1336 if now < nextTime:
1337 self.wait(nextTime - now)
1338 now = currentTimeMillis()
1339 continue
1340 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1341 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1342 _CLASS_IN, ttl, info.name), 0)
1343 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1344 _CLASS_IN, ttl, info.priority, info.weight, info.port,
1345 info.server), 0)
1346 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN,
1347 ttl, info.text), 0)
1348 if info.address:
1349 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A,
1350 _CLASS_IN, ttl, info.address), 0)
1351 self.send(out)
1352 i += 1
1353 nextTime += _REGISTER_TIME
1355 def unregisterService(self, info):
1356 """Unregister a service."""
1357 try:
1358 del(self.services[info.name.lower()])
1359 if self.servicetypes[info.type]>1:
1360 self.servicetypes[info.type]-=1
1361 else:
1362 del self.servicetypes[info.type]
1363 except:
1364 pass
1365 now = currentTimeMillis()
1366 nextTime = now
1367 i = 0
1368 while i < 3:
1369 if now < nextTime:
1370 self.wait(nextTime - now)
1371 now = currentTimeMillis()
1372 continue
1373 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1374 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1375 _CLASS_IN, 0, info.name), 0)
1376 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1377 _CLASS_IN, 0, info.priority, info.weight, info.port,
1378 info.name), 0)
1379 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN,
1380 0, info.text), 0)
1381 if info.address:
1382 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A,
1383 _CLASS_IN, 0, info.address), 0)
1384 self.send(out)
1385 i += 1
1386 nextTime += _UNREGISTER_TIME
1388 def unregisterAllServices(self):
1389 """Unregister all registered services."""
1390 if len(self.services) > 0:
1391 now = currentTimeMillis()
1392 nextTime = now
1393 i = 0
1394 while i < 3:
1395 if now < nextTime:
1396 self.wait(nextTime - now)
1397 now = currentTimeMillis()
1398 continue
1399 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1400 for info in self.services.values():
1401 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1402 _CLASS_IN, 0, info.name), 0)
1403 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1404 _CLASS_IN, 0, info.priority, info.weight,
1405 info.port, info.server), 0)
1406 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT,
1407 _CLASS_IN, 0, info.text), 0)
1408 if info.address:
1409 out.addAnswerAtTime(DNSAddress(info.server,
1410 _TYPE_A, _CLASS_IN, 0, info.address), 0)
1411 self.send(out)
1412 i += 1
1413 nextTime += _UNREGISTER_TIME
1415 def checkService(self, info):
1416 """Checks the network for a unique service name, modifying the
1417 ServiceInfo passed in if it is not unique."""
1418 now = currentTimeMillis()
1419 nextTime = now
1420 i = 0
1421 while i < 3:
1422 for record in self.cache.entriesWithName(info.type):
1423 if (record.type == _TYPE_PTR and
1424 not record.isExpired(now) and
1425 record.alias == info.name):
1426 if info.name.find('.') < 0:
1427 info.name = '%s.[%s:%s].%s' % (info.name,
1428 info.address, info.port, info.type)
1430 self.checkService(info)
1431 return
1432 raise NonUniqueNameException
1433 if now < nextTime:
1434 self.wait(nextTime - now)
1435 now = currentTimeMillis()
1436 continue
1437 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1438 self.debug = out
1439 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1440 out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR,
1441 _CLASS_IN, _DNS_TTL, info.name))
1442 self.send(out)
1443 i += 1
1444 nextTime += _CHECK_TIME
1446 def addListener(self, listener, question):
1447 """Adds a listener for a given question. The listener will have
1448 its updateRecord method called when information is available to
1449 answer the question."""
1450 now = currentTimeMillis()
1451 self.listeners.append(listener)
1452 if question is not None:
1453 for record in self.cache.entriesWithName(question.name):
1454 if question.answeredBy(record) and not record.isExpired(now):
1455 listener.updateRecord(self, now, record)
1456 self.notifyAll()
1458 def removeListener(self, listener):
1459 """Removes a listener."""
1460 try:
1461 self.listeners.remove(listener)
1462 self.notifyAll()
1463 except:
1464 pass
1466 def updateRecord(self, now, rec):
1467 """Used to notify listeners of new information that has updated
1468 a record."""
1469 for listener in self.listeners:
1470 listener.updateRecord(self, now, rec)
1471 self.notifyAll()
1473 def handleResponse(self, msg):
1474 """Deal with incoming response packets. All answers
1475 are held in the cache, and listeners are notified."""
1476 now = currentTimeMillis()
1477 for record in msg.answers:
1478 expired = record.isExpired(now)
1479 if record in self.cache.entries():
1480 if expired:
1481 self.cache.remove(record)
1482 else:
1483 entry = self.cache.get(record)
1484 if entry is not None:
1485 entry.resetTTL(record)
1486 record = entry
1487 else:
1488 self.cache.add(record)
1490 self.updateRecord(now, record)
1492 def handleQuery(self, msg, addr, port):
1493 """Deal with incoming query packets. Provides a response if
1494 possible."""
1495 out = None
1497 # Support unicast client responses
1499 if port != _MDNS_PORT:
1500 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, False)
1501 for question in msg.questions:
1502 out.addQuestion(question)
1504 for question in msg.questions:
1505 if question.type == _TYPE_PTR:
1506 if question.name == "_services._dns-sd._udp.local.":
1507 for stype in self.servicetypes.keys():
1508 if out is None:
1509 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1510 out.addAnswer(msg,
1511 DNSPointer("_services._dns-sd._udp.local.",
1512 _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1513 for service in self.services.values():
1514 if question.name == service.type:
1515 if out is None:
1516 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1517 out.addAnswer(msg,
1518 DNSPointer(service.type, _TYPE_PTR,
1519 _CLASS_IN, _DNS_TTL, service.name))
1520 else:
1521 try:
1522 if out is None:
1523 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1525 # Answer A record queries for any service addresses we know
1526 if question.type in (_TYPE_A, _TYPE_ANY):
1527 for service in self.services.values():
1528 if service.server == question.name.lower():
1529 out.addAnswer(msg, DNSAddress(question.name,
1530 _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
1531 _DNS_TTL, service.address))
1533 service = self.services.get(question.name.lower(), None)
1534 if not service: continue
1536 if question.type in (_TYPE_SRV, _TYPE_ANY):
1537 out.addAnswer(msg, DNSService(question.name,
1538 _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE,
1539 _DNS_TTL, service.priority, service.weight,
1540 service.port, service.server))
1541 if question.type in (_TYPE_TXT, _TYPE_ANY):
1542 out.addAnswer(msg, DNSText(question.name,
1543 _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE,
1544 _DNS_TTL, service.text))
1545 if question.type == _TYPE_SRV:
1546 out.addAdditionalAnswer(DNSAddress(service.server,
1547 _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
1548 _DNS_TTL, service.address))
1549 except:
1550 traceback.print_exc()
1552 if out is not None and out.answers:
1553 out.id = msg.id
1554 self.send(out, addr, port)
1556 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1557 """Sends an outgoing packet."""
1558 # This is a quick test to see if we can parse the packets we generate
1559 #temp = DNSIncoming(out.packet())
1560 try:
1561 bytes_sent = self.socket.sendto(out.packet(), 0, (addr, port))
1562 except:
1563 # Ignore this, it may be a temporary loss of network connection
1564 pass
1566 def close(self):
1567 """Ends the background threads, and prevent this instance from
1568 servicing further queries."""
1569 global _GLOBAL_DONE
1570 if not _GLOBAL_DONE:
1571 _GLOBAL_DONE = True
1572 self.notifyAll()
1573 self.engine.notify()
1574 self.unregisterAllServices()
1575 self.socket.setsockopt(socket.SOL_IP,
1576 socket.IP_DROP_MEMBERSHIP,
1577 socket.inet_aton(_MDNS_ADDR) +
1578 socket.inet_aton('0.0.0.0'))
1579 self.socket.close()
1581 # Test a few module features, including service registration, service
1582 # query (for Zoe), and service unregistration.
1584 if __name__ == '__main__':
1585 print "Multicast DNS Service Discovery for Python, version", __version__
1586 r = Zeroconf()
1587 print "1. Testing registration of a service..."
1588 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1589 info = ServiceInfo("_http._tcp.local.",
1590 "My Service Name._http._tcp.local.",
1591 socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1592 print " Registering service..."
1593 r.registerService(info)
1594 print " Registration done."
1595 print "2. Testing query of service information..."
1596 print " Getting ZOE service:",
1597 print str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1598 print " Query done."
1599 print "3. Testing query of own service..."
1600 print " Getting self:",
1601 print str(r.getServiceInfo("_http._tcp.local.",
1602 "My Service Name._http._tcp.local."))
1603 print " Query done."
1604 print "4. Testing unregister of service information..."
1605 r.unregisterService(info)
1606 print " Unregister done."
1607 r.close()