/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
/* Copyright 2008 litl, LLC. All Rights Reserved. */

#include <config.h>

#include "dbus.h"

#include <dbus/dbus-glib.h>
#include <dbus/dbus-glib-lowlevel.h>

#include <string.h>
#include <stdlib.h>

#include "dbus-private.h"
#include "dbus-proxy.h"
#include "log.h"
#include "glib.h"

typedef struct
{
    const BigDBusConnectFuncs* funcs;
    void* data;
    unsigned int opened : 1;
} ConnectFuncs;

typedef enum {
    NAME_NOT_REQUESTED,
    NAME_PRIMARY_OWNER,
    NAME_IN_QUEUE,
    NAME_NOT_OWNED
} NameOwnershipState;

typedef struct
{
    char* name;
    const BigDBusJsonMethod* methods;
    int n_methods;
} BigJsonIface;

typedef struct
{
    DBusBusType bus_type;
    /* If prev_state != state then we may need to notify */
    NameOwnershipState prev_state;
    NameOwnershipState state;
    const BigDBusNameOwnerFuncs* funcs;
    void* data;
    unsigned int id;
} BigNameOwnershipMonitor;

typedef struct
{
    char* name;
    char* current_owner;
    GSList* watchers;
} BigNameWatch;

typedef struct
{
    BigDBusWatchNameFlags flags;
    const BigDBusWatchNameFuncs* funcs;
    void* data;
    DBusBusType bus_type;
    BigNameWatch* watch;
    guint notify_idle;
    int refcount;
    guint destroyed : 1;
} BigNameWatcher;

typedef struct
{
    DBusBusType bus_type;
    char* name;
    BigNameWatcher* watcher;
} BigPendingNameWatcher;

static DBusConnection* session_bus_weak_ref = NULL;
static GSList* session_bus_weak_refs = NULL;
static DBusConnection* system_bus_weak_ref = NULL;
static GSList* system_bus_weak_refs = NULL;
static guint session_connect_idle_id = 0;
static guint system_connect_idle_id = 0;
static GSList* all_connect_funcs = NULL;

static GSList* pending_name_ownership_monitors = NULL;
static GSList* pending_name_watchers = NULL;

#define BIG_DBUS_NAME_OWNER_MONITOR_INVALID_ID 0

static unsigned int global_monitor_id = 0;

static DBusHandlerResult disconnect_filter_message(DBusConnection* connection,
                                                   DBusMessage* message,
                                                   void* data);
static DBusHandlerResult name_ownership_monitor_filter_message(
  DBusConnection* connection,
  DBusMessage* message,
  void* data);
static void process_name_ownership_monitors(DBusConnection* connection,
                                            BigDBusInfo* info);
static void name_watch_remove_watcher(BigNameWatch* watch,
                                      BigNameWatcher* watcher);
static DBusHandlerResult name_watch_filter_message(DBusConnection* connection,
                                                   DBusMessage* message,
                                                   void* data);
static void process_pending_name_watchers(DBusConnection* connection,
                                          BigDBusInfo* info);
static void json_iface_free(BigJsonIface* iface);
static void info_free(BigDBusInfo* info);
static gboolean notify_watcher_name_appeared(gpointer data);

static dbus_int32_t info_slot = -1;
BigDBusInfo*
_big_dbus_ensure_info(DBusConnection* connection)
{
    BigDBusInfo* info;

    dbus_connection_allocate_data_slot(&info_slot);

    info = dbus_connection_get_data(connection, info_slot);

    if (info == NULL) {
        info = g_slice_new0(BigDBusInfo);

        info->where_connection_was = connection;

        if (connection == session_bus_weak_ref)
            info->bus_type = DBUS_BUS_SESSION;
        else if (connection == system_bus_weak_ref)
            info->bus_type = DBUS_BUS_SYSTEM;
        else
            g_error("Unknown bus type opened in %s", __FILE__);

        info->json_ifaces = g_hash_table_new_full(g_str_hash, g_str_equal, NULL,
                                                  (GFreeFunc) json_iface_free);
        info->name_watches = g_hash_table_new(g_str_hash, g_str_equal);
        dbus_connection_set_data(connection, info_slot, info,
                                 (DBusFreeFunction) info_free);

        dbus_connection_add_filter(connection,
                                   name_ownership_monitor_filter_message, NULL,
                                   NULL);
        dbus_connection_add_filter(connection, name_watch_filter_message, NULL,
                                   NULL);
        dbus_connection_add_filter(connection,
                                   _big_dbus_signal_watch_filter_message, NULL,
                                   NULL);

        /* Important: disconnect_filter_message() must be LAST so
         * it runs last when the disconnect message arrives.
         */
        dbus_connection_add_filter(connection, disconnect_filter_message, NULL,
                                   NULL);

        /* caution, this could get circular if proxy_new() goes back around
         * and tries to use dbus.c - but we'll fix it when it happens.
         * Also, this refs the connection ...
         */
        info->driver_proxy
          = big_dbus_proxy_new(connection, DBUS_SERVICE_DBUS, DBUS_PATH_DBUS,
                               DBUS_INTERFACE_DBUS);
    }

    return info;
}

void
_big_dbus_dispose_info(DBusConnection* connection)
{
    BigDBusInfo* info;

    if (info_slot < 0)
        return;

    info = dbus_connection_get_data(connection, info_slot);

    if (info != NULL) {

        big_debug(BIG_DEBUG_UTIL_DBUS, "Disposing info on connection %p",
                  connection);

        /* the driver proxy refs the connection, we want
         * to break that cycle.
         */
        g_object_unref(info->driver_proxy);
        info->driver_proxy = NULL;

        dbus_connection_set_data(connection, info_slot, NULL, NULL);

        dbus_connection_free_data_slot(&info_slot);
    }
}

DBusConnection*
_big_dbus_get_weak_ref(DBusBusType which_bus)
{
    if (which_bus == DBUS_BUS_SESSION) {
        return session_bus_weak_ref;
    } else if (which_bus == DBUS_BUS_SYSTEM) {
        return system_bus_weak_ref;
    }

    g_assert_not_reached();
    return NULL;
}

