Debugging help for dbus daemons

2007-12-11

Like many KDE application, strigidaemon uses DBus to talk to other programs. Debugging inter-process communication is never very convenient and strigidaemon is no exception. So far, there are no unit tests for checking the quality of the DBus communication in Strigi. I set about to write some and found it was not so easy, so I'm documenting what I did for the benefit of all the other developers using DBus.

Here's a summary of what is needed to start debugging DBus communication. In the tests, we will start a private dbus-daemon for handling the communication between client and server. In our examples, strigidaemon is the server and we test by sending messages from the client (libstrigiqtdbus) to the server. We will be debugging the code that has not been installed, but resides in the build directory, since this is the normal situation for unit tests.

Step 1: clear the environment

Environment variables like PATH, LD_LIBRARY_PATH, XDG_DATA_DIRS, KDEDIRS all point to your installed software. We will clear them all and use absolute paths to make sure we are debugging the right version of our code. I do not want the unit tests to go wild with my production strigi index!

Clearing the environment can be done by calling unsetenv() for each environment variable. For strigidaemon, we still need HOME to be defined at the moment, so we do not clear that variable.

Step 2: start the dbus-daemon

You can start as many dbus-daemons as you like with dbus-launch. In the unit tests, we start dbus-launch with QProcess. dbus-launch launches dbus-daemon and returns immediately. This will print something like this:

DBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbus-gK3yfCY77n,guid=41b8da09fac5220821e05b00475f0d32 DBUS_SESSION_BUS_PID=6926

These two variables tell your applications how to talk to the dbus-daemon. So we read the output from dbus-launch and pass the these variables in the environment of your unit test process. Since the dbus-daemon has detached from dbus-launch, we need to remember its PID so we can stop our private DBus daemon it after we have finished testing.

Step 3: Start the daemon

We are starting strigidaemon. Because the unit test process has the right environment variables for talking to the daemon, strigidaemon can also do this. After starting strigidaemon we give it one second to become responsive to client calls.

Step 4: Run the tests

Now everything is set up to start testing. We have two processes running: dbus-daemon and strigidaemon. The API for doing the DBus calls is provided by the library libstrigiqtdbus. You could also use introspection to figure the API out, though. QtDBus picks up the connection settings for the private DBus daemon when you ask it for a session connection (QDBusConnection::sessionBus()).

At this point, you have to decide if you want to reuse the server process for all your tests. This is much faster, but could make it more difficult to debug some problems, since the root of the problem you see in one test may lie in a previous test.

Step 5: Stopping the daemons

strigidaemon did not detach, so we can stop it with QProcess::terminate(). To stop the DBus daemon, we call kill(dbuspid, 15), sleep one second and call kill(dbuspid, 9) to make sure dbus-daemon is terminated.

So now we have a way of doing DBus unit tests which takes care of starting and stopping the private DBus daemon and server program. It discards environment information and should not influence your running environment. All of this is achieved without needing a completely separate environment.

#include "config.h"
#include "strigiclient.h"
#include #include #include `

/**
 * Retrieve the environment settings as a QMap.
 **/
QMap
getEnvironment() {
    QMap env;
    foreach (const QString& val, QProcess::systemEnvironment()) {
        int p = val.indexOf('=');
        if (p > 0) {
            env[val.left(p).toUpper()] = val.mid(p+1);
        }
    }
    return env;
}
/**
 * Unset all environment variables except HOME.
 **/
void
clearEnvironment() {
    QMap environment = getEnvironment();
    foreach (const QString& s, environment.keys()) {
        if (s != "HOME") {
            unsetenv(s.toAscii());
        }
    }
}
/**
 * Parse the output from the dbus-launch invocation and set the DBUS
 * environment variable in the environment of the current application.
 **/
int
addDBusToEnvironment(QIODevice& io) {
    QByteArray data = io.readLine();
    int pid = -1;
    while (data.size()) {
        if (data[data.size()-1] == 'n') {
            data.resize(data.size()-1);
        }
        QString val(data);
        int p = val.indexOf('=');
        if (p > 0) {
            QString name = val.left(p).toUpper();
            val = val.mid(p+1);
            if (name == "DBUS_SESSION_BUS_PID") {
                pid = val.toInt();
                setenv(name.toAscii(), val.toAscii(), 1);
            } else if (name == "DBUS_SESSION_BUS_ADDRESS") {
                setenv(name.toAscii(), val.toAscii(), 1);
            }
        }
        data = io.readLine();
    }
    return pid;
}
int
startDBusDaemon() {
    // start the dbus process
    QProcess dbusprocess;
    //dbusprocess.setEnvironment(env);
    QStringList dbusargs;
    dbusprocess.start("/usr/bin/dbus-launch", dbusargs);
    bool ok = dbusprocess.waitForStarted() &&
        dbusprocess.waitForFinished();
    if (!ok) {
        qDebug() << "error starting dbus-launch";
        dbusprocess.kill();
        return -1;
    }

    // add the dbus settings to the environment
    int dbuspid = addDBusToEnvironment(dbusprocess);
    return dbuspid;
}
void
stopDBusDaemon(int dbuspid) {
    // stop the dbus-daemon nicely
    if (dbuspid) kill(dbuspid, 15);
    sleep(1);
    // stop the dbus-daemon harsly (if it is still running)
    if (dbuspid) kill(dbuspid, 9);
}
QProcess*
startStrigiDaemon() {
    QString strigiDaemon = BINARYDIR"/src/daemon/strigidaemon";
   
    QProcess* strigiDaemonProcess = new QProcess();
    QStringList args;
    strigiDaemonProcess->start(strigiDaemon, args);
    strigiDaemonProcess->waitForStarted();
   
    return strigiDaemonProcess;
}
void
stopStrigiDaemon(QProcess* strigiDaemonProcess) {
    strigiDaemonProcess->terminate();
    if (!strigiDaemonProcess->waitForFinished(5000)) {
        qDebug() << "Problem finishing process.";
    }
    //qDebug() << strigiDaemonProcess->readAllStandardError();
    //qDebug() << strigiDaemonProcess->readAllStandardOutput();
    strigiDaemonProcess->close();
    delete strigiDaemonProcess;
}
   
void
    doTests() {
    StrigiClient strigiclient;
    qDebug() << strigiclient.getStatus();
}
int
main() {
    // unset all environment variables except HOME
    clearEnvironment();
   
    // start the required daemons and wait for them to start up
    int dbuspid = startDBusDaemon();
    // set some environment variables so that strigi can find the desired
    // files from the source and build directories
    // This ensures we test the development version, not the installed version
    setenv("XDG_DATA_HOME",
        SOURCEDIR"/src/streamanalyzer/fieldproperties", 1);
    setenv("XDG_DATA_DIRS",
        SOURCEDIR"/src/streamanalyzer/fieldproperties", 1);
    setenv("STRIGI_PLUGIN_PATH", BINARYDIR"/src/luceneindexer/:"
    BINARYDIR"/src/estraierindexer:"BINARYDIR"/src/sqliteindexer", 1);
    QProcess* strigiDaemonProcess = startStrigiDaemon();
    sleep(1);
   
    doTests();
   
    // stop the daemons
    stopStrigiDaemon(strigiDaemonProcess);
    stopDBusDaemon(dbuspid);
    return 0;
}

Comments

Nice

This is becoming important in our integrated environment - thanks for taking the time to work out some best practices. I should come up with some unit tests for Solid::Networking and its NetworkManager backends too.