For one of the applications I'm writing, it would be really handy to
have the app advertise itself with avahi so that clients on the
network could automatically find it.
What I've got so far is the following, which has been added to Globals.__init__:
publishInfo = config.get("publish_url") # e.g.
http://hostname.mydomain.com:5002
if publishInfo:
appname = config.get("package")
s = urlparse.urlparse(publishInfo)
hostname = s.hostname
port = str(s.port)
url = s.path
args = ["avahi-publish-service", appname, "_%s._tcp" %
appname, port]
if url:
args.append(url)
try:
self.avahiRegistration = subprocess.Popen(args, close_fds=True)
except OSError:
log.warning("Couldn't start avahi-publish-service")
This kind of works...The problem so far is that when the application
is run under paste with the --reload option, the child
avahi-publish-service processes don't get killed off when the
application is shutdown. When paster restarts the application, the
old avahi process is re-parented to the init process. The same
problem occurs when running with --daemon.
Is there a way to get notified when your application is about to be
shut down? Paste looks like it just sends SIGTERM to the process when
it detects a modified file, but maybe I'm missing something.
Maybe there's a better approach I could be taking?
Thanks!
Chris
Have you tried using the Avahi Python bindings? I'd expect that to work
far better than opening a subprocess.
Regards,
Cliff
If Cliff's suggestion doesn't work -- in general, the way to get a
callback when python quits is via the atexit module.
--
Philip Jenvey
I've tried to register a function via atexit, but it's not being
called. This is probably because, according to the atexit docs,
"Note: the functions registered via this module are not called when
the program is killed by a signal, when a Python fatal internal error
is detected, or when os._exit() is called."
I'll give Cliff's suggestion a try and see how that goes. The avahi
bindings for python are pretty barebones, I was hoping not to have to
get into that :)
Cheers,
Chris
I think I've got it working with the python bindings. I had to fork
off another process since gobject's event loop interferes with pylons'
threading. And this means that I need to check the new process'
parent process id periodically. When it gets set to 1 that means the
old parent has died, so the avahi registration process needs to die as
well. I think the same method could be used to kill off a running
avahi-publish-service process if one didn't want to use the python
bindings.
I'm putting the call to this in config/middleware.py right now, just
after the configuration is loaded. Is this the right place for this
sort of thing? e.g.
# Configure the Pylons environment
load_environment(global_conf, app_conf)
a = AvahiRegistration(config.get("package"), config.get("publish_url"))
a.run()
# The Pylons WSGI app
app = PylonsApp()
Cheers,
Chris
# lib/publish.py
"""
Publish this application via avahi.
Based on example at http://www.avahi.org/wiki/PythonPublishExample
and adapted for use with Pylons
"""
import urlparse
import os, sys
import logging
import signal
import dbus
import gobject
import avahi
from dbus.mainloop.glib import DBusGMainLoop
log = logging.getLogger(__name__)
class AvahiRegistration:
"""
A class to advertise a server on a network via avahi.
Typical usage:
# In config/middleware.py just after load_environment()...
>>> a = AvahiRegistration(config['package'], config['publish_url'])
>>> a.run()
``name``: the name of this server. This will also be used to
create the DNS-SD service type which will be
published as "_<name>._tcp"
``publish_url``: the url that external clients can use to access
this application.
"""
def __init__(self, name, publish_url):
s = urlparse.urlparse(publish_url)
self.hostname = s.hostname
self.port = s.port
self.url = s.path
self.serviceName = name
self.serviceType = "_%s._tcp" % self.serviceName
self.bus = None
self.group = None
self.server = None
self.rename_count = 12
def _add_service(self):
if self.group is None:
self.group = dbus.Interface(
self.bus.get_object( avahi.DBUS_NAME,
self.server.EntryGroupNew()),
avahi.DBUS_INTERFACE_ENTRY_GROUP)
self.group.connect_to_signal('StateChanged',
self._entry_group_state_changed)
log.info("Adding service '%s' of type '%s' ...", self.serviceName,
self.serviceType)
domain = ""
self.group.AddService(
avahi.IF_UNSPEC, #interface
avahi.PROTO_UNSPEC, #protocol
0, #flags
self.serviceName, self.serviceType,
domain, self.hostname,
dbus.UInt16(self.port),
avahi.string_array_to_txt_array(self.url))
self.group.Commit()
def _remove_service(self):
if not self.group is None:
self.group.Reset()
def _server_state_changed(self, state):
if state == avahi.SERVER_COLLISION:
log.warn("Server name collision")
self._remove_service()
elif state == avahi.SERVER_RUNNING:
self._add_service()
def _entry_group_state_changed(self, state, error):
log.debug("state change: %i", state)
if state == avahi.ENTRY_GROUP_ESTABLISHED:
log.info("Service established.")
elif state == avahi.ENTRY_GROUP_COLLISION:
self.rename_count -= 1
if self.rename_count > 0:
name = self.server.GetAlternativeServiceName(name)
log.warn("Service name collision, changing name to '%s' ...",
name)
self._remove_service()
add_service()
else:
log.error("No suitable service name found after %i
retries, exiting.", n_rename)
main_loop.quit()
elif state == avahi.ENTRY_GROUP_FAILURE:
log.error("Error in group state changed %s", error)
main_loop.quit()
return
def _check(self):
"""
If our parent process' id is 1, then our real parent has gone
away, and so should we.
"""
if os.getppid() == 1:
self.quit()
return True
def quit(self):
if not self.group is None:
self.group.Free()
log.info("Exiting...")
sys.exit(0)
def run(self):
# We have to fork here so that we have our own set of threads and
# signal handlers, etc. gobject's MainLoop doesn't seem to play nicely
# with Pylons' threading
if os.fork() != 0:
# Return immediately to the controlling process
return
DBusGMainLoop( set_as_default=True )
main_loop = gobject.MainLoop()
self.bus = dbus.SystemBus()
gobject.timeout_add(1000, self._check)
self.server = dbus.Interface(
self.bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
avahi.DBUS_INTERFACE_SERVER )
self.server.connect_to_signal( "StateChanged",
self._server_state_changed )
self._server_state_changed( self.server.GetState() )
try:
main_loop.run()
except KeyboardInterrupt:
pass
self.quit()