Showing posts with label dbus. Show all posts
Showing posts with label dbus. Show all posts

Sunday, March 3, 2013

Creating libraries and linking to libraries in Vala

This is an example of how to create a library in vala, and how to access it using several different methods. This example includes:
  1. A shared library written in vala, including
    • shared object (.so), and how to install it
    • headers (.h), and
    • bindings (.vapi)
  2. A command-line tool that uses the library by direct-linking
  3. A dbus server that uses the library by direct linking
    • makes the library available to other dbus-aware applications
    • self-terminating process after use (not a daemon)
  4. A command-line tool that uses the library via dbus and the dbus-server.

The first half of the example is based on the official vala tutorial library example,
I have added dbus connectivity and a bit more explanation that I found helpful.





1) Create an empty directory. These steps create a lot of files!

    $ mkdir test_library
    $ cd test_library



2) Create the library. Save this file as libtest.vala:

// BEGIN
public class MyLib : Object {

    public string hello() {
      return "Hello World from MyLib";
   }

   public int sum(int x, int y) {
      return x + y;
   }
}
// END



3) Create the .c, .h (C header), and .vapi (vala binding) files:
   -C --ccode   Output C code instead of compiled
   -H --header=file  Output C header file
      --library=name Assign name to library
      --vapi         Assign name to vapi file
   -b --basedir=dir  Use dir as the base source dir (For me, this is unnecessary)

    $ valac -C -H libtest.h --library libtest libtest.vala --basedir ./



4) Compile the library using:

   -shared       Create a .so shared object
   -fPIC         Position Independent Code (PIC) suitable for use in a shared library
   $(pkg-config --cflags gobject-2.0)
                 Output: -I/usr/include/glib-2.0
                         -I/usr/lib/i386-linux-gnu/glib-2.0/include
   -o filename   Output filename

     $ gcc -shared -fPIC -o libtest.so $(pkg-config --cflags --libs gobject-2.0) libtest.c



5) Create the command-line application that uses the library by linking.
   Save this file as hello.vala:

// BEGIN
void main() {
    var test = new MyLib();

    // MyLib hello()
    stdout.printf("%s\n", test.hello());

    // MyLib sum()
    int x = 4, y = 5;
    stdout.printf("The sum of %d and %d is %d\n", x, y, test.sum(x, y));
}
// END




6) Compile the command-line application.
Using vala, there need to be TWO links:
  • The vala link to a .vapi file (in this example, test.vapi)
  • The gcc link to a C library (in this example, -X -ltest to add libtest.so)

   - The vala link to a .vapi file (in this example, test.vapi)
   - The gcc link to a C library (in this example, -X -ltest to add libtest.so)

   -X --Xcc=-I.    Pass the -I. (include current directory) to gcc.
                   GCC will look for shared libraries in the current directory first
   -X --Xcc=-L.    Pass the -L. (add current directory to search path) to gcc.
   -X --Xcc=-ltest Link library "libtest" which we just created in the current directory. 

   If we had the libtest.so in the local directory, we could use:

    $ valac -X -I. -X -L. -X -ltest -o hello hello.vala libtest.vapi



7) Test the command-line application that uses the library:
The library is in the local directory (uninstalled):

     $ LD_LIBRARY_PATH=$PWD ./hello
     Hello World from MyLib
     The sum of 4 and 5 is 9



8) We cannot easily use the local library for dbus.
So install the library to the expected location.
Copy the library to /usr/lib using:

      $ sudo cp libtest.so /usr/lib/



9) Test the (installed) command-line application:

     $ ./hello
     Hello World from MyLib
     The sum of 4 and 5 is 9



10) Create the dbus server that uses the library.
A server listens for requests from other applications. This server acts as a gateway: It translates other application requests for library methods into a library call, and then sends the response back across dbus to the original requestor.

Save this file as dbus_server.vala:

// BEGIN
// Source: https://live.gnome.org/Vala/DBusServerSample#Server

[DBus (name = "org.example.Demo")]   // dbus interface name
public class DemoServer : Object {

    /* Functions that access the library on the Demo interface
     * Note the LACK of \n in the return strings.
     * Vala automatically mangles my_function_name into the
     *   standard Dbus camelcase MyFunctionName
     * So a function is "hello()" here, but "Hello" on Dbus 
     */
    public string sum () { 
        var test = new MyLib();
        int x = 4, y = 5;
        return "The sum of %d and %d is %d".printf( x, y, test.sum(x, y));
        // Note the LACK of \n in the return strings
    }

    public string hello () { 
        var test = new MyLib();
        return "%s".printf( test.hello());
    }

}

[DBus (name = "org.example.DemoError")]
public errordomain DemoError { SOME_ERROR }

/* Dbus functions */
void on_bus_aquired (DBusConnection conn) {
    try {
        string path = "/org/example/demo";
        conn.register_object (path, new DemoServer ());
    } catch (IOError e) { 
    stderr.printf ("Could not register service\n"); }
}

void main () {
    GLib.MainLoop loop           = new GLib.MainLoop ();
    GLib.TimeoutSource time      = new TimeoutSource(3000);   // 3 sec
    GLib.BusType bus             = BusType.SESSION;
    string destination           = "org.example.demo";
    GLib.BusNameOwnerFlags flags = BusNameOwnerFlags.NONE;

    Bus.own_name ( bus, destination, flags,
                  on_bus_aquired,
                  () => {},
                  () => stderr.printf ("Could not aquire name\n"));

    // Use timeout to quit the loop and exit the program
    time.set_callback( () => { loop.quit(); return false; });
    time.attach( loop.get_context() );  // Attach timeout to loop

    loop.run ();                // Listen for dbus connections

}
// END



11) Compile the dbus server:
   Dbus requires the gio package (--pkg gio-2.0)
   -X --Xcc=-I.    Pass the -I. (include current directory) to gcc.
                   GCC will look for shared libraries in the current directory first
   -X --Xcc=-L.    Pass the -L. (add current directory to search path) to gcc.
   -X --Xcc=-ltest Link library "libtest" which we just created in the current directory. 

   Since the library is installed, we can ignore those local directory flags:

      $ valac --pkg gio-2.0 -X -ltest dbus_server.vala libtest.vapi



12) Create a dbus .service file, so dbus knows how to find our new dbus_server.
Save this file as dbus.service. Your third line will vary - use the real path:

// BEGIN

[D-BUS Service]

Name=org.example.demo

Exec=/home/me/vala/library/dbus_server

// END



13) Install the dbus service file:

      $ sudo cp dbus.service /usr/share/dbus-1/services/org.example.demo.service



14) Test the dbus server using an existing command-line application, dbus-send.
This is a command-line application using the library via dbus, using dbus_server for it's intended purpose as a gatetay.

The sender is different each time because dbus-send creates a new process and connection each time it is used.
The destination is different because the server terminates after 3 seconds.

      $ dbus-send --session --type=method_call --print-reply \
        --dest="org.example.demo" /org/example/demo org.example.Demo.Hello
      method return sender=:1.3173 -> dest=:1.3172 reply_serial=2
      string "Hello World from MyLib"

      $ dbus-send --session --type=method_call --print-reply \
        --dest="org.example.demo" /org/example/demo org.example.Demo.Sum
      method return sender=:1.3175 -> dest=:1.3174 reply_serial=2
      string "The sum of 4 and 5 is 9"


