Python ZeroConf guide, by Jean-Philippe Ouellet, for VT ECE-4564
We want to install [pybonjour][pybonjour]. [pybonjour]: https://code.google.com/p/pybonjour/
Unfortunately it isn't easily installable with pip
or the like, so we must build it ourselves.
First things first, to sidestep a whole host of other potential issues, we update:
$ sudo apt-get update
$ sudo apt-get upgrade
Now, we want to install pybonjour. In order to do so, we must first install a bunch of python headers and a build toolchain:
$ sudo apt-get install python-dev build-essential
In addition, pybonjour has its own dependencies, so we need to install those too. This is kinda tricky because it seems avahi has circular dependencies or something. Try to install it:
$ sudo apt-get install libavahi-compat-libdnssd1
And you'll get a bunch of output ending with errors like this:
update-rc.d: warning: start and stop actions are no longer supported; falling back to defaults
Job for avahi-daemon.service failed because the control process exited with error code. See "systemctl status avahi-daemon.service" and "journalctl -xe" for details.
invoke-rc.d: initscript avahi-daemon, action "start" failed.
dpkg: error processing package avahi-daemon (--configure):
subprocess installed post-installation script returned error exit status 1
dpkg: dependency problems prevent configuration of libnss-mdns:amd64:
libnss-mdns:amd64 depends on avahi-daemon (>= 0.6.16-1); however:
Package avahi-daemon is not configured yet.
dpkg: error processing package libnss-mdns:amd64 (--configure):
dependency problems - leaving unconfigured
The following seems to be the most reliable way of getting it installed, but I do not understand the internals of apt & dpkg enough to explain why.
$ sudo -s
# apt-get install avahi-daemon avahi-utils
... bunch of errors ...
# systemctl disable avahi-daemon
# systemctl stop avahi-daemon
# apt-get autoremove
# apt-get install -f avahi-daemon avahi-utils
It will still report errors, but it will work.
Now we're ready to install pybonjour:
$ wget 'https://pybonjour.googlecode.com/files/pybonjour-1.1.1.tar.gz'
and verify its checksum:
$ echo 92cabd14e04c5f62ce067c47c2057ee3d424d29b expected; \
> openssl dgst -r -sha1 pybonjour-1.1.1.tar.gz
92cabd14e04c5f62ce067c47c2057ee3d424d29b expected
92cabd14e04c5f62ce067c47c2057ee3d424d29b *pybonjour-1.1.1.tar.gz
and make sure they match.
Then extract it:
$ tar -xzvf pybonjour-1.1.1.tar.gz
pybonjour-1.1.1/
pybonjour-1.1.1/examples/
pybonjour-1.1.1/examples/browse_and_resolve.py
pybonjour-1.1.1/examples/browse_resolve_query.py
pybonjour-1.1.1/examples/register.py
pybonjour-1.1.1/NEWS
pybonjour-1.1.1/PKG-INFO
pybonjour-1.1.1/pybonjour.py
pybonjour-1.1.1/README
pybonjour-1.1.1/setup.py
pybonjour-1.1.1/test_pybonjour.py
And build it:
$ cd pybonjour-1.1.1
$ sudo python ./setup.py install
running install
running build
running build_py
creating build
creating build/lib.linux-x86_64-2.7
copying pybonjour.py -> build/lib.linux-x86_64-2.7
running install_lib
copying build/lib.linux-x86_64-2.7/pybonjour.py -> /usr/local/lib/python2.7/dist-packages
byte-compiling /usr/local/lib/python2.7/dist-packages/pybonjour.py to pybonjour.pyc
running install_egg_info
Writing /usr/local/lib/python2.7/dist-packages/pybonjour-1.1.1.egg-info
If the above produced errors instead, go back and go through the dependency installation process again.
pybonjour doesn't actually advertise the service itself, but rather interfaces with a separate daemon that implements all the relevant protocols and tells that to advertise our service on our behalf. Avahi is one such implementation, and the one we're using here.
To enable & start it:
$ sudo systemctl enable avahi-daemon
$ sudo systemctl start avahi-daemon
and then it should auto-start the next time your Pi powers on.
If you are doing this in an LXC container (as I am), then avahi-daemon will fail to start, complaining about:
Found user 'avahi' (UID 107) and group 'avahi' (GID 112).
Successfully dropped root privileges.
chroot.c: fork() failed: Resource temporarily unavailable
failed to start chroot() helper daemon.
This error message is misleading.
To get it to start, you must change the UID and GID of the avahi user & group in the container to not collide with anything in the host or another container. (I changed it to 9912 in the container.) See lxc/lxc#25 for more details.
After changing the UID & GID you should restart the container, because while starting avahi-daemon
as root on the command line still works fine, starting it with service
or systemctl
fails, because it appears the UID is cached somewhere or something.
Now we're ready to actually use pybonjour to do ZeroConf autodiscovery.
To demonstrate a minimal proof-of-concept, lets set up a trivial service that just advertises itself (but doesn't actually implement anything).
#!/usr/bin/env python
import select
import sys
import pybonjour
def advertise_service(name, regtype, port):
def register_callback(sdRef, flags, errorCode, name, regtype, domain):
if errorCode == pybonjour.kDNSServiceErr_NoError:
print 'Registered service:'
print ' name =', name
print ' regtype =', regtype
print ' domain =', domain
sdRef = pybonjour.DNSServiceRegister(
name = name, regtype = regtype, port = port,
callBack = register_callback)
print 'Registering zeroconf service...'
while True:
ready = select.select([sdRef], [], [])
if sdRef in ready[0]:
pybonjour.DNSServiceProcessResult(sdRef)
advertise_service('my_service', '_http._tcp', 8000)
Run it, and you should see:
$ ./service.py
Registering zeroconf service...
Registered service:
name = my_service
regtype = _http._tcp.
domain = local.
If you do not get output and it just seems to hang, it's probably because avahi-daemon is not running, so go back to the step above and ensure it's starting properly. You can check it with service avahi-daemon status
.
If all went well, you should be able to see your service advertised on the network. You can check what services are being announced like this (from any machine on the same network with the avahi-utils
package installed):
$ avahi-browse --all
+ wlan0 IPv4 Deskjet 3050A J611 series [425538] Internet Printer local
+ wlan0 IPv4 ChromeBucket++ _googlecast._tcp local
... probably lots of other random services that happen to be on your network ...
+ lxcbr0 IPv4 my_service Web Site local
...
The one we care about is the my_service
that we're advertising from our Pi.
The above is all well and good, but we probably want to be able to do other things in our program too.
A simple way to do so is to just stick all the zeroconf stuff in a separate thread and forget about it. That's simple enough to do, here's an example advertising an AMQP service to get you started:
#!/usr/bin/env python
import time
import signal
import threading
import select
import sys
import pybonjour
def do_zeroconf():
def register_callback(sdRef, flags, errorCode, name, regtype, domain):
if errorCode == pybonjour.kDNSServiceErr_NoError:
print 'Registered service:'
print ' name =', name
print ' regtype =', regtype
print ' domain =', domain
sdRef = pybonjour.DNSServiceRegister(
name = 'foo-bar',
regtype = '_amqp._tcp',
port = 5672,
callBack = register_callback)
print 'Registering zeroconf service...'
while True:
ready = select.select([sdRef], [], [])
if sdRef in ready[0]:
pybonjour.DNSServiceProcessResult(sdRef)
def bg(cb):
t = threading.Thread(target=cb)
t.daemon = True
t.start()
bg(do_zeroconf)
signal.pause()
First, ensure that your service is actually being advertised, and that you can see it from the same machine that you wish to discover it from (see above).
Enumerating services is somewhat involved than we'd probably like it to be since the pybonjour API seems to have been designed for programs which wish to keep track of other machines as the come and go from the network, and all we want to do is a single lookup.
To that end, I've written this wrapper around pybonjour which abstracts away the details and lets us look things up the way we probably expect. If the service is not on the network right now, it will wait until we see it.
#!/usr/bin/env python
import select
import sys
import pybonjour
import Queue
def resolve(fullname, regtype):
q = Queue.Queue()
def resolve_callback(sdRef, flags, interfaceIndex, errorCode, this_fullname, hosttarget, port, txtRecord):
if errorCode == pybonjour.kDNSServiceErr_NoError and this_fullname == fullname:
q.put((hosttarget, port))
def browse_callback(sdRef, flags, interfaceIndex, errorCode, serviceName, regtype, replyDomain):
if errorCode != pybonjour.kDNSServiceErr_NoError:
return
if not (flags & pybonjour.kDNSServiceFlagsAdd):
print 'Service removed'
return
resolve_sdRef = pybonjour.DNSServiceResolve(0, interfaceIndex, serviceName, regtype, replyDomain, resolve_callback)
try:
while q.empty():
ready = select.select([resolve_sdRef], [], [])
pybonjour.DNSServiceProcessResult(resolve_sdRef)
finally:
resolve_sdRef.close()
browse_sdRef = pybonjour.DNSServiceBrowse(
regtype = regtype,
callBack = browse_callback)
try:
try:
while q.empty():
ready = select.select([browse_sdRef], [], [])
pybonjour.DNSServiceProcessResult(browse_sdRef)
except KeyboardInterrupt:
pass
finally:
browse_sdRef.close()
return q.get()
print 'Looking for zeroconf-advertised HTTP service named `my_service`.'
host, port = resolve('my_service._http._tcp.local.', '_http._tcp')
print 'Found service at %s:%d!' % (host, port)
# and then you'd go on to connect to it, and speak whatever protocol you wanted
And if we run it (make sure the service is running too), we will get:
$ ./discover.py
Looking for zeroconf-advertised HTTP service named `my_service`.
Found service at jpo-pi.local.:8000!
Yay!