root/usr/src/cmd/hal/hald/hald_runner.c
/***************************************************************************
 * CVSID: $Id$
 *
 * hald_runner.c - Interface to the hal runner helper daemon
 *
 * Copyright (C) 2006 Sjoerd Simons, <sjoerd@luon.net>
 *
 * Licensed under the Academic Free License version 2.1
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 **************************************************************************/

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <sys/utsname.h>
#include <stdio.h>

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

#include "hald.h"
#include "util.h"
#include "logger.h"
#include "hald_dbus.h"
#include "hald_runner.h"

typedef struct {
  HalDevice *d;
  HalRunTerminatedCB cb;
  gpointer data1;
  gpointer data2;
} HelperData;

#define DBUS_SERVER_ADDRESS "unix:tmpdir=" HALD_SOCKET_DIR

static DBusConnection *runner_connection = NULL;

typedef struct
{
        GPid pid;
        HalDevice *device;
        HalRunTerminatedCB cb;
        gpointer data1;
        gpointer data2;
} RunningProcess;

/* mapping from PID to RunningProcess */
static GHashTable *running_processes;

static gboolean
rprd_foreach (gpointer key,
              gpointer value,
              gpointer user_data)
{
        gboolean remove = FALSE;
        RunningProcess *rp = value;
        HalDevice *device = user_data;

        if (rp->device == device) {
                remove = TRUE;
                g_free (rp);
        }

        return remove;
}

static void
running_processes_remove_device (HalDevice *device)
{
        g_hash_table_foreach_remove (running_processes, rprd_foreach, device);
}

void
runner_device_finalized (HalDevice *device)
{
        running_processes_remove_device (device);
}


static DBusHandlerResult
runner_server_message_handler (DBusConnection *connection,
                               DBusMessage *message,
                               void *user_data)
{

        /*HAL_INFO (("runner_server_message_handler: destination=%s obj_path=%s interface=%s method=%s",
                   dbus_message_get_destination (message),
                   dbus_message_get_path (message),
                   dbus_message_get_interface (message),
                   dbus_message_get_member (message)));*/
        if (dbus_message_is_signal (message,
                                    "org.freedesktop.HalRunner",
                                    "StartedProcessExited")) {
                dbus_uint64_t dpid;
                DBusError error;
                dbus_error_init (&error);
                if (dbus_message_get_args (message, &error,
                                           DBUS_TYPE_INT64, &dpid,
                                           DBUS_TYPE_INVALID)) {
                        RunningProcess *rp;
                        GPid pid;

                        pid = (GPid) dpid;

                        /*HAL_INFO (("Previously started process with pid %d exited", pid));*/
                        rp = g_hash_table_lookup (running_processes, (gpointer) pid);
                        if (rp != NULL) {
                                rp->cb (rp->device, 0, 0, NULL, rp->data1, rp->data2);
                                g_hash_table_remove (running_processes, (gpointer) pid);
                                g_free (rp);
                        }
                }
        }
        return DBUS_HANDLER_RESULT_HANDLED;
}

static void
runner_server_unregister_handler (DBusConnection *connection, void *user_data)
{
        HAL_INFO (("unregistered"));
}


static void
handle_connection(DBusServer *server,
                  DBusConnection *new_connection,
                  void *data)
{

        if (runner_connection == NULL) {
                DBusObjectPathVTable vtable = { &runner_server_unregister_handler,
                                                &runner_server_message_handler,
                                                NULL, NULL, NULL, NULL};

                runner_connection = new_connection;
                dbus_connection_ref (new_connection);
                dbus_connection_setup_with_g_main (new_connection, NULL);

                dbus_connection_register_fallback (new_connection,
                                                   "/org/freedesktop",
                                                   &vtable,
                                                   NULL);

                /* dbus_server_unref(server); */

        }
}

static void
runner_died(GPid pid, gint status, gpointer data) {
  g_spawn_close_pid (pid);
  DIE (("Runner died"));
}

