As a companion to my Growl Python client, I decided to have a go at a minimalist Rendezvous client to go with it.
The class below was built after looking at pyzeroconf (which appears to be dead and has at least one serious threading bug) and mdnsd (which has an mquery utility that generates minimal PTR record queries).
Please note that this is not a general-purpose service locator: it doesn't even parse the replies (although that can be done in the request handling class with a bit more work), it merely keeps track of who replied - which is good enough for Growl, since Macs do not usually reply to queries for services they don't host themselves.
#!/usr/bin/env python
"""Minimalist Rendezvous Client for Python"""
__version__ = "0.2"
__author__ = "Rui Carmo (http://the.taoofmac.com)"
__copyright__ = "(C) 2004 Rui Carmo. Code under BSD License."
import string, os, socket, struct
from SocketServer import *
_MDNS_ADDR = '224.0.0.251'
_MDNS_PORT = 5353
class PicoRendezvous(UDPServer):
allow_reuse_address = True
replies = []
def __init__(self):
UDPServer.__init__(self, ("localhost", _MDNS_PORT), _ReplyHandler)
# end def
def query(self, proto):
self.data = struct.pack( "!HHHHHH", 0, 0, 1, 0, 0, 0 )
# pack query
parts = proto.split('.')
if parts[-1] == '':
parts = parts[:-1]
for part in parts:
utf = part.encode('utf-8')
l = len(utf)
self.data += struct.pack("B", l)
self.data += struct.pack('!' + str(l) + 's', utf)
self.data += struct.pack("B", 0)
# query for any PTR records
self.data += struct.pack( "!BBH", 0, 12, 1 )
try:
self.socket.sendto(self.data,0,(_MDNS_ADDR,_MDNS_PORT))
except:
pass
tenths = 0
while tenths < 20:
self.handle_request()
tenths = tenths + 1
return self.replies
# end def
def server_bind(self):
self.group = ('', _MDNS_PORT)
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except:
pass
self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
try:
self.socket.bind(self.group)
except:
pass
self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(socket.gethostbyname(socket.gethostname())) + socket.inet_aton('0.0.0.0'))
self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
self.socket.settimeout(0.1)
# end def
# end class
class _ReplyHandler(DatagramRequestHandler):
def handle(self):
ip = self.client_address[0]
if ip not in self.server.replies:
self.server.replies.append(ip)
# end def
# end class
if __name__ == '__main__':
print "Starting Unit Test"
print " - please make sure Growl is listening for network notifications"
p = PicoRendezvous()
print p.query('_growl._tcp.local.')