Better display in ToGo when navigating pyTivo shares (this still doesn't
[pyTivo/wmcbrine/lucasnz.git] / Zeroconf.py
blob69341f8d3d94011db3c076fa4ec3266a268523ae
1 """ Multicast DNS Service Discovery for Python, v0.13-wmcbrine
2 Copyright 2003 Paul Scott-Murphy, 2013 William McBrine
4 This module provides a framework for the use of DNS Service Discovery
5 using IP multicast.
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 """
23 __author__ = 'Paul Scott-Murphy'
24 __maintainer__ = 'William McBrine <wmcbrine@gmail.com>'
25 __version__ = '0.13-wmcbrine'
26 __license__ = 'LGPL'
28 import time
29 import struct
30 import socket
31 import threading
32 import select
33 import traceback
35 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
37 # hook for threads
39 _GLOBAL_DONE = False
41 # Some timing constants
43 _UNREGISTER_TIME = 125
44 _CHECK_TIME = 175
45 _REGISTER_TIME = 225
46 _LISTENER_TIME = 200
47 _BROWSER_TIME = 500
49 # Some DNS constants
51 _MDNS_ADDR = '224.0.0.251'
52 _MDNS_PORT = 5353;
53 _DNS_PORT = 53;
54 _DNS_TTL = 60 * 60; # one hour default TTL
56 _MAX_MSG_TYPICAL = 1460 # unused
57 _MAX_MSG_ABSOLUTE = 8972
59 _FLAGS_QR_MASK = 0x8000 # query response mask
60 _FLAGS_QR_QUERY = 0x0000 # query
61 _FLAGS_QR_RESPONSE = 0x8000 # response
63 _FLAGS_AA = 0x0400 # Authorative answer
64 _FLAGS_TC = 0x0200 # Truncated
65 _FLAGS_RD = 0x0100 # Recursion desired
66 _FLAGS_RA = 0x8000 # Recursion available
68 _FLAGS_Z = 0x0040 # Zero
69 _FLAGS_AD = 0x0020 # Authentic data
70 _FLAGS_CD = 0x0010 # Checking disabled
72 _CLASS_IN = 1
73 _CLASS_CS = 2
74 _CLASS_CH = 3
75 _CLASS_HS = 4
76 _CLASS_NONE = 254
77 _CLASS_ANY = 255
78 _CLASS_MASK = 0x7FFF
79 _CLASS_UNIQUE = 0x8000
81 _TYPE_A = 1
82 _TYPE_NS = 2
83 _TYPE_MD = 3
84 _TYPE_MF = 4
85 _TYPE_CNAME = 5
86 _TYPE_SOA = 6
87 _TYPE_MB = 7
88 _TYPE_MG = 8
89 _TYPE_MR = 9
90 _TYPE_NULL = 10
91 _TYPE_WKS = 11
92 _TYPE_PTR = 12
93 _TYPE_HINFO = 13
94 _TYPE_MINFO = 14
95 _TYPE_MX = 15
96 _TYPE_TXT = 16
97 _TYPE_AAAA = 28
98 _TYPE_SRV = 33
99 _TYPE_ANY = 255
101 # Mapping constants to names
103 _CLASSES = { _CLASS_IN : "in",
104 _CLASS_CS : "cs",
105 _CLASS_CH : "ch",
106 _CLASS_HS : "hs",
107 _CLASS_NONE : "none",
108 _CLASS_ANY : "any" }
110 _TYPES = { _TYPE_A : "a",
111 _TYPE_NS : "ns",
112 _TYPE_MD : "md",
113 _TYPE_MF : "mf",
114 _TYPE_CNAME : "cname",
115 _TYPE_SOA : "soa",
116 _TYPE_MB : "mb",
117 _TYPE_MG : "mg",
118 _TYPE_MR : "mr",
119 _TYPE_NULL : "null",
120 _TYPE_WKS : "wks",
121 _TYPE_PTR : "ptr",
122 _TYPE_HINFO : "hinfo",
123 _TYPE_MINFO : "minfo",
124 _TYPE_MX : "mx",
125 _TYPE_TXT : "txt",
126 _TYPE_AAAA : "quada",
127 _TYPE_SRV : "srv",
128 _TYPE_ANY : "any" }
130 # utility functions
132 def currentTimeMillis():
133 """Current system time in milliseconds"""
134 return time.time() * 1000
136 # Exceptions
138 class NonLocalNameException(Exception):
139 pass
141 class NonUniqueNameException(Exception):
142 pass
144 class NamePartTooLongException(Exception):
145 pass
147 class AbstractMethodException(Exception):
148 pass
150 class BadTypeInNameException(Exception):
151 pass
153 # implementation classes
155 class DNSEntry(object):
156 """A DNS entry"""
158 def __init__(self, name, type, clazz):
159 self.key = name.lower()
160 self.name = name
161 self.type = type
162 self.clazz = clazz & _CLASS_MASK
163 self.unique = (clazz & _CLASS_UNIQUE) != 0
165 def __eq__(self, other):
166 """Equality test on name, type, and class"""
167 return (isinstance(other, DNSEntry) and
168 self.name == other.name and
169 self.type == other.type and
170 self.clazz == other.clazz)
172 def __ne__(self, other):
173 """Non-equality test"""
174 return not self.__eq__(other)
176 def getClazz(self, clazz):
177 """Class accessor"""
178 return _CLASSES.get(clazz, "?(%s)" % clazz)
180 def getType(self, t):
181 """Type accessor"""
182 return _TYPES.get(t, "?(%s)" % t)
184 def toString(self, hdr, other):
185 """String representation with additional information"""
186 result = "%s[%s,%s" % (hdr, self.getType(self.type),
187 self.getClazz(self.clazz))
188 if self.unique:
189 result += "-unique,"
190 else:
191 result += ","
192 result += self.name
193 if other is not None:
194 result += ",%s]" % (other)
195 else:
196 result += "]"
197 return result
199 class DNSQuestion(DNSEntry):
200 """A DNS question entry"""
202 def __init__(self, name, type, clazz):
203 #if not name.endswith(".local."):
204 # raise NonLocalNameException
205 DNSEntry.__init__(self, name, type, clazz)
207 def answeredBy(self, rec):
208 """Returns true if the question is answered by the record"""
209 return (self.clazz == rec.clazz and
210 (self.type == rec.type or self.type == _TYPE_ANY) and
211 self.name == rec.name)
213 def __repr__(self):
214 """String representation"""
215 return DNSEntry.toString(self, "question", None)
218 class DNSRecord(DNSEntry):
219 """A DNS record - like a DNS entry, but has a TTL"""
221 def __init__(self, name, type, clazz, ttl):
222 DNSEntry.__init__(self, name, type, clazz)
223 self.ttl = ttl
224 self.created = currentTimeMillis()
226 def __eq__(self, other):
227 """Tests equality as per DNSRecord"""
228 return isinstance(other, DNSRecord) and DNSEntry.__eq__(self, other)
230 def suppressedBy(self, msg):
231 """Returns true if any answer in a message can suffice for the
232 information held in this record."""
233 for record in msg.answers:
234 if self.suppressedByAnswer(record):
235 return True
236 return False
238 def suppressedByAnswer(self, other):
239 """Returns true if another record has same name, type and class,
240 and if its TTL is at least half of this record's."""
241 return self == other and other.ttl > (self.ttl / 2)
243 def getExpirationTime(self, percent):
244 """Returns the time at which this record will have expired
245 by a certain percentage."""
246 return self.created + (percent * self.ttl * 10)
248 def getRemainingTTL(self, now):
249 """Returns the remaining TTL in seconds."""
250 return max(0, (self.getExpirationTime(100) - now) / 1000)
252 def isExpired(self, now):
253 """Returns true if this record has expired."""
254 return self.getExpirationTime(100) <= now
256 def isStale(self, now):
257 """Returns true if this record is at least half way expired."""
258 return self.getExpirationTime(50) <= now
260 def resetTTL(self, other):
261 """Sets this record's TTL and created time to that of
262 another record."""
263 self.created = other.created
264 self.ttl = other.ttl
266 def write(self, out):
267 """Abstract method"""
268 raise AbstractMethodException
270 def toString(self, other):
271 """String representation with addtional information"""
272 arg = "%s/%s,%s" % (self.ttl,
273 self.getRemainingTTL(currentTimeMillis()), other)
274 return DNSEntry.toString(self, "record", arg)
276 class DNSAddress(DNSRecord):
277 """A DNS address record"""
279 def __init__(self, name, type, clazz, ttl, address):
280 DNSRecord.__init__(self, name, type, clazz, ttl)
281 self.address = address
283 def write(self, out):
284 """Used in constructing an outgoing packet"""
285 out.writeString(self.address)
287 def __eq__(self, other):
288 """Tests equality on address"""
289 return isinstance(other, DNSAddress) and self.address == other.address
291 def __repr__(self):
292 """String representation"""
293 try:
294 return socket.inet_ntoa(self.address)
295 except:
296 return self.address
298 class DNSHinfo(DNSRecord):
299 """A DNS host information record"""
301 def __init__(self, name, type, clazz, ttl, cpu, os):
302 DNSRecord.__init__(self, name, type, clazz, ttl)
303 self.cpu = cpu
304 self.os = os
306 def write(self, out):
307 """Used in constructing an outgoing packet"""
308 out.writeString(self.cpu)
309 out.writeString(self.oso)
311 def __eq__(self, other):
312 """Tests equality on cpu and os"""
313 return (isinstance(other, DNSHinfo) and
314 self.cpu == other.cpu and self.os == other.os)
316 def __repr__(self):
317 """String representation"""
318 return self.cpu + " " + self.os
320 class DNSPointer(DNSRecord):
321 """A DNS pointer record"""
323 def __init__(self, name, type, clazz, ttl, alias):
324 DNSRecord.__init__(self, name, type, clazz, ttl)
325 self.alias = alias
327 def write(self, out):
328 """Used in constructing an outgoing packet"""
329 out.writeName(self.alias)
331 def __eq__(self, other):
332 """Tests equality on alias"""
333 return isinstance(other, DNSPointer) and self.alias == other.alias
335 def __repr__(self):
336 """String representation"""
337 return self.toString(self.alias)
339 class DNSText(DNSRecord):
340 """A DNS text record"""
342 def __init__(self, name, type, clazz, ttl, text):
343 DNSRecord.__init__(self, name, type, clazz, ttl)
344 self.text = text
346 def write(self, out):
347 """Used in constructing an outgoing packet"""
348 out.writeString(self.text)
350 def __eq__(self, other):
351 """Tests equality on text"""
352 return isinstance(other, DNSText) and self.text == other.text
354 def __repr__(self):
355 """String representation"""
356 if len(self.text) > 10:
357 return self.toString(self.text[:7] + "...")
358 else:
359 return self.toString(self.text)
361 class DNSService(DNSRecord):
362 """A DNS service record"""
364 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
365 DNSRecord.__init__(self, name, type, clazz, ttl)
366 self.priority = priority
367 self.weight = weight
368 self.port = port
369 self.server = server
371 def write(self, out):
372 """Used in constructing an outgoing packet"""
373 out.writeShort(self.priority)
374 out.writeShort(self.weight)
375 out.writeShort(self.port)
376 out.writeName(self.server)
378 def __eq__(self, other):
379 """Tests equality on priority, weight, port and server"""
380 return (isinstance(other, DNSService) and
381 self.priority == other.priority and
382 self.weight == other.weight and
383 self.port == other.port and
384 self.server == other.server)
386 def __repr__(self):
387 """String representation"""
388 return self.toString("%s:%s" % (self.server, self.port))
390 class DNSIncoming(object):
391 """Object representation of an incoming DNS packet"""
393 def __init__(self, data):
394 """Constructor from string holding bytes of packet"""
395 self.offset = 0
396 self.data = data
397 self.questions = []
398 self.answers = []
399 self.numQuestions = 0
400 self.numAnswers = 0
401 self.numAuthorities = 0
402 self.numAdditionals = 0
404 self.readHeader()
405 self.readQuestions()
406 self.readOthers()
408 def unpack(self, format):
409 length = struct.calcsize(format)
410 info = struct.unpack(format, self.data[self.offset:self.offset+length])
411 self.offset += length
412 return info
414 def readHeader(self):
415 """Reads header portion of packet"""
416 (self.id, self.flags, self.numQuestions, self.numAnswers,
417 self.numAuthorities, self.numAdditionals) = self.unpack('!6H')
419 def readQuestions(self):
420 """Reads questions section of packet"""
421 for i in xrange(self.numQuestions):
422 name = self.readName()
423 type, clazz = self.unpack('!HH')
425 question = DNSQuestion(name, type, clazz)
426 self.questions.append(question)
428 def readInt(self):
429 """Reads an integer from the packet"""
430 return self.unpack('!I')[0]
432 def readCharacterString(self):
433 """Reads a character string from the packet"""
434 length = ord(self.data[self.offset])
435 self.offset += 1
436 return self.readString(length)
438 def readString(self, length):
439 """Reads a string of a given length from the packet"""
440 info = self.data[self.offset:self.offset+length]
441 self.offset += length
442 return info
444 def readUnsignedShort(self):
445 """Reads an unsigned short from the packet"""
446 return self.unpack('!H')[0]
448 def readOthers(self):
449 """Reads the answers, authorities and additionals section of the
450 packet"""
451 n = self.numAnswers + self.numAuthorities + self.numAdditionals
452 for i in xrange(n):
453 domain = self.readName()
454 type, clazz, ttl, length = self.unpack('!HHiH')
456 rec = None
457 if type == _TYPE_A:
458 rec = DNSAddress(domain, type, clazz, ttl, self.readString(4))
459 elif type == _TYPE_CNAME or type == _TYPE_PTR:
460 rec = DNSPointer(domain, type, clazz, ttl, self.readName())
461 elif type == _TYPE_TXT:
462 rec = DNSText(domain, type, clazz, ttl, self.readString(length))
463 elif type == _TYPE_SRV:
464 rec = DNSService(domain, type, clazz, ttl,
465 self.readUnsignedShort(), self.readUnsignedShort(),
466 self.readUnsignedShort(), self.readName())
467 elif type == _TYPE_HINFO:
468 rec = DNSHinfo(domain, type, clazz, ttl,
469 self.readCharacterString(), self.readCharacterString())
470 elif type == _TYPE_AAAA:
471 rec = DNSAddress(domain, type, clazz, ttl, self.readString(16))
472 else:
473 # Try to ignore types we don't know about
474 # Skip the payload for the resource record so the next
475 # records can be parsed correctly
476 self.offset += length
478 if rec is not None:
479 self.answers.append(rec)
481 def isQuery(self):
482 """Returns true if this is a query"""
483 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
485 def isResponse(self):
486 """Returns true if this is a response"""
487 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
489 def readUTF(self, offset, length):
490 """Reads a UTF-8 string of a given length from the packet"""
491 return unicode(self.data[offset:offset+length], 'utf-8', 'replace')
493 def readName(self):
494 """Reads a domain name from the packet"""
495 result = ''
496 off = self.offset
497 next = -1
498 first = off
500 while True:
501 length = ord(self.data[off])
502 off += 1
503 if length == 0:
504 break
505 t = length & 0xC0
506 if t == 0x00:
507 result = ''.join((result, self.readUTF(off, length) + '.'))
508 off += length
509 elif t == 0xC0:
510 if next < 0:
511 next = off + 1
512 off = ((length & 0x3F) << 8) | ord(self.data[off])
513 if off >= first:
514 raise "Bad domain name (circular) at " + str(off)
515 first = off
516 else:
517 raise "Bad domain name at " + str(off)
519 if next >= 0:
520 self.offset = next
521 else:
522 self.offset = off
524 return result
527 class DNSOutgoing(object):
528 """Object representation of an outgoing packet"""
530 def __init__(self, flags, multicast=True):
531 self.finished = False
532 self.id = 0
533 self.multicast = multicast
534 self.flags = flags
535 self.names = {}
536 self.data = []
537 self.size = 12
539 self.questions = []
540 self.answers = []
541 self.authorities = []
542 self.additionals = []
544 def addQuestion(self, record):
545 """Adds a question"""
546 self.questions.append(record)
548 def addAnswer(self, inp, record):
549 """Adds an answer"""
550 if not record.suppressedBy(inp):
551 self.addAnswerAtTime(record, 0)
553 def addAnswerAtTime(self, record, now):
554 """Adds an answer if if does not expire by a certain time"""
555 if record is not None:
556 if now == 0 or not record.isExpired(now):
557 self.answers.append((record, now))
559 def addAuthorativeAnswer(self, record):
560 """Adds an authoritative answer"""
561 self.authorities.append(record)
563 def addAdditionalAnswer(self, record):
564 """Adds an additional answer"""
565 self.additionals.append(record)
567 def pack(self, format, value):
568 self.data.append(struct.pack(format, value))
569 self.size += struct.calcsize(format)
571 def writeByte(self, value):
572 """Writes a single byte to the packet"""
573 self.pack('!c', chr(value))
575 def insertShort(self, index, value):
576 """Inserts an unsigned short in a certain position in the packet"""
577 self.data.insert(index, struct.pack('!H', value))
578 self.size += 2
580 def writeShort(self, value):
581 """Writes an unsigned short to the packet"""
582 self.pack('!H', value)
584 def writeInt(self, value):
585 """Writes an unsigned integer to the packet"""
586 self.pack('!I', int(value))
588 def writeString(self, value):
589 """Writes a string to the packet"""
590 self.data.append(value)
591 self.size += len(value)
593 def writeUTF(self, s):
594 """Writes a UTF-8 string of a given length to the packet"""
595 utfstr = s.encode('utf-8')
596 length = len(utfstr)
597 if length > 64:
598 raise NamePartTooLongException
599 self.writeByte(length)
600 self.writeString(utfstr)
602 def writeName(self, name):
603 """Writes a domain name to the packet"""
605 if name in self.names:
606 # Find existing instance of this name in packet
608 index = self.names[name]
610 # An index was found, so write a pointer to it
612 self.writeByte((index >> 8) | 0xC0)
613 self.writeByte(index & 0xFF)
614 else:
615 # No record of this name already, so write it
616 # out as normal, recording the location of the name
617 # for future pointers to it.
619 self.names[name] = self.size
620 parts = name.split('.')
621 if parts[-1] == '':
622 parts = parts[:-1]
623 for part in parts:
624 self.writeUTF(part)
625 self.writeByte(0)
627 def writeQuestion(self, question):
628 """Writes a question to the packet"""
629 self.writeName(question.name)
630 self.writeShort(question.type)
631 self.writeShort(question.clazz)
633 def writeRecord(self, record, now):
634 """Writes a record (answer, authoritative answer, additional) to
635 the packet"""
636 self.writeName(record.name)
637 self.writeShort(record.type)
638 if record.unique and self.multicast:
639 self.writeShort(record.clazz | _CLASS_UNIQUE)
640 else:
641 self.writeShort(record.clazz)
642 if now == 0:
643 self.writeInt(record.ttl)
644 else:
645 self.writeInt(record.getRemainingTTL(now))
646 index = len(self.data)
647 # Adjust size for the short we will write before this record
649 self.size += 2
650 record.write(self)
651 self.size -= 2
653 length = len(''.join(self.data[index:]))
654 self.insertShort(index, length) # Here is the short we adjusted for
656 def packet(self):
657 """Returns a string containing the packet's bytes
659 No further parts should be added to the packet once this
660 is done."""
661 if not self.finished:
662 self.finished = True
663 for question in self.questions:
664 self.writeQuestion(question)
665 for answer, time in self.answers:
666 self.writeRecord(answer, time)
667 for authority in self.authorities:
668 self.writeRecord(authority, 0)
669 for additional in self.additionals:
670 self.writeRecord(additional, 0)
672 self.insertShort(0, len(self.additionals))
673 self.insertShort(0, len(self.authorities))
674 self.insertShort(0, len(self.answers))
675 self.insertShort(0, len(self.questions))
676 self.insertShort(0, self.flags)
677 if self.multicast:
678 self.insertShort(0, 0)
679 else:
680 self.insertShort(0, self.id)
681 return ''.join(self.data)
684 class DNSCache(object):
685 """A cache of DNS entries"""
687 def __init__(self):
688 self.cache = {}
690 def add(self, entry):
691 """Adds an entry"""
692 try:
693 list = self.cache[entry.key]
694 except:
695 list = self.cache[entry.key] = []
696 list.append(entry)
698 def remove(self, entry):
699 """Removes an entry"""
700 try:
701 list = self.cache[entry.key]
702 list.remove(entry)
703 except:
704 pass
706 def get(self, entry):
707 """Gets an entry by key. Will return None if there is no
708 matching entry."""
709 try:
710 list = self.cache[entry.key]
711 return list[list.index(entry)]
712 except:
713 return None
715 def getByDetails(self, name, type, clazz):
716 """Gets an entry by details. Will return None if there is
717 no matching entry."""
718 entry = DNSEntry(name, type, clazz)
719 return self.get(entry)
721 def entriesWithName(self, name):
722 """Returns a list of entries whose key matches the name."""
723 try:
724 return self.cache[name]
725 except:
726 return []
728 def entries(self):
729 """Returns a list of all entries"""
730 def add(x, y): return x+y
731 try:
732 return reduce(add, self.cache.values())
733 except:
734 return []
737 class Engine(threading.Thread):
738 """An engine wraps read access to sockets, allowing objects that
739 need to receive data from sockets to be called back when the
740 sockets are ready.
742 A reader needs a handle_read() method, which is called when the socket
743 it is interested in is ready for reading.
745 Writers are not implemented here, because we only send short
746 packets.
749 def __init__(self, zc):
750 threading.Thread.__init__(self)
751 self.zc = zc
752 self.readers = {} # maps socket to reader
753 self.timeout = 5
754 self.condition = threading.Condition()
755 self.start()
757 def run(self):
758 while not _GLOBAL_DONE:
759 rs = self.getReaders()
760 if len(rs) == 0:
761 # No sockets to manage, but we wait for the timeout
762 # or addition of a socket
764 self.condition.acquire()
765 self.condition.wait(self.timeout)
766 self.condition.release()
767 else:
768 try:
769 rr, wr, er = select.select(rs, [], [], self.timeout)
770 for socket in rr:
771 try:
772 self.readers[socket].handle_read()
773 except:
774 traceback.print_exc()
775 except:
776 pass
778 def getReaders(self):
779 result = []
780 self.condition.acquire()
781 result = self.readers.keys()
782 self.condition.release()
783 return result
785 def addReader(self, reader, socket):
786 self.condition.acquire()
787 self.readers[socket] = reader
788 self.condition.notify()
789 self.condition.release()
791 def delReader(self, socket):
792 self.condition.acquire()
793 del(self.readers[socket])
794 self.condition.notify()
795 self.condition.release()
797 def notify(self):
798 self.condition.acquire()
799 self.condition.notify()
800 self.condition.release()
802 class Listener(object):
803 """A Listener is used by this module to listen on the multicast
804 group to which DNS messages are sent, allowing the implementation
805 to cache information as it arrives.
807 It requires registration with an Engine object in order to have
808 the read() method called when a socket is availble for reading."""
810 def __init__(self, zc):
811 self.zc = zc
812 self.zc.engine.addReader(self, self.zc.socket)
814 def handle_read(self):
815 try:
816 data, (addr, port) = self.zc.socket.recvfrom(_MAX_MSG_ABSOLUTE)
817 except socket.error, e:
818 # If the socket was closed by another thread -- which happens
819 # regularly on shutdown -- an EBADF exception is thrown here.
820 # Ignore it.
821 if e[0] == socket.EBADF:
822 return
823 else:
824 raise e
825 self.data = data
826 msg = DNSIncoming(data)
827 if msg.isQuery():
828 # Always multicast responses
830 if port == _MDNS_PORT:
831 self.zc.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
832 # If it's not a multicast query, reply via unicast
833 # and multicast
835 elif port == _DNS_PORT:
836 self.zc.handleQuery(msg, addr, port)
837 self.zc.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
838 else:
839 self.zc.handleResponse(msg)
842 class Reaper(threading.Thread):
843 """A Reaper is used by this module to remove cache entries that
844 have expired."""
846 def __init__(self, zc):
847 threading.Thread.__init__(self)
848 self.zc = zc
849 self.start()
851 def run(self):
852 while True:
853 self.zc.wait(10 * 1000)
854 if _GLOBAL_DONE:
855 return
856 now = currentTimeMillis()
857 for record in self.zc.cache.entries():
858 if record.isExpired(now):
859 self.zc.updateRecord(now, record)
860 self.zc.cache.remove(record)
863 class ServiceBrowser(threading.Thread):
864 """Used to browse for a service of a specific type.
866 The listener object will have its addService() and
867 removeService() methods called when this browser
868 discovers changes in the services availability."""
870 def __init__(self, zc, type, listener):
871 """Creates a browser for a specific type"""
872 threading.Thread.__init__(self)
873 self.zc = zc
874 self.type = type
875 self.listener = listener
876 self.services = {}
877 self.nextTime = currentTimeMillis()
878 self.delay = _BROWSER_TIME
879 self.list = []
881 self.done = False
883 self.zc.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
884 self.start()
886 def updateRecord(self, zc, now, record):
887 """Callback invoked by Zeroconf when new information arrives.
889 Updates information required by browser in the Zeroconf cache."""
890 if record.type == _TYPE_PTR and record.name == self.type:
891 expired = record.isExpired(now)
892 try:
893 oldrecord = self.services[record.alias.lower()]
894 if not expired:
895 oldrecord.resetTTL(record)
896 else:
897 del(self.services[record.alias.lower()])
898 callback = lambda x: self.listener.removeService(x,
899 self.type, record.alias)
900 self.list.append(callback)
901 return
902 except:
903 if not expired:
904 self.services[record.alias.lower()] = record
905 callback = lambda x: self.listener.addService(x,
906 self.type, record.alias)
907 self.list.append(callback)
909 expires = record.getExpirationTime(75)
910 if expires < self.nextTime:
911 self.nextTime = expires
913 def cancel(self):
914 self.done = True
915 self.zc.notifyAll()
917 def run(self):
918 while True:
919 event = None
920 now = currentTimeMillis()
921 if len(self.list) == 0 and self.nextTime > now:
922 self.zc.wait(self.nextTime - now)
923 if _GLOBAL_DONE or self.done:
924 return
925 now = currentTimeMillis()
927 if self.nextTime <= now:
928 out = DNSOutgoing(_FLAGS_QR_QUERY)
929 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
930 for record in self.services.values():
931 if not record.isExpired(now):
932 out.addAnswerAtTime(record, now)
933 self.zc.send(out)
934 self.nextTime = now + self.delay
935 self.delay = min(20 * 1000, self.delay * 2)
937 if len(self.list) > 0:
938 event = self.list.pop(0)
940 if event is not None:
941 event(self.zc)
944 class ServiceInfo(object):
945 """Service information"""
947 def __init__(self, type, name, address=None, port=None, weight=0,
948 priority=0, properties=None, server=None):
949 """Create a service description.
951 type: fully qualified service type name
952 name: fully qualified service name
953 address: IP address as unsigned short, network byte order
954 port: port that the service runs on
955 weight: weight of the service
956 priority: priority of the service
957 properties: dictionary of properties (or a string holding the
958 bytes for the text field)
959 server: fully qualified name for service host (defaults to name)"""
961 if not name.endswith(type):
962 raise BadTypeInNameException
963 self.type = type
964 self.name = name
965 self.address = address
966 self.port = port
967 self.weight = weight
968 self.priority = priority
969 if server:
970 self.server = server
971 else:
972 self.server = name
973 self.setProperties(properties)
975 def setProperties(self, properties):
976 """Sets properties and text of this info from a dictionary"""
977 if isinstance(properties, dict):
978 self.properties = properties
979 list = []
980 result = ''
981 for key in properties:
982 value = properties[key]
983 if value is None:
984 suffix = ''.encode('utf-8')
985 elif isinstance(value, str):
986 suffix = value.encode('utf-8')
987 elif isinstance(value, int):
988 if value:
989 suffix = 'true'
990 else:
991 suffix = 'false'
992 else:
993 suffix = ''.encode('utf-8')
994 list.append('='.join((key, suffix)))
995 for item in list:
996 result = ''.join((result, chr(len(item)), item))
997 self.text = result
998 else:
999 self.text = properties
1001 def setText(self, text):
1002 """Sets properties and text given a text field"""
1003 self.text = text
1004 try:
1005 result = {}
1006 end = len(text)
1007 index = 0
1008 strs = []
1009 while index < end:
1010 length = ord(text[index])
1011 index += 1
1012 strs.append(text[index:index+length])
1013 index += length
1015 for s in strs:
1016 try:
1017 key, value = s.split('=', 1)
1018 if value == 'true':
1019 value = True
1020 elif value == 'false' or not value:
1021 value = False
1022 except:
1023 # No equals sign at all
1024 key = s
1025 value = False
1027 # Only update non-existent properties
1028 if key and result.get(key) == None:
1029 result[key] = value
1031 self.properties = result
1032 except:
1033 traceback.print_exc()
1034 self.properties = None
1036 def getType(self):
1037 """Type accessor"""
1038 return self.type
1040 def getName(self):
1041 """Name accessor"""
1042 if self.type is not None and self.name.endswith("." + self.type):
1043 return self.name[:len(self.name) - len(self.type) - 1]
1044 return self.name
1046 def getAddress(self):
1047 """Address accessor"""
1048 return self.address
1050 def getPort(self):
1051 """Port accessor"""
1052 return self.port
1054 def getPriority(self):
1055 """Pirority accessor"""
1056 return self.priority
1058 def getWeight(self):
1059 """Weight accessor"""
1060 return self.weight
1062 def getProperties(self):
1063 """Properties accessor"""
1064 return self.properties
1066 def getText(self):
1067 """Text accessor"""
1068 return self.text
1070 def getServer(self):
1071 """Server accessor"""
1072 return self.server
1074 def updateRecord(self, zc, now, record):
1075 """Updates service information from a DNS record"""
1076 if record is not None and not record.isExpired(now):
1077 if record.type == _TYPE_A:
1078 #if record.name == self.name:
1079 if record.name == self.server:
1080 self.address = record.address
1081 elif record.type == _TYPE_SRV:
1082 if record.name == self.name:
1083 self.server = record.server
1084 self.port = record.port
1085 self.weight = record.weight
1086 self.priority = record.priority
1087 #self.address = None
1088 self.updateRecord(zc, now,
1089 zc.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1090 elif record.type == _TYPE_TXT:
1091 if record.name == self.name:
1092 self.setText(record.text)
1094 def request(self, zc, timeout):
1095 """Returns true if the service could be discovered on the
1096 network, and updates this object with details discovered.
1098 now = currentTimeMillis()
1099 delay = _LISTENER_TIME
1100 next = now + delay
1101 last = now + timeout
1102 result = False
1103 try:
1104 zc.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1105 while (self.server is None or self.address is None or
1106 self.text is None):
1107 if last <= now:
1108 return False
1109 if next <= now:
1110 out = DNSOutgoing(_FLAGS_QR_QUERY)
1111 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV,
1112 _CLASS_IN))
1113 out.addAnswerAtTime(zc.cache.getByDetails(self.name,
1114 _TYPE_SRV, _CLASS_IN), now)
1115 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT,
1116 _CLASS_IN))
1117 out.addAnswerAtTime(zc.cache.getByDetails(self.name,
1118 _TYPE_TXT, _CLASS_IN), now)
1119 if self.server is not None:
1120 out.addQuestion(DNSQuestion(self.server,
1121 _TYPE_A, _CLASS_IN))
1122 out.addAnswerAtTime(zc.cache.getByDetails(self.server,
1123 _TYPE_A, _CLASS_IN), now)
1124 zc.send(out)
1125 next = now + delay
1126 delay = delay * 2
1128 zc.wait(min(next, last) - now)
1129 now = currentTimeMillis()
1130 result = True
1131 finally:
1132 zc.removeListener(self)
1134 return result
1136 def __eq__(self, other):
1137 """Tests equality of service name"""
1138 if isinstance(other, ServiceInfo):
1139 return other.name == self.name
1140 return False
1142 def __ne__(self, other):
1143 """Non-equality test"""
1144 return not self.__eq__(other)
1146 def __repr__(self):
1147 """String representation"""
1148 result = "service[%s,%s:%s," % (self.name,
1149 socket.inet_ntoa(self.getAddress()), self.port)
1150 if self.text is None:
1151 result += "None"
1152 else:
1153 if len(self.text) < 20:
1154 result += self.text
1155 else:
1156 result += self.text[:17] + "..."
1157 result += "]"
1158 return result
1161 class Zeroconf(object):
1162 """Implementation of Zeroconf Multicast DNS Service Discovery
1164 Supports registration, unregistration, queries and browsing.
1166 def __init__(self, bindaddress=None):
1167 """Creates an instance of the Zeroconf class, establishing
1168 multicast communications, listening and reaping threads."""
1169 global _GLOBAL_DONE
1170 _GLOBAL_DONE = False
1171 if bindaddress is None:
1172 try:
1173 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1174 s.connect(('4.2.2.1', 123))
1175 self.intf = s.getsockname()[0]
1176 except:
1177 self.intf = socket.gethostbyname(socket.gethostname())
1178 else:
1179 self.intf = bindaddress
1180 self.group = ('', _MDNS_PORT)
1181 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1182 try:
1183 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1184 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1185 except:
1186 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1187 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1188 # Volume 2"), but some BSD-derived systems require
1189 # SO_REUSEPORT to be specified explicity. Also, not all
1190 # versions of Python have SO_REUSEPORT available. So
1191 # if you're on a BSD-based system, and haven't upgraded
1192 # to Python 2.3 yet, you may find this library doesn't
1193 # work as expected.
1195 pass
1196 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
1197 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
1198 try:
1199 self.socket.bind(self.group)
1200 except:
1201 # Some versions of linux raise an exception even though
1202 # the SO_REUSE* options have been set, so ignore it
1204 pass
1205 #self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF,
1206 # socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1207 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP,
1208 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1210 self.listeners = []
1211 self.browsers = []
1212 self.services = {}
1213 self.servicetypes = {}
1215 self.cache = DNSCache()
1217 self.condition = threading.Condition()
1219 self.engine = Engine(self)
1220 self.listener = Listener(self)
1221 self.reaper = Reaper(self)
1223 def isLoopback(self):
1224 return self.intf.startswith("127.0.0.1")
1226 def isLinklocal(self):
1227 return self.intf.startswith("169.254.")
1229 def wait(self, timeout):
1230 """Calling thread waits for a given number of milliseconds or
1231 until notified."""
1232 self.condition.acquire()
1233 self.condition.wait(timeout/1000)
1234 self.condition.release()
1236 def notifyAll(self):
1237 """Notifies all waiting threads"""
1238 self.condition.acquire()
1239 self.condition.notifyAll()
1240 self.condition.release()
1242 def getServiceInfo(self, type, name, timeout=3000):
1243 """Returns network's service information for a particular
1244 name and type, or None if no service matches by the timeout,
1245 which defaults to 3 seconds."""
1246 info = ServiceInfo(type, name)
1247 if info.request(self, timeout):
1248 return info
1249 return None
1251 def addServiceListener(self, type, listener):
1252 """Adds a listener for a particular service type. This object
1253 will then have its updateRecord method called when information
1254 arrives for that type."""
1255 self.removeServiceListener(listener)
1256 self.browsers.append(ServiceBrowser(self, type, listener))
1258 def removeServiceListener(self, listener):
1259 """Removes a listener from the set that is currently listening."""
1260 for browser in self.browsers:
1261 if browser.listener == listener:
1262 browser.cancel()
1263 del(browser)
1265 def registerService(self, info, ttl=_DNS_TTL):
1266 """Registers service information to the network with a default TTL
1267 of 60 seconds. Zeroconf will then respond to requests for
1268 information for that service. The name of the service may be
1269 changed if needed to make it unique on the network."""
1270 self.checkService(info)
1271 self.services[info.name.lower()] = info
1272 if info.type in self.servicetypes:
1273 self.servicetypes[info.type]+=1
1274 else:
1275 self.servicetypes[info.type]=1
1276 now = currentTimeMillis()
1277 nextTime = now
1278 i = 0
1279 while i < 3:
1280 if now < nextTime:
1281 self.wait(nextTime - now)
1282 now = currentTimeMillis()
1283 continue
1284 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1285 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1286 _CLASS_IN, ttl, info.name), 0)
1287 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1288 _CLASS_IN, ttl, info.priority, info.weight, info.port,
1289 info.server), 0)
1290 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN,
1291 ttl, info.text), 0)
1292 if info.address:
1293 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A,
1294 _CLASS_IN, ttl, info.address), 0)
1295 self.send(out)
1296 i += 1
1297 nextTime += _REGISTER_TIME
1299 def unregisterService(self, info):
1300 """Unregister a service."""
1301 try:
1302 del(self.services[info.name.lower()])
1303 if self.servicetypes[info.type]>1:
1304 self.servicetypes[info.type]-=1
1305 else:
1306 del self.servicetypes[info.type]
1307 except:
1308 pass
1309 now = currentTimeMillis()
1310 nextTime = now
1311 i = 0
1312 while i < 3:
1313 if now < nextTime:
1314 self.wait(nextTime - now)
1315 now = currentTimeMillis()
1316 continue
1317 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1318 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1319 _CLASS_IN, 0, info.name), 0)
1320 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1321 _CLASS_IN, 0, info.priority, info.weight, info.port,
1322 info.name), 0)
1323 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN,
1324 0, info.text), 0)
1325 if info.address:
1326 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A,
1327 _CLASS_IN, 0, info.address), 0)
1328 self.send(out)
1329 i += 1
1330 nextTime += _UNREGISTER_TIME
1332 def unregisterAllServices(self):
1333 """Unregister all registered services."""
1334 if len(self.services) > 0:
1335 now = currentTimeMillis()
1336 nextTime = now
1337 i = 0
1338 while i < 3:
1339 if now < nextTime:
1340 self.wait(nextTime - now)
1341 now = currentTimeMillis()
1342 continue
1343 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1344 for info in self.services.values():
1345 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1346 _CLASS_IN, 0, info.name), 0)
1347 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1348 _CLASS_IN, 0, info.priority, info.weight,
1349 info.port, info.server), 0)
1350 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT,
1351 _CLASS_IN, 0, info.text), 0)
1352 if info.address:
1353 out.addAnswerAtTime(DNSAddress(info.server,
1354 _TYPE_A, _CLASS_IN, 0, info.address), 0)
1355 self.send(out)
1356 i += 1
1357 nextTime += _UNREGISTER_TIME
1359 def checkService(self, info):
1360 """Checks the network for a unique service name, modifying the
1361 ServiceInfo passed in if it is not unique."""
1362 now = currentTimeMillis()
1363 nextTime = now
1364 i = 0
1365 while i < 3:
1366 for record in self.cache.entriesWithName(info.type):
1367 if (record.type == _TYPE_PTR and
1368 not record.isExpired(now) and
1369 record.alias == info.name):
1370 if info.name.find('.') < 0:
1371 info.name = '%s.[%s:%s].%s' % (info.name,
1372 info.address, info.port, info.type)
1374 self.checkService(info)
1375 return
1376 raise NonUniqueNameException
1377 if now < nextTime:
1378 self.wait(nextTime - now)
1379 now = currentTimeMillis()
1380 continue
1381 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1382 self.debug = out
1383 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1384 out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR,
1385 _CLASS_IN, _DNS_TTL, info.name))
1386 self.send(out)
1387 i += 1
1388 nextTime += _CHECK_TIME
1390 def addListener(self, listener, question):
1391 """Adds a listener for a given question. The listener will have
1392 its updateRecord method called when information is available to
1393 answer the question."""
1394 now = currentTimeMillis()
1395 self.listeners.append(listener)
1396 if question is not None:
1397 for record in self.cache.entriesWithName(question.name):
1398 if question.answeredBy(record) and not record.isExpired(now):
1399 listener.updateRecord(self, now, record)
1400 self.notifyAll()
1402 def removeListener(self, listener):
1403 """Removes a listener."""
1404 try:
1405 self.listeners.remove(listener)
1406 self.notifyAll()
1407 except:
1408 pass
1410 def updateRecord(self, now, rec):
1411 """Used to notify listeners of new information that has updated
1412 a record."""
1413 for listener in self.listeners:
1414 listener.updateRecord(self, now, rec)
1415 self.notifyAll()
1417 def handleResponse(self, msg):
1418 """Deal with incoming response packets. All answers
1419 are held in the cache, and listeners are notified."""
1420 now = currentTimeMillis()
1421 for record in msg.answers:
1422 expired = record.isExpired(now)
1423 if record in self.cache.entries():
1424 if expired:
1425 self.cache.remove(record)
1426 else:
1427 entry = self.cache.get(record)
1428 if entry is not None:
1429 entry.resetTTL(record)
1430 record = entry
1431 else:
1432 self.cache.add(record)
1434 self.updateRecord(now, record)
1436 def handleQuery(self, msg, addr, port):
1437 """Deal with incoming query packets. Provides a response if
1438 possible."""
1439 out = None
1441 # Support unicast client responses
1443 if port != _MDNS_PORT:
1444 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, False)
1445 for question in msg.questions:
1446 out.addQuestion(question)
1448 for question in msg.questions:
1449 if question.type == _TYPE_PTR:
1450 if question.name == "_services._dns-sd._udp.local.":
1451 for stype in self.servicetypes.keys():
1452 if out is None:
1453 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1454 out.addAnswer(msg,
1455 DNSPointer("_services._dns-sd._udp.local.",
1456 _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1457 for service in self.services.values():
1458 if question.name == service.type:
1459 if out is None:
1460 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1461 out.addAnswer(msg,
1462 DNSPointer(service.type, _TYPE_PTR,
1463 _CLASS_IN, _DNS_TTL, service.name))
1464 else:
1465 try:
1466 if out is None:
1467 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1469 # Answer A record queries for any service addresses we know
1470 if question.type in (_TYPE_A, _TYPE_ANY):
1471 for service in self.services.values():
1472 if service.server == question.name.lower():
1473 out.addAnswer(msg, DNSAddress(question.name,
1474 _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
1475 _DNS_TTL, service.address))
1477 service = self.services.get(question.name.lower(), None)
1478 if not service: continue
1480 if question.type in (_TYPE_SRV, _TYPE_ANY):
1481 out.addAnswer(msg, DNSService(question.name,
1482 _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE,
1483 _DNS_TTL, service.priority, service.weight,
1484 service.port, service.server))
1485 if question.type in (_TYPE_TXT, _TYPE_ANY):
1486 out.addAnswer(msg, DNSText(question.name,
1487 _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE,
1488 _DNS_TTL, service.text))
1489 if question.type == _TYPE_SRV:
1490 out.addAdditionalAnswer(DNSAddress(service.server,
1491 _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
1492 _DNS_TTL, service.address))
1493 except:
1494 traceback.print_exc()
1496 if out is not None and out.answers:
1497 out.id = msg.id
1498 self.send(out, addr, port)
1500 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1501 """Sends an outgoing packet."""
1502 packet = out.packet()
1503 try:
1504 while packet:
1505 bytes_sent = self.socket.sendto(packet, 0, (addr, port))
1506 if bytes_sent < 0:
1507 break
1508 packet = packet[bytes_sent:]
1509 except:
1510 # Ignore this, it may be a temporary loss of network connection
1511 pass
1513 def close(self):
1514 """Ends the background threads, and prevent this instance from
1515 servicing further queries."""
1516 global _GLOBAL_DONE
1517 if not _GLOBAL_DONE:
1518 _GLOBAL_DONE = True
1519 self.notifyAll()
1520 self.engine.notify()
1521 self.unregisterAllServices()
1522 self.socket.setsockopt(socket.SOL_IP,
1523 socket.IP_DROP_MEMBERSHIP,
1524 socket.inet_aton(_MDNS_ADDR) +
1525 socket.inet_aton('0.0.0.0'))
1526 self.socket.close()
1528 # Test a few module features, including service registration, service
1529 # query (for Zoe), and service unregistration.
1531 if __name__ == '__main__':
1532 print "Multicast DNS Service Discovery for Python, version", __version__
1533 r = Zeroconf()
1534 print "1. Testing registration of a service..."
1535 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1536 info = ServiceInfo("_http._tcp.local.",
1537 "My Service Name._http._tcp.local.",
1538 socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1539 print " Registering service..."
1540 r.registerService(info)
1541 print " Registration done."
1542 print "2. Testing query of service information..."
1543 print " Getting ZOE service:",
1544 print str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1545 print " Query done."
1546 print "3. Testing query of own service..."
1547 print " Getting self:",
1548 print str(r.getServiceInfo("_http._tcp.local.",
1549 "My Service Name._http._tcp.local."))
1550 print " Query done."
1551 print "4. Testing unregister of service information..."
1552 r.unregisterService(info)
1553 print " Unregister done."
1554 r.close()