static DBusHandlerResult
disconnect_filter_message(DBusConnection* connection,
                          DBusMessage* message,
                          void* data)
{
    /* We should be running after all other filters */
    if (dbus_message_is_signal(message, DBUS_INTERFACE_LOCAL, "Disconnected")) {
        big_debug(BIG_DEBUG_UTIL_DBUS, "Disconnected in %s", G_STRFUNC);

        _big_dbus_dispose_info(connection);

        if (session_bus_weak_ref == connection)
            session_bus_weak_ref = NULL;

        if (system_bus_weak_ref == connection)
            system_bus_weak_ref = NULL;
    }

    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

static DBusConnection*
try_connecting(DBusBusType which_bus)
{

    DBusGConnection* gconnection;
    DBusConnection* connection;
    GError* error;

    connection = _big_dbus_get_weak_ref(which_bus);
    if (connection != NULL)
        return connection;

    big_debug(BIG_DEBUG_UTIL_DBUS, "trying to connect to message bus");

    error = NULL;
    gconnection = dbus_g_bus_get(which_bus, &error);
    if (gconnection == NULL) {
        big_debug(BIG_DEBUG_UTIL_DBUS, "bus connection failed: %s",
                  error->message);
        g_error_free(error);
        return NULL;
    }

    connection = dbus_g_connection_get_connection(gconnection);

    /* Disable this because all our apps will be well-behaved! */
    dbus_connection_set_exit_on_disconnect(connection, FALSE);

    if (which_bus == DBUS_BUS_SESSION && session_bus_weak_ref == NULL) {
        GSList* l;
        session_bus_weak_ref = connection;
        for (l = session_bus_weak_refs; l != NULL; l = l->next) {
            DBusConnection** connection_p = l->data;
            *connection_p = session_bus_weak_ref;
        }
    } else if (which_bus == DBUS_BUS_SYSTEM && system_bus_weak_ref == NULL) {
        GSList* l;
        system_bus_weak_ref = connection;
        for (l = system_bus_weak_refs; l != NULL; l = l->next) {
            DBusConnection** connection_p = l->data;
            *connection_p = system_bus_weak_ref;
        }
    }

    dbus_g_connection_unref(gconnection); /* rely on libdbus holding a ref */

    big_debug(BIG_DEBUG_UTIL_DBUS, "Successfully connected");

    return connection;
}

static gboolean
connect_idle(void* data)
{
    GSList* l;
    DBusConnection* connection;
    BigDBusInfo* info;
    DBusBusType bus_type;

    bus_type = GPOINTER_TO_INT(data);

    if (bus_type == DBUS_BUS_SESSION)
        session_connect_idle_id = 0;
    else if (bus_type == DBUS_BUS_SYSTEM)
        system_connect_idle_id = 0;
    else
        g_assert_not_reached();

    big_debug(BIG_DEBUG_UTIL_DBUS,
              "connection idle with %d connect listeners to traverse",
              g_slist_length(all_connect_funcs));

    connection = try_connecting(bus_type);
    if (connection == NULL) {
        if (bus_type == DBUS_BUS_SESSION) {
            g_printerr("Lost connection to session bus, exiting\n");
            exit(1);
        } else {
            /* Here it would theoretically make sense to reinstall the
             * idle as a timeout or something, but we don't for now,
             * just wait for something to trigger a reconnect. It is
             * not a situation that should happen in reality (we won't
             * restart the system bus without rebooting).
             */
        }
        return FALSE;
    }

    info = _big_dbus_ensure_info(connection);

    /* We first need to call AddMatch on all signal watchers.
     * This is so if on connect, the app calls methods to get
     * the state the signal notifies the app of changes in,
     * the match rule is added before the "get current state"
     * methods are called. Otherwise there's a race where
     * a signal can be missed between a "get current state" method
     * call reply and the AddMatch.
     */
    _big_dbus_process_pending_signal_watchers(connection, info);

    /* We want the app to see notification of connection opening,
     * THEN other notifications, so notify it's open first.
     */

    for (l = all_connect_funcs; l != NULL; l = l->next) {
        ConnectFuncs* f;
        f = l->data;

        if (!f->opened && f->funcs->which_bus == bus_type) {
            f->opened = TRUE;
            (*f->funcs->opened)(connection, f->data);
        }
    }

    /* These two invoke application callbacks, unlike
     * _big_dbus_process_pending_signal_watchers(), so should come after
     * the above calls to the "connection opened" callbacks.
     */

    process_name_ownership_monitors(connection, info);

    process_pending_name_watchers(connection, info);

    return FALSE;
}

void
_big_dbus_ensure_connect_idle(DBusBusType bus_type)
{
    if (bus_type == DBUS_BUS_SESSION) {
        if (session_connect_idle_id == 0) {
            session_connect_idle_id
              = g_idle_add(connect_idle, GINT_TO_POINTER(bus_type));
        }
    } else if (bus_type == DBUS_BUS_SYSTEM) {
        if (system_connect_idle_id == 0) {
            system_connect_idle_id
              = g_idle_add(connect_idle, GINT_TO_POINTER(bus_type));
        }
    } else {
        g_assert_not_reached();
    }
}

static void
internal_add_connect_funcs(const BigDBusConnectFuncs* funcs,
                           void* data,
                           gboolean sync_notify)
{
    ConnectFuncs* f;

    f = g_slice_new0(ConnectFuncs);
    f->funcs = funcs;
    f->data = data;
    f->opened = FALSE;

    all_connect_funcs = g_slist_prepend(all_connect_funcs, f);

    _big_dbus_ensure_connect_idle(f->funcs->which_bus);

    if (sync_notify) {
        /* sync_notify means IF we are already connected
         * (we have a weak ref != NULL) then notify
         * right away before we return.
         */
        DBusConnection* connection;

        connection = _big_dbus_get_weak_ref(f->funcs->which_bus);

        if (connection != NULL && !f->opened) {
            f->opened = TRUE;
            (*f->funcs->opened)(connection, f->data);
        }
    }
}

/* this should guarantee that the funcs are only called async, which is why
 * it does not try_connecting right away; the idea is to defer to inside the
 * main loop.
 */
void
big_dbus_add_connect_funcs(const BigDBusConnectFuncs* funcs, void* data)
{
    internal_add_connect_funcs(funcs, data, FALSE);
}

/* The sync_notify flavor calls the open notification right away if
 * we are already connected.
 */
void
big_dbus_add_connect_funcs_sync_notify(const BigDBusConnectFuncs* funcs,
                                       void* data)
{
    internal_add_connect_funcs(funcs, data, TRUE);
}

void
big_dbus_remove_connect_funcs(const BigDBusConnectFuncs* funcs, void* data)
{
    ConnectFuncs* f;
    GSList* l;

    f = NULL;
    for (l = all_connect_funcs; l != NULL; l = l->next) {
        f = l->data;

        if (f->funcs == funcs && f->data == data)
            break;
    }

    if (l == NULL) {
        g_warning("Could not find functions matching %p %p", funcs, data);
        return;
    }
    g_assert(l->data == f);

    all_connect_funcs = g_slist_delete_link(all_connect_funcs, l);
    g_slice_free(ConnectFuncs, f);
}

void
big_dbus_add_bus_weakref(DBusBusType which_bus, DBusConnection** connection_p)
{
    if (which_bus == DBUS_BUS_SESSION) {
        *connection_p = session_bus_weak_ref;
        session_bus_weak_refs
          = g_slist_prepend(session_bus_weak_refs, connection_p);
    } else if (which_bus == DBUS_BUS_SYSTEM) {
        *connection_p = system_bus_weak_ref;
        system_bus_weak_refs
          = g_slist_prepend(system_bus_weak_refs, connection_p);
    } else {
        g_assert_not_reached();
    }

    _big_dbus_ensure_connect_idle(which_bus);
}

void
big_dbus_remove_bus_weakref(DBusBusType which_bus,
                            DBusConnection** connection_p)
{
    if (which_bus == DBUS_BUS_SESSION) {
        *connection_p = NULL;
        session_bus_weak_refs
          = g_slist_remove(session_bus_weak_refs, connection_p);
    } else if (which_bus == DBUS_BUS_SYSTEM) {
        *connection_p = NULL;
        system_bus_weak_refs
          = g_slist_remove(system_bus_weak_refs, connection_p);
    } else {
        g_assert_not_reached();
    }
}

void
big_dbus_try_connecting_now(DBusBusType which_bus)
{
    try_connecting(which_bus);
}

static BigJsonIface*
json_iface_new(const char* name,
               const BigDBusJsonMethod* methods,
               int n_methods)
{
    BigJsonIface* iface;

    iface = g_slice_new0(BigJsonIface);
    iface->name = g_strdup(name);
    iface->methods = methods;
    iface->n_methods = n_methods;

    return iface;
}

static void
json_iface_free(BigJsonIface* iface)
{
    g_free(iface->name);
    g_slice_free(BigJsonIface, iface);
}

static BigNameOwnershipMonitor*
name_ownership_monitor_new(DBusBusType bus_type,
                           const BigDBusNameOwnerFuncs* funcs,
                           void* data)
{
    BigNameOwnershipMonitor* monitor;

    monitor = g_slice_new0(BigNameOwnershipMonitor);
    monitor->bus_type = bus_type;
    monitor->prev_state = NAME_NOT_REQUESTED;
    monitor->state = NAME_NOT_REQUESTED;
    monitor->funcs = funcs;
    monitor->data = data;
    monitor->id = ++global_monitor_id;

    return monitor;
}

static void
name_ownership_monitor_free(BigNameOwnershipMonitor* monitor)
{

    g_slice_free(BigNameOwnershipMonitor, monitor);
}

static BigNameWatch*
name_watch_new(const char* name)
{
    BigNameWatch* watch;

    watch = g_slice_new0(BigNameWatch);
    watch->name = g_strdup(name);

    /* For unique names, we assume the owner is itself,
     * so we default to "exists" and maybe emit "vanished",
     * while with well-known names we do the opposite.
     */
    if (*watch->name == ':') {
        watch->current_owner = g_strdup(watch->name);
    }

    return watch;
}

static void
name_watch_free(BigNameWatch* watch)
{
    g_assert(watch->watchers == NULL);

    g_free(watch->name);
    g_free(watch->current_owner);
    g_slice_free(BigNameWatch, watch);
}

static BigNameWatcher*
name_watcher_new(BigDBusWatchNameFlags flags,
                 const BigDBusWatchNameFuncs* funcs,
                 void* data,
                 DBusBusType bus_type)
{
    BigNameWatcher* watcher;

    watcher = g_slice_new0(BigNameWatcher);
    watcher->flags = flags;
    watcher->funcs = funcs;
    watcher->data = data;
    watcher->bus_type = bus_type;
    watcher->watch = NULL;
    watcher->refcount = 1;

    return watcher;
}

static void
name_watcher_ref(BigNameWatcher* watcher)
{
    watcher->refcount += 1;
}

static void
name_watcher_unref(BigNameWatcher* watcher)
{
    watcher->refcount -= 1;

    if (watcher->refcount == 0)
        g_slice_free(BigNameWatcher, watcher);
}

static void
info_free(BigDBusInfo* info)
{
    void* key;
    void* value;

    big_debug(BIG_DEBUG_UTIL_DBUS,
              "Destroy notify invoked on bus connection info for %p",
              info->where_connection_was);

    if (info->where_connection_was == session_bus_weak_ref)
        session_bus_weak_ref = NULL;

    if (info->where_connection_was == system_bus_weak_ref)
        system_bus_weak_ref = NULL;

    /* This could create some strange re-entrancy so do it first.
     * If we processed a disconnect message, this should have been done
     * already at that time, but if we were finalized without that,
     * it may not have been.
     */
    if (info->driver_proxy != NULL) {
        g_object_unref(info->driver_proxy);
        info->driver_proxy = NULL;
    }

    while (info->name_ownership_monitors != NULL) {
        name_ownership_monitor_free(info->name_ownership_monitors->data);
        info->name_ownership_monitors
          = g_slist_remove(info->name_ownership_monitors,
                           info->name_ownership_monitors->data);
    }

    while ((value = g_hash_table_lookup(info->name_watches, &key))) {
        BigNameWatch* watch = value;

        while (watch->watchers) {
            name_watch_remove_watcher(watch, watch->watchers->data);
        }

        name_watch_free(watch);
        g_hash_table_steal(info->name_watches, &key);
    }

    if (info->signal_watchers_by_unique_sender) {
        g_hash_table_destroy(info->signal_watchers_by_unique_sender);
    }

    if (info->signal_watchers_by_path) {
        g_hash_table_destroy(info->signal_watchers_by_path);
    }

    if (info->signal_watchers_by_iface) {
        g_hash_table_destroy(info->signal_watchers_by_iface);
    }

    if (info->signal_watchers_by_signal) {
        g_hash_table_destroy(info->signal_watchers_by_signal);
    }

    g_hash_table_destroy(info->name_watches);
    g_hash_table_destroy(info->json_ifaces);
    g_slice_free(BigDBusInfo, info);
}

static DBusHandlerResult
name_ownership_monitor_filter_message(DBusConnection* connection,
                                      DBusMessage* message,
                                      void* data)
{
    BigDBusInfo* info;
    gboolean states_changed;

    info = _big_dbus_ensure_info(connection);

    states_changed = FALSE;

    if (dbus_message_is_signal(message, DBUS_INTERFACE_DBUS, "NameLost")
        && dbus_message_has_sender(message, DBUS_SERVICE_DBUS)) {
        const char* name = NULL;
        if (dbus_message_get_args(message, NULL, DBUS_TYPE_STRING, &name,
                                  DBUS_TYPE_INVALID)) {
            GSList* l;

            big_debug(BIG_DEBUG_UTIL_DBUS, "Lost name %s", name);

            for (l = info->name_ownership_monitors; l != NULL; l = l->next) {
                BigNameOwnershipMonitor* monitor;

                monitor = l->data;

                if (monitor->state == NAME_PRIMARY_OWNER
                    && strcmp(name, monitor->funcs->name) == 0) {
                    monitor->prev_state = monitor->state;
                    monitor->state = NAME_NOT_OWNED;
                    states_changed = TRUE;
                    /* keep going, don't break, there may be more matches */
                }
            }
        } else {
            big_debug(BIG_DEBUG_UTIL_DBUS, "NameLost has wrong arguments???");
        }
    } else if (dbus_message_is_signal(message, DBUS_INTERFACE_DBUS,
                                      "NameAcquired")
               && dbus_message_has_sender(message, DBUS_SERVICE_DBUS)) {
        const char* name = NULL;
        if (dbus_message_get_args(message, NULL, DBUS_TYPE_STRING, &name,
                                  DBUS_TYPE_INVALID)) {
            GSList* l;

            big_debug(BIG_DEBUG_UTIL_DBUS, "Acquired name %s", name);

            for (l = info->name_ownership_monitors; l != NULL; l = l->next) {
                BigNameOwnershipMonitor* monitor;

                monitor = l->data;

                if (monitor->state != NAME_PRIMARY_OWNER
                    && strcmp(name, monitor->funcs->name) == 0) {
                    monitor->prev_state = monitor->state;
                    monitor->state = NAME_PRIMARY_OWNER;
                    states_changed = TRUE;
                    /* keep going, don't break, there may be more matches */
                }
            }
        } else {
            big_debug(BIG_DEBUG_UTIL_DBUS,
                      "NameAcquired has wrong arguments???");
        }
    } else if (dbus_message_is_signal(message, DBUS_INTERFACE_LOCAL,
                                      "Disconnected")) {
        GSList* l;

        big_debug(BIG_DEBUG_UTIL_DBUS, "Disconnected in %s", G_STRFUNC);

        for (l = info->name_ownership_monitors; l != NULL; l = l->next) {
            BigNameOwnershipMonitor* monitor;

            monitor = l->data;

            if (monitor->state != NAME_NOT_REQUESTED) {
                /* Set things up to re-request the name */
                monitor->prev_state = monitor->state;
                monitor->state = NAME_NOT_REQUESTED;
                states_changed = TRUE;
            }
        }

        /* FIXME move the monitors back to the pending list so they'll be found
         * on reconnect */
    }

    if (states_changed)
        process_name_ownership_monitors(connection, info);

    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

static void
process_name_ownership_monitors(DBusConnection* connection, BigDBusInfo* info)
{
    GSList* l;
    gboolean connected;
    GSList* still_pending;

    /* First pull anything out of pending queue */

    still_pending = NULL;
    while (pending_name_ownership_monitors != NULL) {
        BigNameOwnershipMonitor* monitor;

        monitor = pending_name_ownership_monitors->data;
        pending_name_ownership_monitors
          = g_slist_remove(pending_name_ownership_monitors,
                           pending_name_ownership_monitors->data);

        if (monitor->bus_type == info->bus_type) {
            info->name_ownership_monitors
              = g_slist_prepend(info->name_ownership_monitors, monitor);
        } else {
            still_pending = g_slist_prepend(still_pending, monitor);
        }
    }
    g_assert(pending_name_ownership_monitors == NULL);
    pending_name_ownership_monitors = still_pending;

    /* Now send notifications to the app */

    connected = dbus_connection_get_is_connected(connection);

    if (connected) {
        for (l = info->name_ownership_monitors; l != NULL; l = l->next) {
            BigNameOwnershipMonitor* monitor;

            monitor = l->data;

            if (monitor->state == NAME_NOT_REQUESTED) {
                int result;
                unsigned int flags;
                DBusError derror;

                flags = DBUS_NAME_FLAG_ALLOW_REPLACEMENT;
                if (monitor->funcs->type == BIG_DBUS_NAME_SINGLE_INSTANCE)
                    flags |= DBUS_NAME_FLAG_DO_NOT_QUEUE;

                dbus_error_init(&derror);
                result = dbus_bus_request_name(connection, monitor->funcs->name,
                                               flags, &derror);

                /* log 'error' word only when one occurred */
                if (derror.message != NULL) {
                    big_debug(BIG_DEBUG_UTIL_DBUS,
                              "Requested name %s result %d error %s",
                              monitor->funcs->name, result, derror.message);
                } else {
                    big_debug(BIG_DEBUG_UTIL_DBUS,
                              "Requested name %s result %d",
                              monitor->funcs->name, result);
                }

                dbus_error_free(&derror);

                /* An important feature of this code is that we always
                 * transition from NOT_REQUESTED to something else when
                 * a name monitor is first added, so we always notify
                 * the app either "acquired" or "lost" and don't
                 * leave the app in limbo.
                 *
                 * This means the app can "get going" when it gets the name
                 * and exit when it loses it, and that will just work
                 * since one or the other will always happen on startup.
                 */

                monitor->prev_state = monitor->state;

                if (result == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER
                    || result == DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER) {
                    monitor->state = NAME_PRIMARY_OWNER;
                } else if (result == DBUS_REQUEST_NAME_REPLY_IN_QUEUE) {
                    monitor->state = NAME_IN_QUEUE;
                } else if (result == DBUS_REQUEST_NAME_REPLY_EXISTS) {
                    monitor->state = NAME_NOT_OWNED;
                } else {
                    /* reply code we don't understand? */
                    monitor->state = NAME_NOT_OWNED;
                }
            }
        }
    }

    /* Do notifications with a list copy for extra safety
     * (for true safety we also need to refcount each monitor
     * and have a "destroyed" flag)
     */
    l = g_slist_copy(info->name_ownership_monitors);
    while (l != NULL) {
        BigNameOwnershipMonitor* monitor;

        monitor = l->data;
        l = g_slist_remove(l, l->data);

        if (monitor->prev_state != monitor->state) {
            monitor->prev_state = monitor->state;

            if (monitor->state == NAME_PRIMARY_OWNER) {
                big_debug(BIG_DEBUG_UTIL_DBUS, "Notifying acquired %s",
                          monitor->funcs->name);
                (*monitor->funcs->acquired)(connection, monitor->funcs->name,
                                            monitor->data);
            } else if (monitor->state != NAME_PRIMARY_OWNER) {
                big_debug(BIG_DEBUG_UTIL_DBUS, "Notifying lost %s",
                          monitor->funcs->name);
                (*monitor->funcs->lost)(connection, monitor->funcs->name,
                                        monitor->data);
            }
        }
    }
}

unsigned int
big_dbus_acquire_name(DBusBusType bus_type,
                      const BigDBusNameOwnerFuncs* funcs,
                      void* data)
{
    BigNameOwnershipMonitor* monitor;

    monitor = name_ownership_monitor_new(bus_type, funcs, data);
    pending_name_ownership_monitors
      = g_slist_prepend(pending_name_ownership_monitors, monitor);

    _big_dbus_ensure_connect_idle(bus_type);

    return monitor->id;
}

static void
release_name_internal(DBusBusType bus_type,
                      const BigDBusNameOwnerFuncs* funcs,
                      void* data,
                      unsigned int id)
{
    BigDBusInfo* info;
    GSList* l;
    BigNameOwnershipMonitor* monitor;
    DBusConnection* connection;

    connection = _big_dbus_get_weak_ref(bus_type);
    if (!connection)
        return;

    info = _big_dbus_ensure_info(connection);

    /* Check first pending list */
    for (l = pending_name_ownership_monitors; l; l = l->next) {
        monitor = l->data;
        /* If the id is valid an matches, we are done */
        if (monitor->state == NAME_PRIMARY_OWNER
            && ((id != BIG_DBUS_NAME_OWNER_MONITOR_INVALID_ID
                 && monitor->id == id)
                || (monitor->funcs == funcs && monitor->data == data))) {
            dbus_bus_release_name(connection, monitor->funcs->name, NULL);
            pending_name_ownership_monitors
              = g_slist_remove(pending_name_ownership_monitors, monitor);
            name_ownership_monitor_free(monitor);
            /* If the monitor was in the pending list it
             * can't be in the processed list
             */
            return;
        }
    }

    for (l = info->name_ownership_monitors; l; l = l->next) {
        monitor = l->data;
        /* If the id is valid an matches, we are done */
        if (monitor->state == NAME_PRIMARY_OWNER
            && ((id != BIG_DBUS_NAME_OWNER_MONITOR_INVALID_ID
                 && monitor->id == id)
                || (monitor->funcs == funcs && monitor->data == data))) {
            dbus_bus_release_name(connection, monitor->funcs->name, NULL);
            info->name_ownership_monitors
              = g_slist_remove(info->name_ownership_monitors, monitor);
            name_ownership_monitor_free(monitor);
            break;
        }
    }
}

void
big_dbus_release_name_by_id(DBusBusType bus_type, unsigned int id)
{
    release_name_internal(bus_type, NULL, NULL, id);
}

void
big_dbus_release_name(DBusBusType bus_type,
                      const BigDBusNameOwnerFuncs* funcs,
                      void* data)
{
    release_name_internal(bus_type, funcs, data,
                          BIG_DBUS_NAME_OWNER_MONITOR_INVALID_ID);
}

static void
notify_name_owner_changed(DBusConnection* connection,
                          const char* name,
                          const char* new_owner)
{
    BigDBusInfo* info;
    BigNameWatch* watch;
    GSList *l, *watchers;
    gchar* old_owner;

    info = _big_dbus_ensure_info(connection);

    if (*new_owner == '\0')
        new_owner = NULL;

    watch = g_hash_table_lookup(info->name_watches, name);

    if (watch == NULL)
        return;

    if ((watch->current_owner == new_owner)
        || (watch->current_owner && new_owner
            && strcmp(watch->current_owner, new_owner) == 0)) {
        /* No change */
        return;
    }

    /* we copy the list before iterating, because the
     * callbacks may modify it */
    watchers = g_slist_copy(watch->watchers);
    g_slist_foreach(watchers, (GFunc) name_watcher_ref, NULL);

    /* copy the old owner in case the watch is removed in
     * the callbacks */
    old_owner = g_strdup(watch->current_owner);

    /* vanish the old owner */
    if (old_owner != NULL) {
        for (l = watchers; l != NULL; l = l->next) {
            BigNameWatcher* watcher = l->data;

            if (watcher->notify_idle != 0) {
                /* Name owner changed before we notified
                 * the watcher of the initial name. We will notify
                 * him now of the old name, then that this name
                 * vanished.
                 *
                 * This is better than not sending calling any
                 * callback, it might for instance trigger destroying
                 * signal watchers on the unique name.
                 */
                g_source_remove(watcher->notify_idle);
                notify_watcher_name_appeared(watcher);
            }

            if (!watcher->destroyed) {
                (*watcher->funcs->vanished)(connection, name, old_owner,
                                            watcher->data);
            }
        }
    }

    /* lookup for the watch again, since it might have vanished
     * if all watchers were removed in the watcher->vanished
     * callbacks */
    watch = g_hash_table_lookup(info->name_watches, name);

    if (watch) {
        g_free(watch->current_owner);
        watch->current_owner = g_strdup(new_owner);
    }

    /* appear the new owner */
    if (new_owner != NULL) {
        for (l = watchers; l != NULL; l = l->next) {
            BigNameWatcher* watcher = l->data;

            if (!watcher->destroyed) {
                (*watcher->funcs->appeared)(connection, name, new_owner,
                                            watcher->data);
            }
        }
    }

    /* now destroy our copy */
    g_slist_foreach(watchers, (GFunc) name_watcher_unref, NULL);
    g_slist_free(watchers);

    g_free(old_owner);
}

static DBusHandlerResult
name_watch_filter_message(DBusConnection* connection,
                          DBusMessage* message,
                          void* data)
{
    BigDBusInfo* info;

    info = _big_dbus_ensure_info(connection);

    if (dbus_message_is_signal(message, DBUS_INTERFACE_DBUS, "NameOwnerChanged")
        && dbus_message_has_sender(message, DBUS_SERVICE_DBUS)) {
        const char* name = NULL;
        const char* old_owner = NULL;
        const char* new_owner = NULL;
        if (dbus_message_get_args(message, NULL, DBUS_TYPE_STRING, &name,
                                  DBUS_TYPE_STRING, &old_owner,
                                  DBUS_TYPE_STRING, &new_owner,
                                  DBUS_TYPE_INVALID)) {
            big_debug(BIG_DEBUG_UTIL_DBUS, "NameOwnerChanged %s:   %s -> %s",
                      name, old_owner, new_owner);

            notify_name_owner_changed(connection, name, new_owner);
        } else {
            big_debug(BIG_DEBUG_UTIL_DBUS,
                      "NameOwnerChanged has wrong arguments???");
        }
    } else if (dbus_message_is_signal(message, DBUS_INTERFACE_LOCAL,
                                      "Disconnected")) {

        big_debug(BIG_DEBUG_UTIL_DBUS, "Disconnected in %s", G_STRFUNC);

        /* FIXME set all current owners to NULL, and move watches back to the
         * pending
         * list so they are found on reconnect.
         */
    }

    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

void
_big_dbus_set_matching_name_owner_changed(DBusConnection* connection,
                                          const char* bus_name,
                                          gboolean matched)
{
    char* s;

    big_debug(BIG_DEBUG_UTIL_DBUS, "%s NameOwnerChanged on name '%s'",
              matched ? "Matching" : "No longer matching", bus_name);

    s = g_strdup_printf("type='signal',sender='" DBUS_SERVICE_DBUS
                        "',interface='" DBUS_INTERFACE_DBUS "',member='"
                        "NameOwnerChanged"
                        "',arg0='%s'",
                        bus_name);

    if (matched)
        dbus_bus_add_match(connection, s,
                           NULL); /* asking for error would make this block */
    else
        dbus_bus_remove_match(connection, s, NULL);

    g_free(s);
}

static void
on_start_service_reply(BigDBusProxy* proxy, DBusMessage* message, void* data)
{
    big_debug(BIG_DEBUG_UTIL_DBUS, "Got successful reply to service start");
}

static void
on_start_service_error(BigDBusProxy* proxy,
                       const char* error_name,
                       const char* error_message,
                       void* data)
{
    big_debug(BIG_DEBUG_UTIL_DBUS, "Got error starting service: %s: %s",
              error_name, error_message);
}

void
big_dbus_start_service(DBusConnection* connection, const char* name)
{
    DBusMessage* message;
    dbus_uint32_t flags;
    BigDBusInfo* info;

    big_debug(BIG_DEBUG_UTIL_DBUS, "Starting service '%s'", name);

    info = _big_dbus_ensure_info(connection);

    message = big_dbus_proxy_new_method_call(info->driver_proxy,
                                             "StartServiceByName");

    flags = 0;
    if (dbus_message_append_args(message, DBUS_TYPE_STRING, &name,
                                 DBUS_TYPE_UINT32, &flags, DBUS_TYPE_INVALID)) {
        big_dbus_proxy_send(info->driver_proxy, message, on_start_service_reply,
                            on_start_service_error, NULL);
    } else {
        big_debug(BIG_DEBUG_UTIL_DBUS,
                  "No memory appending args to StartServiceByName");
    }

    dbus_message_unref(message);
}

typedef struct
{
    DBusConnection* connection;
    char* name;
    BigDBusWatchNameFlags flags;
} GetOwnerRequest;

static GetOwnerRequest*
get_owner_request_new(DBusConnection* connection,
                      const char* name,
                      BigDBusWatchNameFlags flags)
{
    GetOwnerRequest* gor;

    gor = g_slice_new0(GetOwnerRequest);
    gor->connection = connection;
    gor->name = g_strdup(name);
    gor->flags = flags;
    dbus_connection_ref(connection);

    return gor;
}

static void
get_owner_request_free(GetOwnerRequest* gor)
{
    dbus_connection_unref(gor->connection);
    g_free(gor->name);
    g_slice_free(GetOwnerRequest, gor);
}

static void
on_get_owner_reply(DBusPendingCall* pending, void* user_data)
{
    DBusMessage* reply;
    GetOwnerRequest* gor;

    gor = user_data;

    reply = dbus_pending_call_steal_reply(pending);
    if (reply == NULL) {
        g_warning("NULL reply in on_get_owner_reply?");
        return;
    }

    if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_METHOD_RETURN) {
        const char* current_owner = NULL;

        if (!dbus_message_get_args(reply, NULL, DBUS_TYPE_STRING,
                                   &current_owner, DBUS_TYPE_INVALID)) {
            big_debug(BIG_DEBUG_UTIL_DBUS, "GetNameOwner has wrong args '%s'",
                      dbus_message_get_signature(reply));
        } else {
            big_debug(BIG_DEBUG_UTIL_DBUS, "Got owner '%s' for name '%s'",
                      current_owner, gor->name);
            if (current_owner != NULL) {
                notify_name_owner_changed(gor->connection, gor->name,
                                          current_owner);
            }
        }
    } else if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
        if (g_str_equal(dbus_message_get_error_name(reply),
                        DBUS_ERROR_NAME_HAS_NO_OWNER)) {
            big_debug(BIG_DEBUG_UTIL_DBUS, "'%s' was not running", gor->name);
            if (gor->flags & BIG_DBUS_NAME_START_IF_NOT_FOUND) {
                big_debug(BIG_DEBUG_UTIL_DBUS, "  (starting it up)");
                big_dbus_start_service(gor->connection, gor->name);
            } else {
                /* no owner for now, notify app */
                notify_name_owner_changed(gor->connection, gor->name, "");
            }
        } else {
            big_debug(BIG_DEBUG_UTIL_DBUS,
                      "Error getting owner of name '%s': %s", gor->name,
                      dbus_message_get_error_name(reply));

            /* Notify no owner for now, ensuring the app
             * gets advised "appeared" or "vanished",
             * one or the other.
             */
            notify_name_owner_changed(gor->connection, gor->name, "");
        }
    } else {
        big_debug(BIG_DEBUG_UTIL_DBUS,
                  "Nonsensical reply type to GetNameOwner");
    }

    dbus_message_unref(reply);
}

static void
request_name_owner(DBusConnection* connection,
                   BigDBusInfo* info,
                   BigNameWatch* watch)
{
    DBusMessage* message;
    DBusPendingCall* call;

    message = dbus_message_new_method_call(DBUS_SERVICE_DBUS, DBUS_PATH_DBUS,
                                           DBUS_INTERFACE_DBUS, "GetNameOwner");
    if (message == NULL)
        g_error("no memory");

    if (!dbus_message_append_args(message, DBUS_TYPE_STRING, &watch->name,
                                  DBUS_TYPE_INVALID))
        g_error("no memory");

    call = NULL;
    dbus_connection_send_with_reply(connection, message, &call, -1);
    if (call != NULL) {
        GetOwnerRequest* gor;
        BigDBusWatchNameFlags flags;
        GSList* l;

        big_debug(BIG_DEBUG_UTIL_DBUS, "Sent GetNameOwner for '%s'",
                  watch->name);

        flags = 0;
        for (l = watch->watchers; l != NULL; l = l->next) {
            BigNameWatcher* watcher = l->data;

            if (watcher->flags & BIG_DBUS_NAME_START_IF_NOT_FOUND)
                flags |= BIG_DBUS_NAME_START_IF_NOT_FOUND;
        }

        gor = get_owner_request_new(connection, watch->name, flags);

        if (!dbus_pending_call_set_notify(call, on_get_owner_reply, gor,
                                          (DBusFreeFunction)
                                            get_owner_request_free))
            g_error("no memory");

        /* the connection will hold a ref to the pending call */
        dbus_pending_call_unref(call);
    } else {
        big_debug(BIG_DEBUG_UTIL_DBUS,
                  "GetNameOwner for '%s' not sent, connection disconnected",
                  watch->name);
    }
}

static gboolean
notify_watcher_name_appeared(gpointer data)
{
    BigNameWatcher* watcher;
    DBusConnection* connection;

    watcher = data;
    watcher->notify_idle = 0;

    connection = _big_dbus_get_weak_ref(watcher->bus_type);

    if (!connection)
        return FALSE;

    (*watcher->funcs->appeared)(connection, watcher->watch->name,
                                watcher->watch->current_owner, watcher->data);
    return FALSE;
}

static void
create_watch_for_watcher(DBusConnection* connection,
                         BigDBusInfo* info,
                         const char* name,
                         BigNameWatcher* watcher)
{
    BigNameWatch* watch;

    watch = g_hash_table_lookup(info->name_watches, name);
    if (watch == NULL) {
        watch = name_watch_new(name);

        g_hash_table_replace(info->name_watches, watch->name, watch);

        watch->watchers = g_slist_prepend(watch->watchers, watcher);

        _big_dbus_set_matching_name_owner_changed(connection, watch->name,
                                                  TRUE);

        request_name_owner(connection, info, watch);
    } else {
        watch->watchers = g_slist_prepend(watch->watchers, watcher);
    }
    name_watcher_ref(watcher);

    watcher->watch = watch;
}

static void
process_pending_name_watchers(DBusConnection* connection, BigDBusInfo* info)
{
    GSList* still_pending;

    still_pending = NULL;
    while (pending_name_watchers != NULL) {
        BigPendingNameWatcher* pending;
        BigNameWatch* watch;

        pending = pending_name_watchers->data;
        pending_name_watchers
          = g_slist_remove(pending_name_watchers, pending_name_watchers->data);

        if (pending->bus_type != info->bus_type) {
            still_pending = g_slist_prepend(still_pending, pending);
            continue;
        }

        create_watch_for_watcher(connection, info, pending->name,
                                 pending->watcher);

        watch = pending->watcher->watch;

        /* If we already know the owner, let the new watcher know */
        if (watch->current_owner != NULL) {
            (*pending->watcher->funcs->appeared)(connection, watch->name,
                                                 watch->current_owner,
                                                 pending->watcher->data);
        }

        g_free(pending->name);
        name_watcher_unref(pending->watcher);
        g_slice_free(BigPendingNameWatcher, pending);
    }

    g_assert(pending_name_watchers == NULL);
    pending_name_watchers = still_pending;
}

static void
name_watch_remove_watcher(BigNameWatch* watch, BigNameWatcher* watcher)
{
    watch->watchers = g_slist_remove(watch->watchers, watcher);

    if (watcher->notify_idle) {
        g_source_remove(watcher->notify_idle);
        watcher->notify_idle = 0;
    }

    watcher->destroyed = TRUE;
    name_watcher_unref(watcher);
}

void
big_dbus_watch_name(DBusBusType bus_type,
                    const char* name,
                    BigDBusWatchNameFlags flags,
                    const BigDBusWatchNameFuncs* funcs,
                    void* data)
{
    BigNameWatcher* watcher;
    DBusConnection* connection;

    big_debug(BIG_DEBUG_UTIL_DBUS, "Adding watch on name '%s'", name);

    watcher = name_watcher_new(flags, funcs, data, bus_type);

    connection = _big_dbus_get_weak_ref(bus_type);

    if (connection) {
        BigDBusInfo* info;

        info = _big_dbus_ensure_info(connection);

        create_watch_for_watcher(connection, info, name, watcher);
        /* The initial reference is now transferred to the watch */
        name_watcher_unref(watcher);

        /* If we already know the owner, notify the user in an idle */
        if (watcher->watch->current_owner) {
            watcher->notify_idle
              = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
                                notify_watcher_name_appeared, watcher,
                                (GDestroyNotify) name_watcher_unref);
            name_watcher_ref(watcher);
        }

    } else {
        BigPendingNameWatcher* pending;

        pending = g_slice_new0(BigPendingNameWatcher);

        pending->bus_type = bus_type;
        pending->name = g_strdup(name);
        pending->watcher = watcher;

        pending_name_watchers = g_slist_prepend(pending_name_watchers, pending);

        _big_dbus_ensure_connect_idle(pending->bus_type);
    }
}

