Ratings from .nfo files should no longer be converted to strings.
[pyTivo/wmcbrine.git] / zeroconf.py
blob41f0003eea4f84f7c27fd079c3e4de52f8427d71
1 """ Multicast DNS Service Discovery for Python, v0.14-wmcbrine
2 Copyright 2003 Paul Scott-Murphy, 2014 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
20 USA
22 """
24 __author__ = 'Paul Scott-Murphy'
25 __maintainer__ = 'William McBrine <wmcbrine@gmail.com>'
26 __version__ = '0.14-wmcbrine'
27 __license__ = 'LGPL'
29 import time
30 import struct
31 import socket
32 import threading
33 import select
34 import traceback
36 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
38 # hook for threads
40 _GLOBAL_DONE = False
42 # Some timing constants
44 _UNREGISTER_TIME = 125
45 _CHECK_TIME = 175
46 _REGISTER_TIME = 225
47 _LISTENER_TIME = 200
48 _BROWSER_TIME = 500
50 # Some DNS constants
52 _MDNS_ADDR = '224.0.0.251'
53 _MDNS_PORT = 5353
54 _DNS_PORT = 53
55 _DNS_TTL = 60 * 60 # one hour default TTL
57 _MAX_MSG_TYPICAL = 1460 # unused
58 _MAX_MSG_ABSOLUTE = 8972
60 _FLAGS_QR_MASK = 0x8000 # query response mask
61 _FLAGS_QR_QUERY = 0x0000 # query
62 _FLAGS_QR_RESPONSE = 0x8000 # response
64 _FLAGS_AA = 0x0400 # Authorative answer
65 _FLAGS_TC = 0x0200 # Truncated
66 _FLAGS_RD = 0x0100 # Recursion desired
67 _FLAGS_RA = 0x8000 # Recursion available
69 _FLAGS_Z = 0x0040 # Zero
70 _FLAGS_AD = 0x0020 # Authentic data
71 _FLAGS_CD = 0x0010 # Checking disabled
73 _CLASS_IN = 1
74 _CLASS_CS = 2
75 _CLASS_CH = 3
76 _CLASS_HS = 4
77 _CLASS_NONE = 254
78 _CLASS_ANY = 255
79 _CLASS_MASK = 0x7FFF
80 _CLASS_UNIQUE = 0x8000
82 _TYPE_A = 1
83 _TYPE_NS = 2
84 _TYPE_MD = 3
85 _TYPE_MF = 4
86 _TYPE_CNAME = 5
87 _TYPE_SOA = 6
88 _TYPE_MB = 7
89 _TYPE_MG = 8
90 _TYPE_MR = 9
91 _TYPE_NULL = 10
92 _TYPE_WKS = 11
93 _TYPE_PTR = 12
94 _TYPE_HINFO = 13
95 _TYPE_MINFO = 14
96 _TYPE_MX = 15
97 _TYPE_TXT = 16
98 _TYPE_AAAA = 28
99 _TYPE_SRV = 33
100 _TYPE_ANY = 255
102 # Mapping constants to names
104 _CLASSES = { _CLASS_IN : "in",
105 _CLASS_CS : "cs",
106 _CLASS_CH : "ch",
107 _CLASS_HS : "hs",
108 _CLASS_NONE : "none",
109 _CLASS_ANY : "any" }
111 _TYPES = { _TYPE_A : "a",
112 _TYPE_NS : "ns",
113 _TYPE_MD : "md",
114 _TYPE_MF : "mf",
115 _TYPE_CNAME : "cname",
116 _TYPE_SOA : "soa",
117 _TYPE_MB : "mb",
118 _TYPE_MG : "mg",
119 _TYPE_MR : "mr",
120 _TYPE_NULL : "null",
121 _TYPE_WKS : "wks",
122 _TYPE_PTR : "ptr",
123 _TYPE_HINFO : "hinfo",
124 _TYPE_MINFO : "minfo",
125 _TYPE_MX : "mx",
126 _TYPE_TXT : "txt",
127 _TYPE_AAAA : "quada",
128 _TYPE_SRV : "srv",
129 _TYPE_ANY : "any" }
131 # utility functions
133 def currentTimeMillis():
134 """Current system time in milliseconds"""
135 return time.time() * 1000
137 # Exceptions
139 class NonLocalNameException(Exception):
140 pass
142 class NonUniqueNameException(Exception):
143 pass
145 class NamePartTooLongException(Exception):
146 pass
148 class AbstractMethodException(Exception):
149 pass
151 class BadTypeInNameException(Exception):
152 pass
154 # implementation classes
156 class DNSEntry(object):
157 """A DNS entry"""
159 def __init__(self, name, type, clazz):
160 self.key = name.lower()
161 self.name = name
162 self.type = type
163 self.clazz = clazz & _CLASS_MASK
164 self.unique = (clazz & _CLASS_UNIQUE) != 0
166 def __eq__(self, other):
167 """Equality test on name, type, and class"""
168 return (isinstance(other, DNSEntry) and
169 self.name == other.name and
170 self.type == other.type and
171 self.clazz == other.clazz)
173 def __ne__(self, other):
174 """Non-equality test"""
175 return not self.__eq__(other)
177 def getClazz(self, clazz):
178 """Class accessor"""
179 return _CLASSES.get(clazz, "?(%s)" % clazz)
181 def getType(self, t):
182 """Type accessor"""
183 return _TYPES.get(t, "?(%s)" % t)
185 def toString(self, hdr, other):
186 """String representation with additional information"""
187 result = "%s[%s,%s" % (hdr, self.getType(self.type),
188 self.getClazz(self.clazz))
189 if self.unique:
190 result += "-unique,"
191 else:
192 result += ","
193 result += self.name
194 if other is not None:
195 result += ",%s]" % (other)
196 else:
197 result += "]"
198 return result
200 class DNSQuestion(DNSEntry):
201 """A DNS question entry"""
203 def __init__(self, name, type, clazz):
204 #if not name.endswith(".local."):
205 # raise NonLocalNameException
206 DNSEntry.__init__(self, name, type, clazz)
208 def answeredBy(self, rec):
209 """Returns true if the question is answered by the record"""
210 return (self.clazz == rec.clazz and
211 (self.type == rec.type or self.type == _TYPE_ANY) and
212 self.name == rec.name)
214 def __repr__(self):
215 """String representation"""
216 return DNSEntry.toString(self, "question", None)
219 class DNSRecord(DNSEntry):
220 """A DNS record - like a DNS entry, but has a TTL"""
222 def __init__(self, name, type, clazz, ttl):
223 DNSEntry.__init__(self, name, type, clazz)
224 self.ttl = ttl
225 self.created = currentTimeMillis()
227 def __eq__(self, other):
228 """Tests equality as per DNSRecord"""
229 return isinstance(other, DNSRecord) and DNSEntry.__eq__(self, other)
231 def suppressedBy(self, msg):
232 """Returns true if any answer in a message can suffice for the
233 information held in this record."""
234 for record in msg.answers:
235 if self.suppressedByAnswer(record):
236 return True
237 return False
239 def suppressedByAnswer(self, other):
240 """Returns true if another record has same name, type and class,
241 and if its TTL is at least half of this record's."""
242 return self == other and other.ttl > (self.ttl / 2)
244 def getExpirationTime(self, percent):
245 """Returns the time at which this record will have expired
246 by a certain percentage."""
247 return self.created + (percent * self.ttl * 10)
249 def getRemainingTTL(self, now):
250 """Returns the remaining TTL in seconds."""
251 return max(0, (self.getExpirationTime(100) - now) / 1000)
253 def isExpired(self, now):
254 """Returns true if this record has expired."""
255 return self.getExpirationTime(100) <= now
257 def isStale(self, now):
258 """Returns true if this record is at least half way expired."""
259 return self.getExpirationTime(50) <= now
261 def resetTTL(self, other):
262 """Sets this record's TTL and created time to that of
263 another record."""
264 self.created = other.created
265 self.ttl = other.ttl
267 def write(self, out):
268 """Abstract method"""
269 raise AbstractMethodException
271 def toString(self, other):
272 """String representation with addtional information"""
273 arg = "%s/%s,%s" % (self.ttl,
274 self.getRemainingTTL(currentTimeMillis()), other)
275 return DNSEntry.toString(self, "record", arg)
277 class DNSAddress(DNSRecord):
278 """A DNS address record"""
280 def __init__(self, name, type, clazz, ttl, address):
281 DNSRecord.__init__(self, name, type, clazz, ttl)
282 self.address = address
284 def write(self, out):
285 """Used in constructing an outgoing packet"""
286 out.writeString(self.address)
288 def __eq__(self, other):
289 """Tests equality on address"""
290 return isinstance(other, DNSAddress) and self.address == other.address
292 def __repr__(self):
293 """String representation"""
294 try:
295 return socket.inet_ntoa(self.address)
296 except:
297 return self.address
299 class DNSHinfo(DNSRecord):
300 """A DNS host information record"""
302 def __init__(self, name, type, clazz, ttl, cpu, os):
303 DNSRecord.__init__(self, name, type, clazz, ttl)
304 self.cpu = cpu
305 self.os = os
307 def write(self, out):
308 """Used in constructing an outgoing packet"""
309 out.writeString(self.cpu)
310 out.writeString(self.oso)
312 def __eq__(self, other):
313 """Tests equality on cpu and os"""
314 return (isinstance(other, DNSHinfo) and
315 self.cpu == other.cpu and self.os == other.os)
317 def __repr__(self):
318 """String representation"""
319 return self.cpu + " " + self.os
321 class DNSPointer(DNSRecord):
322 """A DNS pointer record"""
324 def __init__(self, name, type, clazz, ttl, alias):
325 DNSRecord.__init__(self, name, type, clazz, ttl)
326 self.alias = alias
328 def write(self, out):
329 """Used in constructing an outgoing packet"""
330 out.writeName(self.alias)
332 def __eq__(self, other):
333 """Tests equality on alias"""
334 return isinstance(other, DNSPointer) and self.alias == other.alias
336 def __repr__(self):
337 """String representation"""
338 return self.toString(self.alias)
340 class DNSText(DNSRecord):
341 """A DNS text record"""
343 def __init__(self, name, type, clazz, ttl, text):
344 DNSRecord.__init__(self, name, type, clazz, ttl)
345 self.text = text
347 def write(self, out):
348 """Used in constructing an outgoing packet"""
349 out.writeString(self.text)
351 def __eq__(self, other):
352 """Tests equality on text"""
353 return isinstance(other, DNSText) and self.text == other.text
355 def __repr__(self):
356 """String representation"""
357 if len(self.text) > 10:
358 return self.toString(self.text[:7] + "...")
359 else:
360 return self.toString(self.text)
362 class DNSService(DNSRecord):
363 """A DNS service record"""
365 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
366 DNSRecord.__init__(self, name, type, clazz, ttl)
367 self.priority = priority
368 self.weight = weight
369 self.port = port
370 self.server = server
372 def write(self, out):
373 """Used in constructing an outgoing packet"""
374 out.writeShort(self.priority)
375 out.writeShort(self.weight)
376 out.writeShort(self.port)
377 out.writeName(self.server)
379 def __eq__(self, other):
380 """Tests equality on priority, weight, port and server"""
381 return (isinstance(other, DNSService) and
382 self.priority == other.priority and
383 self.weight == other.weight and
384 self.port == other.port and
385 self.server == other.server)
387 def __repr__(self):
388 """String representation"""
389 return self.toString("%s:%s" % (self.server, self.port))
391 class DNSIncoming(object):
392 """Object representation of an incoming DNS packet"""
394 def __init__(self, data):
395 """Constructor from string holding bytes of packet"""
396 self.offset = 0
397 self.data = data
398 self.questions = []
399 self.answers = []
400 self.numQuestions = 0
401 self.numAnswers = 0
402 self.numAuthorities = 0
403 self.numAdditionals = 0
405 self.readHeader()
406 self.readQuestions()
407 self.readOthers()
409 def unpack(self, format):
410 length = struct.calcsize(format)
411 info = struct.unpack(format, self.data[self.offset:self.offset+length])
412 self.offset += length
413 return info
415 def readHeader(self):
416 """Reads header portion of packet"""
417 (self.id, self.flags, self.numQuestions, self.numAnswers,
418 self.numAuthorities, self.numAdditionals) = self.unpack('!6H')
420 def readQuestions(self):
421 """Reads questions section of packet"""
422 for i in xrange(self.numQuestions):
423 name = self.readName()
424 type, clazz = self.unpack('!HH')
426 question = DNSQuestion(name, type, clazz)
427 self.questions.append(question)
429 def readInt(self):
430 """Reads an integer from the packet"""
431 return self.unpack('!I')[0]
433 def readCharacterString(self):
434 """Reads a character string from the packet"""
435 length = ord(self.data[self.offset])
436 self.offset += 1
437 return self.readString(length)
439 def readString(self, length):
440 """Reads a string of a given length from the packet"""
441 info = self.data[self.offset:self.offset+length]
442 self.offset += length
443 return info
445 def readUnsignedShort(self):
446 """Reads an unsigned short from the packet"""
447 return self.unpack('!H')[0]
449 def readOthers(self):
450 """Reads the answers, authorities and additionals section of the
451 packet"""
452 n = self.numAnswers + self.numAuthorities + self.numAdditionals
453 for i in xrange(n):
454 domain = self.readName()
455 type, clazz, ttl, length = self.unpack('!HHiH')
457 rec = None
458 if type == _TYPE_A:
459 rec = DNSAddress(domain, type, clazz, ttl, self.readString(4))
460 elif type == _TYPE_CNAME or type == _TYPE_PTR:
461 rec = DNSPointer(domain, type, clazz, ttl, self.readName())
462 elif type == _TYPE_TXT:
463 rec = DNSText(domain, type, clazz, ttl, self.readString(length))
464 elif type == _TYPE_SRV:
465 rec = DNSService(domain, type, clazz, ttl,
466 self.readUnsignedShort(), self.readUnsignedShort(),
467 self.readUnsignedShort(), self.readName())
468 elif type == _TYPE_HINFO:
469 rec = DNSHinfo(domain, type, clazz, ttl,
470 self.readCharacterString(), self.readCharacterString())
471 elif type == _TYPE_AAAA:
472 rec = DNSAddress(domain, type, clazz, ttl, self.readString(16))
473 else:
474 # Try to ignore types we don't know about
475 # Skip the payload for the resource record so the next
476 # records can be parsed correctly
477 self.offset += length
479 if rec is not None:
480 self.answers.append(rec)
482 def isQuery(self):
483 """Returns true if this is a query"""
484 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
486 def isResponse(self):
487 """Returns true if this is a response"""
488 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
490 def readUTF(self, offset, length):
491 """Reads a UTF-8 string of a given length from the packet"""
492 return unicode(self.data[offset:offset+length], 'utf-8', 'replace')
494 def readName(self):
495 """Reads a domain name from the packet"""
496 result = ''
497 off = self.offset
498 next = -1
499 first = off
501 while True:
502 length = ord(self.data[off])
503 off += 1
504 if length == 0:
505 break
506 t = length & 0xC0
507 if t == 0x00:
508 result = ''.join((result, self.readUTF(off, length) + '.'))
509 off += length
510 elif t == 0xC0:
511 if next < 0:
512 next = off + 1
513 off = ((length & 0x3F) << 8) | ord(self.data[off])
514 if off >= first:
515 raise "Bad domain name (circular) at " + str(off)
516 first = off
517 else:
518 raise "Bad domain name at " + str(off)
520 if next >= 0:
521 self.offset = next
522 else:
523 self.offset = off
525 return result
528 class DNSOutgoing(object):
529 """Object representation of an outgoing packet"""
531 def __init__(self, flags, multicast=True):
532 self.finished = False
533 self.id = 0
534 self.multicast = multicast
535 self.flags = flags
536 self.names = {}
537 self.data = []
538 self.size = 12
540 self.questions = []
541 self.answers = []
542 self.authorities = []
543 self.additionals = []
545 def addQuestion(self, record):
546 """Adds a question"""
547 self.questions.append(record)
549 def addAnswer(self, inp, record):
550 """Adds an answer"""
551 if not record.suppressedBy(inp):
552 self.addAnswerAtTime(record, 0)
554 def addAnswerAtTime(self, record, now):
555 """Adds an answer if if does not expire by a certain time"""
556 if record is not None:
557 if now == 0 or not record.isExpired(now):
558 self.answers.append((record, now))
560 def addAuthorativeAnswer(self, record):
561 """Adds an authoritative answer"""
562 self.authorities.append(record)
564 def addAdditionalAnswer(self, record):
565 """Adds an additional answer"""
566 self.additionals.append(record)
568 def pack(self, format, value):
569 self.data.append(struct.pack(format, value))
570 self.size += struct.calcsize(format)
572 def writeByte(self, value):
573 """Writes a single byte to the packet"""
574 self.pack('!c', chr(value))
576 def insertShort(self, index, value):
577 """Inserts an unsigned short in a certain position in the packet"""
578 self.data.insert(index, struct.pack('!H', value))
579 self.size += 2
581 def writeShort(self, value):
582 """Writes an unsigned short to the packet"""
583 self.pack('!H', value)
585 def writeInt(self, value):
586 """Writes an unsigned integer to the packet"""
587 self.pack('!I', int(value))
589 def writeString(self, value):
590 """Writes a string to the packet"""
591 self.data.append(value)
592 self.size += len(value)
594 def writeUTF(self, s):
595 """Writes a UTF-8 string of a given length to the packet"""
596 utfstr = s.encode('utf-8')
597 length = len(utfstr)
598 if length > 64:
599 raise NamePartTooLongException
600 self.writeByte(length)
601 self.writeString(utfstr)
603 def writeName(self, name):
604 """Writes a domain name to the packet"""
606 if name in self.names:
607 # Find existing instance of this name in packet
609 index = self.names[name]
611 # An index was found, so write a pointer to it
613 self.writeByte((index >> 8) | 0xC0)
614 self.writeByte(index & 0xFF)
615 else:
616 # No record of this name already, so write it
617 # out as normal, recording the location of the name
618 # for future pointers to it.
620 self.names[name] = self.size
621 parts = name.split('.')
622 if parts[-1] == '':
623 parts = parts[:-1]
624 for part in parts:
625 self.writeUTF(part)
626 self.writeByte(0)
628 def writeQuestion(self, question):
629 """Writes a question to the packet"""
630 self.writeName(question.name)
631 self.writeShort(question.type)
632 self.writeShort(question.clazz)
634 def writeRecord(self, record, now):
635 """Writes a record (answer, authoritative answer, additional) to
636 the packet"""
637 self.writeName(record.name)
638 self.writeShort(record.type)
639 if record.unique and self.multicast:
640 self.writeShort(record.clazz | _CLASS_UNIQUE)
641 else:
642 self.writeShort(record.clazz)
643 if now == 0:
644 self.writeInt(record.ttl)
645 else:
646 self.writeInt(record.getRemainingTTL(now))
647 index = len(self.data)
648 # Adjust size for the short we will write before this record
650 self.size += 2
651 record.write(self)
652 self.size -= 2
654 length = len(''.join(self.data[index:]))
655 self.insertShort(index, length) # Here is the short we adjusted for
657 def packet(self):
658 """Returns a string containing the packet's bytes
660 No further parts should be added to the packet once this
661 is done."""
662 if not self.finished:
663 self.finished = True
664 for question in self.questions:
665 self.writeQuestion(question)
666 for answer, time in self.answers:
667 self.writeRecord(answer, time)
668 for authority in self.authorities:
669 self.writeRecord(authority, 0)
670 for additional in self.additionals:
671 self.writeRecord(additional, 0)
673 self.insertShort(0, len(self.additionals))
674 self.insertShort(0, len(self.authorities))
675 self.insertShort(0, len(self.answers))
676 self.insertShort(0, len(self.questions))
677 self.insertShort(0, self.flags)
678 if self.multicast:
679 self.insertShort(0, 0)
680 else:
681 self.insertShort(0, self.id)
682 return ''.join(self.data)
685 class DNSCache(object):
686 """A cache of DNS entries"""
688 def __init__(self):
689 self.cache = {}
691 def add(self, entry):
692 """Adds an entry"""
693 try:
694 list = self.cache[entry.key]
695 except:
696 list = self.cache[entry.key] = []
697 list.append(entry)
699 def remove(self, entry):
700 """Removes an entry"""
701 try:
702 list = self.cache[entry.key]
703 list.remove(entry)
704 except:
705 pass
707 def get(self, entry):
708 """Gets an entry by key. Will return None if there is no
709 matching entry."""
710 try:
711 list = self.cache[entry.key]
712 return list[list.index(entry)]
713 except:
714 return None
716 def getByDetails(self, name, type, clazz):
717 """Gets an entry by details. Will return None if there is
718 no matching entry."""
719 entry = DNSEntry(name, type, clazz)
720 return self.get(entry)
722 def entriesWithName(self, name):
723 """Returns a list of entries whose key matches the name."""
724 try:
725 return self.cache[name]
726 except:
727 return []
729 def entries(self):
730 """Returns a list of all entries"""
731 def add(x, y): return x + y
732 try:
733 return reduce(add, self.cache.values())
734 except:
735 return []
738 class Engine(threading.Thread):
739 """An engine wraps read access to sockets, allowing objects that
740 need to receive data from sockets to be called back when the
741 sockets are ready.
743 A reader needs a handle_read() method, which is called when the socket
744 it is interested in is ready for reading.
746 Writers are not implemented here, because we only send short
747 packets.
750 def __init__(self, zc):
751 threading.Thread.__init__(self)
752 self.zc = zc
753 self.readers = {} # maps socket to reader
754 self.timeout = 5
755 self.condition = threading.Condition()
756 self.start()
758 def run(self):
759 while not _GLOBAL_DONE:
760 rs = self.getReaders()
761 if len(rs) == 0:
762 # No sockets to manage, but we wait for the timeout
763 # or addition of a socket
765 self.condition.acquire()
766 self.condition.wait(self.timeout)
767 self.condition.release()
768 else:
769 try:
770 rr, wr, er = select.select(rs, [], [], self.timeout)
771 for socket in rr:
772 try:
773 self.readers[socket].handle_read()
774 except:
775 traceback.print_exc()
776 except:
777 pass
779 def getReaders(self):
780 result = []
781 self.condition.acquire()
782 result = self.readers.keys()
783 self.condition.release()
784 return result
786 def addReader(self, reader, socket):
787 self.condition.acquire()
788 self.readers[socket] = reader
789 self.condition.notify()
790 self.condition.release()
792 def delReader(self, socket):
793 self.condition.acquire()
794 del(self.readers[socket])
795 self.condition.notify()
796 self.condition.release()
798 def notify(self):
799 self.condition.acquire()
800 self.condition.notify()
801 self.condition.release()
803 class Listener(object):
804 """A Listener is used by this module to listen on the multicast
805 group to which DNS messages are sent, allowing the implementation
806 to cache information as it arrives.
808 It requires registration with an Engine object in order to have
809 the read() method called when a socket is availble for reading."""
811 def __init__(self, zc):
812 self.zc = zc
813 self.zc.engine.addReader(self, self.zc.socket)
815 def handle_read(self):
816 try:
817 data, (addr, port) = self.zc.socket.recvfrom(_MAX_MSG_ABSOLUTE)
818 except socket.error, e:
819 # If the socket was closed by another thread -- which happens
820 # regularly on shutdown -- an EBADF exception is thrown here.
821 # Ignore it.
822 if e[0] == socket.EBADF:
823 return
824 else:
825 raise e
826 self.data = data
827 msg = DNSIncoming(data)
828 if msg.isQuery():
829 # Always multicast responses
831 if port == _MDNS_PORT:
832 self.zc.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
833 # If it's not a multicast query, reply via unicast
834 # and multicast
836 elif port == _DNS_PORT:
837 self.zc.handleQuery(msg, addr, port)
838 self.zc.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
839 else:
840 self.zc.handleResponse(msg)
843 class Reaper(threading.Thread):
844 """A Reaper is used by this module to remove cache entries that
845 have expired."""
847 def __init__(self, zc):
848 threading.Thread.__init__(self)
849 self.zc = zc
850 self.start()
852 def run(self):
853 while True:
854 self.zc.wait(10 * 1000)
855 if _GLOBAL_DONE:
856 return
857 now = currentTimeMillis()
858 for record in self.zc.cache.entries():
859 if record.isExpired(now):
860 self.zc.updateRecord(now, record)
861 self.zc.cache.remove(record)
864 class ServiceBrowser(threading.Thread):
865 """Used to browse for a service of a specific type.
867 The listener object will have its addService() and
868 removeService() methods called when this browser
869 discovers changes in the services availability."""
871 def __init__(self, zc, type, listener):
872 """Creates a browser for a specific type"""
873 threading.Thread.__init__(self)
874 self.zc = zc
875 self.type = type
876 self.listener = listener
877 self.services = {}
878 self.nextTime = currentTimeMillis()
879 self.delay = _BROWSER_TIME
880 self.list = []
882 self.done = False
884 self.zc.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
885 self.start()
887 def updateRecord(self, zc, now, record):
888 """Callback invoked by Zeroconf when new information arrives.
890 Updates information required by browser in the Zeroconf cache."""
891 if record.type == _TYPE_PTR and record.name == self.type:
892 expired = record.isExpired(now)
893 try:
894 oldrecord = self.services[record.alias.lower()]
895 if not expired:
896 oldrecord.resetTTL(record)
897 else:
898 del(self.services[record.alias.lower()])
899 callback = lambda x: self.listener.removeService(x,
900 self.type, record.alias)
901 self.list.append(callback)
902 return
903 except:
904 if not expired:
905 self.services[record.alias.lower()] = record
906 callback = lambda x: self.listener.addService(x,
907 self.type, record.alias)
908 self.list.append(callback)
910 expires = record.getExpirationTime(75)
911 if expires < self.nextTime:
912 self.nextTime = expires
914 def cancel(self):
915 self.done = True
916 self.zc.notifyAll()
918 def run(self):
919 while True:
920 event = None
921 now = currentTimeMillis()
922 if len(self.list) == 0 and self.nextTime > now:
923 self.zc.wait(self.nextTime - now)
924 if _GLOBAL_DONE or self.done:
925 return
926 now = currentTimeMillis()
928 if self.nextTime <= now:
929 out = DNSOutgoing(_FLAGS_QR_QUERY)
930 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
931 for record in self.services.values():
932 if not record.isExpired(now):
933 out.addAnswerAtTime(record, now)
934 self.zc.send(out)
935 self.nextTime = now + self.delay
936 self.delay = min(20 * 1000, self.delay * 2)
938 if len(self.list) > 0:
939 event = self.list.pop(0)
941 if event is not None:
942 event(self.zc)
945 class ServiceInfo(object):
946 """Service information"""
948 def __init__(self, type, name, address=None, port=None, weight=0,
949 priority=0, properties=None, server=None):
950 """Create a service description.
952 type: fully qualified service type name
953 name: fully qualified service name
954 address: IP address as unsigned short, network byte order
955 port: port that the service runs on
956 weight: weight of the service
957 priority: priority of the service
958 properties: dictionary of properties (or a string holding the
959 bytes for the text field)
960 server: fully qualified name for service host (defaults to name)"""
962 if not name.endswith(type):
963 raise BadTypeInNameException
964 self.type = type
965 self.name = name
966 self.address = address
967 self.port = port
968 self.weight = weight
969 self.priority = priority
970 if server:
971 self.server = server
972 else:
973 self.server = name
974 self.setProperties(properties)
976 def setProperties(self, properties):
977 """Sets properties and text of this info from a dictionary"""
978 if isinstance(properties, dict):
979 self.properties = properties
980 list = []
981 result = ''
982 for key in properties:
983 value = properties[key]
984 if value is None:
985 suffix = ''.encode('utf-8')
986 elif isinstance(value, str):
987 suffix = value.encode('utf-8')
988 elif isinstance(value, int):
989 if value:
990 suffix = 'true'
991 else:
992 suffix = 'false'
993 else:
994 suffix = ''.encode('utf-8')
995 list.append('='.join((key, suffix)))
996 for item in list:
997 result = ''.join((result, chr(len(item)), item))
998 self.text = result
999 else:
1000 self.text = properties
1002 def setText(self, text):
1003 """Sets properties and text given a text field"""
1004 self.text = text
1005 try:
1006 result = {}
1007 end = len(text)
1008 index = 0
1009 strs = []
1010 while index < end:
1011 length = ord(text[index])
1012 index += 1
1013 strs.append(text[index:index+length])
1014 index += length
1016 for s in strs:
1017 try:
1018 key, value = s.split('=', 1)
1019 if value == 'true':
1020 value = True
1021 elif value == 'false' or not value:
1022 value = False
1023 except:
1024 # No equals sign at all
1025 key = s
1026 value = False
1028 # Only update non-existent properties
1029 if key and result.get(key) == None:
1030 result[key] = value
1032 self.properties = result
1033 except:
1034 traceback.print_exc()
1035 self.properties = None
1037 def getType(self):
1038 """Type accessor"""
1039 return self.type
1041 def getName(self):
1042 """Name accessor"""
1043 if self.type is not None and self.name.endswith("." + self.type):
1044 return self.name[:len(self.name) - len(self.type) - 1]
1045 return self.name
1047 def getAddress(self):
1048 """Address accessor"""
1049 return self.address
1051 def getPort(self):
1052 """Port accessor"""
1053 return self.port
1055 def getPriority(self):
1056 """Pirority accessor"""
1057 return self.priority
1059 def getWeight(self):
1060 """Weight accessor"""
1061 return self.weight
1063 def getProperties(self):
1064 """Properties accessor"""
1065 return self.properties
1067 def getText(self):
1068 """Text accessor"""
1069 return self.text
1071 def getServer(self):
1072 """Server accessor"""
1073 return self.server
1075 def updateRecord(self, zc, now, record):
1076 """Updates service information from a DNS record"""
1077 if record is not None and not record.isExpired(now):
1078 if record.type == _TYPE_A:
1079 #if record.name == self.name:
1080 if record.name == self.server:
1081 self.address = record.address
1082 elif record.type == _TYPE_SRV:
1083 if record.name == self.name:
1084 self.server = record.server
1085 self.port = record.port
1086 self.weight = record.weight
1087 self.priority = record.priority
1088 #self.address = None
1089 self.updateRecord(zc, now,
1090 zc.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1091 elif record.type == _TYPE_TXT:
1092 if record.name == self.name:
1093 self.setText(record.text)
1095 def request(self, zc, timeout):
1096 """Returns true if the service could be discovered on the
1097 network, and updates this object with details discovered.
1099 now = currentTimeMillis()
1100 delay = _LISTENER_TIME
1101 next = now + delay
1102 last = now + timeout
1103 result = False
1104 try:
1105 zc.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1106 while (self.server is None or self.address is None or
1107 self.text is None):
1108 if last <= now:
1109 return False
1110 if next <= now:
1111 out = DNSOutgoing(_FLAGS_QR_QUERY)
1112 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV,
1113 _CLASS_IN))
1114 out.addAnswerAtTime(zc.cache.getByDetails(self.name,
1115 _TYPE_SRV, _CLASS_IN), now)
1116 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT,
1117 _CLASS_IN))
1118 out.addAnswerAtTime(zc.cache.getByDetails(self.name,
1119 _TYPE_TXT, _CLASS_IN), now)
1120 if self.server is not None:
1121 out.addQuestion(DNSQuestion(self.server,
1122 _TYPE_A, _CLASS_IN))
1123 out.addAnswerAtTime(zc.cache.getByDetails(self.server,
1124 _TYPE_A, _CLASS_IN), now)
1125 zc.send(out)
1126 next = now + delay
1127 delay = delay * 2
1129 zc.wait(min(next, last) - now)
1130 now = currentTimeMillis()
1131 result = True
1132 finally:
1133 zc.removeListener(self)
1135 return result
1137 def __eq__(self, other):
1138 """Tests equality of service name"""
1139 if isinstance(other, ServiceInfo):
1140 return other.name == self.name
1141 return False
1143 def __ne__(self, other):
1144 """Non-equality test"""
1145 return not self.__eq__(other)
1147 def __repr__(self):
1148 """String representation"""
1149 result = "service[%s,%s:%s," % (self.name,
1150 socket.inet_ntoa(self.getAddress()), self.port)
1151 if self.text is None:
1152 result += "None"
1153 else:
1154 if len(self.text) < 20:
1155 result += self.text
1156 else:
1157 result += self.text[:17] + "..."
1158 result += "]"
1159 return result
1162 class Zeroconf(object):
1163 """Implementation of Zeroconf Multicast DNS Service Discovery
1165 Supports registration, unregistration, queries and browsing.
1167 def __init__(self, bindaddress=None):
1168 """Creates an instance of the Zeroconf class, establishing
1169 multicast communications, listening and reaping threads."""
1170 global _GLOBAL_DONE
1171 _GLOBAL_DONE = False
1172 if bindaddress is None:
1173 try:
1174 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1175 s.connect(('4.2.2.1', 123))
1176 self.intf = s.getsockname()[0]
1177 except:
1178 self.intf = socket.gethostbyname(socket.gethostname())
1179 else:
1180 self.intf = bindaddress
1181 self.group = ('', _MDNS_PORT)
1182 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1183 try:
1184 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1185 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1186 except:
1187 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1188 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1189 # Volume 2"), but some BSD-derived systems require
1190 # SO_REUSEPORT to be specified explicity. Also, not all
1191 # versions of Python have SO_REUSEPORT available.
1193 pass
1194 self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255)
1195 self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1)
1196 try:
1197 self.socket.bind(self.group)
1198 except:
1199 # Some versions of linux raise an exception even though
1200 # the SO_REUSE* options have been set, so ignore it
1202 pass
1203 #self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
1204 # socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
1205 self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
1206 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1208 self.listeners = []
1209 self.browsers = []
1210 self.services = {}
1211 self.servicetypes = {}
1213 self.cache = DNSCache()
1215 self.condition = threading.Condition()
1217 self.engine = Engine(self)
1218 self.listener = Listener(self)
1219 self.reaper = Reaper(self)
1221 def isLoopback(self):
1222 return self.intf.startswith("127.0.0.1")
1224 def isLinklocal(self):
1225 return self.intf.startswith("169.254.")
1227 def wait(self, timeout):
1228 """Calling thread waits for a given number of milliseconds or
1229 until notified."""
1230 self.condition.acquire()
1231 self.condition.wait(timeout / 1000)
1232 self.condition.release()
1234 def notifyAll(self):
1235 """Notifies all waiting threads"""
1236 self.condition.acquire()
1237 self.condition.notifyAll()
1238 self.condition.release()
1240 def getServiceInfo(self, type, name, timeout=3000):
1241 """Returns network's service information for a particular
1242 name and type, or None if no service matches by the timeout,
1243 which defaults to 3 seconds."""
1244 info = ServiceInfo(type, name)
1245 if info.request(self, timeout):
1246 return info
1247 return None
1249 def addServiceListener(self, type, listener):
1250 """Adds a listener for a particular service type. This object
1251 will then have its updateRecord method called when information
1252 arrives for that type."""
1253 self.removeServiceListener(listener)
1254 self.browsers.append(ServiceBrowser(self, type, listener))
1256 def removeServiceListener(self, listener):
1257 """Removes a listener from the set that is currently listening."""
1258 for browser in self.browsers:
1259 if browser.listener == listener:
1260 browser.cancel()
1261 del(browser)
1263 def registerService(self, info, ttl=_DNS_TTL):
1264 """Registers service information to the network with a default TTL
1265 of 60 seconds. Zeroconf will then respond to requests for
1266 information for that service. The name of the service may be
1267 changed if needed to make it unique on the network."""
1268 self.checkService(info)
1269 self.services[info.name.lower()] = info
1270 if info.type in self.servicetypes:
1271 self.servicetypes[info.type] += 1
1272 else:
1273 self.servicetypes[info.type] = 1
1274 now = currentTimeMillis()
1275 nextTime = now
1276 i = 0
1277 while i < 3:
1278 if now < nextTime:
1279 self.wait(nextTime - now)
1280 now = currentTimeMillis()
1281 continue
1282 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1283 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1284 _CLASS_IN, ttl, info.name), 0)
1285 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1286 _CLASS_IN, ttl, info.priority, info.weight, info.port,
1287 info.server), 0)
1288 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN,
1289 ttl, info.text), 0)
1290 if info.address:
1291 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A,
1292 _CLASS_IN, ttl, info.address), 0)
1293 self.send(out)
1294 i += 1
1295 nextTime += _REGISTER_TIME
1297 def unregisterService(self, info):
1298 """Unregister a service."""
1299 try:
1300 del(self.services[info.name.lower()])
1301 if self.servicetypes[info.type] > 1:
1302 self.servicetypes[info.type] -= 1
1303 else:
1304 del self.servicetypes[info.type]
1305 except:
1306 pass
1307 now = currentTimeMillis()
1308 nextTime = now
1309 i = 0
1310 while i < 3:
1311 if now < nextTime:
1312 self.wait(nextTime - now)
1313 now = currentTimeMillis()
1314 continue
1315 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1316 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1317 _CLASS_IN, 0, info.name), 0)
1318 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1319 _CLASS_IN, 0, info.priority, info.weight, info.port,
1320 info.name), 0)
1321 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN,
1322 0, info.text), 0)
1323 if info.address:
1324 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A,
1325 _CLASS_IN, 0, info.address), 0)
1326 self.send(out)
1327 i += 1
1328 nextTime += _UNREGISTER_TIME
1330 def unregisterAllServices(self):
1331 """Unregister all registered services."""
1332 if len(self.services) > 0:
1333 now = currentTimeMillis()
1334 nextTime = now
1335 i = 0
1336 while i < 3:
1337 if now < nextTime:
1338 self.wait(nextTime - now)
1339 now = currentTimeMillis()
1340 continue
1341 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1342 for info in self.services.values():
1343 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR,
1344 _CLASS_IN, 0, info.name), 0)
1345 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV,
1346 _CLASS_IN, 0, info.priority, info.weight,
1347 info.port, info.server), 0)
1348 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT,
1349 _CLASS_IN, 0, info.text), 0)
1350 if info.address:
1351 out.addAnswerAtTime(DNSAddress(info.server,
1352 _TYPE_A, _CLASS_IN, 0, info.address), 0)
1353 self.send(out)
1354 i += 1
1355 nextTime += _UNREGISTER_TIME
1357 def checkService(self, info):
1358 """Checks the network for a unique service name, modifying the
1359 ServiceInfo passed in if it is not unique."""
1360 now = currentTimeMillis()
1361 nextTime = now
1362 i = 0
1363 while i < 3:
1364 for record in self.cache.entriesWithName(info.type):
1365 if (record.type == _TYPE_PTR and
1366 not record.isExpired(now) and
1367 record.alias == info.name):
1368 if info.name.find('.') < 0:
1369 info.name = '%s.[%s:%s].%s' % (info.name,
1370 info.address, info.port, info.type)
1372 self.checkService(info)
1373 return
1374 raise NonUniqueNameException
1375 if now < nextTime:
1376 self.wait(nextTime - now)
1377 now = currentTimeMillis()
1378 continue
1379 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1380 self.debug = out
1381 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1382 out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR,
1383 _CLASS_IN, _DNS_TTL, info.name))
1384 self.send(out)
1385 i += 1
1386 nextTime += _CHECK_TIME
1388 def addListener(self, listener, question):
1389 """Adds a listener for a given question. The listener will have
1390 its updateRecord method called when information is available to
1391 answer the question."""
1392 now = currentTimeMillis()
1393 self.listeners.append(listener)
1394 if question is not None:
1395 for record in self.cache.entriesWithName(question.name):
1396 if question.answeredBy(record) and not record.isExpired(now):
1397 listener.updateRecord(self, now, record)
1398 self.notifyAll()
1400 def removeListener(self, listener):
1401 """Removes a listener."""
1402 try:
1403 self.listeners.remove(listener)
1404 self.notifyAll()
1405 except:
1406 pass
1408 def updateRecord(self, now, rec):
1409 """Used to notify listeners of new information that has updated
1410 a record."""
1411 for listener in self.listeners:
1412 listener.updateRecord(self, now, rec)
1413 self.notifyAll()
1415 def handleResponse(self, msg):
1416 """Deal with incoming response packets. All answers
1417 are held in the cache, and listeners are notified."""
1418 now = currentTimeMillis()
1419 for record in msg.answers:
1420 expired = record.isExpired(now)
1421 if record in self.cache.entries():
1422 if expired:
1423 self.cache.remove(record)
1424 else:
1425 entry = self.cache.get(record)
1426 if entry is not None:
1427 entry.resetTTL(record)
1428 record = entry
1429 else:
1430 self.cache.add(record)
1432 self.updateRecord(now, record)
1434 def handleQuery(self, msg, addr, port):
1435 """Deal with incoming query packets. Provides a response if
1436 possible."""
1437 out = None
1439 # Support unicast client responses
1441 if port != _MDNS_PORT:
1442 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, False)
1443 for question in msg.questions:
1444 out.addQuestion(question)
1446 for question in msg.questions:
1447 if question.type == _TYPE_PTR:
1448 if question.name == "_services._dns-sd._udp.local.":
1449 for stype in self.servicetypes.keys():
1450 if out is None:
1451 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1452 out.addAnswer(msg,
1453 DNSPointer("_services._dns-sd._udp.local.",
1454 _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1455 for service in self.services.values():
1456 if question.name == service.type:
1457 if out is None:
1458 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1459 out.addAnswer(msg,
1460 DNSPointer(service.type, _TYPE_PTR,
1461 _CLASS_IN, _DNS_TTL, service.name))
1462 else:
1463 try:
1464 if out is None:
1465 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1467 # Answer A record queries for any service addresses we know
1468 if question.type in (_TYPE_A, _TYPE_ANY):
1469 for service in self.services.values():
1470 if service.server == question.name.lower():
1471 out.addAnswer(msg, DNSAddress(question.name,
1472 _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
1473 _DNS_TTL, service.address))
1475 service = self.services.get(question.name.lower(), None)
1476 if not service: continue
1478 if question.type in (_TYPE_SRV, _TYPE_ANY):
1479 out.addAnswer(msg, DNSService(question.name,
1480 _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE,
1481 _DNS_TTL, service.priority, service.weight,
1482 service.port, service.server))
1483 if question.type in (_TYPE_TXT, _TYPE_ANY):
1484 out.addAnswer(msg, DNSText(question.name,
1485 _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE,
1486 _DNS_TTL, service.text))
1487 if question.type == _TYPE_SRV:
1488 out.addAdditionalAnswer(DNSAddress(service.server,
1489 _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
1490 _DNS_TTL, service.address))
1491 except:
1492 traceback.print_exc()
1494 if out is not None and out.answers:
1495 out.id = msg.id
1496 self.send(out, addr, port)
1498 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1499 """Sends an outgoing packet."""
1500 packet = out.packet()
1501 try:
1502 while packet:
1503 bytes_sent = self.socket.sendto(packet, 0, (addr, port))
1504 if bytes_sent < 0:
1505 break
1506 packet = packet[bytes_sent:]
1507 except:
1508 # Ignore this, it may be a temporary loss of network connection
1509 pass
1511 def close(self):
1512 """Ends the background threads, and prevent this instance from
1513 servicing further queries."""
1514 global _GLOBAL_DONE
1515 if not _GLOBAL_DONE:
1516 _GLOBAL_DONE = True
1517 self.notifyAll()
1518 self.engine.notify()
1519 self.unregisterAllServices()
1520 self.socket.setsockopt(socket.IPPROTO_IP,
1521 socket.IP_DROP_MEMBERSHIP,
1522 socket.inet_aton(_MDNS_ADDR) +
1523 socket.inet_aton('0.0.0.0'))
1524 self.socket.close()
1526 # Test a few module features, including service registration, service
1527 # query (for Zoe), and service unregistration.
1529 if __name__ == '__main__':
1530 print "Multicast DNS Service Discovery for Python, version", __version__
1531 r = Zeroconf()
1532 print "1. Testing registration of a service..."
1533 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1534 info = ServiceInfo("_http._tcp.local.",
1535 "My Service Name._http._tcp.local.",
1536 socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1537 print " Registering service..."
1538 r.registerService(info)
1539 print " Registration done."
1540 print "2. Testing query of service information..."
1541 print " Getting ZOE service:",
1542 print str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1543 print " Query done."
1544 print "3. Testing query of own service..."
1545 print " Getting self:",
1546 print str(r.getServiceInfo("_http._tcp.local.",
1547 "My Service Name._http._tcp.local."))
1548 print " Query done."
1549 print "4. Testing unregister of service information..."
1550 r.unregisterService(info)
1551 print " Unregister done."
1552 r.close()