/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2012  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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 <errno.h>
#include <unistd.h>
#include <stdarg.h>
#include <sys/wait.h>
#include <signal.h>

#include <glib.h>

#include "connman.h"

struct notify_data {
	connman_task_notify_t func;
	void *data;
};

struct connman_task {
	char *path;
	pid_t pid;
	guint child_watch;
	GPtrArray *argv;
	GPtrArray *envp;
	connman_task_exit_t exit_func;
	connman_task_setup_t setup_func;
	void *exit_data;
	GHashTable *notify;
	void *setup_data;
};

static GHashTable *task_hash = NULL;

static volatile int task_counter;

static DBusConnection *connection;

static void free_pointer(gpointer data, gpointer user_data)
{
	g_free(data);
}

static void free_task(gpointer data)
{
	struct connman_task *task = data;

	DBG("task %p", task);

	g_hash_table_destroy(task->notify);
	task->notify = NULL;

	if (task->pid > 0)
		kill(task->pid, SIGTERM);

	if (task->child_watch > 0)
		g_source_remove(task->child_watch);

	g_ptr_array_foreach(task->envp, free_pointer, NULL);
	g_ptr_array_free(task->envp, TRUE);

	g_ptr_array_foreach(task->argv, free_pointer, NULL);
	g_ptr_array_free(task->argv, TRUE);

	g_free(task->path);
	g_free(task);
}

/**
 * connman_task_create:
 * @program: name of executable
 *
 * Allocate a new task of given #program
 *
 * Returns: a newly-allocated #connman_task structure
 */
struct connman_task *connman_task_create(const char *program,
					connman_task_setup_t custom_task_setup,
					void *setup_data)
{
	struct connman_task *task;
	gint counter;
	char *str;

	DBG("");

	task = g_try_new0(struct connman_task, 1);
	if (!task)
		return NULL;

	counter = __sync_fetch_and_add(&task_counter, 1);

	task->path = g_strdup_printf("/task/%d", counter);
	task->pid = -1;

	task->argv = g_ptr_array_new();
	task->envp = g_ptr_array_new();

	str = g_strdup(program);
	g_ptr_array_add(task->argv, str);

	task->setup_func = custom_task_setup;

	task->notify = g_hash_table_new_full(g_str_hash, g_str_equal,
							g_free, g_free);

	task->setup_data = setup_data;

	DBG("task %p", task);

	g_hash_table_insert(task_hash, task->path, task);

	return task;
}

/**
 * connman_task_destory:
 * @task: task structure
 *
 * Remove and destroy #task
 */
void connman_task_destroy(struct connman_task *task)
{
	DBG("task %p", task);

	g_hash_table_remove(task_hash, task->path);
}

/**
 * connman_task_get_path:
 * @task: task structure
 *
 * Get object path
 */
const char *connman_task_get_path(struct connman_task *task)
{
	return task->path;
}

/**
 * connman_task_add_argument:
 * @task: task structure
 * @name: argument name
 * @format: format string
 * @Varargs: list of arguments
 *
 * Add a new command line argument
 */
int connman_task_add_argument(struct connman_task *task,
				const char *name, const char *format, ...)
{
	va_list ap;
	char *str;

	DBG("task %p arg %s", task, name);

	if (!name)
		return -EINVAL;

	str = g_strdup(name);
	g_ptr_array_add(task->argv, str);

	va_start(ap, format);

	if (format) {
		str = g_strdup_vprintf(format, ap);
		g_ptr_array_add(task->argv, str);
	}

	va_end(ap);

	return 0;
}

/**
 * connman_task_add_variable:
 * @task: task structure
 * @key: variable name
 * @format: format string
 * @Varargs: list of arguments
 *
 * Add a new environment variable
 */
int connman_task_add_variable(struct connman_task *task,
				const char *key, const char *format, ...)
{
	va_list ap;
	char *str, *val;

	DBG("task %p key %s", task, key);

	if (!key)
		return -EINVAL;

	va_start(ap, format);

	val = g_strdup_vprintf(format, ap);
	str = g_strdup_printf("%s=%s", key, format ? format : "");
	g_ptr_array_add(task->envp, str);
	g_free(val);

	va_end(ap);

	return 0;
}

/**
 * connman_task_set_notify:
 * @task: task structure
 * @member: notification method name
 * @function: notification callback
 * @user_data: optional notification user data
 *
 * Set notification handler for #member
 */
int connman_task_set_notify(struct connman_task *task, const char *member,
			connman_task_notify_t function, void *user_data)
{
	struct notify_data *notify;

	DBG("task %p", task);

	notify = g_try_new0(struct notify_data, 1);
	if (!notify)
		return -ENOMEM;

	notify->func = function;
	notify->data = user_data;

	g_hash_table_replace(task->notify, g_strdup(member), notify);

	return 0;
}

static void task_died(GPid pid, gint status, gpointer user_data)
{
	struct connman_task *task = user_data;
	int exit_code;

	if (WIFEXITED(status)) {
		exit_code = WEXITSTATUS(status);
		DBG("task %p exit status %d", task, exit_code);
	} else {
		exit_code = 0;
		DBG("task %p signal %d", task, WTERMSIG(status));
	}

	g_spawn_close_pid(pid);
	task->pid = -1;

	task->child_watch = 0;

	if (task->exit_func)
		task->exit_func(task, exit_code, task->exit_data);
}

static void task_setup(gpointer user_data)
{
	sigset_t mask;
	struct connman_task *task = user_data;

	DBG("task %p", task);

	sigemptyset(&mask);
	if (sigprocmask(SIG_SETMASK, &mask, NULL) < 0)
		connman_error("Failed to clean signal mask");

	if (task->setup_func)
		task->setup_func(task->setup_data);
}

