Report megabits per second instead of kilobytes, for consistency with
[pyTivo/wmcbrine.git] / Zeroconf.py
blob84640777446e8c7e83ec1d0e64de33d3c11cf570
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 (e.g. on Mac OS X)
36 tested against Mac OS X 10.3.2's mDNSResponder
37 corrections to removal of list entries for service browser"""
39 """0.10 update - Jonathon Paisley contributed these corrections:
40 always multicast replies, even when query is unicast
41 correct a pointer encoding problem
42 can now write records in any order
43 traceback shown on failure
44 better TXT record parsing
45 server is now separate from name
46 can cancel a service browser
48 modified some unit tests to accommodate these changes"""
50 """0.09 update - remove all records on service unregistration
51 fix DOS security problem with readName"""
53 """0.08 update - changed licensing to LGPL"""
55 """0.07 update - faster shutdown on engine
56 pointer encoding of outgoing names
57 ServiceBrowser now works
58 new unit tests"""
60 """0.06 update - small improvements with unit tests
61 added defined exception types
62 new style objects
63 fixed hostname/interface problem
64 fixed socket timeout problem
65 fixed addServiceListener() typo bug
66 using select() for socket reads
67 tested on Debian unstable with Python 2.2.2"""
69 """0.05 update - ensure case insensitivty on domain names
70 support for unicast DNS queries"""
72 """0.04 update - added some unit tests
73 added __ne__ adjuncts where required
74 ensure names end in '.local.'
75 timeout on receiving socket for clean shutdown"""
77 __author__ = "Paul Scott-Murphy"
78 __email__ = "paul at scott dash murphy dot com"
79 __version__ = "0.12"
81 import time
82 import struct
83 import socket
84 import threading
85 import select
86 import traceback
88 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
90 # hook for threads
92 globals()['_GLOBAL_DONE'] = 0
94 # Some timing constants
96 _UNREGISTER_TIME = 125
97 _CHECK_TIME = 175
98 _REGISTER_TIME = 225
99 _LISTENER_TIME = 200
100 _BROWSER_TIME = 500
102 # Some DNS constants
104 _MDNS_ADDR = '224.0.0.251'
105 _MDNS_PORT = 5353;
106 _DNS_PORT = 53;
107 _DNS_TTL = 60 * 60; # one hour default TTL
109 _MAX_MSG_TYPICAL = 1460 # unused
110 _MAX_MSG_ABSOLUTE = 8972
112 _FLAGS_QR_MASK = 0x8000 # query response mask
113 _FLAGS_QR_QUERY = 0x0000 # query
114 _FLAGS_QR_RESPONSE = 0x8000 # response
116 _FLAGS_AA = 0x0400 # Authorative answer
117 _FLAGS_TC = 0x0200 # Truncated
118 _FLAGS_RD = 0x0100 # Recursion desired
119 _FLAGS_RA = 0x8000 # Recursion available
121 _FLAGS_Z = 0x0040 # Zero
122 _FLAGS_AD = 0x0020 # Authentic data
123 _FLAGS_CD = 0x0010 # Checking disabled
125 _CLASS_IN = 1
126 _CLASS_CS = 2
127 _CLASS_CH = 3
128 _CLASS_HS = 4
129 _CLASS_NONE = 254
130 _CLASS_ANY = 255
131 _CLASS_MASK = 0x7FFF
132 _CLASS_UNIQUE = 0x8000
134 _TYPE_A = 1
135 _TYPE_NS = 2
136 _TYPE_MD = 3
137 _TYPE_MF = 4
138 _TYPE_CNAME = 5
139 _TYPE_SOA = 6
140 _TYPE_MB = 7
141 _TYPE_MG = 8
142 _TYPE_MR = 9
143 _TYPE_NULL = 10
144 _TYPE_WKS = 11
145 _TYPE_PTR = 12
146 _TYPE_HINFO = 13
147 _TYPE_MINFO = 14
148 _TYPE_MX = 15
149 _TYPE_TXT = 16
150 _TYPE_AAAA = 28
151 _TYPE_SRV = 33
152 _TYPE_ANY = 255
154 # Mapping constants to names
156 _CLASSES = { _CLASS_IN : "in",
157 _CLASS_CS : "cs",
158 _CLASS_CH : "ch",
159 _CLASS_HS : "hs",
160 _CLASS_NONE : "none",
161 _CLASS_ANY : "any" }
163 _TYPES = { _TYPE_A : "a",
164 _TYPE_NS : "ns",
165 _TYPE_MD : "md",
166 _TYPE_MF : "mf",
167 _TYPE_CNAME : "cname",
168 _TYPE_SOA : "soa",
169 _TYPE_MB : "mb",
170 _TYPE_MG : "mg",
171 _TYPE_MR : "mr",
172 _TYPE_NULL : "null",
173 _TYPE_WKS : "wks",
174 _TYPE_PTR : "ptr",
175 _TYPE_HINFO : "hinfo",
176 _TYPE_MINFO : "minfo",
177 _TYPE_MX : "mx",
178 _TYPE_TXT : "txt",
179 _TYPE_AAAA : "quada",
180 _TYPE_SRV : "srv",
181 _TYPE_ANY : "any" }
183 # utility functions
185 def currentTimeMillis():
186 """Current system time in milliseconds"""
187 return time.time() * 1000
189 # Exceptions
191 class NonLocalNameException(Exception):
192 pass
194 class NonUniqueNameException(Exception):
195 pass
197 class NamePartTooLongException(Exception):
198 pass
200 class AbstractMethodException(Exception):
201 pass
203 class BadTypeInNameException(Exception):
204 pass
206 # implementation classes
208 class DNSEntry(object):
209 """A DNS entry"""
211 def __init__(self, name, type, clazz):
212 self.key = name.lower()
213 self.name = name
214 self.type = type
215 self.clazz = clazz & _CLASS_MASK
216 self.unique = (clazz & _CLASS_UNIQUE) != 0
218 def __eq__(self, other):
219 """Equality test on name, type, and class"""
220 if isinstance(other, DNSEntry):
221 return self.name == other.name and self.type == other.type and self.clazz == other.clazz
222 return 0
224 def __ne__(self, other):
225 """Non-equality test"""
226 return not self.__eq__(other)
228 def getClazz(self, clazz):
229 """Class accessor"""
230 try:
231 return _CLASSES[clazz]
232 except:
233 return "?(%s)" % (clazz)
235 def getType(self, type):
236 """Type accessor"""
237 try:
238 return _TYPES[type]
239 except:
240 return "?(%s)" % (type)
242 def toString(self, hdr, other):
243 """String representation with additional information"""
244 result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz))
245 if self.unique:
246 result += "-unique,"
247 else:
248 result += ","
249 result += self.name
250 if other is not None:
251 result += ",%s]" % (other)
252 else:
253 result += "]"
254 return result
256 class DNSQuestion(DNSEntry):
257 """A DNS question entry"""
259 def __init__(self, name, type, clazz):
260 #if not name.endswith(".local."):
261 # raise NonLocalNameException
262 DNSEntry.__init__(self, name, type, clazz)
264 def answeredBy(self, rec):
265 """Returns true if the question is answered by the record"""
266 return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name
268 def __repr__(self):
269 """String representation"""
270 return DNSEntry.toString(self, "question", None)
273 class DNSRecord(DNSEntry):
274 """A DNS record - like a DNS entry, but has a TTL"""
276 def __init__(self, name, type, clazz, ttl):
277 DNSEntry.__init__(self, name, type, clazz)
278 self.ttl = ttl
279 self.created = currentTimeMillis()
281 def __eq__(self, other):
282 """Tests equality as per DNSRecord"""
283 if isinstance(other, DNSRecord):
284 return DNSEntry.__eq__(self, other)
285 return 0
287 def suppressedBy(self, msg):
288 """Returns true if any answer in a message can suffice for the
289 information held in this record."""
290 for record in msg.answers:
291 if self.suppressedByAnswer(record):
292 return 1
293 return 0
295 def suppressedByAnswer(self, other):
296 """Returns true if another record has same name, type and class,
297 and if its TTL is at least half of this record's."""
298 if self == other and other.ttl > (self.ttl / 2):
299 return 1
300 return 0
302 def getExpirationTime(self, percent):
303 """Returns the time at which this record will have expired
304 by a certain percentage."""
305 return self.created + (percent * self.ttl * 10)
307 def getRemainingTTL(self, now):
308 """Returns the remaining TTL in seconds."""
309 return max(0, (self.getExpirationTime(100) - now) / 1000)
311 def isExpired(self, now):
312 """Returns true if this record has expired."""
313 return self.getExpirationTime(100) <= now
315 def isStale(self, now):
316 """Returns true if this record is at least half way expired."""
317 return self.getExpirationTime(50) <= now
319 def resetTTL(self, other):
320 """Sets this record's TTL and created time to that of
321 another record."""
322 self.created = other.created
323 self.ttl = other.ttl
325 def write(self, out):
326 """Abstract method"""
327 raise AbstractMethodException
329 def toString(self, other):
330 """String representation with addtional information"""
331 arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other)
332 return DNSEntry.toString(self, "record", arg)
334 class DNSAddress(DNSRecord):
335 """A DNS address record"""
337 def __init__(self, name, type, clazz, ttl, address):
338 DNSRecord.__init__(self, name, type, clazz, ttl)
339 self.address = address
341 def write(self, out):
342 """Used in constructing an outgoing packet"""
343 out.writeString(self.address, len(self.address))
345 def __eq__(self, other):
346 """Tests equality on address"""
347 if isinstance(other, DNSAddress):
348 return self.address == other.address
349 return 0
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, len(self.cpu))
369 out.writeString(self.os, len(self.os))
371 def __eq__(self, other):
372 """Tests equality on cpu and os"""
373 if isinstance(other, DNSHinfo):
374 return self.cpu == other.cpu and self.os == other.os
375 return 0
377 def __repr__(self):
378 """String representation"""
379 return self.cpu + " " + self.os
381 class DNSPointer(DNSRecord):
382 """A DNS pointer record"""
384 def __init__(self, name, type, clazz, ttl, alias):
385 DNSRecord.__init__(self, name, type, clazz, ttl)
386 self.alias = alias
388 def write(self, out):
389 """Used in constructing an outgoing packet"""
390 out.writeName(self.alias)
392 def __eq__(self, other):
393 """Tests equality on alias"""
394 if isinstance(other, DNSPointer):
395 return self.alias == other.alias
396 return 0
398 def __repr__(self):
399 """String representation"""
400 return self.toString(self.alias)
402 class DNSText(DNSRecord):
403 """A DNS text record"""
405 def __init__(self, name, type, clazz, ttl, text):
406 DNSRecord.__init__(self, name, type, clazz, ttl)
407 self.text = text
409 def write(self, out):
410 """Used in constructing an outgoing packet"""
411 out.writeString(self.text, len(self.text))
413 def __eq__(self, other):
414 """Tests equality on text"""
415 if isinstance(other, DNSText):
416 return self.text == other.text
417 return 0
419 def __repr__(self):
420 """String representation"""
421 if len(self.text) > 10:
422 return self.toString(self.text[:7] + "...")
423 else:
424 return self.toString(self.text)
426 class DNSService(DNSRecord):
427 """A DNS service record"""
429 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
430 DNSRecord.__init__(self, name, type, clazz, ttl)
431 self.priority = priority
432 self.weight = weight
433 self.port = port
434 self.server = server
436 def write(self, out):
437 """Used in constructing an outgoing packet"""
438 out.writeShort(self.priority)
439 out.writeShort(self.weight)
440 out.writeShort(self.port)
441 out.writeName(self.server)
443 def __eq__(self, other):
444 """Tests equality on priority, weight, port and server"""
445 if isinstance(other, DNSService):
446 return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server
447 return 0
449 def __repr__(self):
450 """String representation"""
451 return self.toString("%s:%s" % (self.server, self.port))
453 class DNSIncoming(object):
454 """Object representation of an incoming DNS packet"""
456 def __init__(self, data):
457 """Constructor from string holding bytes of packet"""
458 self.offset = 0
459 self.data = data
460 self.questions = []
461 self.answers = []
462 self.numQuestions = 0
463 self.numAnswers = 0
464 self.numAuthorities = 0
465 self.numAdditionals = 0
467 self.readHeader()
468 self.readQuestions()
469 self.readOthers()
471 def readHeader(self):
472 """Reads header portion of packet"""
473 format = '!HHHHHH'
474 length = struct.calcsize(format)
475 info = struct.unpack(format, self.data[self.offset:self.offset+length])
476 self.offset += length
478 self.id = info[0]
479 self.flags = info[1]
480 self.numQuestions = info[2]
481 self.numAnswers = info[3]
482 self.numAuthorities = info[4]
483 self.numAdditionals = info[5]
485 def readQuestions(self):
486 """Reads questions section of packet"""
487 format = '!HH'
488 length = struct.calcsize(format)
489 for i in range(0, self.numQuestions):
490 name = self.readName()
491 info = struct.unpack(format, self.data[self.offset:self.offset+length])
492 self.offset += length
494 question = DNSQuestion(name, info[0], info[1])
495 self.questions.append(question)
497 def readInt(self):
498 """Reads an integer from the packet"""
499 format = '!I'
500 length = struct.calcsize(format)
501 info = struct.unpack(format, self.data[self.offset:self.offset+length])
502 self.offset += length
503 return info[0]
505 def readCharacterString(self):
506 """Reads a character string from the packet"""
507 length = ord(self.data[self.offset])
508 self.offset += 1
509 return self.readString(length)
511 def readString(self, len):
512 """Reads a string of a given length from the packet"""
513 format = '!' + str(len) + 's'
514 length = struct.calcsize(format)
515 info = struct.unpack(format, self.data[self.offset:self.offset+length])
516 self.offset += length
517 return info[0]
519 def readUnsignedShort(self):
520 """Reads an unsigned short from the packet"""
521 format = '!H'
522 length = struct.calcsize(format)
523 info = struct.unpack(format, self.data[self.offset:self.offset+length])
524 self.offset += length
525 return info[0]
527 def readOthers(self):
528 """Reads the answers, authorities and additionals section of the packet"""
529 format = '!HHiH'
530 length = struct.calcsize(format)
531 n = self.numAnswers + self.numAuthorities + self.numAdditionals
532 for i in range(0, n):
533 domain = self.readName()
534 info = struct.unpack(format, self.data[self.offset:self.offset+length])
535 self.offset += length
537 rec = None
538 if info[0] == _TYPE_A:
539 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4))
540 elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
541 rec = DNSPointer(domain, info[0], info[1], info[2], self.readName())
542 elif info[0] == _TYPE_TXT:
543 rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3]))
544 elif info[0] == _TYPE_SRV:
545 rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName())
546 elif info[0] == _TYPE_HINFO:
547 rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString())
548 elif info[0] == _TYPE_AAAA:
549 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16))
550 else:
551 # Try to ignore types we don't know about
552 # Skip the payload for the resource record so the next
553 # records can be parsed correctly
554 self.offset += info[3]
556 if rec is not None:
557 self.answers.append(rec)
559 def isQuery(self):
560 """Returns true if this is a query"""
561 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
563 def isResponse(self):
564 """Returns true if this is a response"""
565 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
567 def readUTF(self, offset, len):
568 """Reads a UTF-8 string of a given length from the packet"""
569 return unicode(self.data[offset:offset+len], 'utf-8', 'replace')
571 def readName(self):
572 """Reads a domain name from the packet"""
573 result = ''
574 off = self.offset
575 next = -1
576 first = off
578 while 1:
579 len = ord(self.data[off])
580 off += 1
581 if len == 0:
582 break
583 t = len & 0xC0
584 if t == 0x00:
585 result = ''.join((result, self.readUTF(off, len) + '.'))
586 off += len
587 elif t == 0xC0:
588 if next < 0:
589 next = off + 1
590 off = ((len & 0x3F) << 8) | ord(self.data[off])
591 if off >= first:
592 raise "Bad domain name (circular) at " + str(off)
593 first = off
594 else:
595 raise "Bad domain name at " + str(off)
597 if next >= 0:
598 self.offset = next
599 else:
600 self.offset = off
602 return result
605 class DNSOutgoing(object):
606 """Object representation of an outgoing packet"""
608 def __init__(self, flags, multicast = 1):
609 self.finished = 0
610 self.id = 0
611 self.multicast = multicast
612 self.flags = flags
613 self.names = {}
614 self.data = []
615 self.size = 12
617 self.questions = []
618 self.answers = []
619 self.authorities = []
620 self.additionals = []
622 def addQuestion(self, record):
623 """Adds a question"""
624 self.questions.append(record)
626 def addAnswer(self, inp, record):
627 """Adds an answer"""
628 if not record.suppressedBy(inp):
629 self.addAnswerAtTime(record, 0)
631 def addAnswerAtTime(self, record, now):
632 """Adds an answer if if does not expire by a certain time"""
633 if record is not None:
634 if now == 0 or not record.isExpired(now):
635 self.answers.append((record, now))
637 def addAuthorativeAnswer(self, record):
638 """Adds an authoritative answer"""
639 self.authorities.append(record)
641 def addAdditionalAnswer(self, record):
642 """Adds an additional answer"""
643 self.additionals.append(record)
645 def writeByte(self, value):
646 """Writes a single byte to the packet"""
647 format = '!c'
648 self.data.append(struct.pack(format, chr(value)))
649 self.size += 1
651 def insertShort(self, index, value):
652 """Inserts an unsigned short in a certain position in the packet"""
653 format = '!H'
654 self.data.insert(index, struct.pack(format, value))
655 self.size += 2
657 def writeShort(self, value):
658 """Writes an unsigned short to the packet"""
659 format = '!H'
660 self.data.append(struct.pack(format, value))
661 self.size += 2
663 def writeInt(self, value):
664 """Writes an unsigned integer to the packet"""
665 format = '!I'
666 self.data.append(struct.pack(format, int(value)))
667 self.size += 4
669 def writeString(self, value, length):
670 """Writes a string to the packet"""
671 format = '!' + str(length) + 's'
672 self.data.append(struct.pack(format, value))
673 self.size += length
675 def writeUTF(self, s):
676 """Writes a UTF-8 string of a given length to the packet"""
677 utfstr = s.encode('utf-8')
678 length = len(utfstr)
679 if length > 64:
680 raise NamePartTooLongException
681 self.writeByte(length)
682 self.writeString(utfstr, length)
684 def writeName(self, name):
685 """Writes a domain name to the packet"""
687 try:
688 # Find existing instance of this name in packet
690 index = self.names[name]
691 except KeyError:
692 # No record of this name already, so write it
693 # out as normal, recording the location of the name
694 # for future pointers to it.
696 self.names[name] = self.size
697 parts = name.split('.')
698 if parts[-1] == '':
699 parts = parts[:-1]
700 for part in parts:
701 self.writeUTF(part)
702 self.writeByte(0)
703 return
705 # An index was found, so write a pointer to it
707 self.writeByte((index >> 8) | 0xC0)
708 self.writeByte(index)
710 def writeQuestion(self, question):
711 """Writes a question to the packet"""
712 self.writeName(question.name)
713 self.writeShort(question.type)
714 self.writeShort(question.clazz)
716 def writeRecord(self, record, now):
717 """Writes a record (answer, authoritative answer, additional) to
718 the packet"""
719 self.writeName(record.name)
720 self.writeShort(record.type)
721 if record.unique and self.multicast:
722 self.writeShort(record.clazz | _CLASS_UNIQUE)
723 else:
724 self.writeShort(record.clazz)
725 if now == 0:
726 self.writeInt(record.ttl)
727 else:
728 self.writeInt(record.getRemainingTTL(now))
729 index = len(self.data)
730 # Adjust size for the short we will write before this record
732 self.size += 2
733 record.write(self)
734 self.size -= 2
736 length = len(''.join(self.data[index:]))
737 self.insertShort(index, length) # Here is the short we adjusted for
739 def packet(self):
740 """Returns a string containing the packet's bytes
742 No further parts should be added to the packet once this
743 is done."""
744 if not self.finished:
745 self.finished = 1
746 for question in self.questions:
747 self.writeQuestion(question)
748 for answer, time in self.answers:
749 self.writeRecord(answer, time)
750 for authority in self.authorities:
751 self.writeRecord(authority, 0)
752 for additional in self.additionals:
753 self.writeRecord(additional, 0)
755 self.insertShort(0, len(self.additionals))
756 self.insertShort(0, len(self.authorities))
757 self.insertShort(0, len(self.answers))
758 self.insertShort(0, len(self.questions))
759 self.insertShort(0, self.flags)
760 if self.multicast:
761 self.insertShort(0, 0)
762 else:
763 self.insertShort(0, self.id)
764 return ''.join(self.data)
767 class DNSCache(object):
768 """A cache of DNS entries"""
770 def __init__(self):
771 self.cache = {}
773 def add(self, entry):
774 """Adds an entry"""
775 try:
776 list = self.cache[entry.key]
777 except:
778 list = self.cache[entry.key] = []
779 list.append(entry)
781 def remove(self, entry):
782 """Removes an entry"""
783 try:
784 list = self.cache[entry.key]
785 list.remove(entry)
786 except:
787 pass
789 def get(self, entry):
790 """Gets an entry by key. Will return None if there is no
791 matching entry."""
792 try:
793 list = self.cache[entry.key]
794 return list[list.index(entry)]
795 except:
796 return None
798 def getByDetails(self, name, type, clazz):
799 """Gets an entry by details. Will return None if there is
800 no matching entry."""
801 entry = DNSEntry(name, type, clazz)
802 return self.get(entry)
804 def entriesWithName(self, name):
805 """Returns a list of entries whose key matches the name."""
806 try:
807 return self.cache[name]
808 except:
809 return []
811 def entries(self):
812 """Returns a list of all entries"""
813 def add(x, y): return x+y
814 try:
815 return reduce(add, self.cache.values())
816 except:
817 return []
820 class Engine(threading.Thread):
821 """An engine wraps read access to sockets, allowing objects that
822 need to receive data from sockets to be called back when the
823 sockets are ready.
825 A reader needs a handle_read() method, which is called when the socket
826 it is interested in is ready for reading.
828 Writers are not implemented here, because we only send short
829 packets.
832 def __init__(self, zeroconf):
833 threading.Thread.__init__(self)
834 self.zeroconf = zeroconf
835 self.readers = {} # maps socket to reader
836 self.timeout = 5
837 self.condition = threading.Condition()
838 self.start()
840 def run(self):
841 while not globals()['_GLOBAL_DONE']:
842 rs = self.getReaders()
843 if len(rs) == 0:
844 # No sockets to manage, but we wait for the timeout
845 # or addition of a socket
847 self.condition.acquire()
848 self.condition.wait(self.timeout)
849 self.condition.release()
850 else:
851 try:
852 rr, wr, er = select.select(rs, [], [], self.timeout)
853 for socket in rr:
854 try:
855 self.readers[socket].handle_read()
856 except:
857 traceback.print_exc()
858 except:
859 pass
861 def getReaders(self):
862 result = []
863 self.condition.acquire()
864 result = self.readers.keys()
865 self.condition.release()
866 return result
868 def addReader(self, reader, socket):
869 self.condition.acquire()
870 self.readers[socket] = reader
871 self.condition.notify()
872 self.condition.release()
874 def delReader(self, socket):
875 self.condition.acquire()
876 del(self.readers[socket])
877 self.condition.notify()
878 self.condition.release()
880 def notify(self):
881 self.condition.acquire()
882 self.condition.notify()
883 self.condition.release()
885 class Listener(object):
886 """A Listener is used by this module to listen on the multicast
887 group to which DNS messages are sent, allowing the implementation
888 to cache information as it arrives.
890 It requires registration with an Engine object in order to have
891 the read() method called when a socket is availble for reading."""
893 def __init__(self, zeroconf):
894 self.zeroconf = zeroconf
895 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
897 def handle_read(self):
898 try:
899 data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE)
900 except socket.error, e:
901 # If the socket was closed by another thread -- which happens
902 # regularly on shutdown -- an EBADF exception is thrown here.
903 # Ignore it.
904 if e[0] == socket.EBADF:
905 return
906 else:
907 raise e
908 self.data = data
909 msg = DNSIncoming(data)
910 if msg.isQuery():
911 # Always multicast responses
913 if port == _MDNS_PORT:
914 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
915 # If it's not a multicast query, reply via unicast
916 # and multicast
918 elif port == _DNS_PORT:
919 self.zeroconf.handleQuery(msg, addr, port)
920 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
921 else:
922 self.zeroconf.handleResponse(msg)
925 class Reaper(threading.Thread):
926 """A Reaper is used by this module to remove cache entries that
927 have expired."""
929 def __init__(self, zeroconf):
930 threading.Thread.__init__(self)
931 self.zeroconf = zeroconf
932 self.start()
934 def run(self):
935 while 1:
936 self.zeroconf.wait(10 * 1000)
937 if globals()['_GLOBAL_DONE']:
938 return
939 now = currentTimeMillis()
940 for record in self.zeroconf.cache.entries():
941 if record.isExpired(now):
942 self.zeroconf.updateRecord(now, record)
943 self.zeroconf.cache.remove(record)
946 class ServiceBrowser(threading.Thread):
947 """Used to browse for a service of a specific type.
949 The listener object will have its addService() and
950 removeService() methods called when this browser
951 discovers changes in the services availability."""
953 def __init__(self, zeroconf, type, listener):
954 """Creates a browser for a specific type"""
955 threading.Thread.__init__(self)
956 self.zeroconf = zeroconf
957 self.type = type
958 self.listener = listener
959 self.services = {}
960 self.nextTime = currentTimeMillis()
961 self.delay = _BROWSER_TIME
962 self.list = []
964 self.done = 0
966 self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
967 self.start()
969 def updateRecord(self, zeroconf, now, record):
970 """Callback invoked by Zeroconf when new information arrives.
972 Updates information required by browser in the Zeroconf cache."""
973 if record.type == _TYPE_PTR and record.name == self.type:
974 expired = record.isExpired(now)
975 try:
976 oldrecord = self.services[record.alias.lower()]
977 if not expired:
978 oldrecord.resetTTL(record)
979 else:
980 del(self.services[record.alias.lower()])
981 callback = lambda x: self.listener.removeService(x, self.type, record.alias)
982 self.list.append(callback)
983 return
984 except:
985 if not expired:
986 self.services[record.alias.lower()] = record
987 callback = lambda x: self.listener.addService(x, self.type, record.alias)
988 self.list.append(callback)
990 expires = record.getExpirationTime(75)
991 if expires < self.nextTime:
992 self.nextTime = expires
994 def cancel(self):
995 self.done = 1
996 self.zeroconf.notifyAll()
998 def run(self):
999 while 1:
1000 event = None
1001 now = currentTimeMillis()
1002 if len(self.list) == 0 and self.nextTime > now:
1003 self.zeroconf.wait(self.nextTime - now)
1004 if globals()['_GLOBAL_DONE'] or self.done:
1005 return
1006 now = currentTimeMillis()
1008 if self.nextTime <= now:
1009 out = DNSOutgoing(_FLAGS_QR_QUERY)
1010 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1011 for record in self.services.values():
1012 if not record.isExpired(now):
1013 out.addAnswerAtTime(record, now)
1014 self.zeroconf.send(out)
1015 self.nextTime = now + self.delay
1016 self.delay = min(20 * 1000, self.delay * 2)
1018 if len(self.list) > 0:
1019 event = self.list.pop(0)
1021 if event is not None:
1022 event(self.zeroconf)
1025 class ServiceInfo(object):
1026 """Service information"""
1028 def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None):
1029 """Create a service description.
1031 type: fully qualified service type name
1032 name: fully qualified service name
1033 address: IP address as unsigned short, network byte order
1034 port: port that the service runs on
1035 weight: weight of the service
1036 priority: priority of the service
1037 properties: dictionary of properties (or a string holding the bytes for the text field)
1038 server: fully qualified name for service host (defaults to name)"""
1040 if not name.endswith(type):
1041 raise BadTypeInNameException
1042 self.type = type
1043 self.name = name
1044 self.address = address
1045 self.port = port
1046 self.weight = weight
1047 self.priority = priority
1048 if server:
1049 self.server = server
1050 else:
1051 self.server = name
1052 self.setProperties(properties)
1054 def setProperties(self, properties):
1055 """Sets properties and text of this info from a dictionary"""
1056 if isinstance(properties, dict):
1057 self.properties = properties
1058 list = []
1059 result = ''
1060 for key in properties:
1061 value = properties[key]
1062 if value is None:
1063 suffix = ''.encode('utf-8')
1064 elif isinstance(value, str):
1065 suffix = value.encode('utf-8')
1066 elif isinstance(value, int):
1067 if value:
1068 suffix = 'true'
1069 else:
1070 suffix = 'false'
1071 else:
1072 suffix = ''.encode('utf-8')
1073 list.append('='.join((key, suffix)))
1074 for item in list:
1075 result = ''.join((result, struct.pack('!c', chr(len(item))), item))
1076 self.text = result
1077 else:
1078 self.text = properties
1080 def setText(self, text):
1081 """Sets properties and text given a text field"""
1082 self.text = text
1083 try:
1084 result = {}
1085 end = len(text)
1086 index = 0
1087 strs = []
1088 while index < end:
1089 length = ord(text[index])
1090 index += 1
1091 strs.append(text[index:index+length])
1092 index += length
1094 for s in strs:
1095 eindex = s.find('=')
1096 if eindex == -1:
1097 # No equals sign at all
1098 key = s
1099 value = 0
1100 else:
1101 key = s[:eindex]
1102 value = s[eindex+1:]
1103 if value == 'true':
1104 value = 1
1105 elif value == 'false' or not value:
1106 value = 0
1108 # Only update non-existent properties
1109 if key and result.get(key) == None:
1110 result[key] = value
1112 self.properties = result
1113 except:
1114 traceback.print_exc()
1115 self.properties = None
1117 def getType(self):
1118 """Type accessor"""
1119 return self.type
1121 def getName(self):
1122 """Name accessor"""
1123 if self.type is not None and self.name.endswith("." + self.type):
1124 return self.name[:len(self.name) - len(self.type) - 1]
1125 return self.name
1127 def getAddress(self):
1128 """Address accessor"""
1129 return self.address
1131 def getPort(self):
1132 """Port accessor"""
1133 return self.port
1135 def getPriority(self):
1136 """Pirority accessor"""
1137 return self.priority
1139 def getWeight(self):
1140 """Weight accessor"""
1141 return self.weight
1143 def getProperties(self):
1144 """Properties accessor"""
1145 return self.properties
1147 def getText(self):
1148 """Text accessor"""
1149 return self.text
1151 def getServer(self):
1152 """Server accessor"""
1153 return self.server
1155 def updateRecord(self, zeroconf, now, record):
1156 """Updates service information from a DNS record"""
1157 if record is not None and not record.isExpired(now):
1158 if record.type == _TYPE_A:
1159 #if record.name == self.name:
1160 if record.name == self.server:
1161 self.address = record.address
1162 elif record.type == _TYPE_SRV:
1163 if record.name == self.name:
1164 self.server = record.server
1165 self.port = record.port
1166 self.weight = record.weight
1167 self.priority = record.priority
1168 #self.address = None
1169 self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1170 elif record.type == _TYPE_TXT:
1171 if record.name == self.name:
1172 self.setText(record.text)
1174 def request(self, zeroconf, timeout):
1175 """Returns true if the service could be discovered on the
1176 network, and updates this object with details discovered.
1178 now = currentTimeMillis()
1179 delay = _LISTENER_TIME
1180 next = now + delay
1181 last = now + timeout
1182 result = 0
1183 try:
1184 zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1185 while self.server is None or self.address is None or self.text is None:
1186 if last <= now:
1187 return 0
1188 if next <= now:
1189 out = DNSOutgoing(_FLAGS_QR_QUERY)
1190 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
1191 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now)
1192 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
1193 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now)
1194 if self.server is not None:
1195 out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
1196 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now)
1197 zeroconf.send(out)
1198 next = now + delay
1199 delay = delay * 2
1201 zeroconf.wait(min(next, last) - now)
1202 now = currentTimeMillis()
1203 result = 1
1204 finally:
1205 zeroconf.removeListener(self)
1207 return result
1209 def __eq__(self, other):
1210 """Tests equality of service name"""
1211 if isinstance(other, ServiceInfo):
1212 return other.name == self.name
1213 return 0
1215 def __ne__(self, other):
1216 """Non-equality test"""
1217 return not self.__eq__(other)
1219 def __repr__(self):
1220 """String representation"""
1221 result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port)
1222 if self.text is None:
1223 result += "None"
1224 else:
1225 if len(self.text) < 20:
1226 result += self.text
1227 else:
1228 result += self.text[:17] + "..."
1229 result += "]"
1230 return result
1233 class Zeroconf(object):
1234 """Implementation of Zeroconf Multicast DNS Service Discovery
1236 Supports registration, unregistration, queries and browsing.
1238 def __init__(self, bindaddress=None):
1239 """Creates an instance of the Zeroconf class, establishing
1240 multicast communications, listening and reaping threads."""
1241 globals()['_GLOBAL_DONE'] = 0
1242 if bindaddress is None:
1243 self.intf = socket.gethostbyname(socket.gethostname())
1244 else:
1245 self.intf = bindaddress
1246 self.group = ('', _MDNS_PORT)
1247 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1248 try:
1249 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1250 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1251 except:
1252 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1253 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1254 # Volume 2"), but some BSD-derived systems require
1255 # SO_REUSEPORT to be specified explicity. Also, not all
1256 # versions of Python have SO_REUSEPORT available. So
1257 # if you're on a BSD-based system, and haven't upgraded
1258 # to Python 2.3 yet, you may find this library doesn't
1259 # work as expected.
1261 pass
1262 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
1263 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
1264 try:
1265 self.socket.bind(self.group)
1266 except:
1267 # Some versions of linux raise an exception even though
1268 # the SO_REUSE* options have been set, so ignore it
1270 pass
1271 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1272 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1274 self.listeners = []
1275 self.browsers = []
1276 self.services = {}
1277 self.servicetypes = {}
1279 self.cache = DNSCache()
1281 self.condition = threading.Condition()
1283 self.engine = Engine(self)
1284 self.listener = Listener(self)
1285 self.reaper = Reaper(self)
1287 def isLoopback(self):
1288 return self.intf.startswith("127.0.0.1")
1290 def isLinklocal(self):
1291 return self.intf.startswith("169.254.")
1293 def wait(self, timeout):
1294 """Calling thread waits for a given number of milliseconds or
1295 until notified."""
1296 self.condition.acquire()
1297 self.condition.wait(timeout/1000)
1298 self.condition.release()
1300 def notifyAll(self):
1301 """Notifies all waiting threads"""
1302 self.condition.acquire()
1303 self.condition.notifyAll()
1304 self.condition.release()
1306 def getServiceInfo(self, type, name, timeout=3000):
1307 """Returns network's service information for a particular
1308 name and type, or None if no service matches by the timeout,
1309 which defaults to 3 seconds."""
1310 info = ServiceInfo(type, name)
1311 if info.request(self, timeout):
1312 return info
1313 return None
1315 def addServiceListener(self, type, listener):
1316 """Adds a listener for a particular service type. This object
1317 will then have its updateRecord method called when information
1318 arrives for that type."""
1319 self.removeServiceListener(listener)
1320 self.browsers.append(ServiceBrowser(self, type, listener))
1322 def removeServiceListener(self, listener):
1323 """Removes a listener from the set that is currently listening."""
1324 for browser in self.browsers:
1325 if browser.listener == listener:
1326 browser.cancel()
1327 del(browser)
1329 def registerService(self, info, ttl=_DNS_TTL):
1330 """Registers service information to the network with a default TTL
1331 of 60 seconds. Zeroconf will then respond to requests for
1332 information for that service. The name of the service may be
1333 changed if needed to make it unique on the network."""
1334 self.checkService(info)
1335 self.services[info.name.lower()] = info
1336 if info.type in self.servicetypes:
1337 self.servicetypes[info.type]+=1
1338 else:
1339 self.servicetypes[info.type]=1
1340 now = currentTimeMillis()
1341 nextTime = now
1342 i = 0
1343 while i < 3:
1344 if now < nextTime:
1345 self.wait(nextTime - now)
1346 now = currentTimeMillis()
1347 continue
1348 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1349 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0)
1350 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0)
1351 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0)
1352 if info.address:
1353 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0)
1354 self.send(out)
1355 i += 1
1356 nextTime += _REGISTER_TIME
1358 def unregisterService(self, info):
1359 """Unregister a service."""
1360 try:
1361 del(self.services[info.name.lower()])
1362 if self.servicetypes[info.type]>1:
1363 self.servicetypes[info.type]-=1
1364 else:
1365 del self.servicetypes[info.type]
1366 except:
1367 pass
1368 now = currentTimeMillis()
1369 nextTime = now
1370 i = 0
1371 while i < 3:
1372 if now < nextTime:
1373 self.wait(nextTime - now)
1374 now = currentTimeMillis()
1375 continue
1376 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1377 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1378 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.name), 0)
1379 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1380 if info.address:
1381 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1382 self.send(out)
1383 i += 1
1384 nextTime += _UNREGISTER_TIME
1386 def unregisterAllServices(self):
1387 """Unregister all registered services."""
1388 if len(self.services) > 0:
1389 now = currentTimeMillis()
1390 nextTime = now
1391 i = 0
1392 while i < 3:
1393 if now < nextTime:
1394 self.wait(nextTime - now)
1395 now = currentTimeMillis()
1396 continue
1397 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1398 for info in self.services.values():
1399 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1400 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0)
1401 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1402 if info.address:
1403 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1404 self.send(out)
1405 i += 1
1406 nextTime += _UNREGISTER_TIME
1408 def checkService(self, info):
1409 """Checks the network for a unique service name, modifying the
1410 ServiceInfo passed in if it is not unique."""
1411 now = currentTimeMillis()
1412 nextTime = now
1413 i = 0
1414 while i < 3:
1415 for record in self.cache.entriesWithName(info.type):
1416 if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name:
1417 if (info.name.find('.') < 0):
1418 info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type
1419 self.checkService(info)
1420 return
1421 raise NonUniqueNameException
1422 if now < nextTime:
1423 self.wait(nextTime - now)
1424 now = currentTimeMillis()
1425 continue
1426 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1427 self.debug = out
1428 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1429 out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name))
1430 self.send(out)
1431 i += 1
1432 nextTime += _CHECK_TIME
1434 def addListener(self, listener, question):
1435 """Adds a listener for a given question. The listener will have
1436 its updateRecord method called when information is available to
1437 answer the question."""
1438 now = currentTimeMillis()
1439 self.listeners.append(listener)
1440 if question is not None:
1441 for record in self.cache.entriesWithName(question.name):
1442 if question.answeredBy(record) and not record.isExpired(now):
1443 listener.updateRecord(self, now, record)
1444 self.notifyAll()
1446 def removeListener(self, listener):
1447 """Removes a listener."""
1448 try:
1449 self.listeners.remove(listener)
1450 self.notifyAll()
1451 except:
1452 pass
1454 def updateRecord(self, now, rec):
1455 """Used to notify listeners of new information that has updated
1456 a record."""
1457 for listener in self.listeners:
1458 listener.updateRecord(self, now, rec)
1459 self.notifyAll()
1461 def handleResponse(self, msg):
1462 """Deal with incoming response packets. All answers
1463 are held in the cache, and listeners are notified."""
1464 now = currentTimeMillis()
1465 for record in msg.answers:
1466 expired = record.isExpired(now)
1467 if record in self.cache.entries():
1468 if expired:
1469 self.cache.remove(record)
1470 else:
1471 entry = self.cache.get(record)
1472 if entry is not None:
1473 entry.resetTTL(record)
1474 record = entry
1475 else:
1476 self.cache.add(record)
1478 self.updateRecord(now, record)
1480 def handleQuery(self, msg, addr, port):
1481 """Deal with incoming query packets. Provides a response if
1482 possible."""
1483 out = None
1485 # Support unicast client responses
1487 if port != _MDNS_PORT:
1488 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1489 for question in msg.questions:
1490 out.addQuestion(question)
1492 for question in msg.questions:
1493 if question.type == _TYPE_PTR:
1494 if question.name == "_services._dns-sd._udp.local.":
1495 for stype in self.servicetypes.keys():
1496 if out is None:
1497 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1498 out.addAnswer(msg, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1499 for service in self.services.values():
1500 if question.name == service.type:
1501 if out is None:
1502 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1503 out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name))
1504 else:
1505 try:
1506 if out is None:
1507 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1509 # Answer A record queries for any service addresses we know
1510 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1511 for service in self.services.values():
1512 if service.server == question.name.lower():
1513 out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1515 service = self.services.get(question.name.lower(), None)
1516 if not service: continue
1518 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
1519 out.addAnswer(msg, DNSService(question.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server))
1520 if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
1521 out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text))
1522 if question.type == _TYPE_SRV:
1523 out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1524 except:
1525 traceback.print_exc()
1527 if out is not None and out.answers:
1528 out.id = msg.id
1529 self.send(out, addr, port)
1531 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1532 """Sends an outgoing packet."""
1533 # This is a quick test to see if we can parse the packets we generate
1534 #temp = DNSIncoming(out.packet())
1535 try:
1536 bytes_sent = self.socket.sendto(out.packet(), 0, (addr, port))
1537 except:
1538 # Ignore this, it may be a temporary loss of network connection
1539 pass
1541 def close(self):
1542 """Ends the background threads, and prevent this instance from
1543 servicing further queries."""
1544 if globals()['_GLOBAL_DONE'] == 0:
1545 globals()['_GLOBAL_DONE'] = 1
1546 self.notifyAll()
1547 self.engine.notify()
1548 self.unregisterAllServices()
1549 self.socket.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1550 self.socket.close()
1552 # Test a few module features, including service registration, service
1553 # query (for Zoe), and service unregistration.
1555 if __name__ == '__main__':
1556 print "Multicast DNS Service Discovery for Python, version", __version__
1557 r = Zeroconf()
1558 print "1. Testing registration of a service..."
1559 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1560 info = ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1561 print " Registering service..."
1562 r.registerService(info)
1563 print " Registration done."
1564 print "2. Testing query of service information..."
1565 print " Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1566 print " Query done."
1567 print "3. Testing query of own service..."
1568 print " Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local."))
1569 print " Query done."
1570 print "4. Testing unregister of service information..."
1571 r.unregisterService(info)
1572 print " Unregister done."
1573 r.close()