Using Growl, Rendezvous and Python for LAN notifications

Update: This is completely deprecated, thanks to Growl's new built-in network notifications. Here's a better way to do most of this, including locating machines to notify.

I've been wanting to do something like this for some time now, but it's taken a while to find some free time, inspiration and a few of the puzzle pieces (and it's still a bit of a hack, since I'm not being a good Rendezvous neighbour, as you'll see later).

The concept is simple: I wanted my UNIX boxes to broadcast simple notifications to my LAN. Typically stuff like:

  • fetchmail has finished
  • arpwatch spotted a new MAC address
  • Something interesting has been spotted in a log file

...etc. You get my drift. I had previously looked at LanOSD, but it's neither open nor simple enough for what I had in mind. Plus, I wanted this to be something machines could react upon too, and it had to be something unprivileged processes had easy access to with a minimal API.

Simple Plumbing

Integrating processes across security contextes has an age-old solution in the UNIX world: piping. You create a pipe someplace in your filesystem, and processes can read and write to it to exchange data. In my case, all a batch script needs to do is this:

# We have a new LAN station:
echo "New station:|$IPADDRESS ($MACADDRESS)" >> /tmp/alert

Anything can do this, or be modified to do this. No network programming is required, and it requires no special privilege levels (other than the ones you set on the pipe, of course).

Abusing Rendezvous

Now, my original idea was to use Rendezvous itself as the messaging protocol. I'll eventually move on to use SIP (since that's something I'd like to do a baseline implementation of), but for now, to send an alert to the LAN I simply create a new _alerter._udp.local. Rendezvous record with a short TTL (15 seconds):

from Rendezvous import *
import socket, time, sys, os


def loop():
  r = Rendezvous()
  while 1:
    f = open(PIPE, 'r')
    line = f.readline()
    f.close() # we only want the one line - avoid buffering issues
    # pack line into mDNS record
    desc = { 'source':'pipe', 'text':line }
    # use MDNS as notifier with short TTL. Note that the socket calls
    # might send the wrong IP address on a multihomed machine
    info = ServiceInfo( "_alerter._udp.local.", "Alert" +
                  str(int(time.time())) + "._alerter._udp.local.",
                  0, 0, 0, desc )
    # we'll need to unregister this later
    r.registerService(info, 15)

if __name__ == '__main__':
  if not os.access(PIPE, os.F_OK):

To do this, I'm using pyzeroconf. It's working so far, except that records seem to outlast their TTL (I've yet to investigate the inner workings of to the point where I fully understand its semantics, so I think I might have to keep track of alerts and unregister them manually later).

The upshot is that, after a while, I get a couple of dozen still "live" mDNS records hanging around (I really should write a couple of scripts to do decent ASCII dumps of mDNS records, since there seem to be so few Rendezvous debugging tools around).

Still, this is a proof of concept, not a finished package - and the sample code is easier to understand this way.

Glueing it to Growl

Picking up on the basic Python bridge I had done earlier, I just create a Rendezvous listener of the appropriate type and invoke Growl when needed:

import os
import socket

APPLESCRIPT = "/usr/bin/osascript"

from Rendezvous import *

class AlertListener(object):
  def __init__(self):

  def addService(self, rendezvous, type, name):
    print "Service", name, "added"
    info = rendezvous.getServiceInfo(type,name)
    properties = info.getProperties()
    text = properties['text']
      (title,text) = text.split( "|", 1 )
      title = ""
    # try to resolve the server address
    addr = socket.inet_ntoa(info.getAddress())
      name = socket.gethostbyaddr(addr)
      name = addr
    notify(title, text + "\n(from %s)" % (name))

def notify(title, description, icon = "Finder"):
  if os.path.exists(APPLESCRIPT): # assume we're on a Mac
    # See if Growl is installed
    if os.path.exists("/Library/Frameworks/GrowlAppBridge.framework"):
      applescript = os.popen(APPLESCRIPT, 'w')
      applescript.write( 'tell application "GrowlHelperApp"\nnotify with ' +
        'title "%s" description "%s" icon of application "%s"\n' %
        (title, description, icon) +
        'end tell')
      pass # use something else
    # use the age old UNIX way
    print "NOTIFICATION - %s: %s" % (title, description)

if __name__ == '__main__':
  notify( "Ready", "Alert monitor running" )
  r = Rendezvous()
  type = "_alerter._udp.local."
  listener = AlertListener()
  browser = ServiceBrowser(r, type, listener )

Stuff to Improve

The server side of things will eventually become a bit more complex and incorporate my own "reaper" to unregister services after a while (again, this is mostly a proof of concept). I'll also need to create a system startup script for it, tie it in with all sorts of system events, and (this is the juicy bit) create listeners to act upon alerts (like nmaping new machines that pop up on my network, rsyncing files when batches finish, etc.).

Setting the correct permissions on the pipe is also something I'll have to look into, as is checking pipe input correctly, making sure buffering works properly, etc. - right now I blindly trust whatever comes in, regardless of size and format, and have the client figure out what it is (if data reaches the client, then it hasn't crashed the Rendezvous layer - and that is likely to have size limitations, too). Also, pipes work a little differently on Linux - I keep getting null data out of readline, but it might be just my lack of Python experience.

And, of course, I'll have to start using Rendezvous properly (i.e., merely to announce that a server exists) and start using SIP in a hub-and-spoke configuration, with clients waking up, looking for server(s) using Rendezvous, registering with them using SIP and getting notified in the same way.

I actually have a few unconventional ideas I'd like to try out, such as making it a fully P2P, self-organizing setup, but, as always, it will take some time for all the pieces to fall in the right place.

(As usual, the working bits will find their way into my CVS repository someplace, and I'll post regarding any updates.)

After all, I've got a lot of relaxing to catch up on. In the meanwhile, maybe the code snippets above are of use to someone.