/**
 * connman_task_run:
 * @task: task structure
 * @function: exit callback
 * @user_data: optional exit user data
 * @fd: optional spawn with pipe
 *
 * Execute program specified by #task
 */
int connman_task_run(struct connman_task *task,
			connman_task_exit_t function, void *user_data,
			int *stdin_fd, int *stdout_fd, int *stderr_fd)
{
	GSpawnFlags flags = G_SPAWN_DO_NOT_REAP_CHILD;
	bool result;
	char **argv, **envp;

	DBG("task %p", task);

	if (task->pid > 0)
		return -EALREADY;

	if (!stdout_fd)
		flags |= G_SPAWN_STDOUT_TO_DEV_NULL;

	if (!stderr_fd)
		flags |= G_SPAWN_STDERR_TO_DEV_NULL;

	task->exit_func = function;
	task->exit_data = user_data;

	if (g_ptr_array_index(task->argv, task->argv->len - 1))
		g_ptr_array_add(task->argv, NULL);

	if (task->envp->len == 0 ||
			g_ptr_array_index(task->envp, task->envp->len - 1)) {
		if (g_hash_table_size(task->notify) > 0) {
			const char *busname;
			char *str;

			busname = dbus_bus_get_unique_name(connection);
			str = g_strdup_printf("CONNMAN_BUSNAME=%s", busname);
			g_ptr_array_add(task->envp, str);

			str = g_strdup_printf("CONNMAN_INTERFACE=%s",
						CONNMAN_TASK_INTERFACE);
			g_ptr_array_add(task->envp, str);

			str = g_strdup_printf("CONNMAN_PATH=%s", task->path);
			g_ptr_array_add(task->envp, str);
		}

		g_ptr_array_add(task->envp, NULL);
	}

	argv = (char **) task->argv->pdata;
	envp = (char **) task->envp->pdata;

	result = g_spawn_async_with_pipes(NULL, argv, envp, flags,
					task_setup, task, &task->pid,
					stdin_fd, stdout_fd, stderr_fd, NULL);
	if (!result) {
		connman_error("Failed to spawn %s", argv[0]);
		return -EIO;
	}

	task->child_watch = g_child_watch_add(task->pid, task_died, task);

	return 0;
}

static gboolean force_kill_timeout(gpointer user_data)
{
	pid_t pid = GPOINTER_TO_INT(user_data);
	if (pid > 0) {
		if (kill(pid, SIGKILL) == 0)
			connman_warn("killing pid %d by force", pid);
	}

	return FALSE;
}

static gboolean kill_timeout(gpointer user_data)
{
	pid_t pid = GPOINTER_TO_INT(user_data);
	if (pid > 0) {
		if (kill(pid, SIGINT) == 0)
			g_timeout_add_seconds(1, force_kill_timeout,
					GINT_TO_POINTER(pid));
	}

	return FALSE;
}

static gboolean check_kill(gpointer user_data)
{
	pid_t pid = GPOINTER_TO_INT(user_data);
	if (pid > 0) {
		if (kill(pid, 0) == 0) {
			connman_info("pid %d was not killed, "
					"retrying after 2 sec", pid);
			g_timeout_add_seconds(2, kill_timeout,
					GINT_TO_POINTER(pid));
		}
	}

	return FALSE;
}

/**
 * connman_task_stop:
 * @task: task structure
 *
 * Stop program specified by #task
 */
int connman_task_stop(struct connman_task *task)
{
	DBG("task %p", task);

	if (task->pid > 0) {
		kill(task->pid, SIGTERM);

		g_idle_add(check_kill, GINT_TO_POINTER(task->pid));
	}

	return 0;
}

static DBusHandlerResult task_filter(DBusConnection *conn,
					DBusMessage *message, void *user_data)
{
	struct connman_task *task;
	struct notify_data *notify;
	const char *path, *member;
	DBusMessage *reply = NULL;

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

	if (!dbus_message_has_interface(message, CONNMAN_TASK_INTERFACE))
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

	path = dbus_message_get_path(message);
	if (!path)
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

	task = g_hash_table_lookup(task_hash, path);
	if (!task)
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

	member = dbus_message_get_member(message);
	if (!member)
		goto send_reply;

	notify = g_hash_table_lookup(task->notify, member);
	if (!notify)
		goto send_reply;

	if (notify->func)
		reply = notify->func(task, message, notify->data);

send_reply:
	if (!dbus_message_get_no_reply(message) &&
						!reply) {

		reply = dbus_message_new_method_return(message);
		if (!reply)
			return DBUS_HANDLER_RESULT_NEED_MEMORY;
	}

	if (reply) {
		dbus_connection_send(conn, reply, NULL);

		dbus_message_unref(reply);
	}

	return DBUS_HANDLER_RESULT_HANDLED;
}

static const char *task_rule = "type=method_call"
					",interface=" CONNMAN_TASK_INTERFACE;

int __connman_task_init(void)
{
	DBG("");

	connection = connman_dbus_get_connection();

	dbus_connection_add_filter(connection, task_filter, NULL, NULL);

	task_counter = 0;
	__sync_synchronize();

	task_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
							NULL, free_task);

	dbus_bus_add_match(connection, task_rule, NULL);
	dbus_connection_flush(connection);

	return 0;
}

void __connman_task_cleanup(void)
{
	DBG("");

	dbus_bus_remove_match(connection, task_rule, NULL);
	dbus_connection_flush(connection);

	g_hash_table_destroy(task_hash);
	task_hash = NULL;

	dbus_connection_remove_filter(connection, task_filter, NULL);

	dbus_connection_unref(connection);
}
