Saturday, October 13, 2012

Dbus Tutorial - Create a service

Introduction
Introspection
Network Manager 
Create a Service
GObject Introspection



A dbus service is usable by other applications. It listens for input from another process, and responds with output.

When you create a service, you need to make a couple decisions about when you want to start your service, and when you want to terminate it...

Start: Startup, login, first-use, on-demand?
End: Each time? logout? Shutdown?
In other words, is this a single-use service, or a forever-running daemon?

Happily, the actual code differences are trivial. Dbus itself can launch a service that's not running yet. (Indeed, a lot of startup and login depends on that!)



Example Dbus Service in Python3

Here's an example of  a self-contained daemon written in Python 3 (source). It's introspectable and executable from dbus-send or d-feet. When called, it simply returns a "Hello, World!" string.

It can be started by either dbus or another process (like Upstart or a script). Since it runs in an endless loop awaiting input, it will run until logout. It can also be manually terminated by uncommenting the Gtk.main_quit() command.

#!/usr/bin/env/python3
# This file is /home/me/test-dbus.py
# Remember to make it executable if you want dbus to launch it
# It works with both Python2 and Python3

from gi.repository import Gtk
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop

class MyDBUSService(dbus.service.Object):
    def __init__(self):
        bus_name = dbus.service.BusName('org.me.test', bus=dbus.SessionBus())
        dbus.service.Object.__init__(self, bus_name, '/org/me/test')

    @dbus.service.method('org.me.test')
    def hello(self):
        #Gtk.main_quit()   # Terminate after running. Daemons don't use this.
        return "Hello,World!"

DBusGMainLoop(set_as_default=True)
myservice = MyDBUSService()
Gtk.main() 



Daemon that runs all the time

Just run the script at startup (or login). Or send a dbus-send message to the service, and dbus will start it. It will be terminated as part of shutdown (or logout). While it's running, it's introspectable and visible from d-feet.


Dbus-initiated start

Add a .service file. This file simply tells dbus how to start the service.

Here's an example service file:

# Service file: /usr/share/dbus-1/services/test.service
[D-BUS Service]
Name=org.me.test
Exec="/home/me/test-dbus.py" 

Dbus should automatically pick up the new service without need for any restart. Let's test if dbus discovered the service:

$ dbus-send --session --print-reply \
       --dest="org.freedesktop.DBus" \
       /org/freedesktop/DBus \
       org.freedesktop.DBus.ListActivatableNames \
       | grep test
string "org.me.test"

The new service does not show up in d-feet until after it is run the first time, since before there is nothing to probe or introspect. But it does exist, and is findable and usable by other dbus-aware applications.

Let's try the new service:

$ dbus-send --session --print-reply \
--dest="org.me.test" /org/me/test org.me.test.hello

method return sender=:1.239 -> dest=:1.236 reply_serial=2
   string "Hello,World!"

$ dbus-send --session --print-reply \
--dest="org.me.test" /org/me/test org.me.test.Frank

Error org.freedesktop.DBus.Error.UnknownMethod: Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/dbus/service.py", line 654, in _message_cb
    (candidate_method, parent_method) = _method_lookup(self, method_name, interface_name)
  File "/usr/lib/python3/dist-packages/dbus/service.py", line 246, in _method_lookup
    raise UnknownMethodException('%s is not a valid method of interface %s' % (method_name, dbus_interface))
dbus.exceptions.UnknownMethodException: org.freedesktop.DBus.Error.UnknownMethod: Unknown method: Frank is not a valid method of interface org.me.test

It worked! Dbus launches the script, waits for the service to come up, then asks the service for the appropriate method.Upon execution of the method, the waiting loop terminates, and the script finishes and shuts down.

As a test, you can see that 'hello' is indeed a valid method and returns a valid response, while the invalid method 'Frank' causes a not-found error.


Dbus-initiated stop

Dbus doesn't stop scripts or processes. But a script can stop itself.

In order to wait for input, python-dbus uses a Gtk.main() loop. In this case, simply uncomment the line Gtk.main_quit(). When the method is called,the main() loop gets terminated, and the script continues to the next loop or end.

If you use on-demand starting and stopping, be aware that the service will exist, but will be visible in d-feet or introspectable only for the few seconds it's actually running.



Obsolete: Before python included introspection, you needed to include an interface definition. But you don't need this anymore - introspection seems to have replaced it. Avoid confusion - some old tutorials out there still include it.

<?xml version="1.0" encoding="UTF-8"?>
<!-- /usr/share/dbus-1/interfaces/org.me.test.xml -->
<node name="/org/me/test">
        <interface name="org.me.test">
                <annotation name="org.freedesktop.DBus.GLib.CSymbol" value="server"/>
                <method name="EchoString">
                        <arg type="s" name="original" direction="in" />
                        <arg type="s" name="echo" direction="out" />
                </method>
                <!-- Add more methods/signals if you want -->
        </interface>
</node>




Wednesday, October 3, 2012

Python 3: Using httplib2 to download just a page header

I want to use the National Weather Service geolocator for my private weather script.

When my laptop moves to a new location, the script automatically figures out the correct weather to show.

The NWS geolocator uses web page redirection. If I tell it that I want the web page for "Denver, CO," it redirects me to a web page for the appropriate Latitude/Longitude. I don't actually want the web page - I get the data from other sources...but I do sometimes want that redirect so I can parse the lat/lon pair.


>>> import httplib2
>>> url = "http://www.example.org"
>>> h = httplib2.Http()
>>> h.follow_redirects = False
>>> head = h.request(url, "HEAD")
>>> head
({'status': '302', 'connection': 'Keep-Alive', 'location': 'http://www.iana.org/domains/example/', 'content-length': '0', 'server': 'BigIP'}, b'')
>>> head[0]['location']
'http://www.iana.org/domains/example/'