Sunday, 30 January 2011

D-Bus Experiments in Vala

Most modern Linux desktop distributions now include D-Bus. It enables different applications in the same user session to communicate with each other or with system services. So I thought I'd experiment with D-Bus using Vala and starting with the published example.

Example 1: Ping Loop

I started with a simple ping client and server based on the example above, the main difference being that I added a loop in the client. The server code takes a message and an integer, prints out the message and the received integer, then adds one to the integer before returning the result:

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

    public int ping (string msg, int i) {
        stdout.printf ("%s [%d]\n", msg, i);
        return i+1;
    }
}

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

void main () {
    Bus.own_name (
        BusType.SESSION, "org.example.Demo",
        BusNameOwnerFlags.NONE,
        on_bus_aquired,
        () => {},
        () => stderr.printf ("Could not aquire name\n"));

    new MainLoop ().run ();
}

server.vala

And the client simply queries the server in a loop every second and prints out the input and output values:

[DBus (name = "org.example.Demo")]
interface DemoClient : Object {
    public abstract int ping (string msg, int i)
        throws IOError;
}

void main () {
    int i = 1;
    int j;
    while(true) {
        try {
            DemoClient client = Bus.get_proxy_sync (
                BusType.SESSION, "org.example.Demo",
                "/org/example/demo");

            j = client.ping ("ping", i);
            stdout.printf ("%d => %d\n", i, j);
            i = j;

        } catch (IOError e) {
            stderr.printf ("%s\n", e.message);
        }
        Thread.usleep(1000000);
    }
}

client.vala

Compiling both programs requires the gio-2.0 package:

$ valac --pkg gio-2.0 server.vala
$ valac --pkg gio-2.0 client.vala

Start the server followed by the client in two separate terminal windows and you should see them exchange data.

Example 2: Graceful Termination

The problem with the code above is that if the server process terminates while the client is still running, an exception occurs and the client doesn't recover. It would be nice if we could get the client to terminate gracefully when the server stops. The Vala GDBus library provides the ability to detect when a service comes up or is brought down using watches. When you setup a watch, you need a callback method for the watch to call. That callback method needs to be called on a different thread than the main client thread. This is handled by the MainLoop class but it means that the core client loop needs to be run in its own thread, which complicates the client code a bit. In the code below, the client code has been encapsulated into a Demo class and the client loop thread is controlled using a simple boolean attribute.

[DBus (name = "org.example.Demo")]
interface DemoClient : Object {
    public abstract int ping (string msg, int i)
        throws IOError;
}

public class Demo : Object {
    private bool server_up = true;
    
    private DemoClient client;
    
    private uint watch;
    
    private MainLoop main_loop;
    
    public Demo() {
        try {
            watch = Bus.watch_name(
                BusType.SESSION,
                "org.example.Demo",
                BusNameWatcherFlags.AUTO_START,
                () => {},
                on_name_vanished
            );
            client = Bus.get_proxy_sync (
                BusType.SESSION,
                "org.example.Demo",
                "/org/example/demo");
            server_up = true;
        } catch (IOError e) {
            stderr.printf ("%s\n", e.message);
            server_up = false;
        }
    }
    
    public void start() {
        main_loop = new MainLoop();
        Thread.create(() => {
            run();
            return null;
        }, false);
        main_loop.run();
    }
    
    public void run() {
        int i = 1;
        int j;
        while(server_up) {
            try {
                j = client.ping ("ping", i);
                stdout.printf ("%d => %d\n", i, j);
                i = j;
            } catch (IOError e) {
                stderr.printf ("%s\n", e.message);
            }
            if (server_up)
                Thread.usleep(1000000);
        }
        main_loop.quit();
    }
    
    void on_name_vanished(DBusConnection conn, string name) {
        stdout.printf ("%s vanished, closing down.\n", name);
        server_up = false;
    }
}

void main () {
    Demo demo = new Demo();
    demo.start();
}

client.vala

The server code is unchanged.

With this version, once the server and client are started, stopping the server through CTRL-C will notify the client which will then stop gracefully. You can even have multiple clients, they will all stop in the same way. The advantage of doing this is that the client has a chance to clean up any resource it holds before stopping.

Example 3: Peers and Service Migration

All of the above is great and we can now design client programs that share a common service. But when dealing with software that is started by the user, the client/server model is not always the best option: you need to ensure that the server is started before any client is and that it is only stopped after the last client has stopped. And what happens is the server fails? What would be really nice is if we could have peers: no difference between client and server, just a single executable that can be run multiple times, the first process to start acts as a server for the other ones and when it stops responsibility for the service is taken over by one of the other processes if any are still running. The last process to terminate closes the service. D-Bus makes it easy to implement for the following reasons:

  • Only one process can own a service;
  • All service calls go via D-Bus so a client process will not notice if the server process actually changes, as long as the service is still available;
  • The client and server processes can be the same process or different processes, it makes no difference to the client.

Therefore, the implementation of the peer program is simple:

  • Start the server, followed by the client and ignore any failure in starting the server: if another process has already started, the service will be available to the client;
  • When the peer is notified of the loss of service, try to start the server but ignore any failure: if another process was notified first, it will have started the server and there will be no interruption in service for the client thread.

We only need a single Vala file to implement the peer:

[DBus (name = "org.example.Demo")]
interface DemoClient : Object {
    public abstract int ping (string msg, int i)
        throws IOError;
}

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

    public int ping (string msg, int i) {
        stdout.printf ("%s [%d]\n", msg, i);
        return i+1;
    }
}