15) Clean up:
Remove the dbus service file:
Remove the test library from /usr/lib

     $ sudo rm /usr/share/dbus-1/services/org.example.demo.service
     $ sudo rm /usr/lib/libtest.so

Saturday, February 9, 2013

Starting to learn Vala

I have decided to learn Vala. Instructions are here.




Hello Dbus:

Everyone starts with "Hello World." I did too. Little is gained by repeating it. So here is something different. Here is my first dbus introspection in vala:

// This vala file is named dbus-introspection.vala
[DBus (name = "org.freedesktop.DBus.Introspectable")]   // Dbus interface
interface Introspectable : Object {
    [DBus (name = "Introspect")]                        // Dbus method
    public abstract string introspect() throws IOError;
}

void main () {
    Introspectable conn = null;    // If inside the try, causes fail
    try {
        conn = Bus.get_proxy_sync (BusType.SESSION,    // Dbus session
                   "org.freedesktop.Notifications",    // Dbus destination
                   "/org/freedesktop/Notifications");  // Dbus path
        string reply = conn.introspect();              // the actual event
        stdout.printf ("%s\n", reply);                 // print response
    } catch (IOError response) {                       // print error
        stderr.printf ("IO Error: %s\n", response.message);
    }
}

Compile using $ valac --pkg gio-2.0 dbus-introspect.vala
Run using $ ./ dbus-introspect

The vala file is 19 lines.
The vala-generated C file is 300 lines, and about 15 times the size of the vala file.
The compiled binary is 19.4K, 130% the size of the C file.
But 19.4K is still pretty small, and it does run really fast!

For my own reference:

  • The dbus server information we need to connect. This is a common service that has Introspection:
    bus         = session bus
    destination = org.freedesktop.Notifications
    path        = /org/freedesktop/Notifications
    interface   = org.freedesktop.DBus.Introspectable
    method      = Introspect
    message     = (none)

  • main () { } is where the program starts.
    Equivalent to python's if "__name__" == "__main__":.
    Since it's the main, it returns either a return code (int) or nothing (void), and nothing else. The example above returns nothing.
    To add a return code, add return 0; to the end of the try { } block, and add return 1; to the end of the catch { } block

  • Michael Brown pointed out a very useful tidbit to avoid a lot of confusion:

  • Vala class = DBus interface
  • Vala interface = DBus proxy
  • This is the tricky part.
    Building the dbus connection (proxy) means we call that variable an interface in vala.
    And each dbus interface must be defined as a separate class in vala.
    To that, I will add that each class must be a GLib.Object.

    So a connection must start by opening an instance of the class, then adding the proxy information. Interface first, then proxy. That's backwards from the way dbus-send or python do it.

    For example:
    The dbus interface org.freedesktop.DBus.Introspectable gets defined as a vala class:
    interface Introspectable : Object { }


    Next, let's add the method and the labels to this class:
    [DBus (name = "org.freedesktop.DBus.Introspectable")]   // Dbus interface
    interface Introspectable : Object {
        [DBus (name = "Introspect")]                        // Dbus method
        public abstract string introspect() throws IOError;
    }
    

  • Methods must be public and abstract. We expect it to return a string (introspection returns a string of XML). As far as I can tell, methods should throw IOError in case the dbus message is malformed, the receiving service does not exist or has no such message, etc. The method is part of the interface's Object.


  • Main uses the try { } catch { } blocks to locate those IOErrors.
    Initial creation of the interface must be outside the try block, or the program won't be listening for the dbus response. Adding the bus and proxy information within the try block is not required...if you never make spelling mistakes.




Friday, December 28, 2012

From raw idea to useful source code

A couple months ago I had an Idea.

I even blogged about it: A lookup service for US National Weather Service codes. Those codes are necessary to access their machine-readable products.

In this post, I will show how I developed the idea into some code, how I grew the code into a project, added structure and version control, and finally moved the project onto online hosting.

This is not the only way to create a project.
This is probably not the best way for many projects.
It's just the way I did it, so you can avoid the most common mistakes.

You can see my final product hosted online at Launchpad.



From Idea to Code:

I know barely enough C to be able to ask there the bathroom is, so it's easier for me to use Python.

Code starts out as a single Python script:
- geolocation.py

As we add more features, a single script gets big and unwieldly, and we break it into smaller pieces.

For example, this structure easily allows more interfaces to be added.
- geolocation_service.py
- command_line_interface.py

Let's add a dbus interface, too. Dbus will semd messages to the interface if it knows about it. Let dbus know about it using a service file.
- dbus_interface.py
- dbus_service_file.service

Let's add an http interface, so not everyone in the world needs to download 5-6MB of mostly-unused lookup databases:
- http_interface.py
- specialized_webserver.py

Let's go back and formalize how we create the databases:
- database_creator.py

We have a lot of hard-coded variables in these scripts. Let's break them out into a config file.
- configfile.conf

There are other possible files, that we're not using. For example:
- Upstart config file (if we want the service to run/stop at boot or upon some system signal, lives in /etc/init)
- Udev rule file (if we want the service to run/stop when a device is plugged in, lives in /etc/udev/rules.d)

But that's a lot of files and scripts! 8 files, plus the databases.
 


Adding Version Control:

It's time to get serious about these eight files. We have invested a lot of time creating them, and it's time to start organizing the project so others can contribute, so we can track new bugs and features, and to protect all our invested work.

First, we need to introduce version control. Ideally, we would have done that from the start. But we didn't. So let's fix that.

Version control offers a lot of advantages:
    We can undo mistakes.
    It helps us package the software later.
    It helps us track bugs.
    It helps us apply patches.
    It helps us document changes.

There are plenty of good version control systems available. For this example, I'll use bazaar. The developers have a very good tutorial.

Installing bazaar:

$ sudo apt-get install bzr
$ bzr whoami "My Name "

Since we didn't start with proper version control, we need to create a new directory using version control, move our files into it, and add our files to version control.