void
big_dbus_unwatch_name(DBusBusType bus_type,
                      const char* name,
                      const BigDBusWatchNameFuncs* funcs,
                      void* data)
{
    DBusConnection* connection;
    BigDBusInfo* info;
    BigNameWatch* watch;
    GSList* l;
    BigNameWatcher* watcher;

    big_debug(BIG_DEBUG_UTIL_DBUS, "Removing watch on name '%s'", name);

    connection = _big_dbus_get_weak_ref(bus_type);
    if (connection == NULL) {
        /* right now our state is entirely hosed if we disconnect
         * (we don't move the watchers out of the connection data),
         * so can't do much here without larger changes to the file
         */
        g_warning("Have not implemented disconnect handling");
        return;
    }

    info = _big_dbus_ensure_info(connection);

    /* could still be pending */
    process_pending_name_watchers(connection, info);

    watch = g_hash_table_lookup(info->name_watches, name);

    if (watch == NULL) {
        g_warning("attempt to unwatch name %s but nobody is watching that",
                  name);
        return;
    }

    watcher = NULL;
    for (l = watch->watchers; l != NULL; l = l->next) {
        watcher = l->data;

        if (watcher->funcs == funcs && watcher->data == data)
            break;
    }

    if (l == NULL) {
        g_warning("Could not find a watch on %s matching %p %p", name, funcs,
                  data);
        return;
    }
    g_assert(l->data == watcher);

    name_watch_remove_watcher(watch, watcher);

    /* Clear out the watch if it's gone */
    if (watch->watchers == NULL) {
        g_hash_table_remove(info->name_watches, watch->name);

        _big_dbus_set_matching_name_owner_changed(connection, watch->name,
                                                  FALSE);

        name_watch_free(watch);
    }
}

const char*
big_dbus_get_watched_name_owner(DBusBusType bus_type, const char* name)
{
    DBusConnection* connection;
    BigNameWatch* watch;
    BigDBusInfo* info;

    connection = _big_dbus_get_weak_ref(bus_type);
    if (connection == NULL) {
        return NULL;
    }

    info = _big_dbus_ensure_info(connection);

    /* could still be pending */
    process_pending_name_watchers(connection, info);

    watch = g_hash_table_lookup(info->name_watches, name);
    if (watch == NULL) {
        g_warning("Tried to get owner of '%s' but there is no watch on it",
                  name);
        return NULL;
    }

    return watch->current_owner;
}

void
big_dbus_register_json(DBusConnection* connection,
                       const char* iface_name,
                       const BigDBusJsonMethod* methods,
                       int n_methods)
{
    BigDBusInfo* info;
    BigJsonIface* iface;

    info = _big_dbus_ensure_info(connection);

    iface = json_iface_new(iface_name, methods, n_methods);

    g_hash_table_replace(info->json_ifaces, iface->name, iface);
}

void
big_dbus_unregister_json(DBusConnection* connection, const char* iface_name)
{
    BigDBusInfo* info;

    info = _big_dbus_ensure_info(connection);

    g_hash_table_remove(info->json_ifaces, iface_name);
}

typedef struct
{
    DBusConnection* connection;
    GObject* gobj;
    char* iface_name;
} BigDBusGObject;

static void
gobj_path_unregistered(DBusConnection* connection, void* user_data)
{
    BigDBusGObject* g;

    g = user_data;

    if (g->gobj) {
        g_object_remove_weak_pointer(g->gobj, (void**) &g->gobj);
        g->gobj = NULL;
    }

    g_free(g->iface_name);
    g_slice_free(BigDBusGObject, g);
}

static DBusHandlerResult
gobj_path_message(DBusConnection* connection,
                  DBusMessage* message,
                  void* user_data)
{
    BigDBusGObject* g;
    BigDBusInfo* info;
    BigJsonIface* iface;
    const char* message_iface;
    const char* message_method;
    DBusError derror;
    int i;
    const BigDBusJsonMethod* method;
    DBusMessageIter arg_iter, dict_iter;

    info = _big_dbus_ensure_info(connection);
    g = user_data;

    big_debug(BIG_DEBUG_UTIL_DBUS, "Received message to iface %s gobj %p",
              g->iface_name, g->gobj);

    if (g->gobj == NULL) {
        /* GObject was destroyed */
        return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
    }

    if (dbus_message_get_type(message) != DBUS_MESSAGE_TYPE_METHOD_CALL)
        return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

    dbus_error_init(&derror);

    message_iface = dbus_message_get_interface(message);

    /* FIXME implement Introspectable() just to enable dbus debugger */

    if (message_iface != NULL && strcmp(message_iface, g->iface_name) != 0) {

        dbus_set_error(
          &derror, DBUS_ERROR_UNKNOWN_METHOD,
          "Interface '%s' not implemented by this object, did you mean '%s'?",
          message_iface, g->iface_name);

        goto out;
    }

    iface = g_hash_table_lookup(info->json_ifaces, g->iface_name);
    if (iface == NULL) {
        g_warning(
          "Object registered with iface %s but that iface is not registered",
          g->iface_name);
        dbus_set_error(&derror, DBUS_ERROR_UNKNOWN_METHOD,
                       "Bug - '%s' is not registered", g->iface_name);
        goto out;
    }

    method = NULL;
    message_method = dbus_message_get_member(message);
    for (i = 0; i < iface->n_methods; ++i) {
        if (strcmp(message_method, iface->methods[i].name) == 0) {
            method = &iface->methods[i];
            break;
        }
    }

    if (method == NULL) {
        dbus_set_error(&derror, DBUS_ERROR_UNKNOWN_METHOD,
                       "Interface '%s' has no method '%s'", g->iface_name,
                       message_method);
        goto out;
    }

    if (!dbus_message_has_signature(message, "a{sv}")) {
        dbus_set_error(
          &derror, DBUS_ERROR_INVALID_ARGS,
          "Method %s.%s should have 1 argument which is a dictionary",
          g->iface_name, message_method);
        goto out;
    }

    dbus_message_iter_init(message, &arg_iter);
    dbus_message_iter_recurse(&arg_iter, &dict_iter);

    if (method->sync_func != NULL) {
        DBusMessage* reply;
        DBusMessageIter out_arg_iter, out_dict_iter;

        reply = dbus_message_new_method_return(message);
        if (reply == NULL) {
            dbus_set_error(&derror, DBUS_ERROR_NO_MEMORY, "No memory");
            goto out;
        }

        dbus_message_iter_init_append(reply, &out_arg_iter);
        dbus_message_iter_open_container(&out_arg_iter, DBUS_TYPE_ARRAY, "{sv}",
                                         &out_dict_iter);

        g_object_ref(g->gobj);
        (*method->sync_func)(connection, message, &dict_iter, &out_dict_iter,
                             g->gobj, &derror);
        g_object_unref(g->gobj);

        dbus_message_iter_close_container(&out_arg_iter, &out_dict_iter);

        if (!dbus_error_is_set(&derror)) {
            dbus_connection_send(connection, reply, NULL);
        }
        dbus_message_unref(reply);

    } else if (method->async_func != NULL) {
        g_object_ref(g->gobj);
        (*method->async_func)(connection, message, &dict_iter, g->gobj);
        g_object_unref(g->gobj);
    } else {
        g_warning("Method %s does not have any implementation", method->name);
    }

out:
    if (dbus_error_is_set(&derror)) {
        DBusMessage* reply;

        reply = dbus_message_new_error(message, derror.name, derror.message);
        dbus_error_free(&derror);

        if (reply != NULL) {
            dbus_connection_send(connection, reply, NULL);

            dbus_message_unref(reply);
        } else {
            /* use g_printerr not g_warning since this is NOT a "can
             * never happen" just a "probably will never happen"
             */
            g_printerr("Could not send OOM error\n");
        }
    }

    return DBUS_HANDLER_RESULT_HANDLED;
}

static DBusObjectPathVTable gobj_vtable = {
    gobj_path_unregistered, gobj_path_message, NULL,
};

/* Note that because of how this works, each object can be registered
 * at multiple paths but only once per path. Which is sort of bizarre,
 * but we'll fix it when we need it.
 */
void
big_dbus_register_g_object(DBusConnection* connection,
                           const char* path,
                           GObject* gobj,
                           const char* iface_name)
{
    BigDBusGObject* g;

    g = g_slice_new0(BigDBusGObject);
    g->iface_name = g_strdup(iface_name);
    g->gobj = gobj;

    if (!dbus_connection_register_object_path(connection, path, &gobj_vtable,
                                              g)) {
        g_warning("Failed to register object path %s", path);
    }

    g_object_add_weak_pointer(g->gobj, (void**) &g->gobj);
}

void
big_dbus_unregister_g_object(DBusConnection* connection, const char* path)
{
    dbus_connection_unregister_object_path(connection, path);
}

static void
open_json_entry(DBusMessageIter* dict_iter,
                const char* key,
                const char* signature,
                DBusMessageIter* entry_iter,
                DBusMessageIter* variant_iter)
{
    dbus_message_iter_open_container(dict_iter, DBUS_TYPE_DICT_ENTRY, NULL,
                                     entry_iter);

    dbus_message_iter_append_basic(entry_iter, DBUS_TYPE_STRING, &key);

    dbus_message_iter_open_container(entry_iter, DBUS_TYPE_VARIANT, signature,
                                     variant_iter);
}

static void
close_json_entry(DBusMessageIter* dict_iter,
                 DBusMessageIter* entry_iter,
                 DBusMessageIter* variant_iter)
{
    dbus_message_iter_close_container(entry_iter, variant_iter);

    dbus_message_iter_close_container(dict_iter, entry_iter);
}