gboolean
hald_runner_start_runner(void)
{
  DBusServer *server = NULL;
  DBusError err;
  GError *error = NULL;
  GPid pid;
  char *argv[] = { NULL, NULL};
  char *env[] =  { NULL, NULL, NULL, NULL};
  const char *hald_runner_path;
  char *server_addr;

  running_processes = g_hash_table_new (g_direct_hash, g_direct_equal);

  dbus_error_init(&err);
  server = dbus_server_listen(DBUS_SERVER_ADDRESS, &err);
  if (server == NULL) {
    HAL_ERROR (("Cannot create D-BUS server for the runner"));
    goto error;
  }

  dbus_server_setup_with_g_main(server, NULL);
  dbus_server_set_new_connection_function(server, handle_connection,
                                          NULL, NULL);


  argv[0] = "hald-runner";
  server_addr = dbus_server_get_address (server);
  env[0] = g_strdup_printf("HALD_RUNNER_DBUS_ADDRESS=%s", server_addr);
  dbus_free (server_addr);
  hald_runner_path = g_getenv("HALD_RUNNER_PATH");
  if (hald_runner_path != NULL) {
          env[1] = g_strdup_printf ("PATH=%s:" PACKAGE_LIBEXEC_DIR ":" PACKAGE_SCRIPT_DIR ":" PACKAGE_BIN_DIR, hald_runner_path);
  } else {
          env[1] = g_strdup_printf ("PATH=" PACKAGE_LIBEXEC_DIR ":" PACKAGE_SCRIPT_DIR ":" PACKAGE_BIN_DIR);
  }

  /*env[2] = "DBUS_VERBOSE=1";*/


  if (!g_spawn_async(NULL, argv, env, G_SPAWN_DO_NOT_REAP_CHILD|G_SPAWN_SEARCH_PATH,
        NULL, NULL, &pid, &error)) {
    HAL_ERROR (("Could not spawn runner : '%s'", error->message));
    g_error_free (error);
    goto error;
  }
  g_free(env[0]);
  g_free(env[1]);

  HAL_INFO (("Runner has pid %d", pid));

  g_child_watch_add(pid, runner_died, NULL);
  while (runner_connection == NULL) {
    /* Wait for the runner */
    g_main_context_iteration(NULL, TRUE);
  }
  return TRUE;

error:
  if (server != NULL)
    dbus_server_unref(server);
  return FALSE;
}

static gboolean
add_property_to_msg (HalDevice *device, HalProperty *property,
                                     gpointer user_data)
{
  char *prop_upper, *value;
  char *c;
  gchar *env;
  DBusMessageIter *iter = (DBusMessageIter *)user_data;

  prop_upper = g_ascii_strup (hal_property_get_key (property), -1);

  /* periods aren't valid in the environment, so replace them with
   * underscores. */
  for (c = prop_upper; *c; c++) {
    if (*c == '.')
      *c = '_';
  }

  value = hal_property_to_string (property);
  env = g_strdup_printf ("HAL_PROP_%s=%s", prop_upper, value);
  dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &env);

  g_free (env);
  g_free (value);
  g_free (prop_upper);

  return TRUE;
}

static void
add_env(DBusMessageIter *iter, const gchar *key, const gchar *value) {
  gchar *env;
  env = g_strdup_printf ("%s=%s", key, value);
  dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &env);
  g_free(env);
}

static void
add_basic_env(DBusMessageIter *iter, const gchar *udi) {
  struct utsname un;
  char *server_addr;

  if (hald_is_verbose) {
    add_env(iter, "HALD_VERBOSE", "1");
  }
  if (hald_is_initialising) {
    add_env(iter, "HALD_STARTUP", "1");
  }
  if (hald_use_syslog) {
    add_env(iter, "HALD_USE_SYSLOG", "1");
  }
  add_env(iter, "UDI", udi);
  server_addr = hald_dbus_local_server_addr();
  add_env(iter, "HALD_DIRECT_ADDR", server_addr);
  dbus_free (server_addr);
#ifdef HAVE_POLKIT
  add_env(iter, "HAVE_POLKIT", "1");
#endif

  if (uname(&un) >= 0) {
    char *sysname;

    sysname = g_ascii_strdown(un.sysname, -1);
    add_env(iter, "HALD_UNAME_S", sysname);
    g_free(sysname);
  }
}

static void
add_extra_env(DBusMessageIter *iter, gchar **env) {
  int i;
  if (env != NULL) for (i = 0; env[i] != NULL; i++) {
    dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &env[i]);
  }
}

