Skip to content

Instantly share code, notes, and snippets.

@jpouellet
Last active March 3, 2021 09:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jpouellet/c0d0698d669f1f364ab3 to your computer and use it in GitHub Desktop.
Save jpouellet/c0d0698d669f1f364ab3 to your computer and use it in GitHub Desktop.
Python ZeroConf guide for ECE-4564

Python ZeroConf guide, by Jean-Philippe Ouellet, for VT ECE-4564

Installing pybonjour

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.

Dependencies

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.

Pybonjour

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.

Start avahi-daemon

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.

A note on LXC

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.

Advertising a service

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.

A note on multithreading

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()

Discovering a service

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment