Add some icons to the togo web page to make it a little easier to read.
[pyTivo/TheBayer.git] / Zeroconf.py
blob6f6cc0f92008180728db5b63e21f39ae80f3817b
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 string
82 import time
83 import struct
84 import socket
85 import threading
86 import select
87 import traceback
89 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
91 # hook for threads
93 globals()['_GLOBAL_DONE'] = 0
95 # Some timing constants
97 _UNREGISTER_TIME = 125
98 _CHECK_TIME = 175
99 _REGISTER_TIME = 225
100 _LISTENER_TIME = 200
101 _BROWSER_TIME = 500
103 # Some DNS constants
105 _MDNS_ADDR = '224.0.0.251'
106 _MDNS_PORT = 5353;
107 _DNS_PORT = 53;
108 _DNS_TTL = 60 * 60; # one hour default TTL
110 _MAX_MSG_TYPICAL = 1460 # unused
111 _MAX_MSG_ABSOLUTE = 8972
113 _FLAGS_QR_MASK = 0x8000 # query response mask
114 _FLAGS_QR_QUERY = 0x0000 # query
115 _FLAGS_QR_RESPONSE = 0x8000 # response
117 _FLAGS_AA = 0x0400 # Authorative answer
118 _FLAGS_TC = 0x0200 # Truncated
119 _FLAGS_RD = 0x0100 # Recursion desired
120 _FLAGS_RA = 0x8000 # Recursion available
122 _FLAGS_Z = 0x0040 # Zero
123 _FLAGS_AD = 0x0020 # Authentic data
124 _FLAGS_CD = 0x0010 # Checking disabled
126 _CLASS_IN = 1
127 _CLASS_CS = 2
128 _CLASS_CH = 3
129 _CLASS_HS = 4
130 _CLASS_NONE = 254
131 _CLASS_ANY = 255
132 _CLASS_MASK = 0x7FFF
133 _CLASS_UNIQUE = 0x8000
135 _TYPE_A = 1
136 _TYPE_NS = 2
137 _TYPE_MD = 3
138 _TYPE_MF = 4
139 _TYPE_CNAME = 5
140 _TYPE_SOA = 6
141 _TYPE_MB = 7
142 _TYPE_MG = 8
143 _TYPE_MR = 9
144 _TYPE_NULL = 10
145 _TYPE_WKS = 11
146 _TYPE_PTR = 12
147 _TYPE_HINFO = 13
148 _TYPE_MINFO = 14
149 _TYPE_MX = 15
150 _TYPE_TXT = 16
151 _TYPE_AAAA = 28
152 _TYPE_SRV = 33
153 _TYPE_ANY = 255
155 # Mapping constants to names
157 _CLASSES = { _CLASS_IN : "in",
158 _CLASS_CS : "cs",
159 _CLASS_CH : "ch",
160 _CLASS_HS : "hs",
161 _CLASS_NONE : "none",
162 _CLASS_ANY : "any" }
164 _TYPES = { _TYPE_A : "a",
165 _TYPE_NS : "ns",
166 _TYPE_MD : "md",
167 _TYPE_MF : "mf",
168 _TYPE_CNAME : "cname",
169 _TYPE_SOA : "soa",
170 _TYPE_MB : "mb",
171 _TYPE_MG : "mg",
172 _TYPE_MR : "mr",
173 _TYPE_NULL : "null",
174 _TYPE_WKS : "wks",
175 _TYPE_PTR : "ptr",
176 _TYPE_HINFO : "hinfo",
177 _TYPE_MINFO : "minfo",
178 _TYPE_MX : "mx",
179 _TYPE_TXT : "txt",
180 _TYPE_AAAA : "quada",
181 _TYPE_SRV : "srv",
182 _TYPE_ANY : "any" }
184 # utility functions
186 def currentTimeMillis():
187 """Current system time in milliseconds"""
188 return time.time() * 1000
190 # Exceptions
192 class NonLocalNameException(Exception):
193 pass
195 class NonUniqueNameException(Exception):
196 pass
198 class NamePartTooLongException(Exception):
199 pass
201 class AbstractMethodException(Exception):
202 pass
204 class BadTypeInNameException(Exception):
205 pass
207 # implementation classes
209 class DNSEntry(object):
210 """A DNS entry"""
212 def __init__(self, name, type, clazz):
213 self.key = string.lower(name)
214 self.name = name
215 self.type = type
216 self.clazz = clazz & _CLASS_MASK
217 self.unique = (clazz & _CLASS_UNIQUE) != 0
219 def __eq__(self, other):
220 """Equality test on name, type, and class"""
221 if isinstance(other, DNSEntry):
222 return self.name == other.name and self.type == other.type and self.clazz == other.clazz
223 return 0
225 def __ne__(self, other):
226 """Non-equality test"""
227 return not self.__eq__(other)
229 def getClazz(self, clazz):
230 """Class accessor"""
231 try:
232 return _CLASSES[clazz]
233 except:
234 return "?(%s)" % (clazz)
236 def getType(self, type):
237 """Type accessor"""
238 try:
239 return _TYPES[type]
240 except:
241 return "?(%s)" % (type)
243 def toString(self, hdr, other):
244 """String representation with additional information"""
245 result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz))
246 if self.unique:
247 result += "-unique,"
248 else:
249 result += ","
250 result += self.name
251 if other is not None:
252 result += ",%s]" % (other)
253 else:
254 result += "]"
255 return result
257 class DNSQuestion(DNSEntry):
258 """A DNS question entry"""
260 def __init__(self, name, type, clazz):
261 #if not name.endswith(".local."):
262 # raise NonLocalNameException
263 DNSEntry.__init__(self, name, type, clazz)
265 def answeredBy(self, rec):
266 """Returns true if the question is answered by the record"""
267 return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name
269 def __repr__(self):
270 """String representation"""
271 return DNSEntry.toString(self, "question", None)
274 class DNSRecord(DNSEntry):
275 """A DNS record - like a DNS entry, but has a TTL"""
277 def __init__(self, name, type, clazz, ttl):
278 DNSEntry.__init__(self, name, type, clazz)
279 self.ttl = ttl
280 self.created = currentTimeMillis()
282 def __eq__(self, other):
283 """Tests equality as per DNSRecord"""
284 if isinstance(other, DNSRecord):
285 return DNSEntry.__eq__(self, other)
286 return 0
288 def suppressedBy(self, msg):
289 """Returns true if any answer in a message can suffice for the
290 information held in this record."""
291 for record in msg.answers:
292 if self.suppressedByAnswer(record):
293 return 1
294 return 0
296 def suppressedByAnswer(self, other):
297 """Returns true if another record has same name, type and class,
298 and if its TTL is at least half of this record's."""
299 if self == other and other.ttl > (self.ttl / 2):
300 return 1
301 return 0
303 def getExpirationTime(self, percent):
304 """Returns the time at which this record will have expired
305 by a certain percentage."""
306 return self.created + (percent * self.ttl * 10)
308 def getRemainingTTL(self, now):
309 """Returns the remaining TTL in seconds."""
310 return max(0, (self.getExpirationTime(100) - now) / 1000)
312 def isExpired(self, now):
313 """Returns true if this record has expired."""
314 return self.getExpirationTime(100) <= now
316 def isStale(self, now):
317 """Returns true if this record is at least half way expired."""
318 return self.getExpirationTime(50) <= now
320 def resetTTL(self, other):
321 """Sets this record's TTL and created time to that of
322 another record."""
323 self.created = other.created
324 self.ttl = other.ttl
326 def write(self, out):
327 """Abstract method"""
328 raise AbstractMethodException
330 def toString(self, other):
331 """String representation with addtional information"""
332 arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other)
333 return DNSEntry.toString(self, "record", arg)
335 class DNSAddress(DNSRecord):
336 """A DNS address record"""
338 def __init__(self, name, type, clazz, ttl, address):
339 DNSRecord.__init__(self, name, type, clazz, ttl)
340 self.address = address
342 def write(self, out):
343 """Used in constructing an outgoing packet"""
344 out.writeString(self.address, len(self.address))
346 def __eq__(self, other):
347 """Tests equality on address"""
348 if isinstance(other, DNSAddress):
349 return self.address == other.address
350 return 0
352 def __repr__(self):
353 """String representation"""
354 try:
355 return socket.inet_ntoa(self.address)
356 except:
357 return self.address
359 class DNSHinfo(DNSRecord):
360 """A DNS host information record"""
362 def __init__(self, name, type, clazz, ttl, cpu, os):
363 DNSRecord.__init__(self, name, type, clazz, ttl)
364 self.cpu = cpu
365 self.os = os
367 def write(self, out):
368 """Used in constructing an outgoing packet"""
369 out.writeString(self.cpu, len(self.cpu))
370 out.writeString(self.os, len(self.os))
372 def __eq__(self, other):
373 """Tests equality on cpu and os"""
374 if isinstance(other, DNSHinfo):
375 return self.cpu == other.cpu and self.os == other.os
376 return 0
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 if isinstance(other, DNSPointer):
396 return self.alias == other.alias
397 return 0
399 def __repr__(self):
400 """String representation"""
401 return self.toString(self.alias)
403 class DNSText(DNSRecord):
404 """A DNS text record"""
406 def __init__(self, name, type, clazz, ttl, text):
407 DNSRecord.__init__(self, name, type, clazz, ttl)
408 self.text = text
410 def write(self, out):
411 """Used in constructing an outgoing packet"""
412 out.writeString(self.text, len(self.text))
414 def __eq__(self, other):
415 """Tests equality on text"""
416 if isinstance(other, DNSText):
417 return self.text == other.text
418 return 0
420 def __repr__(self):
421 """String representation"""
422 if len(self.text) > 10:
423 return self.toString(self.text[:7] + "...")
424 else:
425 return self.toString(self.text)
427 class DNSService(DNSRecord):
428 """A DNS service record"""
430 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
431 DNSRecord.__init__(self, name, type, clazz, ttl)
432 self.priority = priority
433 self.weight = weight
434 self.port = port
435 self.server = server
437 def write(self, out):
438 """Used in constructing an outgoing packet"""
439 out.writeShort(self.priority)
440 out.writeShort(self.weight)
441 out.writeShort(self.port)
442 out.writeName(self.server)
444 def __eq__(self, other):
445 """Tests equality on priority, weight, port and server"""
446 if isinstance(other, DNSService):
447 return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server
448 return 0
450 def __repr__(self):
451 """String representation"""
452 return self.toString("%s:%s" % (self.server, self.port))
454 class DNSIncoming(object):
455 """Object representation of an incoming DNS packet"""
457 def __init__(self, data):
458 """Constructor from string holding bytes of packet"""
459 self.offset = 0
460 self.data = data
461 self.questions = []
462 self.answers = []
463 self.numQuestions = 0
464 self.numAnswers = 0
465 self.numAuthorities = 0
466 self.numAdditionals = 0
468 self.readHeader()
469 self.readQuestions()
470 self.readOthers()
472 def readHeader(self):
473 """Reads header portion of packet"""
474 format = '!HHHHHH'
475 length = struct.calcsize(format)
476 info = struct.unpack(format, self.data[self.offset:self.offset+length])
477 self.offset += length
479 self.id = info[0]
480 self.flags = info[1]
481 self.numQuestions = info[2]
482 self.numAnswers = info[3]
483 self.numAuthorities = info[4]
484 self.numAdditionals = info[5]
486 def readQuestions(self):
487 """Reads questions section of packet"""
488 format = '!HH'
489 length = struct.calcsize(format)
490 for i in range(0, self.numQuestions):
491 name = self.readName()
492 info = struct.unpack(format, self.data[self.offset:self.offset+length])
493 self.offset += length
495 question = DNSQuestion(name, info[0], info[1])
496 self.questions.append(question)
498 def readInt(self):
499 """Reads an integer from the packet"""
500 format = '!I'
501 length = struct.calcsize(format)
502 info = struct.unpack(format, self.data[self.offset:self.offset+length])
503 self.offset += length
504 return info[0]
506 def readCharacterString(self):
507 """Reads a character string from the packet"""
508 length = ord(self.data[self.offset])
509 self.offset += 1
510 return self.readString(length)
512 def readString(self, len):
513 """Reads a string of a given length from the packet"""
514 format = '!' + str(len) + 's'
515 length = struct.calcsize(format)
516 info = struct.unpack(format, self.data[self.offset:self.offset+length])
517 self.offset += length
518 return info[0]
520 def readUnsignedShort(self):
521 """Reads an unsigned short from the packet"""
522 format = '!H'
523 length = struct.calcsize(format)
524 info = struct.unpack(format, self.data[self.offset:self.offset+length])
525 self.offset += length
526 return info[0]
528 def readOthers(self):
529 """Reads the answers, authorities and additionals section of the packet"""
530 format = '!HHiH'
531 length = struct.calcsize(format)
532 n = self.numAnswers + self.numAuthorities + self.numAdditionals
533 for i in range(0, n):
534 domain = self.readName()
535 info = struct.unpack(format, self.data[self.offset:self.offset+length])
536 self.offset += length
538 rec = None
539 if info[0] == _TYPE_A:
540 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4))
541 elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
542 rec = DNSPointer(domain, info[0], info[1], info[2], self.readName())
543 elif info[0] == _TYPE_TXT:
544 rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3]))
545 elif info[0] == _TYPE_SRV:
546 rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName())
547 elif info[0] == _TYPE_HINFO:
548 rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString())
549 elif info[0] == _TYPE_AAAA:
550 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16))
551 else:
552 # Try to ignore types we don't know about
553 # this may mean the rest of the name is
554 # unable to be parsed, and may show errors
555 # so this is left for debugging. New types
556 # encountered need to be parsed properly.
558 #print "UNKNOWN TYPE = " + str(info[0])
559 #raise BadTypeInNameException
560 pass
562 if rec is not None:
563 self.answers.append(rec)
565 def isQuery(self):
566 """Returns true if this is a query"""
567 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
569 def isResponse(self):
570 """Returns true if this is a response"""
571 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
573 def readUTF(self, offset, len):
574 """Reads a UTF-8 string of a given length from the packet"""
575 result = self.data[offset:offset+len].decode('utf-8')
576 return result
578 def readName(self):
579 """Reads a domain name from the packet"""
580 result = ''
581 off = self.offset
582 next = -1
583 first = off
585 while 1:
586 len = ord(self.data[off])
587 off += 1
588 if len == 0:
589 break
590 t = len & 0xC0
591 if t == 0x00:
592 result = ''.join((result, self.readUTF(off, len) + '.'))
593 off += len
594 elif t == 0xC0:
595 if next < 0:
596 next = off + 1
597 off = ((len & 0x3F) << 8) | ord(self.data[off])
598 if off >= first:
599 raise "Bad domain name (circular) at " + str(off)
600 first = off
601 else:
602 raise "Bad domain name at " + str(off)
604 if next >= 0:
605 self.offset = next
606 else:
607 self.offset = off
609 return result
612 class DNSOutgoing(object):
613 """Object representation of an outgoing packet"""
615 def __init__(self, flags, multicast = 1):
616 self.finished = 0
617 self.id = 0
618 self.multicast = multicast
619 self.flags = flags
620 self.names = {}
621 self.data = []
622 self.size = 12
624 self.questions = []
625 self.answers = []
626 self.authorities = []
627 self.additionals = []
629 def addQuestion(self, record):
630 """Adds a question"""
631 self.questions.append(record)
633 def addAnswer(self, inp, record):
634 """Adds an answer"""
635 if not record.suppressedBy(inp):
636 self.addAnswerAtTime(record, 0)
638 def addAnswerAtTime(self, record, now):
639 """Adds an answer if if does not expire by a certain time"""
640 if record is not None:
641 if now == 0 or not record.isExpired(now):
642 self.answers.append((record, now))
644 def addAuthorativeAnswer(self, record):
645 """Adds an authoritative answer"""
646 self.authorities.append(record)
648 def addAdditionalAnswer(self, record):
649 """Adds an additional answer"""
650 self.additionals.append(record)
652 def writeByte(self, value):
653 """Writes a single byte to the packet"""
654 format = '!c'
655 self.data.append(struct.pack(format, chr(value)))
656 self.size += 1
658 def insertShort(self, index, value):
659 """Inserts an unsigned short in a certain position in the packet"""
660 format = '!H'
661 self.data.insert(index, struct.pack(format, value))
662 self.size += 2
664 def writeShort(self, value):
665 """Writes an unsigned short to the packet"""
666 format = '!H'
667 self.data.append(struct.pack(format, value))
668 self.size += 2
670 def writeInt(self, value):
671 """Writes an unsigned integer to the packet"""
672 format = '!I'
673 self.data.append(struct.pack(format, int(value)))
674 self.size += 4
676 def writeString(self, value, length):
677 """Writes a string to the packet"""
678 format = '!' + str(length) + 's'
679 self.data.append(struct.pack(format, value))
680 self.size += length
682 def writeUTF(self, s):
683 """Writes a UTF-8 string of a given length to the packet"""
684 utfstr = s.encode('utf-8')
685 length = len(utfstr)
686 if length > 64:
687 raise NamePartTooLongException
688 self.writeByte(length)
689 self.writeString(utfstr, length)
691 def writeName(self, name):
692 """Writes a domain name to the packet"""
694 try:
695 # Find existing instance of this name in packet
697 index = self.names[name]
698 except KeyError:
699 # No record of this name already, so write it
700 # out as normal, recording the location of the name
701 # for future pointers to it.
703 self.names[name] = self.size
704 parts = name.split('.')
705 if parts[-1] == '':
706 parts = parts[:-1]
707 for part in parts:
708 self.writeUTF(part)
709 self.writeByte(0)
710 return
712 # An index was found, so write a pointer to it
714 self.writeByte((index >> 8) | 0xC0)
715 self.writeByte(index)
717 def writeQuestion(self, question):
718 """Writes a question to the packet"""
719 self.writeName(question.name)
720 self.writeShort(question.type)
721 self.writeShort(question.clazz)
723 def writeRecord(self, record, now):
724 """Writes a record (answer, authoritative answer, additional) to
725 the packet"""
726 self.writeName(record.name)
727 self.writeShort(record.type)
728 if record.unique and self.multicast:
729 self.writeShort(record.clazz | _CLASS_UNIQUE)
730 else:
731 self.writeShort(record.clazz)
732 if now == 0:
733 self.writeInt(record.ttl)
734 else:
735 self.writeInt(record.getRemainingTTL(now))
736 index = len(self.data)
737 # Adjust size for the short we will write before this record
739 self.size += 2
740 record.write(self)
741 self.size -= 2
743 length = len(''.join(self.data[index:]))
744 self.insertShort(index, length) # Here is the short we adjusted for
746 def packet(self):
747 """Returns a string containing the packet's bytes
749 No further parts should be added to the packet once this
750 is done."""
751 if not self.finished:
752 self.finished = 1
753 for question in self.questions:
754 self.writeQuestion(question)
755 for answer, time in self.answers:
756 self.writeRecord(answer, time)
757 for authority in self.authorities:
758 self.writeRecord(authority, 0)
759 for additional in self.additionals:
760 self.writeRecord(additional, 0)
762 self.insertShort(0, len(self.additionals))
763 self.insertShort(0, len(self.authorities))
764 self.insertShort(0, len(self.answers))
765 self.insertShort(0, len(self.questions))
766 self.insertShort(0, self.flags)
767 if self.multicast:
768 self.insertShort(0, 0)
769 else:
770 self.insertShort(0, self.id)
771 return ''.join(self.data)
774 class DNSCache(object):
775 """A cache of DNS entries"""
777 def __init__(self):
778 self.cache = {}
780 def add(self, entry):
781 """Adds an entry"""
782 try:
783 list = self.cache[entry.key]
784 except:
785 list = self.cache[entry.key] = []
786 list.append(entry)
788 def remove(self, entry):
789 """Removes an entry"""
790 try:
791 list = self.cache[entry.key]
792 list.remove(entry)
793 except:
794 pass
796 def get(self, entry):
797 """Gets an entry by key. Will return None if there is no
798 matching entry."""
799 try:
800 list = self.cache[entry.key]
801 return list[list.index(entry)]
802 except:
803 return None
805 def getByDetails(self, name, type, clazz):
806 """Gets an entry by details. Will return None if there is
807 no matching entry."""
808 entry = DNSEntry(name, type, clazz)
809 return self.get(entry)
811 def entriesWithName(self, name):
812 """Returns a list of entries whose key matches the name."""
813 try:
814 return self.cache[name]
815 except:
816 return []
818 def entries(self):
819 """Returns a list of all entries"""
820 def add(x, y): return x+y
821 try:
822 return reduce(add, self.cache.values())
823 except:
824 return []
827 class Engine(threading.Thread):
828 """An engine wraps read access to sockets, allowing objects that
829 need to receive data from sockets to be called back when the
830 sockets are ready.
832 A reader needs a handle_read() method, which is called when the socket
833 it is interested in is ready for reading.
835 Writers are not implemented here, because we only send short
836 packets.
839 def __init__(self, zeroconf):
840 threading.Thread.__init__(self)
841 self.zeroconf = zeroconf
842 self.readers = {} # maps socket to reader
843 self.timeout = 5
844 self.condition = threading.Condition()
845 self.start()
847 def run(self):
848 while not globals()['_GLOBAL_DONE']:
849 rs = self.getReaders()
850 if len(rs) == 0:
851 # No sockets to manage, but we wait for the timeout
852 # or addition of a socket
854 self.condition.acquire()
855 self.condition.wait(self.timeout)
856 self.condition.release()
857 else:
858 try:
859 rr, wr, er = select.select(rs, [], [], self.timeout)
860 for socket in rr:
861 try:
862 self.readers[socket].handle_read()
863 except:
864 traceback.print_exc()
865 except:
866 pass
868 def getReaders(self):
869 result = []
870 self.condition.acquire()
871 result = self.readers.keys()
872 self.condition.release()
873 return result
875 def addReader(self, reader, socket):
876 self.condition.acquire()
877 self.readers[socket] = reader
878 self.condition.notify()
879 self.condition.release()
881 def delReader(self, socket):
882 self.condition.acquire()
883 del(self.readers[socket])
884 self.condition.notify()
885 self.condition.release()
887 def notify(self):
888 self.condition.acquire()
889 self.condition.notify()
890 self.condition.release()
892 class Listener(object):
893 """A Listener is used by this module to listen on the multicast
894 group to which DNS messages are sent, allowing the implementation
895 to cache information as it arrives.
897 It requires registration with an Engine object in order to have
898 the read() method called when a socket is availble for reading."""
900 def __init__(self, zeroconf):
901 self.zeroconf = zeroconf
902 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
904 def handle_read(self):
905 try:
906 data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE)
907 except socket.error, e:
908 # If the socket was closed by another thread -- which happens
909 # regularly on shutdown -- an EBADF exception is thrown here.
910 # Ignore it.
911 if e[0] == socket.EBADF:
912 return
913 else:
914 raise e
915 self.data = data
916 msg = DNSIncoming(data)
917 if msg.isQuery():
918 # Always multicast responses
920 if port == _MDNS_PORT:
921 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
922 # If it's not a multicast query, reply via unicast
923 # and multicast
925 elif port == _DNS_PORT:
926 self.zeroconf.handleQuery(msg, addr, port)
927 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
928 else:
929 self.zeroconf.handleResponse(msg)
932 class Reaper(threading.Thread):
933 """A Reaper is used by this module to remove cache entries that
934 have expired."""
936 def __init__(self, zeroconf):
937 threading.Thread.__init__(self)
938 self.zeroconf = zeroconf
939 self.start()
941 def run(self):
942 while 1:
943 self.zeroconf.wait(10 * 1000)
944 if globals()['_GLOBAL_DONE']:
945 return
946 now = currentTimeMillis()
947 for record in self.zeroconf.cache.entries():
948 if record.isExpired(now):
949 self.zeroconf.updateRecord(now, record)
950 self.zeroconf.cache.remove(record)
953 class ServiceBrowser(threading.Thread):
954 """Used to browse for a service of a specific type.
956 The listener object will have its addService() and
957 removeService() methods called when this browser
958 discovers changes in the services availability."""
960 def __init__(self, zeroconf, type, listener):
961 """Creates a browser for a specific type"""
962 threading.Thread.__init__(self)
963 self.zeroconf = zeroconf
964 self.type = type
965 self.listener = listener
966 self.services = {}
967 self.nextTime = currentTimeMillis()
968 self.delay = _BROWSER_TIME
969 self.list = []
971 self.done = 0
973 self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
974 self.start()
976 def updateRecord(self, zeroconf, now, record):
977 """Callback invoked by Zeroconf when new information arrives.
979 Updates information required by browser in the Zeroconf cache."""
980 if record.type == _TYPE_PTR and record.name == self.type:
981 expired = record.isExpired(now)
982 try:
983 oldrecord = self.services[record.alias.lower()]
984 if not expired:
985 oldrecord.resetTTL(record)
986 else:
987 del(self.services[record.alias.lower()])
988 callback = lambda x: self.listener.removeService(x, self.type, record.alias)
989 self.list.append(callback)
990 return
991 except:
992 if not expired:
993 self.services[record.alias.lower()] = record
994 callback = lambda x: self.listener.addService(x, self.type, record.alias)
995 self.list.append(callback)
997 expires = record.getExpirationTime(75)
998 if expires < self.nextTime:
999 self.nextTime = expires
1001 def cancel(self):
1002 self.done = 1
1003 self.zeroconf.notifyAll()
1005 def run(self):
1006 while 1:
1007 event = None
1008 now = currentTimeMillis()
1009 if len(self.list) == 0 and self.nextTime > now:
1010 self.zeroconf.wait(self.nextTime - now)
1011 if globals()['_GLOBAL_DONE'] or self.done:
1012 return
1013 now = currentTimeMillis()
1015 if self.nextTime <= now:
1016 out = DNSOutgoing(_FLAGS_QR_QUERY)
1017 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1018 for record in self.services.values():
1019 if not record.isExpired(now):
1020 out.addAnswerAtTime(record, now)
1021 self.zeroconf.send(out)
1022 self.nextTime = now + self.delay
1023 self.delay = min(20 * 1000, self.delay * 2)
1025 if len(self.list) > 0:
1026 event = self.list.pop(0)
1028 if event is not None:
1029 event(self.zeroconf)
1032 class ServiceInfo(object):
1033 """Service information"""
1035 def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None):
1036 """Create a service description.
1038 type: fully qualified service type name
1039 name: fully qualified service name
1040 address: IP address as unsigned short, network byte order
1041 port: port that the service runs on
1042 weight: weight of the service
1043 priority: priority of the service
1044 properties: dictionary of properties (or a string holding the bytes for the text field)
1045 server: fully qualified name for service host (defaults to name)"""
1047 if not name.endswith(type):
1048 raise BadTypeInNameException
1049 self.type = type
1050 self.name = name
1051 self.address = address
1052 self.port = port
1053 self.weight = weight
1054 self.priority = priority
1055 if server:
1056 self.server = server
1057 else:
1058 self.server = name
1059 self.setProperties(properties)
1061 def setProperties(self, properties):
1062 """Sets properties and text of this info from a dictionary"""
1063 if isinstance(properties, dict):
1064 self.properties = properties
1065 list = []
1066 result = ''
1067 for key in properties:
1068 value = properties[key]
1069 if value is None:
1070 suffix = ''.encode('utf-8')
1071 elif isinstance(value, str):
1072 suffix = value.encode('utf-8')
1073 elif isinstance(value, int):
1074 if value:
1075 suffix = 'true'
1076 else:
1077 suffix = 'false'
1078 else:
1079 suffix = ''.encode('utf-8')
1080 list.append('='.join((key, suffix)))
1081 for item in list:
1082 result = ''.join((result, struct.pack('!c', chr(len(item))), item))
1083 self.text = result
1084 else:
1085 self.text = properties
1087 def setText(self, text):
1088 """Sets properties and text given a text field"""
1089 self.text = text
1090 try:
1091 result = {}
1092 end = len(text)
1093 index = 0
1094 strs = []
1095 while index < end:
1096 length = ord(text[index])
1097 index += 1
1098 strs.append(text[index:index+length])
1099 index += length
1101 for s in strs:
1102 eindex = s.find('=')
1103 if eindex == -1:
1104 # No equals sign at all
1105 key = s
1106 value = 0
1107 else:
1108 key = s[:eindex]
1109 value = s[eindex+1:]
1110 if value == 'true':
1111 value = 1
1112 elif value == 'false' or not value:
1113 value = 0
1115 # Only update non-existent properties
1116 if key and result.get(key) == None:
1117 result[key] = value
1119 self.properties = result
1120 except:
1121 traceback.print_exc()
1122 self.properties = None
1124 def getType(self):
1125 """Type accessor"""
1126 return self.type
1128 def getName(self):
1129 """Name accessor"""
1130 if self.type is not None and self.name.endswith("." + self.type):
1131 return self.name[:len(self.name) - len(self.type) - 1]
1132 return self.name
1134 def getAddress(self):
1135 """Address accessor"""
1136 return self.address
1138 def getPort(self):
1139 """Port accessor"""
1140 return self.port
1142 def getPriority(self):
1143 """Pirority accessor"""
1144 return self.priority
1146 def getWeight(self):
1147 """Weight accessor"""
1148 return self.weight
1150 def getProperties(self):
1151 """Properties accessor"""
1152 return self.properties
1154 def getText(self):
1155 """Text accessor"""
1156 return self.text
1158 def getServer(self):
1159 """Server accessor"""
1160 return self.server
1162 def updateRecord(self, zeroconf, now, record):
1163 """Updates service information from a DNS record"""
1164 if record is not None and not record.isExpired(now):
1165 if record.type == _TYPE_A:
1166 #if record.name == self.name:
1167 if record.name == self.server:
1168 self.address = record.address
1169 elif record.type == _TYPE_SRV:
1170 if record.name == self.name:
1171 self.server = record.server
1172 self.port = record.port
1173 self.weight = record.weight
1174 self.priority = record.priority
1175 #self.address = None
1176 self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1177 elif record.type == _TYPE_TXT:
1178 if record.name == self.name:
1179 self.setText(record.text)
1181 def request(self, zeroconf, timeout):
1182 """Returns true if the service could be discovered on the
1183 network, and updates this object with details discovered.
1185 now = currentTimeMillis()
1186 delay = _LISTENER_TIME
1187 next = now + delay
1188 last = now + timeout
1189 result = 0
1190 try:
1191 zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1192 while self.server is None or self.address is None or self.text is None:
1193 if last <= now:
1194 return 0
1195 if next <= now:
1196 out = DNSOutgoing(_FLAGS_QR_QUERY)
1197 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
1198 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now)
1199 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
1200 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now)
1201 if self.server is not None:
1202 out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
1203 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now)
1204 zeroconf.send(out)
1205 next = now + delay
1206 delay = delay * 2
1208 zeroconf.wait(min(next, last) - now)
1209 now = currentTimeMillis()
1210 result = 1
1211 finally:
1212 zeroconf.removeListener(self)
1214 return result
1216 def __eq__(self, other):
1217 """Tests equality of service name"""
1218 if isinstance(other, ServiceInfo):
1219 return other.name == self.name
1220 return 0
1222 def __ne__(self, other):
1223 """Non-equality test"""
1224 return not self.__eq__(other)
1226 def __repr__(self):
1227 """String representation"""
1228 result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port)
1229 if self.text is None:
1230 result += "None"
1231 else:
1232 if len(self.text) < 20:
1233 result += self.text
1234 else:
1235 result += self.text[:17] + "..."
1236 result += "]"
1237 return result
1240 class Zeroconf(object):
1241 """Implementation of Zeroconf Multicast DNS Service Discovery
1243 Supports registration, unregistration, queries and browsing.
1245 def __init__(self, bindaddress=None):
1246 """Creates an instance of the Zeroconf class, establishing
1247 multicast communications, listening and reaping threads."""
1248 globals()['_GLOBAL_DONE'] = 0
1249 if bindaddress is None:
1250 self.intf = socket.gethostbyname(socket.gethostname())
1251 else:
1252 self.intf = bindaddress
1253 self.group = ('', _MDNS_PORT)
1254 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1255 try:
1256 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1257 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1258 except:
1259 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1260 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1261 # Volume 2"), but some BSD-derived systems require
1262 # SO_REUSEPORT to be specified explicity. Also, not all
1263 # versions of Python have SO_REUSEPORT available. So
1264 # if you're on a BSD-based system, and haven't upgraded
1265 # to Python 2.3 yet, you may find this library doesn't
1266 # work as expected.
1268 pass
1269 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
1270 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
1271 try:
1272 self.socket.bind(self.group)
1273 except:
1274 # Some versions of linux raise an exception even though
1275 # the SO_REUSE* options have been set, so ignore it
1277 pass
1278 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1279 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1281 self.listeners = []
1282 self.browsers = []
1283 self.services = {}
1284 self.servicetypes = {}
1286 self.cache = DNSCache()
1288 self.condition = threading.Condition()
1290 self.engine = Engine(self)
1291 self.listener = Listener(self)
1292 self.reaper = Reaper(self)
1294 def isLoopback(self):
1295 return self.intf.startswith("127.0.0.1")
1297 def isLinklocal(self):
1298 return self.intf.startswith("169.254.")
1300 def wait(self, timeout):
1301 """Calling thread waits for a given number of milliseconds or
1302 until notified."""
1303 self.condition.acquire()
1304 self.condition.wait(timeout/1000)
1305 self.condition.release()
1307 def notifyAll(self):
1308 """Notifies all waiting threads"""
1309 self.condition.acquire()
1310 self.condition.notifyAll()
1311 self.condition.release()
1313 def getServiceInfo(self, type, name, timeout=3000):
1314 """Returns network's service information for a particular
1315 name and type, or None if no service matches by the timeout,
1316 which defaults to 3 seconds."""
1317 info = ServiceInfo(type, name)
1318 if info.request(self, timeout):
1319 return info
1320 return None
1322 def addServiceListener(self, type, listener):
1323 """Adds a listener for a particular service type. This object
1324 will then have its updateRecord method called when information
1325 arrives for that type."""
1326 self.removeServiceListener(listener)
1327 self.browsers.append(ServiceBrowser(self, type, listener))
1329 def removeServiceListener(self, listener):
1330 """Removes a listener from the set that is currently listening."""
1331 for browser in self.browsers:
1332 if browser.listener == listener:
1333 browser.cancel()
1334 del(browser)
1336 def registerService(self, info, ttl=_DNS_TTL):
1337 """Registers service information to the network with a default TTL
1338 of 60 seconds. Zeroconf will then respond to requests for
1339 information for that service. The name of the service may be
1340 changed if needed to make it unique on the network."""
1341 self.checkService(info)
1342 self.services[info.name.lower()] = info
1343 if info.type in self.servicetypes:
1344 self.servicetypes[info.type]+=1
1345 else:
1346 self.servicetypes[info.type]=1
1347 now = currentTimeMillis()
1348 nextTime = now
1349 i = 0
1350 while i < 3:
1351 if now < nextTime:
1352 self.wait(nextTime - now)
1353 now = currentTimeMillis()
1354 continue
1355 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1356 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0)
1357 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0)
1358 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0)
1359 if info.address:
1360 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0)
1361 self.send(out)
1362 i += 1
1363 nextTime += _REGISTER_TIME
1365 def unregisterService(self, info):
1366 """Unregister a service."""
1367 try:
1368 del(self.services[info.name.lower()])
1369 if self.servicetypes[info.type]>1:
1370 self.servicetypes[info.type]-=1
1371 else:
1372 del self.servicetypes[info.type]
1373 except:
1374 pass
1375 now = currentTimeMillis()
1376 nextTime = now
1377 i = 0
1378 while i < 3:
1379 if now < nextTime:
1380 self.wait(nextTime - now)
1381 now = currentTimeMillis()
1382 continue
1383 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1384 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1385 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.name), 0)
1386 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1387 if info.address:
1388 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1389 self.send(out)
1390 i += 1
1391 nextTime += _UNREGISTER_TIME
1393 def unregisterAllServices(self):
1394 """Unregister all registered services."""
1395 if len(self.services) > 0:
1396 now = currentTimeMillis()
1397 nextTime = now
1398 i = 0
1399 while i < 3:
1400 if now < nextTime:
1401 self.wait(nextTime - now)
1402 now = currentTimeMillis()
1403 continue
1404 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1405 for info in self.services.values():
1406 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1407 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0)
1408 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1409 if info.address:
1410 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1411 self.send(out)
1412 i += 1
1413 nextTime += _UNREGISTER_TIME
1415 def checkService(self, info):
1416 """Checks the network for a unique service name, modifying the
1417 ServiceInfo passed in if it is not unique."""
1418 now = currentTimeMillis()
1419 nextTime = now
1420 i = 0
1421 while i < 3:
1422 for record in self.cache.entriesWithName(info.type):
1423 if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name:
1424 if (info.name.find('.') < 0):
1425 info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type
1426 self.checkService(info)
1427 return
1428 raise NonUniqueNameException
1429 if now < nextTime:
1430 self.wait(nextTime - now)
1431 now = currentTimeMillis()
1432 continue
1433 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1434 self.debug = out
1435 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1436 out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name))
1437 self.send(out)
1438 i += 1
1439 nextTime += _CHECK_TIME
1441 def addListener(self, listener, question):
1442 """Adds a listener for a given question. The listener will have
1443 its updateRecord method called when information is available to
1444 answer the question."""
1445 now = currentTimeMillis()
1446 self.listeners.append(listener)
1447 if question is not None:
1448 for record in self.cache.entriesWithName(question.name):
1449 if question.answeredBy(record) and not record.isExpired(now):
1450 listener.updateRecord(self, now, record)
1451 self.notifyAll()
1453 def removeListener(self, listener):
1454 """Removes a listener."""
1455 try:
1456 self.listeners.remove(listener)
1457 self.notifyAll()
1458 except:
1459 pass
1461 def updateRecord(self, now, rec):
1462 """Used to notify listeners of new information that has updated
1463 a record."""
1464 for listener in self.listeners:
1465 listener.updateRecord(self, now, rec)
1466 self.notifyAll()
1468 def handleResponse(self, msg):
1469 """Deal with incoming response packets. All answers
1470 are held in the cache, and listeners are notified."""
1471 now = currentTimeMillis()
1472 for record in msg.answers:
1473 expired = record.isExpired(now)
1474 if record in self.cache.entries():
1475 if expired:
1476 self.cache.remove(record)
1477 else:
1478 entry = self.cache.get(record)
1479 if entry is not None:
1480 entry.resetTTL(record)
1481 record = entry
1482 else:
1483 self.cache.add(record)
1485 self.updateRecord(now, record)
1487 def handleQuery(self, msg, addr, port):
1488 """Deal with incoming query packets. Provides a response if
1489 possible."""
1490 out = None
1492 # Support unicast client responses
1494 if port != _MDNS_PORT:
1495 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1496 for question in msg.questions:
1497 out.addQuestion(question)
1499 for question in msg.questions:
1500 if question.type == _TYPE_PTR:
1501 if question.name == "_services._dns-sd._udp.local.":
1502 for stype in self.servicetypes.keys():
1503 if out is None:
1504 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1505 out.addAnswer(msg, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1506 for service in self.services.values():
1507 if question.name == service.type:
1508 if out is None:
1509 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1510 out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name))
1511 else:
1512 try:
1513 if out is None:
1514 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1516 # Answer A record queries for any service addresses we know
1517 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1518 for service in self.services.values():
1519 if service.server == question.name.lower():
1520 out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1522 service = self.services.get(question.name.lower(), None)
1523 if not service: continue
1525 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
1526 out.addAnswer(msg, DNSService(question.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server))
1527 if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
1528 out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text))
1529 if question.type == _TYPE_SRV:
1530 out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1531 except:
1532 traceback.print_exc()
1534 if out is not None and out.answers:
1535 out.id = msg.id
1536 self.send(out, addr, port)
1538 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1539 """Sends an outgoing packet."""
1540 # This is a quick test to see if we can parse the packets we generate
1541 #temp = DNSIncoming(out.packet())
1542 try:
1543 bytes_sent = self.socket.sendto(out.packet(), 0, (addr, port))
1544 except:
1545 # Ignore this, it may be a temporary loss of network connection
1546 pass
1548 def close(self):
1549 """Ends the background threads, and prevent this instance from
1550 servicing further queries."""
1551 if globals()['_GLOBAL_DONE'] == 0:
1552 globals()['_GLOBAL_DONE'] = 1
1553 self.notifyAll()
1554 self.engine.notify()
1555 self.unregisterAllServices()
1556 self.socket.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1557 self.socket.close()
1559 # Test a few module features, including service registration, service
1560 # query (for Zoe), and service unregistration.
1562 if __name__ == '__main__':
1563 print "Multicast DNS Service Discovery for Python, version", __version__
1564 r = Zeroconf()
1565 print "1. Testing registration of a service..."
1566 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1567 info = ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1568 print " Registering service..."
1569 r.registerService(info)
1570 print " Registration done."
1571 print "2. Testing query of service information..."
1572 print " Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1573 print " Query done."
1574 print "3. Testing query of own service..."
1575 print " Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local."))
1576 print " Query done."
1577 print "4. Testing unregister of service information..."
1578 r.unregisterService(info)
1579 print " Unregister done."
1580 r.close()