static gboolean
add_command(DBusMessageIter *iter, const gchar *command_line) {
  gint argc;
  gint x;
  char **argv;
  GError *err = NULL;
  DBusMessageIter array_iter;

  if (!g_shell_parse_argv(command_line, &argc, &argv, &err)) {
    HAL_ERROR (("Error parsing commandline '%s': %s",
                 command_line, err->message));
    g_error_free (err);
    return FALSE;
  }
  if (!dbus_message_iter_open_container(iter,
                                   DBUS_TYPE_ARRAY,
                                   DBUS_TYPE_STRING_AS_STRING,
                                   &array_iter))
    DIE (("No memory"));
  for (x = 0 ; argv[x] != NULL; x++) {
    dbus_message_iter_append_basic(&array_iter, DBUS_TYPE_STRING, &argv[x]);
  }
  dbus_message_iter_close_container(iter, &array_iter);

  g_strfreev(argv);
  return TRUE;
}

static gboolean
add_first_part(DBusMessageIter *iter, HalDevice *device,
                   const gchar *command_line, char **extra_env) {
  DBusMessageIter array_iter;
  const char *udi;

  udi = hal_device_get_udi(device);
  dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &udi);

  dbus_message_iter_open_container(iter,
                                   DBUS_TYPE_ARRAY,
                                   DBUS_TYPE_STRING_AS_STRING,
                                   &array_iter);
  hal_device_property_foreach (device, add_property_to_msg, &array_iter);
  add_basic_env(&array_iter, udi);
  add_extra_env(&array_iter, extra_env);
  dbus_message_iter_close_container(iter, &array_iter);

  if (!add_command(iter, command_line)) {
    return FALSE;
  }
  return TRUE;
}

/* Start a helper, returns true on a successfull start */
gboolean
hald_runner_start (HalDevice *device, const gchar *command_line, char **extra_env,
                   HalRunTerminatedCB cb, gpointer data1, gpointer data2)
{
  DBusMessage *msg, *reply;
  DBusError err;
  DBusMessageIter iter;

  dbus_error_init(&err);
  msg = dbus_message_new_method_call("org.freedesktop.HalRunner",
                                     "/org/freedesktop/HalRunner",
                                     "org.freedesktop.HalRunner",
                                     "Start");
  if (msg == NULL)
    DIE(("No memory"));
  dbus_message_iter_init_append(msg, &iter);

  if (!add_first_part(&iter, device, command_line, extra_env))
    goto error;

  /* Wait for the reply, should be almost instantanious */
  reply =
    dbus_connection_send_with_reply_and_block(runner_connection,
                                              msg, -1, &err);
  if (reply) {
    gboolean ret =
      (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_METHOD_RETURN);

    if (ret) {
        dbus_int64_t pid_from_runner;
        if (dbus_message_get_args (reply, &err,
                                   DBUS_TYPE_INT64, &pid_from_runner,
                                   DBUS_TYPE_INVALID)) {
                if (cb != NULL) {
                        RunningProcess *rp;
                        rp = g_new0 (RunningProcess, 1);
                        rp->pid = (GPid) pid_from_runner;
                        rp->cb = cb;
                        rp->device = device;
                        rp->data1 = data1;
                        rp->data2 = data2;

                        g_hash_table_insert (running_processes, (gpointer) rp->pid, rp);
                }
        } else {
          HAL_ERROR (("Error extracting out_pid from runner's Start()"));
        }
    }

    dbus_message_unref(reply);
    dbus_message_unref(msg);
    return ret;
  }

error:
  dbus_message_unref(msg);
  return FALSE;
}

static void
call_notify(DBusPendingCall *pending, void *user_data)
{
  HelperData *hb = (HelperData *)user_data;
  dbus_uint32_t exitt = HALD_RUN_SUCCESS;
  dbus_int32_t return_code = 0;
  DBusMessage *m;
  GArray *error = NULL;
  DBusMessageIter iter;

  error = g_array_new(TRUE, FALSE, sizeof(char *));

  m = dbus_pending_call_steal_reply(pending);
  if (dbus_message_get_type(m) != DBUS_MESSAGE_TYPE_METHOD_RETURN)
    goto malformed;

  if (!dbus_message_iter_init(m, &iter) ||
       dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32)
    goto malformed;
  dbus_message_iter_get_basic(&iter, &exitt);

  if (!dbus_message_iter_next(&iter) ||
        dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INT32)
    goto malformed;
  dbus_message_iter_get_basic(&iter, &return_code);

  while (dbus_message_iter_next(&iter) &&
    dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING) {
    const char *value;
    dbus_message_iter_get_basic(&iter, &value);
    g_array_append_vals(error, &value, 1);
  }

  hb->cb(hb->d, exitt, return_code,
      (gchar **)error->data, hb->data1, hb->data2);

  g_object_unref (hb->d);

  dbus_message_unref(m);
  dbus_pending_call_unref (pending);
  g_array_free(error, TRUE);

  return;
malformed:
  /* Send a Fail callback on malformed messages */
  HAL_ERROR (("Malformed or unexpected reply message"));
  hb->cb(hb->d, HALD_RUN_FAILED, return_code, NULL, hb->data1, hb->data2);

  g_object_unref (hb->d);

  dbus_message_unref(m);
  dbus_pending_call_unref (pending);
  g_array_free(error, TRUE);
}