static void
open_json_entry_array(DBusMessageIter* dict_iter,
                      const char* key,
                      int array_element_type,
                      DBusMessageIter* entry_iter,
                      DBusMessageIter* variant_iter,
                      DBusMessageIter* array_iter)
{
    char buf[3];
    buf[0] = 'a';
    buf[1] = array_element_type;
    buf[2] = '\0';

    open_json_entry(dict_iter, key, buf, entry_iter, variant_iter);

    dbus_message_iter_open_container(variant_iter, DBUS_TYPE_ARRAY, &buf[1],
                                     array_iter);
}

static void
close_json_entry_array(DBusMessageIter* dict_iter,
                       DBusMessageIter* entry_iter,
                       DBusMessageIter* variant_iter,
                       DBusMessageIter* array_iter)
{
    dbus_message_iter_close_container(variant_iter, array_iter);

    close_json_entry(dict_iter, entry_iter, variant_iter);
}

void
big_dbus_append_json_entry(DBusMessageIter* dict_iter,
                           const char* key,
                           int dbus_type,
                           void* basic_value_p)
{
    DBusMessageIter entry_iter, variant_iter;
    char buf[2];

    buf[0] = dbus_type;
    buf[1] = '\0';

    open_json_entry(dict_iter, key, buf, &entry_iter, &variant_iter);

    dbus_message_iter_append_basic(&variant_iter, dbus_type, basic_value_p);

    close_json_entry(dict_iter, &entry_iter, &variant_iter);
}

void
big_dbus_append_json_entry_STRING(DBusMessageIter* dict_iter,
                                  const char* key,
                                  const char* value)
{
    big_dbus_append_json_entry(dict_iter, key, DBUS_TYPE_STRING, &value);
}

void
big_dbus_append_json_entry_INT32(DBusMessageIter* dict_iter,
                                 const char* key,
                                 dbus_int32_t value)
{
    big_dbus_append_json_entry(dict_iter, key, DBUS_TYPE_INT32, &value);
}

void
big_dbus_append_json_entry_DOUBLE(DBusMessageIter* dict_iter,
                                  const char* key,
                                  double value)
{
    big_dbus_append_json_entry(dict_iter, key, DBUS_TYPE_DOUBLE, &value);
}

void
big_dbus_append_json_entry_BOOLEAN(DBusMessageIter* dict_iter,
                                   const char* key,
                                   dbus_bool_t value)
{
    big_dbus_append_json_entry(dict_iter, key, DBUS_TYPE_BOOLEAN, &value);
}

/* when coming from a dynamic language, we don't know what type of array '[]' is
 * supposed to be */
void
big_dbus_append_json_entry_EMPTY_ARRAY(DBusMessageIter* dict_iter,
                                       const char* key)
{
    DBusMessageIter entry_iter, variant_iter, array_iter;

    /* so just say VARIANT even though there won't be any elements in the array
     */
    open_json_entry_array(dict_iter, key, DBUS_TYPE_VARIANT, &entry_iter,
                          &variant_iter, &array_iter);

    close_json_entry_array(dict_iter, &entry_iter, &variant_iter, &array_iter);
}

void
big_dbus_append_json_entry_STRING_ARRAY(DBusMessageIter* dict_iter,
                                        const char* key,
                                        const char** value)
{
    DBusMessageIter entry_iter, variant_iter, array_iter;
    int i;

    open_json_entry_array(dict_iter, key, DBUS_TYPE_STRING, &entry_iter,
                          &variant_iter, &array_iter);

    for (i = 0; value[i] != NULL; ++i) {
        dbus_message_iter_append_basic(&array_iter, DBUS_TYPE_STRING,
                                       &value[i]);
    }

    close_json_entry_array(dict_iter, &entry_iter, &variant_iter, &array_iter);
}

gboolean
big_dbus_message_iter_get_gsize(DBusMessageIter* iter, gsize* value_p)
{
    switch (dbus_message_iter_get_arg_type(iter)) {
        case DBUS_TYPE_INT32: {
            dbus_int32_t v;
            dbus_message_iter_get_basic(iter, &v);
            if (v < 0)
                return FALSE;
            *value_p = v;
        } break;
        case DBUS_TYPE_UINT32: {
            dbus_uint32_t v;
            dbus_message_iter_get_basic(iter, &v);
            *value_p = v;
        } break;
        case DBUS_TYPE_INT64: {
            dbus_int64_t v;
            dbus_message_iter_get_basic(iter, &v);
            if (v < 0)
                return FALSE;
            if (((guint64) v) > G_MAXSIZE)
                return FALSE;
            *value_p = v;
        } break;
        case DBUS_TYPE_UINT64: {
            dbus_uint64_t v;
            dbus_message_iter_get_basic(iter, &v);
            if (v > G_MAXSIZE)
                return FALSE;
            *value_p = v;
        } break;
        default:
            return FALSE;
    }

    return TRUE;
}

gboolean
big_dbus_message_iter_get_gssize(DBusMessageIter* iter, gssize* value_p)
{
    switch (dbus_message_iter_get_arg_type(iter)) {
        case DBUS_TYPE_INT32: {
            dbus_int32_t v;
            dbus_message_iter_get_basic(iter, &v);
            *value_p = v;
        } break;
        case DBUS_TYPE_UINT32: {
            dbus_uint32_t v;
            dbus_message_iter_get_basic(iter, &v);
            if (v > (guint32) G_MAXSSIZE)
                return FALSE;
            *value_p = v;
        } break;
        case DBUS_TYPE_INT64: {
            dbus_int64_t v;
            dbus_message_iter_get_basic(iter, &v);
            if (v > (gint64) G_MAXSSIZE)
                return FALSE;
            if (v < (gint64) G_MINSSIZE)
                return FALSE;
            *value_p = v;
        } break;
        case DBUS_TYPE_UINT64: {
            dbus_uint64_t v;
            dbus_message_iter_get_basic(iter, &v);
            if (v > (guint64) G_MAXSSIZE)
                return FALSE;
            *value_p = v;
        } break;
        default:
            return FALSE;
    }

    return TRUE;
}

#if BIG_BUILD_TESTS

#include "dbus-proxy.h"
#include "dbus-input-stream.h"
#include "dbus-output-stream.h"

#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/wait.h>

static pid_t test_service_pid = 0;
static BigDBusProxy* test_service_proxy = NULL;

static pid_t test_io_pid = 0;
static BigDBusProxy* test_io_proxy = NULL;

static GMainLoop* client_loop = NULL;

static int n_running_children = 0;

static BigDBusInputStream* input_from_io_service;
static BigDBusOutputStream* output_to_io_service;

static const char stream_data_to_io_service[]
  = "This is sent from the main test process to the IO service.";
static const char stream_data_from_io_service[] = "This is sent from the IO "
                                                  "service to the main test "
                                                  "process. The quick brown "
                                                  "fox, etc.";

static void do_test_service_child(void);
static void do_test_io_child(void);

/* quit when all children are gone */
static void
another_child_down(void)
{
    g_assert(n_running_children > 0);
    n_running_children -= 1;

    if (n_running_children == 0) {
        g_main_loop_quit(client_loop);
    }
}

static const char*
extract_string_arg(DBusMessageIter* in_iter,
                   const char* prop_name,
                   DBusError* error)
{
    const char* s;

    s = NULL;
    while (dbus_message_iter_get_arg_type(in_iter) == DBUS_TYPE_DICT_ENTRY) {
        DBusMessageIter entry_iter, variant_iter;
        const char* key;

        dbus_message_iter_recurse(in_iter, &entry_iter);

        dbus_message_iter_get_basic(&entry_iter, &key);

        if (strcmp(key, prop_name) == 0) {
            dbus_message_iter_next(&entry_iter);

            dbus_message_iter_recurse(&entry_iter, &variant_iter);
            if (dbus_message_iter_get_arg_type(&variant_iter)
                != DBUS_TYPE_STRING) {
                dbus_set_error(error, DBUS_ERROR_INVALID_ARGS,
                               "Value of '%s' prop should be a string",
                               prop_name);
                return NULL;
            }

            dbus_message_iter_get_basic(&variant_iter, &s);

            return s;
        }
    }

    dbus_set_error(error, DBUS_ERROR_INVALID_ARGS, "No '%s' prop provided",
                   prop_name);
    return NULL;
}

static void
fork_child_test_service(void)
{
    pid_t child_pid;

    /* it would break to fork after we already connected */
    g_assert(session_bus_weak_ref == NULL);
    g_assert(system_bus_weak_ref == NULL);
    g_assert(test_service_pid == 0);

    child_pid = fork();

    if (child_pid == -1) {
        g_error("Failed to fork dbus service");
    } else if (child_pid > 0) {
        /* We are the parent */
        test_service_pid = child_pid;
        n_running_children += 1;

        return;
    }

    /* we are the child, set up a service for main test process to talk to */

    do_test_service_child();
}

/* This test function doesn't really test anything, just sets up
 * for the following one
 */
static void
fork_child_test_io(void)
{
    pid_t child_pid;

    /* it would break to fork after we already connected */
    g_assert(session_bus_weak_ref == NULL);
    g_assert(system_bus_weak_ref == NULL);
    g_assert(test_io_pid == 0);

    child_pid = fork();

    if (child_pid == -1) {
        g_error("Failed to fork dbus service");
    } else if (child_pid > 0) {
        /* We are the parent */
        test_io_pid = child_pid;
        n_running_children += 1;

        return;
    }

    /* we are the child, set up a service for main test process to talk to */

    do_test_io_child();
}

static void
on_expected_fnf_error_reply_kill_child(BigDBusProxy* proxy,
                                       const char* error_name,
                                       const char* error_message,
                                       void* data)
{
    big_debug(BIG_DEBUG_IN_TESTS,
              "got expected error reply to alwaysErrorSync, killing child");

    /* We were expecting an error, good. */
    if (strcmp(error_name, DBUS_ERROR_FILE_NOT_FOUND) != 0) {
        g_error("Got error we did not expect %s: %s", error_name,
                error_message);
    }

    if (kill(test_service_pid, SIGTERM) < 0) {
        g_error("Test service was no longer around... it must have failed "
                "somehow (%s)",
                strerror(errno));
    }

    /* We will quit main loop when we see the child go away */
}

static void
on_unexpected_error_reply(BigDBusProxy* proxy,
                          const char* error_name,
                          const char* error_message,
                          void* data)
{
    const char* context_text = data;

    g_error("Got error %s: '%s' context was: %s", error_name, error_message,
            context_text);
}

static void
on_get_always_error_reply(BigDBusProxy* proxy,
                          DBusMessage* message,
                          DBusMessageIter* return_value_iter,
                          void* data)
{
    g_error("alwaysError json method supposed to return an error always, not a "
            "valid reply");
}

