/***************************************************************************
 * 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);
}