/* Run a helper program using the commandline, with input as infomation on
 * stdin */
void
hald_runner_run_method(HalDevice *device,
                           const gchar *command_line, char **extra_env,
                           gchar *input, gboolean error_on_stderr,
                           guint32 timeout,
                           HalRunTerminatedCB  cb,
                           gpointer data1, gpointer data2) {
  DBusMessage *msg;
  DBusMessageIter iter;
  DBusPendingCall *call;
  HelperData *hd = NULL;
  msg = dbus_message_new_method_call("org.freedesktop.HalRunner",
                             "/org/freedesktop/HalRunner",
                             "org.freedesktop.HalRunner",
                             "Run");
  if (msg == NULL)
    DIE(("No memory"));
  dbus_message_iter_init_append(msg, &iter);

  if (!add_first_part(&iter, device, command_line, extra_env))
    goto error;

  dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &input);
  dbus_message_iter_append_basic(&iter, DBUS_TYPE_BOOLEAN, &error_on_stderr);
  dbus_message_iter_append_basic(&iter, DBUS_TYPE_UINT32, &timeout);

  if (!dbus_connection_send_with_reply(runner_connection,
                                              msg, &call, INT_MAX))
    DIE (("No memory"));

  /* the connection was disconnected */
  if (call == NULL)
    goto error;

  hd = malloc(sizeof(HelperData));
  hd->d = device;
  hd->cb = cb;
  hd->data1 = data1;
  hd->data2 = data2;

  g_object_ref (device);

  dbus_pending_call_set_notify(call, call_notify, hd, free);
  dbus_message_unref(msg);
  return;
error:
  dbus_message_unref(msg);
  free(hd);
  cb(device, HALD_RUN_FAILED, 0, NULL, data1, data2);
}

void
hald_runner_run(HalDevice *device,
                    const gchar *command_line, char **extra_env,
                    guint timeout,
                    HalRunTerminatedCB  cb,
                    gpointer data1, gpointer data2) {
  hald_runner_run_method(device, command_line, extra_env,
                             "", FALSE, timeout, cb, data1, data2);
}



void
hald_runner_kill_device(HalDevice *device) {
  DBusMessage *msg, *reply;
  DBusError err;
  DBusMessageIter iter;
  const char *udi;

  running_processes_remove_device (device);

  msg = dbus_message_new_method_call("org.freedesktop.HalRunner",
                             "/org/freedesktop/HalRunner",
                             "org.freedesktop.HalRunner",
                             "Kill");
  if (msg == NULL)
    DIE(("No memory"));
  dbus_message_iter_init_append(msg, &iter);
  udi = hal_device_get_udi(device);
  dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &udi);

  /* Wait for the reply, should be almost instantanious */
  dbus_error_init(&err);
  reply =
    dbus_connection_send_with_reply_and_block(runner_connection, msg, -1, &err);
  if (reply) {
    dbus_message_unref(reply);
  }

  dbus_message_unref(msg);
}

void
hald_runner_kill_all(HalDevice *device) {
  DBusMessage *msg, *reply;
  DBusError err;

  /* hald_runner has not yet started, just return */
  if (runner_connection == NULL) {
    return;
  }

  running_processes_remove_device (device);

  msg = dbus_message_new_method_call("org.freedesktop.HalRunner",
                             "/org/freedesktop/HalRunner",
                             "org.freedesktop.HalRunner",
                             "KillAll");
  if (msg == NULL)
    DIE(("No memory"));

  /* Wait for the reply, should be almost instantanious */
  dbus_error_init(&err);
  reply =
    dbus_connection_send_with_reply_and_block(runner_connection,
                                              msg, -1, &err);
  if (reply) {
    dbus_message_unref(reply);
  }

  dbus_message_unref(msg);
}