static void
on_get_some_stuff_reply(BigDBusProxy* proxy,
                        DBusMessage* message,
                        DBusMessageIter* return_value_iter,
                        void* data)
{
    big_debug(BIG_DEBUG_IN_TESTS, "reply received to getSomeStuffSync");

    /* FIXME look at the return value to see if it's what
     * the test service sends
     */

    big_dbus_proxy_call_json_async(test_service_proxy, "alwaysErrorSync",
                                   on_get_always_error_reply,
                                   on_expected_fnf_error_reply_kill_child, NULL,
                                   NULL);
}

static void
on_test_service_appeared(DBusConnection* connection,
                         const char* name,
                         const char* new_owner_unique_name,
                         void* data)
{
    dbus_int32_t v_INT32;

    big_debug(BIG_DEBUG_IN_TESTS, "%s appeared", name);

    test_service_proxy
      = big_dbus_proxy_new(connection, new_owner_unique_name,
                           "/com/litl/test/object42", "com.litl.TestIface");
    v_INT32 = 42;
    big_dbus_proxy_call_json_async(
      test_service_proxy, "getSomeStuffSync", on_get_some_stuff_reply,
      on_unexpected_error_reply,
      "getSomeStuffSync call from on_test_service_appeared", "yourNameIs",
      DBUS_TYPE_STRING, &name, "yourUniqueNameIs", DBUS_TYPE_STRING,
      &new_owner_unique_name, "anIntegerIs", DBUS_TYPE_INT32, &v_INT32, NULL);
}

static void
on_test_service_vanished(DBusConnection* connection,
                         const char* name,
                         const char* old_owner_unique_name,
                         void* data)
{
    big_debug(BIG_DEBUG_IN_TESTS, "%s vanished", name);

    another_child_down();
}

static BigDBusWatchNameFuncs watch_test_service_funcs
  = { on_test_service_appeared, on_test_service_vanished };

static void
on_confirm_streams_reply(BigDBusProxy* proxy,
                         DBusMessage* message,
                         DBusMessageIter* return_value_iter,
                         void* data)
{
    const char* received;

    received = extract_string_arg(return_value_iter, "received", NULL);
    g_assert(received != NULL);

    if (strcmp(received, stream_data_to_io_service) != 0) {
        g_error("We sent the child process '%s' but it says it got '%s'",
                stream_data_to_io_service, received);
    }

    big_debug(BIG_DEBUG_IN_TESTS, "com.litl.TestIO says it got: '%s'",
              received);

    /* We've exchanged all our streams - time to kill the TestIO
     * child process
     */
    big_debug(BIG_DEBUG_IN_TESTS, "Sending TERM to TestIO child");
    if (kill(test_io_pid, SIGTERM) < 0) {
        g_error("Test IO service was no longer around... it must have failed "
                "somehow (%s)",
                strerror(errno));
    }
}

static void
on_setup_streams_reply(BigDBusProxy* proxy,
                       DBusMessage* message,
                       DBusMessageIter* return_value_iter,
                       void* data)
{
    const char* stream_path;
    gsize total;
    gssize result;
    gsize read_size;
    GError* error;
    GString* str;
    char buf[10];

    big_debug(BIG_DEBUG_IN_TESTS, "Got reply to setupStreams");

    stream_path = extract_string_arg(return_value_iter, "stream", NULL);
    g_assert(stream_path != NULL);

    output_to_io_service
      = big_dbus_output_stream_new(big_dbus_proxy_get_connection(proxy),
                                   dbus_message_get_sender(message),
                                   stream_path);

    g_assert(input_from_io_service && output_to_io_service);

    /* Write to the output stream */

    total = strlen(stream_data_to_io_service);

    error = NULL;
    result = g_output_stream_write(G_OUTPUT_STREAM(output_to_io_service),
                                   stream_data_to_io_service, 10, NULL, &error);
    if (result < 0) {
        g_error("Error writing to output stream: %s", error->message);
        g_error_free(error);
    }

    if (result != 10) {
        g_error("Wrote %d instead of 10 bytes", (int) result);
    }

    if (!g_output_stream_write_all(G_OUTPUT_STREAM(output_to_io_service),
                                   stream_data_to_io_service + 10, total - 10,
                                   NULL, NULL, &error)) {
        g_error("Error writing all to output stream: %s", error->message);
        g_error_free(error);
    }

    /* flush should do nothing here, and is not needed, but
     * just calling it to test it
     */
    if (!g_output_stream_flush(G_OUTPUT_STREAM(output_to_io_service), NULL,
                               &error)) {
        g_error("Error flushing output stream: %s", error->message);
        g_error_free(error);
    }

    if (!g_output_stream_close(G_OUTPUT_STREAM(output_to_io_service), NULL,
                               &error)) {
        g_error("Error closing output stream: %s", error->message);
        g_error_free(error);
    }
    g_object_unref(output_to_io_service);
    output_to_io_service = NULL;

    /* Now read from the input stream - in an inefficient way to be sure
     * we test multiple, partial reads
     */

    read_size = 1;
    str = g_string_new(NULL);

    while (TRUE) {
        /* test get_received() */
        g_assert(big_dbus_input_stream_get_received(input_from_io_service)
                 <= strlen(stream_data_from_io_service));

        /* This is a blocking read... in production code, you would
         * want to use the ready-to-read signal instead to avoid
         * blocking when there is nothing to read.
         */
        result = g_input_stream_read(G_INPUT_STREAM(input_from_io_service), buf,
                                     read_size, NULL, &error);
        if (result < 0) {
            g_error("Error reading %d bytes from input stream: %s",
                    (int) read_size, error->message);
            g_error_free(error);
        }

        if (result == 0) {
            /* EOF */
            break;
        }

        g_string_append_len(str, buf, result);

        if (read_size < sizeof(buf))
            read_size += 1;
    }

    if (!g_input_stream_close(G_INPUT_STREAM(input_from_io_service), NULL,
                              &error)) {
        g_error("Error closing input stream: %s", error->message);
        g_error_free(error);
    }
    g_object_unref(input_from_io_service);
    input_from_io_service = NULL;

    /* Now make the confirmStreams call
     */
    big_debug(BIG_DEBUG_IN_TESTS, "Confirming to com.litl.TestIO we got: '%s'",
              str->str);

    big_dbus_proxy_call_json_async(
      test_io_proxy, "confirmStreamsData", on_confirm_streams_reply,
      on_unexpected_error_reply,
      "confirmStreamsData call from on_setup_streams_reply", "received",
      DBUS_TYPE_STRING, &str->str, NULL);

    g_string_free(str, TRUE);
}

static void
on_test_io_appeared(DBusConnection* connection,
                    const char* name,
                    const char* new_owner_unique_name,
                    void* data)
{
    const char* stream_path;

    big_debug(BIG_DEBUG_IN_TESTS, "%s appeared", name);

    test_io_proxy
      = big_dbus_proxy_new(connection, new_owner_unique_name,
                           "/com/litl/test/object47", "com.litl.TestIO");

    input_from_io_service = g_object_new(BIG_TYPE_DBUS_INPUT_STREAM, NULL);
    big_dbus_input_stream_attach(input_from_io_service, connection);

    stream_path = big_dbus_input_stream_get_path(input_from_io_service);

    big_dbus_proxy_call_json_async(test_io_proxy, "setupStreams",
                                   on_setup_streams_reply,
                                   on_unexpected_error_reply,
                                   "setupStreams call from on_test_io_appeared",
                                   "stream", DBUS_TYPE_STRING, &stream_path,
                                   NULL);
}

static void
on_test_io_vanished(DBusConnection* connection,
                    const char* name,
                    const char* old_owner_unique_name,
                    void* data)
{
    big_debug(BIG_DEBUG_IN_TESTS, "%s vanished", name);

    another_child_down();
}

static BigDBusWatchNameFuncs watch_test_io_funcs
  = { on_test_io_appeared, on_test_io_vanished };

void
bigtest_test_func_util_dbus_client(void)
{
    pid_t result;
    int status;

    /* We have to fork() to avoid creating the DBusConnection*
     * and thus preventing other dbus-using tests from forking
     * children.  This dbus bug, when the fix makes it into Ubuntu,
     * should solve the problem:
     * https://bugs.freedesktop.org/show_bug.cgi?id=15570
     *
     * The symptom of that bug is failure to connect to the bus in
     * dbus-signals.c tests. The symptom of opening a connection
     * before forking children is the connection FD shared among
     * multiple processes, i.e. huge badness.
     */
    if (!g_test_trap_fork(0, 0)) {
        /* We are the parent */
        g_test_trap_assert_passed();
        return;
    }

    /* All this stuff runs in the forked child only */

    fork_child_test_service();
    fork_child_test_io();

    g_type_init();

    g_assert(test_service_pid != 0);
    g_assert(test_io_pid != 0);

    big_dbus_watch_name(DBUS_BUS_SESSION, "com.litl.TestService", 0,
                        &watch_test_service_funcs, NULL);

    big_dbus_watch_name(DBUS_BUS_SESSION, "com.litl.TestIO", 0,
                        &watch_test_io_funcs, NULL);

    client_loop = g_main_loop_new(NULL, FALSE);

    g_main_loop_run(client_loop);

    if (test_service_proxy != NULL)
        g_object_unref(test_service_proxy);

    if (test_io_proxy != NULL)
        g_object_unref(test_io_proxy);

    /* child was killed already, or should have been */

    big_debug(BIG_DEBUG_IN_TESTS, "waitpid() for first child");

    result = waitpid(test_service_pid, &status, 0);
    if (result < 0) {
        g_error("Failed to waitpid() for forked child: %s", strerror(errno));
    }

    if (WIFEXITED(status) && WEXITSTATUS(status) != 0) {
        g_error("Forked dbus service child exited with error code %d",
                WEXITSTATUS(status));
    }

    if (WIFSIGNALED(status) && WTERMSIG(status) != SIGTERM) {
        g_error("Forked dbus service child exited on wrong signal number %d",
                WTERMSIG(status));
    }

    big_debug(BIG_DEBUG_IN_TESTS, "waitpid() for second child");

    result = waitpid(test_io_pid, &status, 0);
    if (result < 0) {
        g_error("Failed to waitpid() for forked child: %s", strerror(errno));
    }

    if (WIFEXITED(status) && WEXITSTATUS(status) != 0) {
        g_error("Forked dbus service child exited with error code %d",
                WEXITSTATUS(status));
    }

    if (WIFSIGNALED(status) && WTERMSIG(status) != SIGTERM) {
        g_error("Forked dbus service child exited on wrong signal number %d",
                WTERMSIG(status));
    }

    big_debug(BIG_DEBUG_IN_TESTS, "dbus client test completed");

    /* We want to kill dbus so the weak refs are NULL to start the
     * next dbus-related test, which allows those tests
     * to fork new child processes.
     */
    _big_dbus_dispose_info(_big_dbus_get_weak_ref(DBUS_BUS_SESSION));
    dbus_shutdown();

    big_debug(BIG_DEBUG_IN_TESTS, "dbus shut down");

    /* FIXME this is here only while we need g_test_trap_fork(),
     * see comment above.
     */
    exit(0);
}

