Merge branch 'master' of git://repo.or.cz/pyTivo/wmcbrine.git
[pyTivo/wmcbrine/lucasnz.git] / Zeroconf.py
blobf04c3392f655bec77d47fec07df7641a22d88121
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 try:
1234 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1235 s.connect(('4.2.2.1', 123))
1236 self.intf = s.getsockname()[0]
1237 except:
1238 self.intf = socket.gethostbyname(socket.gethostname())
1239 else:
1240 self.intf = bindaddress
1241 self.group = ('', _MDNS_PORT)
1242 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1243 try:
1244 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1245 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1246 except:
1247 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1248 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1249 # Volume 2"), but some BSD-derived systems require
1250 # SO_REUSEPORT to be specified explicity. Also, not all
1251 # versions of Python have SO_REUSEPORT available. So
1252 # if you're on a BSD-based system, and haven't upgraded
1253 # to Python 2.3 yet, you may find this library doesn't
1254 # work as expected.
1256 pass
1257 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
1258 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
1259 try:
1260 self.socket.bind(self.group)
1261 except:
1262 # Some versions of linux raise an exception even though
1263 # the SO_REUSE* options have been set, so ignore it
1265 pass
1266 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF,
1267 # socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1268 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP,
1269 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1271 self.listeners = []
1272 self.browsers = []
1273 self.services = {}
1274 self.servicetypes = {}
1276 self.cache = DNSCache()
1278 self.condition = threading.Condition()
1280 self.engine = Engine(self)
1281 self.listener = Listener(self)
1282 self.reaper = Reaper(self)
1284 def isLoopback(self):
1285 return self.intf.startswith("127.0.0.1")
1287 def isLinklocal(self):
1288 return self.intf.startswith("169.254.")
1290 def wait(self, timeout):
1291 """Calling thread waits for a given number of milliseconds or
1292 until notified."""
1293 self.condition.acquire()
1294 self.condition.wait(timeout/1000)
1295 self.condition.release()
1297 def notifyAll(self):
1298 """Notifies all waiting threads"""
1299 self.condition.acquire()
1300 self.condition.notifyAll()
1301 self.condition.release()
1303 def getServiceInfo(self, type, name, timeout=3000):
1304 """Returns network's service information for a particular
1305 name and type, or None if no service matches by the timeout,
1306 which defaults to 3 seconds."""
1307 info = ServiceInfo(type, name)
1308 if info.request(self, timeout):
1309 return info
1310 return None
1312 def addServiceListener(self, type, listener):
1313 """Adds a listener for a particular service type. This object
1314 will then have its updateRecord method called when information
1315 arrives for that type."""
1316 self.removeServiceListener(listener)
1317 self.browsers.append(ServiceBrowser(self, type, listener))
1319 def removeServiceListener(self, listener):
1320 """Removes a listener from the set that is currently listening."""
1321 for browser in self.browsers:
1322 if browser.listener == listener:
1323 browser.cancel()
1324 del(browser)
1326 def registerService(self, info, ttl=_DNS_TTL):
1327 """Registers service information to the network with a default TTL
1328 of 60 seconds. Zeroconf will then respond to requests for
1329 information for that service. The name of the service may be
1330 changed if needed to make it unique on the network."""
1331 self.checkService(info)
1332 self.services[info.name.lower()] = info
1333 if info.type in self.servicetypes:
1334 self.servicetypes[info.type]+=1
1335 else:
1336 self.servicetypes[info.type]=1
1337 now = currentTimeMillis()
1338 nextTime = now
1339 i = 0
1340 while i < 3:
1341 if now < nextTime:
1342 self.wait(nextTime - now)
1343 now = currentTimeMillis()
1344 continue
1345 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1346 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1347 _CLASS_IN, ttl, info.name), 0)
1348 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1349 _CLASS_IN, ttl, info.priority, info.weight, info.port,
1350 info.server), 0)
1351 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN,
1352 ttl, info.text), 0)
1353 if info.address:
1354 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A,
1355 _CLASS_IN, ttl, info.address), 0)
1356 self.send(out)
1357 i += 1
1358 nextTime += _REGISTER_TIME
1360 def unregisterService(self, info):
1361 """Unregister a service."""
1362 try:
1363 del(self.services[info.name.lower()])
1364 if self.servicetypes[info.type]>1:
1365 self.servicetypes[info.type]-=1
1366 else:
1367 del self.servicetypes[info.type]
1368 except:
1369 pass
1370 now = currentTimeMillis()
1371 nextTime = now
1372 i = 0
1373 while i < 3:
1374 if now < nextTime:
1375 self.wait(nextTime - now)
1376 now = currentTimeMillis()
1377 continue
1378 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1379 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1380 _CLASS_IN, 0, info.name), 0)
1381 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1382 _CLASS_IN, 0, info.priority, info.weight, info.port,
1383 info.name), 0)
1384 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN,
1385 0, info.text), 0)
1386 if info.address:
1387 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A,
1388 _CLASS_IN, 0, info.address), 0)
1389 self.send(out)
1390 i += 1
1391 nextTime += _UNREGISTER_TIME
1393 def unregisterAllServices(self):
1394 """Unregister all registered services."""
1395 if len(self.services) > 0:
1396 now = currentTimeMillis()
1397 nextTime = now
1398 i = 0
1399 while i < 3:
1400 if now < nextTime:
1401 self.wait(nextTime - now)
1402 now = currentTimeMillis()
1403 continue
1404 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1405 for info in self.services.values():
1406 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1407 _CLASS_IN, 0, info.name), 0)
1408 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1409 _CLASS_IN, 0, info.priority, info.weight,
1410 info.port, info.server), 0)
1411 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT,
1412 _CLASS_IN, 0, info.text), 0)
1413 if info.address:
1414 out.addAnswerAtTime(DNSAddress(info.server,
1415 _TYPE_A, _CLASS_IN, 0, info.address), 0)
1416 self.send(out)
1417 i += 1
1418 nextTime += _UNREGISTER_TIME
1420 def checkService(self, info):
1421 """Checks the network for a unique service name, modifying the
1422 ServiceInfo passed in if it is not unique."""
1423 now = currentTimeMillis()
1424 nextTime = now
1425 i = 0
1426 while i < 3:
1427 for record in self.cache.entriesWithName(info.type):
1428 if (record.type == _TYPE_PTR and
1429 not record.isExpired(now) and
1430 record.alias == info.name):
1431 if info.name.find('.') < 0:
1432 info.name = '%s.[%s:%s].%s' % (info.name,
1433 info.address, info.port, info.type)
1435 self.checkService(info)
1436 return
1437 raise NonUniqueNameException
1438 if now < nextTime:
1439 self.wait(nextTime - now)
1440 now = currentTimeMillis()
1441 continue
1442 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1443 self.debug = out
1444 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1445 out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR,
1446 _CLASS_IN, _DNS_TTL, info.name))
1447 self.send(out)
1448 i += 1
1449 nextTime += _CHECK_TIME
1451 def addListener(self, listener, question):
1452 """Adds a listener for a given question. The listener will have
1453 its updateRecord method called when information is available to
1454 answer the question."""
1455 now = currentTimeMillis()
1456 self.listeners.append(listener)
1457 if question is not None:
1458 for record in self.cache.entriesWithName(question.name):
1459 if question.answeredBy(record) and not record.isExpired(now):
1460 listener.updateRecord(self, now, record)
1461 self.notifyAll()
1463 def removeListener(self, listener):
1464 """Removes a listener."""
1465 try:
1466 self.listeners.remove(listener)
1467 self.notifyAll()
1468 except:
1469 pass
1471 def updateRecord(self, now, rec):
1472 """Used to notify listeners of new information that has updated
1473 a record."""
1474 for listener in self.listeners:
1475 listener.updateRecord(self, now, rec)
1476 self.notifyAll()
1478 def handleResponse(self, msg):
1479 """Deal with incoming response packets. All answers
1480 are held in the cache, and listeners are notified."""
1481 now = currentTimeMillis()
1482 for record in msg.answers:
1483 expired = record.isExpired(now)
1484 if record in self.cache.entries():
1485 if expired:
1486 self.cache.remove(record)
1487 else:
1488 entry = self.cache.get(record)
1489 if entry is not None:
1490 entry.resetTTL(record)
1491 record = entry
1492 else:
1493 self.cache.add(record)
1495 self.updateRecord(now, record)
1497 def handleQuery(self, msg, addr, port):
1498 """Deal with incoming query packets. Provides a response if
1499 possible."""
1500 out = None
1502 # Support unicast client responses
1504 if port != _MDNS_PORT:
1505 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, False)
1506 for question in msg.questions:
1507 out.addQuestion(question)
1509 for question in msg.questions:
1510 if question.type == _TYPE_PTR:
1511 if question.name == "_services._dns-sd._udp.local.":
1512 for stype in self.servicetypes.keys():
1513 if out is None:
1514 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1515 out.addAnswer(msg,
1516 DNSPointer("_services._dns-sd._udp.local.",
1517 _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1518 for service in self.services.values():
1519 if question.name == service.type:
1520 if out is None:
1521 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1522 out.addAnswer(msg,
1523 DNSPointer(service.type, _TYPE_PTR,
1524 _CLASS_IN, _DNS_TTL, service.name))
1525 else:
1526 try:
1527 if out is None:
1528 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1530 # Answer A record queries for any service addresses we know
1531 if question.type in (_TYPE_A, _TYPE_ANY):
1532 for service in self.services.values():
1533 if service.server == question.name.lower():
1534 out.addAnswer(msg, DNSAddress(question.name,
1535 _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
1536 _DNS_TTL, service.address))
1538 service = self.services.get(question.name.lower(), None)
1539 if not service: continue
1541 if question.type in (_TYPE_SRV, _TYPE_ANY):
1542 out.addAnswer(msg, DNSService(question.name,
1543 _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE,
1544 _DNS_TTL, service.priority, service.weight,
1545 service.port, service.server))
1546 if question.type in (_TYPE_TXT, _TYPE_ANY):
1547 out.addAnswer(msg, DNSText(question.name,
1548 _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE,
1549 _DNS_TTL, service.text))
1550 if question.type == _TYPE_SRV:
1551 out.addAdditionalAnswer(DNSAddress(service.server,
1552 _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
1553 _DNS_TTL, service.address))
1554 except:
1555 traceback.print_exc()
1557 if out is not None and out.answers:
1558 out.id = msg.id
1559 self.send(out, addr, port)
1561 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1562 """Sends an outgoing packet."""
1563 # This is a quick test to see if we can parse the packets we generate
1564 #temp = DNSIncoming(out.packet())
1565 try:
1566 bytes_sent = self.socket.sendto(out.packet(), 0, (addr, port))
1567 except:
1568 # Ignore this, it may be a temporary loss of network connection
1569 pass
1571 def close(self):
1572 """Ends the background threads, and prevent this instance from
1573 servicing further queries."""
1574 global _GLOBAL_DONE
1575 if not _GLOBAL_DONE:
1576 _GLOBAL_DONE = True
1577 self.notifyAll()
1578 self.engine.notify()
1579 self.unregisterAllServices()
1580 self.socket.setsockopt(socket.SOL_IP,
1581 socket.IP_DROP_MEMBERSHIP,
1582 socket.inet_aton(_MDNS_ADDR) +
1583 socket.inet_aton('0.0.0.0'))
1584 self.socket.close()
1586 # Test a few module features, including service registration, service
1587 # query (for Zoe), and service unregistration.
1589 if __name__ == '__main__':
1590 print "Multicast DNS Service Discovery for Python, version", __version__
1591 r = Zeroconf()
1592 print "1. Testing registration of a service..."
1593 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1594 info = ServiceInfo("_http._tcp.local.",
1595 "My Service Name._http._tcp.local.",
1596 socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1597 print " Registering service..."
1598 r.registerService(info)
1599 print " Registration done."
1600 print "2. Testing query of service information..."
1601 print " Getting ZOE service:",
1602 print str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1603 print " Query done."
1604 print "3. Testing query of own service..."
1605 print " Getting self:",
1606 print str(r.getServiceInfo("_http._tcp.local.",
1607 "My Service Name._http._tcp.local."))
1608 print " Query done."
1609 print "4. Testing unregister of service information..."
1610 r.unregisterService(info)
1611 print " Unregister done."
1612 r.close()