$ bzr init-repo My_NEW_project_directory
$ bzr init My_NEW_project_directory
$ mv My_OLD_project_directory/* My_NEW_project_directory/
$ cd My_NEW_project_directory
$ bzr add *

Finally, we need to clean up the old directory, and commit the changes.

$ rm ../My_OLD_project_directory
$ bzr commit -m "Initial setup"


Organizing the code

My project directory is starting to get disorganized, with eight scripts and files, plus six database files, plus version control, plus more to come. I'm going to restructure my project folder like this:

My_project_directory
  +-- data   (all the database files)
  +-- src    (all the python scripts and other non-data files)
  +-- .bzr   (bzr's version control tracking)


Once version control is active, we cannot just move things around. We need to use the version control tools so it can keep tracking the right files.

$ bzr mkdir data src
$ bzr mv *.gz data/
$ bzr mv *.py src/
$ bzr mv dbus_service_file.service src/
$ bzr mv configfile.conf src/

See how bazaar adds the directories and performs the moves?

Now My_project_directory should be empty of files. Once reorganization is complete, remember to commit the change:

$ bzr commit -m "Reorganize the files to a better project structure"




Integrating into the system:

We have a problem with our eight files. They run beautifully, but only if they are in our home directory.

That won't work in the long run. A server should not be run as a user with shell access - that's a security hole. Nor should it be run out of a user's /home. Nor should it be run as root. Also, other applications that are looking for our server won't find it - all the files are in the wrong places.

So we need to put our files into the right places. And often that means fixing the scripts to replace hard-coded temporary paths (like '~/server/foo') with the proper locations ('/usr/lib/foo-server/foo').

Where are the right places?

The Linux Filesystem Hierarchy Standard (FHS) is used by Debian to define the right places.

Two files are directly user-launched in regular use:
- specialized_webserver.py: /usr/bin
- command_line_interface.py: /usr/bin

The database files are read-only and available to any application:
- database files: /usr/shared

Three files are launched or imported by other applications or scripts:
- geolocation_service.py: /usr/lib
- dbus_interface.py: /usr/lib
- http_interface.py: /usr/lib

One file is very rarely user-launched under unusual circumstances:
- database_creator.py

The  dbus service file will be looked for by dbus in a specific location:
- geolocation_dbus.service: /usr/share/dbus-1/services

Config files belong in /etc
- geolocation.conf: /etc

Makefiles make organization easier:

Now that we know where the right places are, let's create a Makefile that will install and uninstall the files to the right place. Our originals stay where they are - the makefile copies them during the install, and deletes the copies during uninstall.

Makefiles are really config files for the make application (included in the build-essential metapackage). Makefiles tell make which files depend upon which, which files to compile (we won't be compiling), and where the installed application files should be located, and how to remove the application.

Here is a sample makefile for my project (wbs-server):
DATADIR = $(DESTDIR)/usr/share/weather-location
LIBDIR  = $(DESTDIR)/usr/lib/wbs-server
BINDIR  = $(DESTDIR)/usr/bin
DBUSDIR = $(DESTDIR)/usr/share/dbus-1/services
CONFDIR = $(DESTDIR)/etc
CACHDIR = $(DESTDIR)/var/cache/wbs-webserver

install: 
 # Indents use TABS, not SPACES! Space indents will cause make to fail
 mkdir -p $(DATADIR)
 cp data/*.gz $(DATADIR)/

 mkdir -p $(LIBDIR)
 cp src/geolocation.py $(LIBDIR)/
 cp src/wbs_dbus_api.py $(LIBDIR)/
 cp src/wbs_http_api.py $(LIBDIR)/
 cp src/wbs_database_creator.py $(LIBDIR)/

 cp src/wbs_cli_api.py $(BINDIR)/
 cp src/wbs_webserver.py $(BINDIR)/
 cp src/wbs-server.service $(DBUSDIR)/
 cp src/confile.conf $(CONFDIR)/wbs-server.conf
 mkdir -p $(CACHDIR)

uninstall:
 rm -rf $(DATADIR)
 rm -rf $(LIBDIR)

 rm -f $(BINDIR)/wbs_cli_api.py
 rm -f $(BINDIR)/wbs_webserver.py
 rm -f $(DBUSDIR)/wbs-server.service
 rm -f $(CONFDIR)/wbs-server.conf
 rm -rf $(CACHDIR)

Let's save the makefile as Makefile, and run it using sudo make install and sudo make uninstall.

We run a test:

$ sudo make install
$ /usr/bin/wbs_cli_api.py zipcode 43210
bash: /usr/lib/wbs-server/wbs_cli_api.py: Permission denied

Uh-oh. Let's investigate:

$ ls -l /usr/lib/wbs-server/wbs_cli_api.py 
-rw-r--r-- 1 root root 3287 Dec 23 20:46 /usr/lib/wbs-server/wbs_cli_api.py

Aha. Permissions are correct, but the executable flag is not set. Let's uninstall the application so we can fix the makefile.

$ sudo make uninstall

In the makefile, we can make a few changes if we wish. We can set the executable flag. We can also create links or symlinks, or rename the copy.

For example, wbs_cli_api.py is a rather obtuse name for a command-line executable. Instead of copying it to /usr/bin, let's copy it to /usr/lib with its fellow scripts, make it executable, and create a symlink to /usr/bin with a better name like 'weather-lookup'

install:
        ...
 cp src/wbs_cli_api.py $(LIBDIR)/
 chmod +x $(LIBDIR)/wbs_cli_api.py
 ln -s $(LIBDIR)/wbs_cli_api.py $(BINDIR)/weather-lookup
        ...

uninstall:
        ...
 rm -f $(BINDIR)/weather-lookup
        ...


Another example: It's a bad idea to run a webserver as root. So let's add a couple lines to the makefile to create (and delete) a separate system user to run the webserver.

USERNAM = wbserver
        ...
install:
        ...
 adduser --system --group --no-create-home --shell /bin/false $(USERNAM)
 cp chgrp $(USERNAM) $(LIBDIR)/*
 cp chgrp $(USERNAM) $(CACHDIR)
 # Launch the webserver using the command 'sudo -u wbserver wbs-server'
        ...

uninstall:
        ...
 deluser --system --quiet $(USERNAM)
        ...




Sharing the code

Now we have a complete application, ready to distribute.

Thanks to the Makefile, we include a way to install and uninstall.

It's not a package yet. It's not even a source package yet. It's just source code and an install/uninstall script.

We can add a README file, a brief description of how to install and use the application.

We can also add an INSTALL file, detailed instructions on how to unpack (if necessary) and install the application.

It would be very very wise to add a copyright and/or license file, so other people know how they can distribute the code.

After all that, remember to add those files to version control! And to finally commit the changes:

bzr commit -m "Initial code upload. Add README, INSTALL, copyright, and license files."

Finally, we need a place to host the code online. Since I already have a Launchpad account and use bzr, I can easily create a new project on bzr.

And then uploading the version-controlled files is as simple as:

bzr launchpad-login my-launchpad-name
bzr push lp:~my-launchpad-name/my-project/trunk 

You can see my online code hosted at Launchpad.


Next time, we'll get into how to package this source code.

Saturday, November 24, 2012

Dbus Tutorial - GObject Introspection instead of python-dbus

Introduction
Introspection
Network Manager
Create a Service
GObject Introspection


In previous posts, I have looked at using the python-dbus to communicate with other processes, essentially using it the same way we use the dbus-send command.

There is another way to create DBus messages. It's a bit more complicated than python-dbus, and it depends upon Gnome, but it's also more robust and perhaps better maintained.

Using Gobject Introspection replacement for python-dbus is described several places, but the best example is here. Python-dbus as a separate bindings project has also suffered with complaints of "lightly maintained," and an awkward method of exposing properties that has been unfixed for years.


These examples only work for clients.  Gnome Bug #656330 shows that services cannot yet use PyGI.




Here's an example notification using Pygi instead of Python-DBus. It's based on this blog post by Martin Pitt, but expanded a bit to show all the variables I can figure out....


1) Header and load gi

#!/usr/bin/env python3

import gi.repository
from gi.repository import Gio, GLib


2) Connect to the DBus Session Bus
Documentation: http://developer.gnome.org/gio/2.29/GDBusConnection.html

session_bus = Gio.BusType.SESSION
cancellable = None
connection = Gio.bus_get_sync(session_bus, cancellable)


3) Create (but don't send) the DBus message header
Documentation: http://developer.gnome.org/gio/2.29/GDBusProxy.html

proxy_property = 0
interface_properties_array = None
destination = 'org.freedesktop.Notifications'
path = '/org/freedesktop/Notifications'
interface = destination
notify = Gio.DBusProxy.new_sync(
     connection,
     proxy_property,
     interface_properties_array,
     destination,
     path,
     interface,
     cancellable)


4) Create (but don't send) the DBus message data
The order is determined by arg order of the Notification system
Documentation: http://developer.gnome.org/notification-spec/#protocol

application_name = 'test'
title = 'Hello World!'
body_text = 'Subtext'
id_num_to_replace = 0
actions_list = []
hints_dict = {}
display_milliseconds = 5000
icon = 'gtk-ok'  # Can use full path, too '/usr/share/icons/Humanity/actions/'
args = GLib.Variant('(susssasa{sv}i)', (
                    application_name, 
                    id_num_to_replace,
                    icon, 
                    title,
                    body_text,
                    actions_list,
                    hints_dict,
                    display_milliseconds))


5) Send the DBus message header and data to the notification service
Documentation: http://developer.gnome.org/gio/2.29/GDBusProxy.html

method = 'Notify'
timeout = -1
result = notify.call_sync(method, args, proxy_property, timeout, cancellable)


6) (Optional) Convert the result value from a Uint32 to a python integer

id = result.unpack()[0]
print(id)

Play with it a bit, and you will quickly see how the pieces work together.



Here is a different, original example DBus client using introspection and this askubuntu question. You can see this is a modified and simplified version of the above example:

#!/usr/bin/env python3
import gi.repository
from gi.repository import Gio, GLib

# Create the DBus message
destination = 'org.freedesktop.NetworkManager'
path        = '/org/freedesktop/NetworkManager'
interface   = 'org.freedesktop.DBus.Introspectable'
method      = 'Introspect'
args        = None
answer_fmt  = GLib.VariantType.new ('(v)')
proxy_prpty = Gio.DBusCallFlags.NONE
timeout     = -1
cancellable = None

# Connect to DBus, send the DBus message, and receive the reply
bus   = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
reply = bus.call_sync(destination, path, interface,
                      method, args, answer_fmt,
                      proxy_prpty, timeout, cancellable)

# Convert the result value to a formatted python element
print(reply.unpack()[0])




Here is a final DBus client example, getting the properties of the current Network Manager connection

#!/usr/bin/env python3
import gi.repository
from gi.repository import Gio, GLib

# Create the DBus message
destination = 'org.freedesktop.NetworkManager'
path        = '/org/freedesktop/NetworkManager/ActiveConnection/19'
interface   = 'org.freedesktop.DBus.Properties'
method      = 'GetAll'
args        = GLib.Variant('(ss)', 
              ('org.freedesktop.NetworkManager.Connection.Active', 'None'))
answer_fmt  = GLib.VariantType.new ('(v)')
proxy_prpty = Gio.DBusCallFlags.NONE
timeout     = -1
cancellable = None

# Connect to DBus, send the DBus message, and receive the reply
bus   = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
reply = bus.call_sync(destination, path, interface,
                      method, args, answer_fmt,
                      proxy_prpty, timeout, cancellable)

# Convert the result value to a useful python object and print
[print(item[0], item[1]) for item in result.unpack()[0].items()]

As you can see from this example, dbus communication is actually pretty easy using GLib: Assign the nine variables, turn the crank, and unpack the result.

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>




Saturday, September 22, 2012

Dbus Tutorial - Fun with Network Manager

Introduction
Introspection: figuring out the rules 
Fun with Network Manager
Create a service 
Gobject Introspection


Let's figure out how to use  dbus to get detailed information out of Network Manager (NM), and then to plug our own information back into NM.

Example #1: This script determines if a specific network is the active connection. This is handy for, say, mapping network printers or networked drives, or setting the time, or automating backups, or lots of other stuff.

#!/bin/sh
# What network am I on?

# Get the Active Connection path
# Result should be like: /org/freedesktop/NetworkManager/ActiveConnection/187
active_connection_path=$( dbus-send --system --print-reply \
                          --dest=org.freedesktop.NetworkManager \
                          /org/freedesktop/NetworkManager \
                          org.freedesktop.DBus.Properties.Get \
                          string:"org.freedesktop.NetworkManager" \
                          string:"ActiveConnections" \
                          | grep ActiveConnection/ | cut -d'"' -f2 )

# Get the Access Point path
# Result should be like: /org/freedesktop/NetworkManager/AccessPoint/194
access_point_path=$( dbus-send --system --print-reply \
                     --dest=org.freedesktop.NetworkManager \
                     "$active_connection_path" \
                     org.freedesktop.DBus.Properties.Get \
                     string:"org.freedesktop.NetworkManager.Connection.Active" \
                     string:"SpecificObject" \
                     | grep variant | cut -d'"' -f2 )

# Get the Access Point ESSID
# Result should be something like "NETGEAR"
essid=$( dbus-send --system --print-reply \
                   --dest=org.freedesktop.NetworkManager \
                   "$access_point_path" \
                   org.freedesktop.DBus.Properties.Get \
                   string:"org.freedesktop.NetworkManager.AccessPoint" \
                   string:"Ssid" \
                   | grep variant | cut -d'"' -f2 )



# If we are on the HOME network
if [ "$essid"=="MyHomeNet" ]; then
   # Do network-specific changes here

elif [ "$essid"=="WorkCorporateNet" ]
   # Do network-specific changes here

else
   # Do changes for unrecognized network or no network at all here

fi
exit 0


Example #2: Disable networking

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager \
            org.freedesktop.DBus.Properties.Set \
            string:"org.freedesktop.NetworkManager" \
            string:"NetworkingEnabled" \
            variant:boolean:false


Example #3: Enable networking. It's exactly the same as the previous example, except for the last line.

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager \
            org.freedesktop.DBus.Properties.Set \
            string:"org.freedesktop.NetworkManager" \
            string:"NetworkingEnabled" \
            variant:boolean:true


Example #4: Check networking status

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager \
            org.freedesktop.DBus.Properties.Get \
            string:"org.freedesktop.NetworkManager" \
            string:"NetworkingEnabled"

method return sender=:1.4 -> dest=:1.325 reply_serial=2
   variant       boolean true


Example #5: Disable Wireless

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager \
            org.freedesktop.DBus.Properties.Set \
            string:"org.freedesktop.NetworkManager" \
            string:"WirelessEnabled" \
            variant:boolean:false


Example #6: Enable Wireless

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager \
            org.freedesktop.DBus.Properties.Set \
            string:"org.freedesktop.NetworkManager" \
            string:"WirelessEnabled" \
            variant:boolean:true


Example #7: Check Wireless Status

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager \
            org.freedesktop.DBus.Properties.Get \
            string:"org.freedesktop.NetworkManager" \
            string:"WirelessEnabled"

method return sender=:1.4 -> dest=:1.326 reply_serial=2
   variant       boolean true


Example #8:




Example #9: List all active network connections

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager \
            org.freedesktop.DBus.Properties.Get \
            string:"org.freedesktop.NetworkManager" \
            string:"ActiveConnections"

method return sender=:1.4 -> dest=:1.328 reply_serial=2
   variant       array [
         object path "/org/freedesktop/NetworkManager/ActiveConnection/18"
      ]


Example #10: Which interface is the active connection using?

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager/ActiveConnection/18 \
            org.freedesktop.DBus.Properties.Get \
            string:"org.freedesktop.NetworkManager.Connection.Active" \
            string:"Devices"

method return sender=:1.4 -> dest=:1.331 reply_serial=2
   variant       array [
         object path "/org/freedesktop/NetworkManager/Devices/0"

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager/Devices/0 \
            org.freedesktop.DBus.Properties.Get \
            string:"org.freedesktop.NetworkManager.Device" \
            string:"Interface"

method return sender=:1.4 -> dest=:1.332 reply_serial=2
   variant       string "wlan0"


Example #11: Get the current wireless access point

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager/ActiveConnection/18 \
            org.freedesktop.DBus.Properties.Get \
            string:"org.freedesktop.NetworkManager.Connection.Active" \
            string:"SpecificObject"

method return sender=:1.4 -> dest=:1.334 reply_serial=2
   variant       object path "/org/freedesktop/NetworkManager/AccessPoint/209"


Example #12: Get the list of all visible wireless access points

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager 
            /org/freedesktop/NetworkManager/Devices/0 \
            org.freedesktop.NetworkManager.Device.Wireless.GetAccessPoints

method return sender=:1.4 -> dest=:1.333 reply_serial=2
   array [
      object path "/org/freedesktop/NetworkManager/AccessPoint/209"
      object path "/org/freedesktop/NetworkManager/AccessPoint/208"
      object path "/org/freedesktop/NetworkManager/AccessPoint/207"
      object path "/org/freedesktop/NetworkManager/AccessPoint/206"
   ]


Example #13: Read the SSID of the active wireless access point

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager/AccessPoint/209 \
            org.freedesktop.DBus.Properties.Get \
            string:"org.freedesktop.NetworkManager.AccessPoint" \
            string:"Ssid"

method return sender=:1.4 -> dest=:1.335 reply_serial=2
   variant       array of bytes "NETGEAR"


Example #14: Read the signal strength of the active wireless point

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager/AccessPoint/209 \
            org.freedesktop.DBus.Properties.Get \
            string:"org.freedesktop.NetworkManager.AccessPoint" \
            string:"Strength"

method return sender=:1.4 -> dest=:1.340 reply_serial=2
   variant       byte 54
# 54 in byte (hexadecimal) = 84 in decimal. Strength = 84%


Example #15: Read the stored NM connection that is currently active

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager/ActiveConnection/18 \
            org.freedesktop.DBus.Properties.Get \
            string:"org.freedesktop.NetworkManager.Connection.Active" \
            string:"Connection"

method return sender=:1.4 -> dest=:1.379 reply_serial=2
   variant       object path "/org/freedesktop/NetworkManager/Settings/10"


Example #16: Disconnect from the current network connection (auto-reconnect)

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager \
            org.freedesktop.NetworkManager.DeactivateConnection \
            objpath:"/org/freedesktop/NetworkManager/ActiveConnection/18"

method return sender=:1.4 -> dest=:1.370 reply_serial=2


Example #17: Disconnect from the current network connection and stay disconnected

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager/Devices/0 \
            org.freedesktop.NetworkManager.Device.Disconnect

method return sender=:1.4 -> dest=:1.354 reply_serial=2


Example #18: Connect to a specific wireless network

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            org/freedesktop/NetworkManager \
            org.freedesktop.NetworkManager.ActivateConnection \
            objpath:"/org/freedesktop/NetworkManager/Settings/10" \
            objpath:"/org/freedesktop/NetworkManager/Devices/0" \
            objpath:"/org/freedesktop/NetworkManager/AccessPoint/209"

method return sender=:1.4 -> dest=:1.382 reply_serial=2
   object path "/org/freedesktop/NetworkManager/ActiveConnection/20"


Example #19: Dump all information about an access point

$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            /org/freedesktop/NetworkManager/Settings/10 \
            org.freedesktop.NetworkManager.Settings.Connection.GetSettings

method return sender=:1.4 -> dest=:1.386 reply_serial=2
   array [## EDITED - it's really long]


Thursday, August 2, 2012

Dbus Tutorial - Introspection: Figuring Out The Rules

Introduction
Introspection
Network Manager 
Create a Service
Gobject Introspection


Last time, we discussed what to use dbus for and the basics of structuring a dbus command. We went over how to structure the grammar of a command so it makes sense (mapping the destination, path, method, and message elements), and we went over the syntax (stringing together those elements in a coherent way).

This lesson is about figuring out what methods are available and how to use them.


Introspection

dbus is introspectable. That means you can ask dbus what commands are available.

Here's an introspection example. You can see that the return is XML wrapped inside a string (you don't need to read it all):

$ dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager /org/freedesktop/NetworkManager org.freedesktop.DBus.Introspectable.Introspect
method return sender=:1.4 -&gt; dest=:1.441 reply_serial=2
   string "
<node>
  <interface name="org.freedesktop.DBus.Introspectable">
    <method name="Introspect">
      <arg direction="out" name="data" type="s">
    </arg></method>
  <interface name="org.freedesktop.DBus.Properties">
    <method name="Get">
      <arg direction="in" name="interface" type="s">
      <arg direction="in" name="propname" type="s">
      <arg direction="out" name="value" type="v">
    </arg></arg></arg></method>
    <method name="Set">
      <arg direction="in" name="interface" type="s">
      <arg direction="in" name="propname" type="s">
      <arg direction="in" name="value" type="v">
    </arg></arg></arg></method>
    <method name="GetAll">
      <arg direction="in" name="interface" type="s">
      <arg direction="out" name="props" type="a{sv}">
    </arg></arg></method>
  </interface>
  <interface name="org.freedesktop.NetworkManager">
    <method name="state">
      <arg direction="out" name="state" type="u">
    </arg></method>
    <method name="SetLogging">
      <arg direction="in" name="level" type="s">
      <arg direction="in" name="domains" type="s">
    </arg></arg></method>
    <method name="GetPermissions">
      <arg direction="out" name="permissions" type="a{ss}">
    </arg></method>
    <method name="Enable">
      <arg direction="in" name="enable" type="b">
    </arg></method>
    <method name="Sleep">
      <arg direction="in" name="sleep" type="b">
    </arg></method>
    <method name="DeactivateConnection">
      <arg direction="in" name="active_connection" type="o">
    </arg></method>
    <method name="AddAndActivateConnection">
      <arg direction="in" name="connection" type="a{sa{sv}}">
      <arg direction="in" name="device" type="o">
      <arg direction="in" name="specific_object" type="o">
      <arg direction="out" name="path" type="o">
      <arg direction="out" name="active_connection" type="o">
    </arg></arg></arg></arg></arg></method>
    <method name="ActivateConnection">
      <arg direction="in" name="connection" type="o">
      <arg direction="in" name="device" type="o">
      <arg direction="in" name="specific_object" type="o">
      <arg direction="out" name="active_connection" type="o">
    </arg></arg></arg></arg></method>
    <method name="GetDeviceByIpIface">
      <arg direction="in" name="iface" type="s">
      <arg direction="out" name="device" type="o">
    </arg></arg><<method>
    <method name="GetDevices">
      <arg direction="out" name="devices" type="ao">
    </arg></method>
     <signal name="DeviceRemoved">
       <arg type="o">
     </arg> </signal>
     <signal name="DeviceAdded">
       <arg type="o">
     </arg> </signal>
     <signal name="PropertiesChanged">
       <arg type="a{sv}">
     </arg> </signal>
     <signal name="StateChanged">
       <arg type="u">
     </arg> </signal>
     <signal name="CheckPermissions">
     </signal>
     <property access="read" name="State" type="u">
     <property access="read" name="Version" type="s">
     <property access="read" name="ActiveConnections" type="ao">
     <property access="read" name="WimaxHardwareEnabled" type="b">
     <property access="readwrite" name="WimaxEnabled" type="b">
     <property access="read" name="WwanHardwareEnabled" type="b">
     <property access="readwrite" name="WwanEnabled" type="b">
     <property access="read" name="WirelessHardwareEnabled" type="b">
     <property access="readwrite" name="WirelessEnabled" type="b">
     <property access="read" name="NetworkingEnabled" type="b">
   </property> </property> </property> </property> </property> </property> </property> </property> </property> </property>
   <node name="AccessPoint">
   <node name="ActiveConnection">
   <node name="AgentManager">
   <node name="DHCP4Config">
   <node name="Devices">
   <node name="IP4Config">
   <node name="Settings">
 </node>
" </node> </node> </node> </node> </node> </node>

 
Let's see if we can translate this into something more human readable.

For example,

<node>
  <interface name="org.freedesktop.DBus.Introspectable">
    <method name="Introspect">
      <arg direction="out" name="data" type="s">
    </arg&gt;&lt;/method> 
  ... 
</node>

is the method we used for introspection (remember?).

See the org.freedesktop.DBus.Introspectable.Introspect?  The arg direction "out" means the the response, and type "d" means data string).


How to use introspection to create a dbus query or command

Here's a slightly more complex example of an introspection response:

  <interface name="org.freedesktop.DBus.Properties">
    ...
    <method name="GetAll">
      <arg direction="in" name="interface" type="s">
      <arg direction="out" name="props" type="a{sv}">
    </arg> </arg> </method>
    ...
  <interface name="org.freedesktop.NetworkManager"> 

This generic method, org.freedesktop.DBus.Properties.GetAll, returns a complete dump of all of some other interface's properties.

Let's go back to grammar (destination, path, method, message) and say that again:

I want to see a dump of Network manager's top-level properties.
"Hey Network Manager, please give me a printout of all of Network Manager's top-level properties."

Destination: "Hey, Network Manager": org.freedesktop.NetworkManager

Path:  "Network Manager's": org/freedesktop/NetworkManager

Method: "Give me a printout of properties": org.freedesktop.DBus.Properties.GetAll

Message: "top-level": org.freedesktop.NetworkManager

You probably noticed that this example has some duplication. When working with dbus, get used to it.


Now let's put it into the right syntax:

dbus-send   [ --system| --session] --print-reply --dest=DEST PATH METHOD [MESSAGE]


$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            org/freedesktop/NetworkManager \
            org.freedesktop.DBus.Properties.GetAll \
            string:"org.freedesktop.NetworkManager"


Let's do one more slightly different example:

<interface name="org.freedesktop.NetworkManager">
    ...
    <method name="ActivateConnection">
      <arg direction="in" name="connection" type="o">
      <arg direction="in" name="device" type="o">
      <arg direction="in" name="specific_object" type="o">
      <arg direction="out" name="active_connection" type="o">
    </arg> </arg> </arg> </arg> </method>

 </node>
" </node> </node> </node> </node> </node> </node>

This method, ActivateConnection, makes a known connection into the active network connection. For example, when switching from one access point to another. There are three extra pieces of information needed, and network manager returns one piece of information in the response.

Let's go back to grammar (destination, path, method, message) and say that again:

"Hey Network Manager, make Access Point Foo (using wireless device 0 and network password settings 67) the active connection."

Destination: "Hey, Network Manager": org.freedesktop.NetworkManager

Path:  "Network Manager": org/freedesktop/NetworkManager

Method: "Make...the active connection": org.freedesktop.NetworkManager.ActivateConnection

Message 1: "Access Point Foo": org/freedesktop/NetworkManager/AccessPoint/220

Message 2: "wireless device 0": org/freedesktop/NetworkManager/Device/0

Message 3: "network password info 67": org/freedesktop/NetworkManager/Settings/67

Now let's put it into the right syntax:

dbus-send   [ --system| --session] --print-reply --dest=DEST PATH METHOD [MESSAGE]


$ dbus-send --system --print-reply \
            --dest=org.freedesktop.NetworkManager \
            org/freedesktop/NetworkManager \
            org.freedesktop.NetworkManager/ActivateConnection \
            objpath:"org/freedesktop/NetworkManager/AccessPoint/220" \
            objpath:"org/freedesktop/NetworkManager/Device/0" \
            objpath:"org/freedesktop/NetworkManager/Settings/67"

Obviously, this example won't work for you unless you do the introspection to find valid Access Points, Devices, and Settings that work together. dbus will tell you a lot of it...if you ask...but it's not a user-friendly graphical user interface. You need to ask the right questions and use your own logic.

Making introspection easier is where d-feet comes in.


d-feet makes introspection easy

d-feet is a python application (part of the d-feet package in Debian and Ubuntu) that does introspection for you while you write your program.


In this screenshot, you can see lots of the same introspection information that we retrieved before. Easier to read and understand, isn't it? See how the Interfaces and Methods are listed for each Object Path?

Without d-feet, use the following dbus-send command to find out what's available on the bus:

$ dbus-send --session --print-reply --dest="org.freedesktop.DBus" /org/freedesktop/DBus org.freedesktop.DBus.ListActivatableNames


Now you know how to find the information you need to use dbus properly.

Thursday, July 26, 2012

Dbus Tutorial - Intro and Resources

Introduction
Introspection
Network Manager 
Create a Service 
Gobject Introspection



Introduction to dbus

dbus (Desktop Bus) is a system, used mostly in Linux, that various applications and daemons and the operating system use to communicate with each other.

You can also use dbus to send text command, instead of a mouse click, that control applications and settings and windows and actions and much more.

For example, you can use dbus to pull information from Network Manager, like the name of a wireless access point. You can send commands to it, like to enable networking or to connect to a specific access point. Many of these actions you can also do other ways, for example through shell commands.

Learning dbus is learning a whole new dimension of how to control your system. And it's much like learning a new language.


Resources

We will use the d-feet application (sudo apt-get install d-feet) to search for dbus resources on our system. We will use the dbus-send shell command to interact with dbus.

That's it. One helper application and one command. The rest is up to you.


dbus Grammar

Here's a sample dbus command. All it does is tell Network Manager to enable networking, just like you right-clicked on the NM icon and ticked "Enable Networking". In english, we would say something a bit more like: "Hey Network Manager, (you) please turn on (entire) networking"

dbus commands have four important elements: Destination, path, method, and message.

$ dbus-send --system --print-reply \                  # (Hey,)
            --dest=org.freedesktop.NetworkManager \   # destination (Network Manager)
            /org/freedesktop/NetworkManager \         # path    (you)
            org.freedesktop.DBus.Properties.Set \     # method  (turn)
            string:"org.freedesktop.NetworkManager" \ # message (networking)
            string:"NetworkingEnabled" \              # message (entire) 
            variant:boolean:true                      # message (on)


dbus grammar is very simple, but that doesn't mean it is easy the first time you try it. The grammar is meant for machines.

Destination: Which process/program/application you are talking to.
Path: Which resource you are talking about.
Method: What you are telling the destination to do with the path.
Message: Specific information needed for the method to make sense.

For example: "Sally, please throw the red ball to Fred."

You are talking to Sally (in english Grammar, often but not always the subject).
Sally is the destination.

The red ball is the resource (in english grammar, usually the direct object).
The red ball is the path.

"Throw" is the action you want Sally to do with the ball (in english grammar, usually the verb)
Throw is the method.

"To Fred" is additional information that makes the throw successful.
To Fred is message that is needed by the method.


dbus Syntax

The grammar is the hardest part - restructuring your statement clearly in those four terms. After that, it's easy.

The syntax of the actual command using dbus-send  is explained thoroughly in man dbus-send, though some of the teminology differs a bit.

dbus-send   [--system   |   --session]   [--dest=NAME]  [--print-reply]
       [--type=TYPE] <destination object="" path=""> <message name=""> [contents ...]

system vs. session simply refers to which bus. There are usually two running, one at user (session) level, and one at admin/sudo/root (system) level.

"type" simply means if it's a signal or a method call. Signals are usually one-way, little response is generated. A method call usually creates a response, even if just an acknowledgement.

"print reply" prints the response from dbus, if any.


A more simple (and slightly rearranged) syntax that we use is:

dbus-send   [ --system| --session] --print-reply --dest=DEST PATH METHOD [MESSAGE]

$ dbus-send --system --print-reply \                  # Root-level, since it's hardware
            --dest=org.freedesktop.NetworkManager \   # destination  (dest)
            /org/freedesktop/NetworkManager \         # path         (path)
            org.freedesktop.DBus.Properties.Set \     # message name (method)
            string:"org.freedesktop.NetworkManager" \ # message      (contents)
            string:"NetworkingEnabled" \              # message      (contents) 
            variant:boolean:true                      # message      (contents)

This multi-line command connected with backslashes (\) is just for readability, especially in scripts. When I really type them in, they look like this:

$ dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager /org/freedesktop/NetworkManager org.freedesktop.DBus.Properties.Set string:"org.freedesktop.NetworkManager" string:"NetworkingEnabled" variant:boolean:true



Using Shell Variables

Simple grammar and syntax means that it's easy to use shell variables to improve readbility and reduce typos, especially in scripts.

dest="org.freedesktop.NetworkManager"
path="/org/freedesktop/NetworkManager"
method="org.freedesktop.DBus.Properties.Set"

# Enable Networking
dbus-send --system --print-reply --dest="$dest" "$path" "$method" \
          string:"$dest" string:"NetworkingEnabled" variant:boolean:true

And, of course, you can nest entire commands within variables:

dest="org.freedesktop.NetworkManager"
path="/org/freedesktop/NetworkManager"
method="org.freedesktop.DBus.Properties.Set"

network_command="dbus-send --system --print-reply --dest=$dest $path $method"
enable_network="string:$dest string:"NetworkingEnabled" variant:boolean:true"

# Enable Networking
$network_command $enable_network

Next, let's ligure out how to find all those weird dbus properties using introspection.

Sunday, March 22, 2009

Scanning for wireless networks

Two methods to scan for wireless networks. One requires sudo/root, the other requires Network Manager.

#! /usr/bin/env python
"""This python 2.5 script uses iwlist to scan for nearby wireless networks. It must be run as sudo/root to work."""
import subprocess as SU
command = ['iwlist', 'eth1', 'scan']
output = SU.Popen(command, stdout=SU.PIPE).stdout.readlines()
data = []
for item in output:
    print item.strip()
    if item.strip().startswith('ESSID:'): 
        data.append(item.lstrip(' ESSID:"').rstrip('"\n'))
    if item.strip().startswith('Quality'): 
        data.append(int(item.split()[0].lstrip(' Quality=').rstrip('/100 ')))
    if item.strip().startswith('Encryption key:off'): data.append('OPEN')
    if item.strip().startswith('Encryption key:on'): data.append('encrypted')        
print data


#! /usr/bin/env python
"""This python 2.5 script uses dbus to query Network Manager, which scans regularly for wireless networks. It does NOT require root/sudo."""
import dbus
item = 'org.freedesktop.NetworkManager'
path = '/org/freedesktop/NetworkManager/Devices/eth1'
interface = item + '.Device'

bus = dbus.SystemBus()
data = []
wireless = dbus.Interface(bus.get_object(item, path), interface)
for network_path in wireless.getNetworks():
    network = dbus.Interface(bus.get_object(item, network_path), interface)
    data.append(network.getName())                        # also network.getProperties[1]
    data.append(network.getStrength())                    # also network.getProperties[3]
    if network.getEncrypted(): data.append('encrypted')
    else: data.append('OPEN')
print data

Sunday, March 8, 2009

Using DBus on Xubuntu 8.04

This post is obsolete has been superseded by my 2012 dbus tutorial series.

DBus is a system that permits different applications to exchange information. Tutorial Reference Other Reference.
Sometimes, DBus crashes upon restart from a suspend or hibernation. These bash commands will help you figure out if it has crashed, and how to restart it.

$ps -e | grep `cat /var/run/dbus/pid` # Confirm if DBus is running by checking for the PID number in the list of live processes.
                                      # If DBus is running, this will return the process number. 
                                      # If not, it will return nothing.
$sudo rm /var/run/dbus/pid            # Remove the stale pid file so DBus can be restarted.
$sudo dbus-daemon                     # Start DBus again.


A python script uses DBus to see if the network connection is available by asking Network Manager:

#! /usr/bin/env python
import dbus
bus = dbus.SystemBus()
item = 'org.freedesktop.NetworkManager'
eth0_path = '/org/freedesktop/NetworkManager/Devices/eth0'
eth1_path = '/org/freedesktop/NetworkManager/Devices/eth1'
interface = 'org.freedesktop.NetworkManager.Devices'

# There are two possible network interfaces: eth0 (wired) and eth1 (wireless).
eth0 = dbus.Interface(bus.get_object(item, eth0_path), interface)
if eth0.getLinkActive(): print('The wired network is up') # getLinkActive() is a boolean, TRUE if the network link is active
eth1 = dbus.Interface(bus.get_object(item, eth1_path), interface)
if eth1.getLinkActive(): print('The wireless network is up')

This shell script does exactly the same thing, using the same DBus call:
# This shell script checks Network Manager if the network is up, using dbus as the communications medium.
# There are two possible network interfaces: eth0 (wired) and eth1 (wireless). Of course, you may need to alter these to meet your own circumstances.
# The basic format of dbus-send is: dbus-send --system --dest=org.freedesktop.NetworkManager /org/freedesktop/NetworkManager/Devices/eth0 --print-reply org.freedesktop.NetworkManager.Devices.eth0.getLinkActive
DEST='org.freedesktop.NetworkManager'
PPATH='/org/freedesktop/NetworkManager/Devices'
DEVICE='org.freedesktop.NetworkManager.Devices'

result_eth0=`dbus-send --system --dest=$DEST $PPATH'/eth0' --print-reply $DEVICE'.eth0.getLinkActive'`
shaved_eth0=`echo $result_eth0 | cut -d ' ' -f8`
if [ $shaved_eth0 = 'true' ]; then echo 'The wired network is up'; fi

result_eth1=`dbus-send --system --dest=$DEST $PPATH'/eth1' --print-reply $DEVICE'.eth1.getLinkActive'`
shaved_eth1=`echo $result_eth1 | cut -d ' ' -f8`
if [ $shaved_eth1 = 'true' ]; then echo 'The wireless network is up'; fi


A Python script that queries Network Manager to get the list of wireless networks.

import dbus
item = 'org.freedesktop.NetworkManager'
path = '/org/freedesktop/NetworkManager'
interface = item + '.Device'

network_list = []
bus = dbus.SystemBus()

# Create a Network Manager interface and get the list of network devices
event = dbus.Interface(bus.get_object(item, path), interface)

# Create an interface for each device
# Query each interface to see if it's wireless
# Query each wireless interface for the networks it sees
for device in event.getDevices():
    device_interface = dbus.Interface(bus.get_object(item, device), interface)
    if device_interface.getType() == 2:  # 0 unknown, 1 wired, 2 wireless
        network_list.extend(device_interface.getNetworks())

# Reformat the network names in the list to be more readable
if network_list:
    for entry in network_list:
        #print entry    # String of path/to/network_name
        entry_list = entry.split('/')
        print entry_list[-1]  # String of network_name



A Python listener that catches the changes in wireless signal strength using both available methods.

import dbus, gobject
from dbus.mainloop.glib import DBusGMainLoop

def print_device_strength1(*args):  #Use the received signals
    signal_strength = args[1]
    print ('Signal Strength (Method 1): ' + str(signal_strength) + '%')

def print_device_strength2(*args, **kwargs):  #Use the received signals
    signal_strength = args[1]
    print ('Signal Strength (Method 2): ' + str(signal_strength) + '%')

DBusGMainLoop(set_as_default=True)    # Set up the event loop before connecting to the bus
bus_object = dbus.SystemBus()

# The variables you need. I used the shell command 'dbus-monitor --system' to find this information
sender = 'org.freedesktop.NetworkManager'
path = '/org/freedesktop/NetworkManager'
interface = sender
member = 'DeviceStrengthChanged'

# Method 1 - bus_object.proxy_object.connect_to_signal(method, action, filter, message_parts)
proxy_object = bus_object.get_object(sender, path)
proxy_object.connect_to_signal(member, print_device_strength1, dbus_interface = interface)

# Method 2 - bus_object.add_signal_receiver(action, [filters])
bus_object.add_signal_receiver(print_device_strength2, dbus_interface = interface, member_keyword = member)

# Start the loop
loop = gobject.MainLoop()
loop.run()




Thunar responds beautifully to D-Bus. Introspection is fully set up, so it's easy to use with the d-feet application. Useful for launching programs, opening folders and windows, and manipulating the trash. Launching a program by this method means that the the window manager launches the program, not the script or terminal, so the program can remain open after the script or terminal terminates.

#!/usr/bin/env python
import dbus
item = ('org.xfce.Thunar')
path = ('/org/xfce/FileManager')
interface = ('org.xfce.FileManager')
event = dbus.Interface(dbus.SessionBus().get_object(item, path), interface)

# These three lines at the end of the script open the file's 'properties' window
display = (':0')         # The current session screen
uri = ('/home/me/dbus_test.py')
event.DisplayFileProperties(uri, display)

# These three lines at the end of the script launch a new application
display = (':0')         # The current session screen
uri = ('/usr/bin/gftp-gtk')
event.Launch(uri, display)

# These four lines at the end of the script open a folder window and optionally select a file
display = (':0')         # The current session screen
uri = ('/home/me/.cron')
filename = ('anacrontab.daily')
event.DisplayFolderAndSelect(uri, filename, display)


A sample hal script.

#!/usr/bin/python
"""This python 2.5 script uses dbus to check if the lid switch is open.
Based on an original python script at http://schurger.org/wordpress/?p=49"""
import dbus

dest = 'org.freedesktop.Hal'
hal_path = '/org/freedesktop/Hal/Manager'
hal_interface = 'org.freedesktop.Hal.Manager'
udi_interface = 'org.freedesktop.Hal.Device'

# Get the list of possible input switches. The return is a list of paths.
bus = dbus.SystemBus()
hal = dbus.Interface(bus.get_object(dest, hal_path), hal_interface)
list_of_udi_paths = hal.FindDeviceByCapability('input.switch')

# Filter the list for the word 'lid'. Print the status for each one.
for udi_path in list_of_udi_paths:
    udi = dbus.Interface(bus.get_object(dest, udi_path), udi_interface)
    if udi.GetProperty('button.type') == "lid":
        # The button.state.value is FALSE if the lid is open.
        if udi.GetProperty('button.state.value'): print ('Lid is closed')
        else: print ('Lid is open') 
    else: print ('Problem: I could not find the lid switch. Sorry.')



Notes:

  • The D-feet application is very handy for exploring DBus, and figuring out how to communicate with it. It's available in the Ubuntu repositories.
  • More information on the destination/path/interface settings is available from each application's XML config files, found in the /etc/dbus-1/system.d and /session.d directories.
  • The system-tools-backends DBus interfaces look promising, with methods for network interfaces, time, users and groups, and more. But I couldn't get any of it to work. One hint suggested that the DBus message must be sent by root instead of user.
  • xfce4-terminal has a Launch method, seemingly for launching items in the terminal (source code). I can see how that would be handy, but I couldn't get it to work.

Wednesday, May 28, 2008

Adding python event notifications to the Desktop

Figured out how to get a Python script pop up a notification bubble on the desktop:

It uses the pynotify frontend to libnotify. It's not part of the python 2.5 base, but is included with the Ubuntu default install. Here's a test script for my Ubuntu system. It pops up a little bubble from the system tray

import pygtk
import pynotify

pynotify.init( "Some Application or Title" )
n = pynotify.Notification("Title", "body", "dialog-warning")
n.set_urgency(pynotify.URGENCY_NORMAL)
n.show()

Tip: The .init() call is neccessary, or you'll get a lot of ugly DBUS and GTK errors.

Changing Stuff: Simply replace the elements("Title", "body", "dialog-warning") with your desired title, body, and image path. The image display will take .jpg, .png, .svg, and likely others. For example...


n = pynotify.Notification("Milwaukee Without A Car", "The Python script MWC_Webcrawler has completed a scheduled run. The logfile has been added to your desktop", "/usr/share/icons/Rodent/48x48/apps/gnome-info.png")