/*
 * First child service we forked, tests general dbus API
 */

static gboolean currently_have_test_service = FALSE;
static GObject* test_service_object = NULL;

static void
test_service_get_some_stuff_sync(DBusConnection* connection,
                                 DBusMessage* message,
                                 DBusMessageIter* in_iter,
                                 DBusMessageIter* out_iter,
                                 void* data,
                                 DBusError* error)
{
    big_debug(BIG_DEBUG_IN_TESTS, "com.litl.TestService got getSomeStuffSync");

    g_assert(G_IS_OBJECT(data));

    big_dbus_append_json_entry_BOOLEAN(out_iter, "haveTestService",
                                       currently_have_test_service);
}

static void
test_service_always_error_sync(DBusConnection* connection,
                               DBusMessage* message,
                               DBusMessageIter* in_iter,
                               DBusMessageIter* out_iter,
                               void* data,
                               DBusError* error)
{
    big_debug(BIG_DEBUG_IN_TESTS, "com.litl.TestService got alwaysErrorSync");

    g_assert(G_IS_OBJECT(data));

    dbus_set_error(error, DBUS_ERROR_FILE_NOT_FOUND,
                   "Did not find some kind of file! Help!");
}

static BigDBusJsonMethod test_service_methods[]
  = { { "getSomeStuffSync", test_service_get_some_stuff_sync, NULL },
      { "alwaysErrorSync", test_service_always_error_sync, NULL } };

static void
on_test_service_acquired(DBusConnection* connection,
                         const char* name,
                         void* data)
{
    g_assert(!currently_have_test_service);
    currently_have_test_service = TRUE;

    big_debug(BIG_DEBUG_IN_TESTS, "com.litl.TestService acquired by child");

    big_dbus_register_json(connection, "com.litl.TestIface",
                           test_service_methods,
                           G_N_ELEMENTS(test_service_methods));

    test_service_object = g_object_new(G_TYPE_OBJECT, NULL);

    big_dbus_register_g_object(connection, "/com/litl/test/object42",
                               test_service_object, "com.litl.TestIface");
}

static void
on_test_service_lost(DBusConnection* connection, const char* name, void* data)
{
    g_assert(currently_have_test_service);
    currently_have_test_service = FALSE;

    big_debug(BIG_DEBUG_IN_TESTS, "com.litl.TestService lost by child");

    big_dbus_unregister_g_object(connection, "/com/litl/test/object42");

    big_dbus_unregister_json(connection, "com.litl.TestIface");
}

static BigDBusNameOwnerFuncs test_service_funcs
  = { "com.litl.TestService", BIG_DBUS_NAME_SINGLE_INSTANCE,
      on_test_service_acquired, on_test_service_lost };

static void
do_test_service_child(void)
{
    GMainLoop* loop;

    g_type_init();

    loop = g_main_loop_new(NULL, FALSE);

    big_dbus_acquire_name(DBUS_BUS_SESSION, &test_service_funcs, NULL);

    g_main_loop_run(loop);

    /* Don't return to the test program main() */
    exit(0);
}

/*
 * Second child service we forked, tests IO streams
 */

static gboolean currently_have_test_io = FALSE;
static GObject* test_io_object = NULL;

static BigDBusInputStream* io_input_stream = NULL;
static BigDBusOutputStream* io_output_stream = NULL;

static GString* input_buffer = NULL;

static void
test_io_confirm_streams_data(DBusConnection* connection,
                             DBusMessage* message,
                             DBusMessageIter* in_iter,
                             DBusMessageIter* out_iter,
                             void* data,
                             DBusError* error)
{
    const char* received;

    big_debug(BIG_DEBUG_IN_TESTS, "com.litl.TestIO got confirmStreamsData");

    g_assert(G_IS_OBJECT(data));

    received = extract_string_arg(in_iter, "received", error);
    if (received == NULL) {
        g_assert(error == NULL || dbus_error_is_set(error));
        return;
    }

    if (strcmp(received, stream_data_from_io_service) != 0) {
        g_error("We sent the main process '%s' but it says it got '%s'",
                stream_data_from_io_service, received);
        return;
    }

    /* We were reading from the main process in the main loop.
     * As a hack, we'll block in the main loop here to test.
     * In a real app, never block in the main loop; you would
     * just plain block, e.g. in g_input_stream_read(), if
     * you wanted to block. But don't block.
     */
    while (io_input_stream != NULL) {
        g_main_context_iteration(NULL, TRUE);
    }

    big_dbus_append_json_entry_STRING(out_iter, "received", input_buffer->str);

    g_string_free(input_buffer, TRUE);
    input_buffer = NULL;
}

static void
on_input_ready(BigDBusInputStream* dbus_stream, void* data)
{
    GInputStream* stream;
    char buf[3];
    gssize result;
    GError* error;

    stream = G_INPUT_STREAM(dbus_stream);

    g_assert(dbus_stream == io_input_stream);

    /* test get_received() */
    g_assert(big_dbus_input_stream_get_received(dbus_stream)
             <= strlen(stream_data_to_io_service));

    /* Should not block, since we got the ready-to-read signal */
    error = NULL;
    result = g_input_stream_read(G_INPUT_STREAM(io_input_stream), buf,
                                 sizeof(buf), NULL, &error);
    if (result < 0) {
        g_error("Error reading bytes from input stream: %s", error->message);
        g_error_free(error);
    }

    if (result == 0) {
        /* EOF */
        if (!g_input_stream_close(G_INPUT_STREAM(io_input_stream), NULL,
                                  &error)) {
            g_error("Error closing input stream in child: %s", error->message);
            g_error_free(error);
        }
        g_object_unref(io_input_stream);
        io_input_stream = NULL;

        return;
    }

    g_string_append_len(input_buffer, buf, result);

    /* We should automatically get another callback if there's more data or EOF
     * was not yet reached.
     */
}

static void
test_io_setup_streams(DBusConnection* connection,
                      DBusMessage* message,
                      DBusMessageIter* in_iter,
                      DBusMessageIter* out_iter,
                      void* data,
                      DBusError* error)
{
    const char* stream_path;
    gsize total;
    gsize remaining;
    gssize result;
    GError* gerror;

    big_debug(BIG_DEBUG_IN_TESTS, "com.litl.TestIO got setupStreams");

    g_assert(G_IS_OBJECT(data));

    stream_path = extract_string_arg(in_iter, "stream", error);

    if (stream_path == NULL) {
        g_assert(error == NULL || dbus_error_is_set(error));
        return;
    }

    /* Create output stream to write to caller's path */
    io_output_stream
      = big_dbus_output_stream_new(connection, dbus_message_get_sender(message),
                                   stream_path);

    /* Create input stream and return its path to caller */
    io_input_stream = g_object_new(BIG_TYPE_DBUS_INPUT_STREAM, NULL);
    big_dbus_input_stream_attach(io_input_stream, connection);
    stream_path = big_dbus_input_stream_get_path(io_input_stream);

    big_dbus_append_json_entry_STRING(out_iter, "stream", stream_path);

    /* Set up callbacks to read input stream in an async way */
    input_buffer = g_string_new(NULL);

    g_signal_connect(io_input_stream, "ready-to-read",
                     G_CALLBACK(on_input_ready), NULL);

    /* Write to output stream */
    gerror = NULL;
    total = strlen(stream_data_from_io_service);
    remaining = total;
    while (remaining > 0) {
        /* One byte at a time, fun torture test, totally silly in real
         * code of course
         */
        result = g_output_stream_write(G_OUTPUT_STREAM(io_output_stream),
                                       stream_data_from_io_service
                                         + (total - remaining),
                                       1, NULL, &gerror);
        if (result < 0) {
            g_assert(gerror != NULL);
            g_error("Error writing to output stream: %s", gerror->message);
            g_error_free(gerror);
        }

        if (result != 1) {
            g_error("Wrote %d instead of 1 bytes", (int) result);
        }

        remaining -= 1;
    }

    /* flush should do nothing here, and is not needed, but
     * just calling it to test it
     */
    if (!g_output_stream_flush(G_OUTPUT_STREAM(io_output_stream), NULL,
                               &gerror)) {
        g_assert(gerror != NULL);
        g_error("Error flushing output stream: %s", gerror->message);
        g_error_free(gerror);
    }

    if (!g_output_stream_close(G_OUTPUT_STREAM(io_output_stream), NULL,
                               &gerror)) {
        g_assert(gerror != NULL);
        g_error("Error closing output stream: %s", gerror->message);
        g_error_free(gerror);
    }
    g_object_unref(io_output_stream);
    io_output_stream = NULL;

    /* Now return, and wait for our input stream data to come in from
     * the main process
     */
}

static BigDBusJsonMethod test_io_methods[]
  = { { "setupStreams", test_io_setup_streams, NULL },
      { "confirmStreamsData", test_io_confirm_streams_data, NULL } };

static void
on_test_io_acquired(DBusConnection* connection, const char* name, void* data)
{
    g_assert(!currently_have_test_io);
    currently_have_test_io = TRUE;

    big_debug(BIG_DEBUG_IN_TESTS, "com.litl.TestIO acquired by child");

    big_dbus_register_json(connection, "com.litl.TestIO", test_io_methods,
                           G_N_ELEMENTS(test_io_methods));

    test_io_object = g_object_new(G_TYPE_OBJECT, NULL);

    big_dbus_register_g_object(connection, "/com/litl/test/object47",
                               test_io_object, "com.litl.TestIO");
}

static void
on_test_io_lost(DBusConnection* connection, const char* name, void* data)
{
    g_assert(currently_have_test_io);
    currently_have_test_io = FALSE;

    big_debug(BIG_DEBUG_IN_TESTS, "com.litl.TestIO lost by child");

    big_dbus_unregister_g_object(connection, "/com/litl/test/object47");

    big_dbus_unregister_json(connection, "com.litl.TestIO");
}

static BigDBusNameOwnerFuncs test_io_funcs
  = { "com.litl.TestIO", BIG_DBUS_NAME_SINGLE_INSTANCE, on_test_io_acquired,
      on_test_io_lost };

static void
do_test_io_child(void)
{
    GMainLoop* loop;

    g_type_init();

    loop = g_main_loop_new(NULL, FALSE);

    big_dbus_acquire_name(DBUS_BUS_SESSION, &test_io_funcs, NULL);

    g_main_loop_run(loop);

    /* Don't return to the test program main() */
    exit(0);
}

#endif /* BIG_BUILD_TESTS */
