Of course I can't leave well enough alone.
[pyTivo/wmcbrine.git] / Zeroconf.py
blobda7e693f919b94e32353d72638b9148dbfd67db7
1 """ Multicast DNS Service Discovery for Python, v0.12-wmcbrine
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-wmcbrine update - see git for changes"""
27 """0.12 update - allow selection of binding interface
28 typo fix - Thanks A. M. Kuchlingi
29 removed all use of word 'Rendezvous' - this is an API change"""
31 """0.11 update - correction to comments for addListener method
32 support for new record types seen from OS X
33 - IPv6 address
34 - hostinfo
35 ignore unknown DNS record types
36 fixes to name decoding
37 works alongside other processes using port 5353
38 (e.g. on Mac OS X)
39 tested against Mac OS X 10.3.2's mDNSResponder
40 corrections to removal of list entries for service browser"""
42 """0.10 update - Jonathon Paisley contributed these corrections:
43 always multicast replies, even when query is unicast
44 correct a pointer encoding problem
45 can now write records in any order
46 traceback shown on failure
47 better TXT record parsing
48 server is now separate from name
49 can cancel a service browser
51 modified some unit tests to accommodate these changes"""
53 """0.09 update - remove all records on service unregistration
54 fix DOS security problem with readName"""
56 """0.08 update - changed licensing to LGPL"""
58 """0.07 update - faster shutdown on engine
59 pointer encoding of outgoing names
60 ServiceBrowser now works
61 new unit tests"""
63 """0.06 update - small improvements with unit tests
64 added defined exception types
65 new style objects
66 fixed hostname/interface problem
67 fixed socket timeout problem
68 fixed addServiceListener() typo bug
69 using select() for socket reads
70 tested on Debian unstable with Python 2.2.2"""
72 """0.05 update - ensure case insensitivty on domain names
73 support for unicast DNS queries"""
75 """0.04 update - added some unit tests
76 added __ne__ adjuncts where required
77 ensure names end in '.local.'
78 timeout on receiving socket for clean shutdown"""
80 __author__ = "Paul Scott-Murphy"
81 __email__ = "paul at scott dash murphy dot com"
82 __version__ = "0.12-wmcbrine"
84 import time
85 import struct
86 import socket
87 import threading
88 import select
89 import traceback
91 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
93 # hook for threads
95 _GLOBAL_DONE = False
97 # Some timing constants
99 _UNREGISTER_TIME = 125
100 _CHECK_TIME = 175
101 _REGISTER_TIME = 225
102 _LISTENER_TIME = 200
103 _BROWSER_TIME = 500
105 # Some DNS constants
107 _MDNS_ADDR = '224.0.0.251'
108 _MDNS_PORT = 5353;
109 _DNS_PORT = 53;
110 _DNS_TTL = 60 * 60; # one hour default TTL
112 _MAX_MSG_TYPICAL = 1460 # unused
113 _MAX_MSG_ABSOLUTE = 8972
115 _FLAGS_QR_MASK = 0x8000 # query response mask
116 _FLAGS_QR_QUERY = 0x0000 # query
117 _FLAGS_QR_RESPONSE = 0x8000 # response
119 _FLAGS_AA = 0x0400 # Authorative answer
120 _FLAGS_TC = 0x0200 # Truncated
121 _FLAGS_RD = 0x0100 # Recursion desired
122 _FLAGS_RA = 0x8000 # Recursion available
124 _FLAGS_Z = 0x0040 # Zero
125 _FLAGS_AD = 0x0020 # Authentic data
126 _FLAGS_CD = 0x0010 # Checking disabled
128 _CLASS_IN = 1
129 _CLASS_CS = 2
130 _CLASS_CH = 3
131 _CLASS_HS = 4
132 _CLASS_NONE = 254
133 _CLASS_ANY = 255
134 _CLASS_MASK = 0x7FFF
135 _CLASS_UNIQUE = 0x8000
137 _TYPE_A = 1
138 _TYPE_NS = 2
139 _TYPE_MD = 3
140 _TYPE_MF = 4
141 _TYPE_CNAME = 5
142 _TYPE_SOA = 6
143 _TYPE_MB = 7
144 _TYPE_MG = 8
145 _TYPE_MR = 9
146 _TYPE_NULL = 10
147 _TYPE_WKS = 11
148 _TYPE_PTR = 12
149 _TYPE_HINFO = 13
150 _TYPE_MINFO = 14
151 _TYPE_MX = 15
152 _TYPE_TXT = 16
153 _TYPE_AAAA = 28
154 _TYPE_SRV = 33
155 _TYPE_ANY = 255
157 # Mapping constants to names
159 _CLASSES = { _CLASS_IN : "in",
160 _CLASS_CS : "cs",
161 _CLASS_CH : "ch",
162 _CLASS_HS : "hs",
163 _CLASS_NONE : "none",
164 _CLASS_ANY : "any" }
166 _TYPES = { _TYPE_A : "a",
167 _TYPE_NS : "ns",
168 _TYPE_MD : "md",
169 _TYPE_MF : "mf",
170 _TYPE_CNAME : "cname",
171 _TYPE_SOA : "soa",
172 _TYPE_MB : "mb",
173 _TYPE_MG : "mg",
174 _TYPE_MR : "mr",
175 _TYPE_NULL : "null",
176 _TYPE_WKS : "wks",
177 _TYPE_PTR : "ptr",
178 _TYPE_HINFO : "hinfo",
179 _TYPE_MINFO : "minfo",
180 _TYPE_MX : "mx",
181 _TYPE_TXT : "txt",
182 _TYPE_AAAA : "quada",
183 _TYPE_SRV : "srv",
184 _TYPE_ANY : "any" }
186 # utility functions
188 def currentTimeMillis():
189 """Current system time in milliseconds"""
190 return time.time() * 1000
192 # Exceptions
194 class NonLocalNameException(Exception):
195 pass
197 class NonUniqueNameException(Exception):
198 pass
200 class NamePartTooLongException(Exception):
201 pass
203 class AbstractMethodException(Exception):
204 pass
206 class BadTypeInNameException(Exception):
207 pass
209 # implementation classes
211 class DNSEntry(object):
212 """A DNS entry"""
214 def __init__(self, name, type, clazz):
215 self.key = name.lower()
216 self.name = name
217 self.type = type
218 self.clazz = clazz & _CLASS_MASK
219 self.unique = (clazz & _CLASS_UNIQUE) != 0
221 def __eq__(self, other):
222 """Equality test on name, type, and class"""
223 return (isinstance(other, DNSEntry) and
224 self.name == other.name and
225 self.type == other.type and
226 self.clazz == other.clazz)
228 def __ne__(self, other):
229 """Non-equality test"""
230 return not self.__eq__(other)
232 def getClazz(self, clazz):
233 """Class accessor"""
234 try:
235 return _CLASSES[clazz]
236 except:
237 return "?(%s)" % (clazz)
239 def getType(self, type):
240 """Type accessor"""
241 try:
242 return _TYPES[type]
243 except:
244 return "?(%s)" % (type)
246 def toString(self, hdr, other):
247 """String representation with additional information"""
248 result = "%s[%s,%s" % (hdr, self.getType(self.type),
249 self.getClazz(self.clazz))
250 if self.unique:
251 result += "-unique,"
252 else:
253 result += ","
254 result += self.name
255 if other is not None:
256 result += ",%s]" % (other)
257 else:
258 result += "]"
259 return result
261 class DNSQuestion(DNSEntry):
262 """A DNS question entry"""
264 def __init__(self, name, type, clazz):
265 #if not name.endswith(".local."):
266 # raise NonLocalNameException
267 DNSEntry.__init__(self, name, type, clazz)
269 def answeredBy(self, rec):
270 """Returns true if the question is answered by the record"""
271 return (self.clazz == rec.clazz and
272 (self.type == rec.type or self.type == _TYPE_ANY) and
273 self.name == rec.name)
275 def __repr__(self):
276 """String representation"""
277 return DNSEntry.toString(self, "question", None)
280 class DNSRecord(DNSEntry):
281 """A DNS record - like a DNS entry, but has a TTL"""
283 def __init__(self, name, type, clazz, ttl):
284 DNSEntry.__init__(self, name, type, clazz)
285 self.ttl = ttl
286 self.created = currentTimeMillis()
288 def __eq__(self, other):
289 """Tests equality as per DNSRecord"""
290 return isinstance(other, DNSRecord) and DNSEntry.__eq__(self, other)
292 def suppressedBy(self, msg):
293 """Returns true if any answer in a message can suffice for the
294 information held in this record."""
295 for record in msg.answers:
296 if self.suppressedByAnswer(record):
297 return True
298 return False
300 def suppressedByAnswer(self, other):
301 """Returns true if another record has same name, type and class,
302 and if its TTL is at least half of this record's."""
303 return self == other and other.ttl > (self.ttl / 2)
305 def getExpirationTime(self, percent):
306 """Returns the time at which this record will have expired
307 by a certain percentage."""
308 return self.created + (percent * self.ttl * 10)
310 def getRemainingTTL(self, now):
311 """Returns the remaining TTL in seconds."""
312 return max(0, (self.getExpirationTime(100) - now) / 1000)
314 def isExpired(self, now):
315 """Returns true if this record has expired."""
316 return self.getExpirationTime(100) <= now
318 def isStale(self, now):
319 """Returns true if this record is at least half way expired."""
320 return self.getExpirationTime(50) <= now
322 def resetTTL(self, other):
323 """Sets this record's TTL and created time to that of
324 another record."""
325 self.created = other.created
326 self.ttl = other.ttl
328 def write(self, out):
329 """Abstract method"""
330 raise AbstractMethodException
332 def toString(self, other):
333 """String representation with addtional information"""
334 arg = "%s/%s,%s" % (self.ttl,
335 self.getRemainingTTL(currentTimeMillis()), other)
336 return DNSEntry.toString(self, "record", arg)
338 class DNSAddress(DNSRecord):
339 """A DNS address record"""
341 def __init__(self, name, type, clazz, ttl, address):
342 DNSRecord.__init__(self, name, type, clazz, ttl)
343 self.address = address
345 def write(self, out):
346 """Used in constructing an outgoing packet"""
347 out.writeString(self.address)
349 def __eq__(self, other):
350 """Tests equality on address"""
351 return isinstance(other, DNSAddress) and self.address == other.address
353 def __repr__(self):
354 """String representation"""
355 try:
356 return socket.inet_ntoa(self.address)
357 except:
358 return self.address
360 class DNSHinfo(DNSRecord):
361 """A DNS host information record"""
363 def __init__(self, name, type, clazz, ttl, cpu, os):
364 DNSRecord.__init__(self, name, type, clazz, ttl)
365 self.cpu = cpu
366 self.os = os
368 def write(self, out):
369 """Used in constructing an outgoing packet"""
370 out.writeString(self.cpu)
371 out.writeString(self.oso)
373 def __eq__(self, other):
374 """Tests equality on cpu and os"""
375 return (isinstance(other, DNSHinfo) and
376 self.cpu == other.cpu and self.os == other.os)
378 def __repr__(self):
379 """String representation"""
380 return self.cpu + " " + self.os
382 class DNSPointer(DNSRecord):
383 """A DNS pointer record"""
385 def __init__(self, name, type, clazz, ttl, alias):
386 DNSRecord.__init__(self, name, type, clazz, ttl)
387 self.alias = alias
389 def write(self, out):
390 """Used in constructing an outgoing packet"""
391 out.writeName(self.alias)
393 def __eq__(self, other):
394 """Tests equality on alias"""
395 return isinstance(other, DNSPointer) and self.alias == other.alias
397 def __repr__(self):
398 """String representation"""
399 return self.toString(self.alias)
401 class DNSText(DNSRecord):
402 """A DNS text record"""
404 def __init__(self, name, type, clazz, ttl, text):
405 DNSRecord.__init__(self, name, type, clazz, ttl)
406 self.text = text
408 def write(self, out):
409 """Used in constructing an outgoing packet"""
410 out.writeString(self.text)
412 def __eq__(self, other):
413 """Tests equality on text"""
414 return isinstance(other, DNSText) and self.text == other.text
416 def __repr__(self):
417 """String representation"""
418 if len(self.text) > 10:
419 return self.toString(self.text[:7] + "...")
420 else:
421 return self.toString(self.text)
423 class DNSService(DNSRecord):
424 """A DNS service record"""
426 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
427 DNSRecord.__init__(self, name, type, clazz, ttl)
428 self.priority = priority
429 self.weight = weight
430 self.port = port
431 self.server = server
433 def write(self, out):
434 """Used in constructing an outgoing packet"""
435 out.writeShort(self.priority)
436 out.writeShort(self.weight)
437 out.writeShort(self.port)
438 out.writeName(self.server)
440 def __eq__(self, other):
441 """Tests equality on priority, weight, port and server"""
442 return (isinstance(other, DNSService) and
443 self.priority == other.priority and
444 self.weight == other.weight and
445 self.port == other.port and
446 self.server == other.server)
448 def __repr__(self):
449 """String representation"""
450 return self.toString("%s:%s" % (self.server, self.port))
452 class DNSIncoming(object):
453 """Object representation of an incoming DNS packet"""
455 def __init__(self, data):
456 """Constructor from string holding bytes of packet"""
457 self.offset = 0
458 self.data = data
459 self.questions = []
460 self.answers = []
461 self.numQuestions = 0
462 self.numAnswers = 0
463 self.numAuthorities = 0
464 self.numAdditionals = 0
466 self.readHeader()
467 self.readQuestions()
468 self.readOthers()
470 def unpack(self, format):
471 length = struct.calcsize(format)
472 info = struct.unpack(format, self.data[self.offset:self.offset+length])
473 self.offset += length
474 return info
476 def readHeader(self):
477 """Reads header portion of packet"""
478 (self.id, self.flags, self.numQuestions, self.numAnswers,
479 self.numAuthorities, self.numAdditionals) = self.unpack('!6H')
481 def readQuestions(self):
482 """Reads questions section of packet"""
483 for i in xrange(self.numQuestions):
484 name = self.readName()
485 type, clazz = self.unpack('!HH')
487 question = DNSQuestion(name, type, clazz)
488 self.questions.append(question)
490 def readInt(self):
491 """Reads an integer from the packet"""
492 return self.unpack('!I')[0]
494 def readCharacterString(self):
495 """Reads a character string from the packet"""
496 length = ord(self.data[self.offset])
497 self.offset += 1
498 return self.readString(length)
500 def readString(self, length):
501 """Reads a string of a given length from the packet"""
502 info = self.data[self.offset:self.offset+length]
503 self.offset += length
504 return info
506 def readUnsignedShort(self):
507 """Reads an unsigned short from the packet"""
508 return self.unpack('!H')[0]
510 def readOthers(self):
511 """Reads the answers, authorities and additionals section of the
512 packet"""
513 n = self.numAnswers + self.numAuthorities + self.numAdditionals
514 for i in xrange(n):
515 domain = self.readName()
516 type, clazz, ttl, length = self.unpack('!HHiH')
518 rec = None
519 if type == _TYPE_A:
520 rec = DNSAddress(domain, type, clazz, ttl, self.readString(4))
521 elif type == _TYPE_CNAME or type == _TYPE_PTR:
522 rec = DNSPointer(domain, type, clazz, ttl, self.readName())
523 elif type == _TYPE_TXT:
524 rec = DNSText(domain, type, clazz, ttl, self.readString(length))
525 elif type == _TYPE_SRV:
526 rec = DNSService(domain, type, clazz, ttl,
527 self.readUnsignedShort(), self.readUnsignedShort(),
528 self.readUnsignedShort(), self.readName())
529 elif type == _TYPE_HINFO:
530 rec = DNSHinfo(domain, type, clazz, ttl,
531 self.readCharacterString(), self.readCharacterString())
532 elif type == _TYPE_AAAA:
533 rec = DNSAddress(domain, type, clazz, ttl, self.readString(16))
534 else:
535 # Try to ignore types we don't know about
536 # Skip the payload for the resource record so the next
537 # records can be parsed correctly
538 self.offset += length
540 if rec is not None:
541 self.answers.append(rec)
543 def isQuery(self):
544 """Returns true if this is a query"""
545 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
547 def isResponse(self):
548 """Returns true if this is a response"""
549 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
551 def readUTF(self, offset, len):
552 """Reads a UTF-8 string of a given length from the packet"""
553 return unicode(self.data[offset:offset+len], 'utf-8', 'replace')
555 def readName(self):
556 """Reads a domain name from the packet"""
557 result = ''
558 off = self.offset
559 next = -1
560 first = off
562 while True:
563 len = ord(self.data[off])
564 off += 1
565 if len == 0:
566 break
567 t = len & 0xC0
568 if t == 0x00:
569 result = ''.join((result, self.readUTF(off, len) + '.'))
570 off += len
571 elif t == 0xC0:
572 if next < 0:
573 next = off + 1
574 off = ((len & 0x3F) << 8) | ord(self.data[off])
575 if off >= first:
576 raise "Bad domain name (circular) at " + str(off)
577 first = off
578 else:
579 raise "Bad domain name at " + str(off)
581 if next >= 0:
582 self.offset = next
583 else:
584 self.offset = off
586 return result
589 class DNSOutgoing(object):
590 """Object representation of an outgoing packet"""
592 def __init__(self, flags, multicast=True):
593 self.finished = False
594 self.id = 0
595 self.multicast = multicast
596 self.flags = flags
597 self.names = {}
598 self.data = []
599 self.size = 12
601 self.questions = []
602 self.answers = []
603 self.authorities = []
604 self.additionals = []
606 def addQuestion(self, record):
607 """Adds a question"""
608 self.questions.append(record)
610 def addAnswer(self, inp, record):
611 """Adds an answer"""
612 if not record.suppressedBy(inp):
613 self.addAnswerAtTime(record, 0)
615 def addAnswerAtTime(self, record, now):
616 """Adds an answer if if does not expire by a certain time"""
617 if record is not None:
618 if now == 0 or not record.isExpired(now):
619 self.answers.append((record, now))
621 def addAuthorativeAnswer(self, record):
622 """Adds an authoritative answer"""
623 self.authorities.append(record)
625 def addAdditionalAnswer(self, record):
626 """Adds an additional answer"""
627 self.additionals.append(record)
629 def pack(self, format, value):
630 self.data.append(struct.pack(format, value))
631 self.size += struct.calcsize(format)
633 def writeByte(self, value):
634 """Writes a single byte to the packet"""
635 self.pack('!c', chr(value))
637 def insertShort(self, index, value):
638 """Inserts an unsigned short in a certain position in the packet"""
639 self.data.insert(index, struct.pack('!H', value))
640 self.size += 2
642 def writeShort(self, value):
643 """Writes an unsigned short to the packet"""
644 self.pack('!H', value)
646 def writeInt(self, value):
647 """Writes an unsigned integer to the packet"""
648 self.pack('!I', int(value))
650 def writeString(self, value):
651 """Writes a string to the packet"""
652 self.data.append(value)
653 self.size += len(value)
655 def writeUTF(self, s):
656 """Writes a UTF-8 string of a given length to the packet"""
657 utfstr = s.encode('utf-8')
658 length = len(utfstr)
659 if length > 64:
660 raise NamePartTooLongException
661 self.writeByte(length)
662 self.writeString(utfstr)
664 def writeName(self, name):
665 """Writes a domain name to the packet"""
667 try:
668 # Find existing instance of this name in packet
670 index = self.names[name]
671 except KeyError:
672 # No record of this name already, so write it
673 # out as normal, recording the location of the name
674 # for future pointers to it.
676 self.names[name] = self.size
677 parts = name.split('.')
678 if parts[-1] == '':
679 parts = parts[:-1]
680 for part in parts:
681 self.writeUTF(part)
682 self.writeByte(0)
683 return
685 # An index was found, so write a pointer to it
687 self.writeByte((index >> 8) | 0xC0)
688 self.writeByte(index)
690 def writeQuestion(self, question):
691 """Writes a question to the packet"""
692 self.writeName(question.name)
693 self.writeShort(question.type)
694 self.writeShort(question.clazz)
696 def writeRecord(self, record, now):
697 """Writes a record (answer, authoritative answer, additional) to
698 the packet"""
699 self.writeName(record.name)
700 self.writeShort(record.type)
701 if record.unique and self.multicast:
702 self.writeShort(record.clazz | _CLASS_UNIQUE)
703 else:
704 self.writeShort(record.clazz)
705 if now == 0:
706 self.writeInt(record.ttl)
707 else:
708 self.writeInt(record.getRemainingTTL(now))
709 index = len(self.data)
710 # Adjust size for the short we will write before this record
712 self.size += 2
713 record.write(self)
714 self.size -= 2
716 length = len(''.join(self.data[index:]))
717 self.insertShort(index, length) # Here is the short we adjusted for
719 def packet(self):
720 """Returns a string containing the packet's bytes
722 No further parts should be added to the packet once this
723 is done."""
724 if not self.finished:
725 self.finished = True
726 for question in self.questions:
727 self.writeQuestion(question)
728 for answer, time in self.answers:
729 self.writeRecord(answer, time)
730 for authority in self.authorities:
731 self.writeRecord(authority, 0)
732 for additional in self.additionals:
733 self.writeRecord(additional, 0)
735 self.insertShort(0, len(self.additionals))
736 self.insertShort(0, len(self.authorities))
737 self.insertShort(0, len(self.answers))
738 self.insertShort(0, len(self.questions))
739 self.insertShort(0, self.flags)
740 if self.multicast:
741 self.insertShort(0, 0)
742 else:
743 self.insertShort(0, self.id)
744 return ''.join(self.data)
747 class DNSCache(object):
748 """A cache of DNS entries"""
750 def __init__(self):
751 self.cache = {}
753 def add(self, entry):
754 """Adds an entry"""
755 try:
756 list = self.cache[entry.key]
757 except:
758 list = self.cache[entry.key] = []
759 list.append(entry)
761 def remove(self, entry):
762 """Removes an entry"""
763 try:
764 list = self.cache[entry.key]
765 list.remove(entry)
766 except:
767 pass
769 def get(self, entry):
770 """Gets an entry by key. Will return None if there is no
771 matching entry."""
772 try:
773 list = self.cache[entry.key]
774 return list[list.index(entry)]
775 except:
776 return None
778 def getByDetails(self, name, type, clazz):
779 """Gets an entry by details. Will return None if there is
780 no matching entry."""
781 entry = DNSEntry(name, type, clazz)
782 return self.get(entry)
784 def entriesWithName(self, name):
785 """Returns a list of entries whose key matches the name."""
786 try:
787 return self.cache[name]
788 except:
789 return []
791 def entries(self):
792 """Returns a list of all entries"""
793 def add(x, y): return x+y
794 try:
795 return reduce(add, self.cache.values())
796 except:
797 return []
800 class Engine(threading.Thread):
801 """An engine wraps read access to sockets, allowing objects that
802 need to receive data from sockets to be called back when the
803 sockets are ready.
805 A reader needs a handle_read() method, which is called when the socket
806 it is interested in is ready for reading.
808 Writers are not implemented here, because we only send short
809 packets.
812 def __init__(self, zc):
813 threading.Thread.__init__(self)
814 self.zc = zc
815 self.readers = {} # maps socket to reader
816 self.timeout = 5
817 self.condition = threading.Condition()
818 self.start()
820 def run(self):
821 while not _GLOBAL_DONE:
822 rs = self.getReaders()
823 if len(rs) == 0:
824 # No sockets to manage, but we wait for the timeout
825 # or addition of a socket
827 self.condition.acquire()
828 self.condition.wait(self.timeout)
829 self.condition.release()
830 else:
831 try:
832 rr, wr, er = select.select(rs, [], [], self.timeout)
833 for socket in rr:
834 try:
835 self.readers[socket].handle_read()
836 except:
837 traceback.print_exc()
838 except:
839 pass
841 def getReaders(self):
842 result = []
843 self.condition.acquire()
844 result = self.readers.keys()
845 self.condition.release()
846 return result
848 def addReader(self, reader, socket):
849 self.condition.acquire()
850 self.readers[socket] = reader
851 self.condition.notify()
852 self.condition.release()
854 def delReader(self, socket):
855 self.condition.acquire()
856 del(self.readers[socket])
857 self.condition.notify()
858 self.condition.release()
860 def notify(self):
861 self.condition.acquire()
862 self.condition.notify()
863 self.condition.release()
865 class Listener(object):
866 """A Listener is used by this module to listen on the multicast
867 group to which DNS messages are sent, allowing the implementation
868 to cache information as it arrives.
870 It requires registration with an Engine object in order to have
871 the read() method called when a socket is availble for reading."""
873 def __init__(self, zc):
874 self.zc = zc
875 self.zc.engine.addReader(self, self.zc.socket)
877 def handle_read(self):
878 try:
879 data, (addr, port) = self.zc.socket.recvfrom(_MAX_MSG_ABSOLUTE)
880 except socket.error, e:
881 # If the socket was closed by another thread -- which happens
882 # regularly on shutdown -- an EBADF exception is thrown here.
883 # Ignore it.
884 if e[0] == socket.EBADF:
885 return
886 else:
887 raise e
888 self.data = data
889 msg = DNSIncoming(data)
890 if msg.isQuery():
891 # Always multicast responses
893 if port == _MDNS_PORT:
894 self.zc.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
895 # If it's not a multicast query, reply via unicast
896 # and multicast
898 elif port == _DNS_PORT:
899 self.zc.handleQuery(msg, addr, port)
900 self.zc.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
901 else:
902 self.zc.handleResponse(msg)
905 class Reaper(threading.Thread):
906 """A Reaper is used by this module to remove cache entries that
907 have expired."""
909 def __init__(self, zc):
910 threading.Thread.__init__(self)
911 self.zc = zc
912 self.start()
914 def run(self):
915 while True:
916 self.zc.wait(10 * 1000)
917 if _GLOBAL_DONE:
918 return
919 now = currentTimeMillis()
920 for record in self.zc.cache.entries():
921 if record.isExpired(now):
922 self.zc.updateRecord(now, record)
923 self.zc.cache.remove(record)
926 class ServiceBrowser(threading.Thread):
927 """Used to browse for a service of a specific type.
929 The listener object will have its addService() and
930 removeService() methods called when this browser
931 discovers changes in the services availability."""
933 def __init__(self, zc, type, listener):
934 """Creates a browser for a specific type"""
935 threading.Thread.__init__(self)
936 self.zc = zc
937 self.type = type
938 self.listener = listener
939 self.services = {}
940 self.nextTime = currentTimeMillis()
941 self.delay = _BROWSER_TIME
942 self.list = []
944 self.done = False
946 self.zc.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
947 self.start()
949 def updateRecord(self, zc, now, record):
950 """Callback invoked by Zeroconf when new information arrives.
952 Updates information required by browser in the Zeroconf cache."""
953 if record.type == _TYPE_PTR and record.name == self.type:
954 expired = record.isExpired(now)
955 try:
956 oldrecord = self.services[record.alias.lower()]
957 if not expired:
958 oldrecord.resetTTL(record)
959 else:
960 del(self.services[record.alias.lower()])
961 callback = lambda x: self.listener.removeService(x,
962 self.type, record.alias)
963 self.list.append(callback)
964 return
965 except:
966 if not expired:
967 self.services[record.alias.lower()] = record
968 callback = lambda x: self.listener.addService(x,
969 self.type, record.alias)
970 self.list.append(callback)
972 expires = record.getExpirationTime(75)
973 if expires < self.nextTime:
974 self.nextTime = expires
976 def cancel(self):
977 self.done = True
978 self.zc.notifyAll()
980 def run(self):
981 while True:
982 event = None
983 now = currentTimeMillis()
984 if len(self.list) == 0 and self.nextTime > now:
985 self.zc.wait(self.nextTime - now)
986 if _GLOBAL_DONE or self.done:
987 return
988 now = currentTimeMillis()
990 if self.nextTime <= now:
991 out = DNSOutgoing(_FLAGS_QR_QUERY)
992 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
993 for record in self.services.values():
994 if not record.isExpired(now):
995 out.addAnswerAtTime(record, now)
996 self.zc.send(out)
997 self.nextTime = now + self.delay
998 self.delay = min(20 * 1000, self.delay * 2)
1000 if len(self.list) > 0:
1001 event = self.list.pop(0)
1003 if event is not None:
1004 event(self.zc)
1007 class ServiceInfo(object):
1008 """Service information"""
1010 def __init__(self, type, name, address=None, port=None, weight=0,
1011 priority=0, properties=None, server=None):
1012 """Create a service description.
1014 type: fully qualified service type name
1015 name: fully qualified service name
1016 address: IP address as unsigned short, network byte order
1017 port: port that the service runs on
1018 weight: weight of the service
1019 priority: priority of the service
1020 properties: dictionary of properties (or a string holding the
1021 bytes for the text field)
1022 server: fully qualified name for service host (defaults to name)"""
1024 if not name.endswith(type):
1025 raise BadTypeInNameException
1026 self.type = type
1027 self.name = name
1028 self.address = address
1029 self.port = port
1030 self.weight = weight
1031 self.priority = priority
1032 if server:
1033 self.server = server
1034 else:
1035 self.server = name
1036 self.setProperties(properties)
1038 def setProperties(self, properties):
1039 """Sets properties and text of this info from a dictionary"""
1040 if isinstance(properties, dict):
1041 self.properties = properties
1042 list = []
1043 result = ''
1044 for key in properties:
1045 value = properties[key]
1046 if value is None:
1047 suffix = ''.encode('utf-8')
1048 elif isinstance(value, str):
1049 suffix = value.encode('utf-8')
1050 elif isinstance(value, int):
1051 if value:
1052 suffix = 'true'
1053 else:
1054 suffix = 'false'
1055 else:
1056 suffix = ''.encode('utf-8')
1057 list.append('='.join((key, suffix)))
1058 for item in list:
1059 result = ''.join((result, chr(len(item)), item))
1060 self.text = result
1061 else:
1062 self.text = properties
1064 def setText(self, text):
1065 """Sets properties and text given a text field"""
1066 self.text = text
1067 try:
1068 result = {}
1069 end = len(text)
1070 index = 0
1071 strs = []
1072 while index < end:
1073 length = ord(text[index])
1074 index += 1
1075 strs.append(text[index:index+length])
1076 index += length
1078 for s in strs:
1079 try:
1080 key, value = s.split('=', 1)
1081 if value == 'true':
1082 value = True
1083 elif value == 'false' or not value:
1084 value = False
1085 except:
1086 # No equals sign at all
1087 key = s
1088 value = False
1090 # Only update non-existent properties
1091 if key and result.get(key) == None:
1092 result[key] = value
1094 self.properties = result
1095 except:
1096 traceback.print_exc()
1097 self.properties = None
1099 def getType(self):
1100 """Type accessor"""
1101 return self.type
1103 def getName(self):
1104 """Name accessor"""
1105 if self.type is not None and self.name.endswith("." + self.type):
1106 return self.name[:len(self.name) - len(self.type) - 1]
1107 return self.name
1109 def getAddress(self):
1110 """Address accessor"""
1111 return self.address
1113 def getPort(self):
1114 """Port accessor"""
1115 return self.port
1117 def getPriority(self):
1118 """Pirority accessor"""
1119 return self.priority
1121 def getWeight(self):
1122 """Weight accessor"""
1123 return self.weight
1125 def getProperties(self):
1126 """Properties accessor"""
1127 return self.properties
1129 def getText(self):
1130 """Text accessor"""
1131 return self.text
1133 def getServer(self):
1134 """Server accessor"""
1135 return self.server
1137 def updateRecord(self, zc, now, record):
1138 """Updates service information from a DNS record"""
1139 if record is not None and not record.isExpired(now):
1140 if record.type == _TYPE_A:
1141 #if record.name == self.name:
1142 if record.name == self.server:
1143 self.address = record.address
1144 elif record.type == _TYPE_SRV:
1145 if record.name == self.name:
1146 self.server = record.server
1147 self.port = record.port
1148 self.weight = record.weight
1149 self.priority = record.priority
1150 #self.address = None
1151 self.updateRecord(zc, now,
1152 zc.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1153 elif record.type == _TYPE_TXT:
1154 if record.name == self.name:
1155 self.setText(record.text)
1157 def request(self, zc, timeout):
1158 """Returns true if the service could be discovered on the
1159 network, and updates this object with details discovered.
1161 now = currentTimeMillis()
1162 delay = _LISTENER_TIME
1163 next = now + delay
1164 last = now + timeout
1165 result = False
1166 try:
1167 zc.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1168 while (self.server is None or self.address is None or
1169 self.text is None):
1170 if last <= now:
1171 return False
1172 if next <= now:
1173 out = DNSOutgoing(_FLAGS_QR_QUERY)
1174 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV,
1175 _CLASS_IN))
1176 out.addAnswerAtTime(zc.cache.getByDetails(self.name,
1177 _TYPE_SRV, _CLASS_IN), now)
1178 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT,
1179 _CLASS_IN))
1180 out.addAnswerAtTime(zc.cache.getByDetails(self.name,
1181 _TYPE_TXT, _CLASS_IN), now)
1182 if self.server is not None:
1183 out.addQuestion(DNSQuestion(self.server,
1184 _TYPE_A, _CLASS_IN))
1185 out.addAnswerAtTime(zc.cache.getByDetails(self.server,
1186 _TYPE_A, _CLASS_IN), now)
1187 zc.send(out)
1188 next = now + delay
1189 delay = delay * 2
1191 zc.wait(min(next, last) - now)
1192 now = currentTimeMillis()
1193 result = True
1194 finally:
1195 zc.removeListener(self)
1197 return result
1199 def __eq__(self, other):
1200 """Tests equality of service name"""
1201 if isinstance(other, ServiceInfo):
1202 return other.name == self.name
1203 return False
1205 def __ne__(self, other):
1206 """Non-equality test"""
1207 return not self.__eq__(other)
1209 def __repr__(self):
1210 """String representation"""
1211 result = "service[%s,%s:%s," % (self.name,
1212 socket.inet_ntoa(self.getAddress()), self.port)
1213 if self.text is None:
1214 result += "None"
1215 else:
1216 if len(self.text) < 20:
1217 result += self.text
1218 else:
1219 result += self.text[:17] + "..."
1220 result += "]"
1221 return result
1224 class Zeroconf(object):
1225 """Implementation of Zeroconf Multicast DNS Service Discovery
1227 Supports registration, unregistration, queries and browsing.
1229 def __init__(self, bindaddress=None):
1230 """Creates an instance of the Zeroconf class, establishing
1231 multicast communications, listening and reaping threads."""
1232 global _GLOBAL_DONE
1233 _GLOBAL_DONE = False
1234 if bindaddress is None:
1235 try:
1236 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1237 s.connect(('4.2.2.1', 123))
1238 self.intf = s.getsockname()[0]
1239 except:
1240 self.intf = socket.gethostbyname(socket.gethostname())
1241 else:
1242 self.intf = bindaddress
1243 self.group = ('', _MDNS_PORT)
1244 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1245 try:
1246 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1247 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1248 except:
1249 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1250 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1251 # Volume 2"), but some BSD-derived systems require
1252 # SO_REUSEPORT to be specified explicity. Also, not all
1253 # versions of Python have SO_REUSEPORT available. So
1254 # if you're on a BSD-based system, and haven't upgraded
1255 # to Python 2.3 yet, you may find this library doesn't
1256 # work as expected.
1258 pass
1259 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
1260 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
1261 try:
1262 self.socket.bind(self.group)
1263 except:
1264 # Some versions of linux raise an exception even though
1265 # the SO_REUSE* options have been set, so ignore it
1267 pass
1268 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF,
1269 # socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1270 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP,
1271 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1273 self.listeners = []
1274 self.browsers = []
1275 self.services = {}
1276 self.servicetypes = {}
1278 self.cache = DNSCache()
1280 self.condition = threading.Condition()
1282 self.engine = Engine(self)
1283 self.listener = Listener(self)
1284 self.reaper = Reaper(self)
1286 def isLoopback(self):
1287 return self.intf.startswith("127.0.0.1")
1289 def isLinklocal(self):
1290 return self.intf.startswith("169.254.")
1292 def wait(self, timeout):
1293 """Calling thread waits for a given number of milliseconds or
1294 until notified."""
1295 self.condition.acquire()
1296 self.condition.wait(timeout/1000)
1297 self.condition.release()
1299 def notifyAll(self):
1300 """Notifies all waiting threads"""
1301 self.condition.acquire()
1302 self.condition.notifyAll()
1303 self.condition.release()
1305 def getServiceInfo(self, type, name, timeout=3000):
1306 """Returns network's service information for a particular
1307 name and type, or None if no service matches by the timeout,
1308 which defaults to 3 seconds."""
1309 info = ServiceInfo(type, name)
1310 if info.request(self, timeout):
1311 return info
1312 return None
1314 def addServiceListener(self, type, listener):
1315 """Adds a listener for a particular service type. This object
1316 will then have its updateRecord method called when information
1317 arrives for that type."""
1318 self.removeServiceListener(listener)
1319 self.browsers.append(ServiceBrowser(self, type, listener))
1321 def removeServiceListener(self, listener):
1322 """Removes a listener from the set that is currently listening."""
1323 for browser in self.browsers:
1324 if browser.listener == listener:
1325 browser.cancel()
1326 del(browser)
1328 def registerService(self, info, ttl=_DNS_TTL):
1329 """Registers service information to the network with a default TTL
1330 of 60 seconds. Zeroconf will then respond to requests for
1331 information for that service. The name of the service may be
1332 changed if needed to make it unique on the network."""
1333 self.checkService(info)
1334 self.services[info.name.lower()] = info
1335 if info.type in self.servicetypes:
1336 self.servicetypes[info.type]+=1
1337 else:
1338 self.servicetypes[info.type]=1
1339 now = currentTimeMillis()
1340 nextTime = now
1341 i = 0
1342 while i < 3:
1343 if now < nextTime:
1344 self.wait(nextTime - now)
1345 now = currentTimeMillis()
1346 continue
1347 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1348 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1349 _CLASS_IN, ttl, info.name), 0)
1350 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1351 _CLASS_IN, ttl, info.priority, info.weight, info.port,
1352 info.server), 0)
1353 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN,
1354 ttl, info.text), 0)
1355 if info.address:
1356 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A,
1357 _CLASS_IN, ttl, info.address), 0)
1358 self.send(out)
1359 i += 1
1360 nextTime += _REGISTER_TIME
1362 def unregisterService(self, info):
1363 """Unregister a service."""
1364 try:
1365 del(self.services[info.name.lower()])
1366 if self.servicetypes[info.type]>1:
1367 self.servicetypes[info.type]-=1
1368 else:
1369 del self.servicetypes[info.type]
1370 except:
1371 pass
1372 now = currentTimeMillis()
1373 nextTime = now
1374 i = 0
1375 while i < 3:
1376 if now < nextTime:
1377 self.wait(nextTime - now)
1378 now = currentTimeMillis()
1379 continue
1380 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1381 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1382 _CLASS_IN, 0, info.name), 0)
1383 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1384 _CLASS_IN, 0, info.priority, info.weight, info.port,
1385 info.name), 0)
1386 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN,
1387 0, info.text), 0)
1388 if info.address:
1389 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A,
1390 _CLASS_IN, 0, info.address), 0)
1391 self.send(out)
1392 i += 1
1393 nextTime += _UNREGISTER_TIME
1395 def unregisterAllServices(self):
1396 """Unregister all registered services."""
1397 if len(self.services) > 0:
1398 now = currentTimeMillis()
1399 nextTime = now
1400 i = 0
1401 while i < 3:
1402 if now < nextTime:
1403 self.wait(nextTime - now)
1404 now = currentTimeMillis()
1405 continue
1406 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1407 for info in self.services.values():
1408 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1409 _CLASS_IN, 0, info.name), 0)
1410 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1411 _CLASS_IN, 0, info.priority, info.weight,
1412 info.port, info.server), 0)
1413 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT,
1414 _CLASS_IN, 0, info.text), 0)
1415 if info.address:
1416 out.addAnswerAtTime(DNSAddress(info.server,
1417 _TYPE_A, _CLASS_IN, 0, info.address), 0)
1418 self.send(out)
1419 i += 1
1420 nextTime += _UNREGISTER_TIME
1422 def checkService(self, info):
1423 """Checks the network for a unique service name, modifying the
1424 ServiceInfo passed in if it is not unique."""
1425 now = currentTimeMillis()
1426 nextTime = now
1427 i = 0
1428 while i < 3:
1429 for record in self.cache.entriesWithName(info.type):
1430 if (record.type == _TYPE_PTR and
1431 not record.isExpired(now) and
1432 record.alias == info.name):
1433 if info.name.find('.') < 0:
1434 info.name = '%s.[%s:%s].%s' % (info.name,
1435 info.address, info.port, info.type)
1437 self.checkService(info)
1438 return
1439 raise NonUniqueNameException
1440 if now < nextTime:
1441 self.wait(nextTime - now)
1442 now = currentTimeMillis()
1443 continue
1444 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1445 self.debug = out
1446 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1447 out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR,
1448 _CLASS_IN, _DNS_TTL, info.name))
1449 self.send(out)
1450 i += 1
1451 nextTime += _CHECK_TIME
1453 def addListener(self, listener, question):
1454 """Adds a listener for a given question. The listener will have
1455 its updateRecord method called when information is available to
1456 answer the question."""
1457 now = currentTimeMillis()
1458 self.listeners.append(listener)
1459 if question is not None:
1460 for record in self.cache.entriesWithName(question.name):
1461 if question.answeredBy(record) and not record.isExpired(now):
1462 listener.updateRecord(self, now, record)
1463 self.notifyAll()
1465 def removeListener(self, listener):
1466 """Removes a listener."""
1467 try:
1468 self.listeners.remove(listener)
1469 self.notifyAll()
1470 except:
1471 pass
1473 def updateRecord(self, now, rec):
1474 """Used to notify listeners of new information that has updated
1475 a record."""
1476 for listener in self.listeners:
1477 listener.updateRecord(self, now, rec)
1478 self.notifyAll()
1480 def handleResponse(self, msg):
1481 """Deal with incoming response packets. All answers
1482 are held in the cache, and listeners are notified."""
1483 now = currentTimeMillis()
1484 for record in msg.answers:
1485 expired = record.isExpired(now)
1486 if record in self.cache.entries():
1487 if expired:
1488 self.cache.remove(record)
1489 else:
1490 entry = self.cache.get(record)
1491 if entry is not None:
1492 entry.resetTTL(record)
1493 record = entry
1494 else:
1495 self.cache.add(record)
1497 self.updateRecord(now, record)
1499 def handleQuery(self, msg, addr, port):
1500 """Deal with incoming query packets. Provides a response if
1501 possible."""
1502 out = None
1504 # Support unicast client responses
1506 if port != _MDNS_PORT:
1507 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, False)
1508 for question in msg.questions:
1509 out.addQuestion(question)
1511 for question in msg.questions:
1512 if question.type == _TYPE_PTR:
1513 if question.name == "_services._dns-sd._udp.local.":
1514 for stype in self.servicetypes.keys():
1515 if out is None:
1516 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1517 out.addAnswer(msg,
1518 DNSPointer("_services._dns-sd._udp.local.",
1519 _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1520 for service in self.services.values():
1521 if question.name == service.type:
1522 if out is None:
1523 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1524 out.addAnswer(msg,
1525 DNSPointer(service.type, _TYPE_PTR,
1526 _CLASS_IN, _DNS_TTL, service.name))
1527 else:
1528 try:
1529 if out is None:
1530 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1532 # Answer A record queries for any service addresses we know
1533 if question.type in (_TYPE_A, _TYPE_ANY):
1534 for service in self.services.values():
1535 if service.server == question.name.lower():
1536 out.addAnswer(msg, DNSAddress(question.name,
1537 _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
1538 _DNS_TTL, service.address))
1540 service = self.services.get(question.name.lower(), None)
1541 if not service: continue
1543 if question.type in (_TYPE_SRV, _TYPE_ANY):
1544 out.addAnswer(msg, DNSService(question.name,
1545 _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE,
1546 _DNS_TTL, service.priority, service.weight,
1547 service.port, service.server))
1548 if question.type in (_TYPE_TXT, _TYPE_ANY):
1549 out.addAnswer(msg, DNSText(question.name,
1550 _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE,
1551 _DNS_TTL, service.text))
1552 if question.type == _TYPE_SRV:
1553 out.addAdditionalAnswer(DNSAddress(service.server,
1554 _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
1555 _DNS_TTL, service.address))
1556 except:
1557 traceback.print_exc()
1559 if out is not None and out.answers:
1560 out.id = msg.id
1561 self.send(out, addr, port)
1563 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1564 """Sends an outgoing packet."""
1565 packet = out.packet()
1566 try:
1567 while packet:
1568 bytes_sent = self.socket.sendto(packet, 0, (addr, port))
1569 if bytes_sent < 0:
1570 break
1571 packet = packet[bytes_sent:]
1572 except:
1573 # Ignore this, it may be a temporary loss of network connection
1574 pass
1576 def close(self):
1577 """Ends the background threads, and prevent this instance from
1578 servicing further queries."""
1579 global _GLOBAL_DONE
1580 if not _GLOBAL_DONE:
1581 _GLOBAL_DONE = True
1582 self.notifyAll()
1583 self.engine.notify()
1584 self.unregisterAllServices()
1585 self.socket.setsockopt(socket.SOL_IP,
1586 socket.IP_DROP_MEMBERSHIP,
1587 socket.inet_aton(_MDNS_ADDR) +
1588 socket.inet_aton('0.0.0.0'))
1589 self.socket.close()
1591 # Test a few module features, including service registration, service
1592 # query (for Zoe), and service unregistration.
1594 if __name__ == '__main__':
1595 print "Multicast DNS Service Discovery for Python, version", __version__
1596 r = Zeroconf()
1597 print "1. Testing registration of a service..."
1598 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1599 info = ServiceInfo("_http._tcp.local.",
1600 "My Service Name._http._tcp.local.",
1601 socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1602 print " Registering service..."
1603 r.registerService(info)
1604 print " Registration done."
1605 print "2. Testing query of service information..."
1606 print " Getting ZOE service:",
1607 print str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1608 print " Query done."
1609 print "3. Testing query of own service..."
1610 print " Getting self:",
1611 print str(r.getServiceInfo("_http._tcp.local.",
1612 "My Service Name._http._tcp.local."))
1613 print " Query done."
1614 print "4. Testing unregister of service information..."
1615 r.unregisterService(info)
1616 print " Unregister done."
1617 r.close()