public class Demo : Object {
    private DemoClient client;
    
    private uint watch;
    
    public Demo() {
        // nothing to initialise
    }
    
    public void start() {
        start_server();
        start_client();
        new MainLoop().run();
    }
    
    public void start_client() {
        try {
            watch = Bus.watch_name(
                BusType.SESSION,
                "org.example.Demo",
                BusNameWatcherFlags.AUTO_START,
                on_name_appeared,
                on_name_vanished
            );
            client = Bus.get_proxy_sync (
                BusType.SESSION,
                "org.example.Demo",
                "/org/example/demo");
            Thread.create(() => {
                run_client();
                return null;
            }, false);
        } catch (IOError e) {
            stderr.printf ("Could not create proxy\n");
        }
    }
    
    public void run_client() {
        int i = 1;
        int j;
        while(true) {
            try {
                j = client.ping ("ping", i);
                stdout.printf ("%d => %d\n", i, j);
                i = j;
            } catch (IOError e) {
                stderr.printf ("Could not send message\n");
            }
            Thread.usleep(1000000);
        }
    }
    
    public void start_server() {
        Bus.own_name (
            BusType.SESSION,
            "org.example.Demo",
            BusNameOwnerFlags.NONE,
            on_bus_aquired,
            () => stderr.printf ("Name aquired\n"),
            () => stderr.printf ("Could not aquire name\n"));
    }
    
    void on_name_appeared(DBusConnection conn, string name) {
        stdout.printf ("%s appeared.\n", name);
    }
    
    void on_name_vanished(DBusConnection conn, string name) {
        stdout.printf (
            "%s vanished, attempting to start server.\n",
            name);
        start_server();
    }

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

void main () {
    Demo demo = new Demo();
    demo.start();
}

peer.vala

Start several peers in different terminal windows, at least 3 or 4. You will notice that the first process starts a server thread that all client threads connect to. When the process that provides the service terminates, the next process in the chain takes over the responsibility for the service. And finally, when the last process terminates, the service is closed.

Note that if you want to check D-Bus activity while running the programs in this introduction, you can either install the D-Feet tool or run dbus-monitor from the command line.

9 comments:

Anonymous said...

In the last example you do not need to start the server at the start.
watch_name() is guaranteed to execute either appeared or vanished callback at the start.

Bruno said...

Thanks for the comment: I just tried and you're right, you can remove the start_server()line in the start() function and it still works. Good to know!

Anonymous said...

Hi,

Can't seem to compile the peer.vala and the second client.vala example. The compilation error is:

$ valac --pkg gio-2.0 peer.vala
peer.vala:43.13-43.25: error: cannot infer generic type argument for type parameter `GLib.Thread.create.T'
Thread.create(() => {
^^^^^^^^^^^^^
Compilation failed: 1 error(s), 0 warning(s)

Tried with adding '--thread' to the list of compiler option, but no change.

Maybe the valac compiler (ver. 0.12.0) that I am using is too old? (The system that I am using is a plain vanilla Ubuntu 11.04 installation.)

br,

Bruno said...

It's probably because your valac is newer than the one I used when I wrote this, rather than older. Vala has moved a lot over the past 12 months. I have valac 0.12 on my machine so will try out and report. My guess is that they made the Thread.create method more restrictive in terms of the types you can pass to it.

Javin @ java enum` said...

This is first time I heard about Vala , my ignorant but the solution which you have presented its seems to be longer than in case if I write same program in Java.

Bruno said...

Javin, I don't disagree with you. However, this article is not meant to present a short solution but one that is easy to understand. Also note that I am a rookie in Vala so I may have overlooked some simplification as pointed out in the first comment. Having said this, if you have example code that does inter-process communication over D-Bus in Java, feel free to post it, it'd be interesting to compare how both languages do it.

Bruno said...

Regarding the compilation error, you should correct line 44 to read this:

Thread.create<void*> (() => {

But there now seems to be another problem when starting the client thread the first time you run the peer program so I'll have to investigate further.

Anonymous said...

$ valac --pkg gio-2.0 peer.vala
peer.vala:100.1-100.1: error: syntax error, expected declaration
v[DBus (name = "org.example.Demo")]
^
peer.vala:101.1-101.29: error: The root namespace already contains a definition for `DemoClient'
interface DemoClient : Object {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
peer.vala:2.1-2.29: note: previous definition of `DemoClient' was here
interface DemoClient : Object {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
peer.vala:107.1-107.32: error: The root namespace already contains a definition for `DemoServer'
public class DemoServer : Object {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
peer.vala:8.1-8.32: note: previous definition of `DemoServer' was here
public class DemoServer : Object {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
peer.vala:115.1-115.26: error: The root namespace already contains a definition for `Demo'
public class Demo : Object {
^^^^^^^^^^^^^^^^^^^^^^^^^^
peer.vala:16.1-16.26: note: previous definition of `Demo' was here
public class Demo : Object {
^^^^^^^^^^^^^^^^^^^^^^^^^^
peer.vala:202.2-202.9: error: The root namespace already contains a definition for `main'
}oid main () {
^^^^^^^^
peer.vala:199.1-199.9: note: previous definition of `main' was here
void main () {
^^^^^^^^^
Compilation failed: 5 error(s), 0 warning(s)

Bruno Girin said...

@Anonymous: Vala is a language that evolves very quickly so if you are using a recent version, my code will likely not compile. If I can find the time to debug it I will and will then post updated code here. Although based on your errors, it seems the code got mangled when you copied it.