/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2006 Red Hat, Inc.
 * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
 *
 * 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.
 *
 */

#include "config.h"

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#if HAVE_EXECINFO_H
	#include <execinfo.h>
#endif
#include <syslog.h>
#include <sys/wait.h>
#include <sys/stat.h>

#include <glib.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <glib-object.h>

#include "mdm-signal-handler.h"

#define MDM_SIGNAL_HANDLER_GET_PRIVATE(o) \
	(G_TYPE_INSTANCE_GET_PRIVATE((o), MDM_TYPE_SIGNAL_HANDLER, MdmSignalHandlerPrivate))

#ifdef __GNUC__
#define UNUSED_VARIABLE __attribute__ ((unused))
#else
#define UNUSED_VARIABLE
#endif

typedef struct {
	int signal_number;
	MdmSignalHandlerFunc func;
	gpointer data;
	guint id;
} CallbackData;

struct MdmSignalHandlerPrivate {
	GHashTable* lookup;
	GHashTable* id_lookup;
	GHashTable* action_lookup;
	guint next_id;
	GDestroyNotify fatal_func;
	gpointer fatal_data;
};

static void mdm_signal_handler_class_init(MdmSignalHandlerClass* klass);
static void mdm_signal_handler_init(MdmSignalHandler* signal_handler);
static void mdm_signal_handler_finalize(GObject* object);

static gpointer signal_handler_object = NULL;
static int signal_pipes[2];
static int signals_blocked = 0;
static sigset_t signals_block_mask;
static sigset_t signals_oldmask;

G_DEFINE_TYPE(MdmSignalHandler, mdm_signal_handler, G_TYPE_OBJECT)

static void block_signals_push(void)
{
	signals_blocked++;

	if (signals_blocked == 1)
	{
		/* Set signal mask */
		sigemptyset(&signals_block_mask);
		sigfillset(&signals_block_mask);
		sigprocmask(SIG_BLOCK, &signals_block_mask, &signals_oldmask);
	}
}

static void block_signals_pop(void)
{
	signals_blocked--;

	if (signals_blocked == 0)
	{
		/* Set signal mask */
		sigprocmask(SIG_SETMASK, &signals_oldmask, NULL);
	}
}

static gboolean signal_io_watch(GIOChannel* ioc, GIOCondition condition, MdmSignalHandler* handler)
{
	char buf[256];
	gboolean is_fatal;
	gsize bytes_read;
	int i;

	block_signals_push();

	g_io_channel_read_chars(ioc, buf, sizeof(buf), &bytes_read, NULL);

	is_fatal = FALSE;

	for (i = 0; i < bytes_read; i++)
	{
		int signum;
		GSList* handlers;
		GSList* l;

		signum = (gint32) buf[i];

		g_debug("MdmSignalHandler: handling signal %d", signum);
		handlers = g_hash_table_lookup(handler->priv->lookup, GINT_TO_POINTER(signum));

		g_debug("MdmSignalHandler: Found %u callbacks", g_slist_length(handlers));

		for (l = handlers; l != NULL; l = l->next)
		{
			gboolean res;
			CallbackData* data;

			data = g_hash_table_lookup(handler->priv->id_lookup, l->data);

			if (data != NULL)
			{
				if (data->func != NULL)
				{
					g_debug("MdmSignalHandler: running %d handler: %p", signum, data->func);

					res = data->func(signum, data->data);

					if (!res)
					{
						is_fatal = TRUE;
					}
				}
			}
		}
	}

	block_signals_pop();

	if (is_fatal)
	{
		if (handler->priv->fatal_func != NULL)
		{
			g_debug("MdmSignalHandler: Caught termination signal - calling fatal func");
			handler->priv->fatal_func(handler->priv->fatal_data);
		}
		else
		{
			g_debug("MdmSignalHandler: Caught termination signal - exiting");
			exit (1);
		}

		return FALSE;
	}

	g_debug("MdmSignalHandler: Done handling signals");

	return TRUE;
}

static void fallback_get_backtrace(void)
{
	#if HAVE_EXECINFO_H
		void* frames[64];
		size_t size;
		char** strings;
		size_t i;

		size = backtrace(frames, G_N_ELEMENTS(frames));

		if ((strings = backtrace_symbols(frames, size)))
		{
			syslog(LOG_CRIT, "******************* START ********************************");

			for (i = 0; i < size; i++)
			{
				syslog(LOG_CRIT, "Frame %zd: %s", i, strings[i]);
			}

			free(strings);
			syslog(LOG_CRIT, "******************* END **********************************");
		}
		else
		{
	#endif
			g_warning ("MDM crashed, but symbols couldn't be retrieved.");
	#if HAVE_EXECINFO_H
		}
	#endif
}

static gboolean crashlogger_get_backtrace(void)
{
	gboolean success = FALSE;
	int pid;

	pid = fork();

	if (pid > 0)
	{
		/* Wait for the child to finish */
		int estatus;

		if (waitpid(pid, &estatus, 0) != -1)
		{
			/* Only succeed if the crashlogger succeeded */
			if (WIFEXITED(estatus) && (WEXITSTATUS(estatus) == 0))
			{
				success = TRUE;
			}
		}
	}
	else if (pid == 0)
	{
		/* Child process */
		execl(LIBEXECDIR "/mdm-crash-logger", LIBEXECDIR "/mdm-crash-logger", NULL);
	}

	return success;
}


static void mdm_signal_handler_backtrace(void)
{
	struct stat s;
	gboolean fallback = TRUE;

	/* Try to use gdb via mdm-crash-logger if it exists, since
	 * we get much better information out of it.  Otherwise
	 * fall back to execinfo.
	 */
	if (g_stat(LIBEXECDIR "/mdm-crash-logger", &s) == 0)
	{
		fallback = crashlogger_get_backtrace() ? FALSE : TRUE;
	}

	if (fallback)
	{
		fallback_get_backtrace();
	}
}

static void signal_handler(int signo)
{
	static int in_fatal = 0;
	int UNUSED_VARIABLE ignore;
	guchar signo_byte = signo;

	/* avoid loops */
	if (in_fatal > 0)
	{
		return;
	}

	++in_fatal;

	switch (signo)
	{
		case SIGSEGV:
		case SIGBUS:
		case SIGILL:
		case SIGABRT:
		case SIGTRAP:
			mdm_signal_handler_backtrace();
			exit(1);
			break;
		case SIGFPE:
		case SIGPIPE:
			/* let the fatal signals interrupt us */
			--in_fatal;
			mdm_signal_handler_backtrace();
			ignore = write(signal_pipes [1], &signo_byte, 1);
			break;
		default:
			--in_fatal;
			ignore = write(signal_pipes [1], &signo_byte, 1);
			break;
	}
}

static void catch_signal(MdmSignalHandler *handler, int signal_number)
{
	struct sigaction action;
	struct sigaction* old_action;

	g_debug("MdmSignalHandler: Registering for %d signals", signal_number);

	action.sa_handler = signal_handler;
	sigemptyset(&action.sa_mask);
	action.sa_flags = 0;

	old_action = g_new0(struct sigaction, 1);

	sigaction(signal_number, &action, old_action);

	g_hash_table_insert(handler->priv->action_lookup, GINT_TO_POINTER(signal_number), old_action);
}

static void uncatch_signal(MdmSignalHandler* handler, int signal_number)
{
	struct sigaction* old_action;

	g_debug("MdmSignalHandler: Unregistering for %d signals", signal_number);

	old_action = g_hash_table_lookup(handler->priv->action_lookup, GINT_TO_POINTER(signal_number));
	g_hash_table_remove(handler->priv->action_lookup, GINT_TO_POINTER(signal_number));

	sigaction(signal_number, old_action, NULL);

	g_free(old_action);
}

guint mdm_signal_handler_add(MdmSignalHandler* handler, int signal_number, MdmSignalHandlerFunc callback, gpointer data)
{
	CallbackData* cdata;
	GSList* list;

	g_return_val_if_fail(MDM_IS_SIGNAL_HANDLER(handler), 0);

	cdata = g_new0(CallbackData, 1);
	cdata->signal_number = signal_number;
	cdata->func = callback;
	cdata->data = data;
	cdata->id = handler->priv->next_id++;

	g_debug("MdmSignalHandler: Adding handler %u: signum=%d %p", cdata->id, cdata->signal_number, cdata->func);

	if (g_hash_table_lookup(handler->priv->action_lookup, GINT_TO_POINTER(signal_number)) == NULL)
	{
		catch_signal(handler, signal_number);
	}

	/* ID lookup owns the CallbackData */
	g_hash_table_insert(handler->priv->id_lookup, GUINT_TO_POINTER(cdata->id), cdata);

	list = g_hash_table_lookup(handler->priv->lookup, GINT_TO_POINTER(signal_number));
	list = g_slist_prepend(list, GUINT_TO_POINTER (cdata->id));

	g_hash_table_insert(handler->priv->lookup, GINT_TO_POINTER(signal_number), list);

	return cdata->id;
}

void mdm_signal_handler_add_fatal(MdmSignalHandler* handler)
{
	g_return_if_fail(MDM_IS_SIGNAL_HANDLER(handler));

	mdm_signal_handler_add(handler, SIGILL, NULL, NULL);
	mdm_signal_handler_add(handler, SIGBUS, NULL, NULL);
	mdm_signal_handler_add(handler, SIGSEGV, NULL, NULL);
	mdm_signal_handler_add(handler, SIGABRT, NULL, NULL);
	mdm_signal_handler_add(handler, SIGTRAP, NULL, NULL);
}

static void callback_data_free(CallbackData* d)
{
	g_free(d);
}

static void mdm_signal_handler_remove_and_free_data(MdmSignalHandler* handler, CallbackData* cdata)
{
	GSList* list;

	g_return_if_fail(MDM_IS_SIGNAL_HANDLER(handler));

	list = g_hash_table_lookup(handler->priv->lookup, GINT_TO_POINTER(cdata->signal_number));
	list = g_slist_remove_all(list, GUINT_TO_POINTER(cdata->id));

	if (list == NULL)
	{
		uncatch_signal(handler, cdata->signal_number);
	}

	g_debug("MdmSignalHandler: Removing handler %u: signum=%d %p", cdata->signal_number, cdata->id, cdata->func);
	/* put changed list back in */
	g_hash_table_insert(handler->priv->lookup, GINT_TO_POINTER(cdata->signal_number), list);

	g_hash_table_remove(handler->priv->id_lookup, GUINT_TO_POINTER(cdata->id));
}

void mdm_signal_handler_remove(MdmSignalHandler* handler, guint id)
{
	CallbackData* found;

	g_return_if_fail(MDM_IS_SIGNAL_HANDLER(handler));

	found = g_hash_table_lookup(handler->priv->id_lookup, GUINT_TO_POINTER(id));

	if (found != NULL)
	{
		mdm_signal_handler_remove_and_free_data(handler, found);
		found = NULL;
	}
}

static CallbackData* find_callback_data_by_func(MdmSignalHandler* handler, guint signal_number, MdmSignalHandlerFunc callback, gpointer data)
{
	GSList* list;
	GSList* l;
	CallbackData* found;

	found = NULL;

	list = g_hash_table_lookup(handler->priv->lookup, GINT_TO_POINTER(signal_number));

	for (l = list; l != NULL; l = l->next)
	{
		guint id;
		CallbackData* d;

		id = GPOINTER_TO_UINT(l->data);

		d = g_hash_table_lookup(handler->priv->id_lookup, GUINT_TO_POINTER (id));

		if (d != NULL && d->func == callback && d->data == data)
		{
			found = d;
			break;
		}
	}

	return found;
}

void mdm_signal_handler_remove_func(MdmSignalHandler* handler, guint signal_number, MdmSignalHandlerFunc callback, gpointer data)
{
	CallbackData* found;

	g_return_if_fail(MDM_IS_SIGNAL_HANDLER(handler));

	found = find_callback_data_by_func(handler, signal_number, callback, data);

	if (found != NULL)
	{
		mdm_signal_handler_remove_and_free_data(handler, found);
		found = NULL;
	}

	/* FIXME: once all handlers are removed deregister signum handler */
}

static void mdm_signal_handler_class_init(MdmSignalHandlerClass* klass)
{
	GObjectClass* object_class = G_OBJECT_CLASS(klass);

	object_class->finalize = mdm_signal_handler_finalize;

	g_type_class_add_private(klass, sizeof(MdmSignalHandlerPrivate));
}

static void signal_list_free(GSList *list)
{
	g_slist_free(list);
}

void mdm_signal_handler_set_fatal_func(MdmSignalHandler* handler, MdmShutdownHandlerFunc func, gpointer user_data)
{
	g_return_if_fail(MDM_IS_SIGNAL_HANDLER(handler));

	handler->priv->fatal_func = func;
	handler->priv->fatal_data = user_data;
}

static void mdm_signal_handler_init(MdmSignalHandler* handler)
{
	GIOChannel* ioc;

	handler->priv = MDM_SIGNAL_HANDLER_GET_PRIVATE(handler);

	handler->priv->next_id = 1;

	handler->priv->lookup = g_hash_table_new(NULL, NULL);
	handler->priv->id_lookup = g_hash_table_new(NULL, NULL);
	handler->priv->action_lookup = g_hash_table_new(NULL, NULL);

	if (pipe(signal_pipes) == -1)
	{
		g_error ("Could not create pipe() for signal handling");
	}

	ioc = g_io_channel_unix_new(signal_pipes[0]);
	g_io_channel_set_flags(ioc, G_IO_FLAG_NONBLOCK, NULL);
	g_io_add_watch_full(ioc, G_PRIORITY_HIGH, G_IO_IN, (GIOFunc) signal_io_watch, handler, NULL);
	g_io_channel_set_close_on_unref(ioc, TRUE);
	g_io_channel_unref(ioc);
}

static void mdm_signal_handler_finalize(GObject* object)
{
	MdmSignalHandler* handler;
	GList* l;

	g_return_if_fail(object != NULL);
	g_return_if_fail(MDM_IS_SIGNAL_HANDLER(object));

	handler = MDM_SIGNAL_HANDLER(object);

	g_debug("MdmSignalHandler: Finalizing signal handler");

	g_return_if_fail(handler->priv != NULL);

	for (l = g_hash_table_get_values(handler->priv->lookup); l != NULL; l = l->next)
	{
		signal_list_free((GSList*) l->data);
	}

	g_hash_table_destroy(handler->priv->lookup);

	for (l = g_hash_table_get_values(handler->priv->id_lookup); l != NULL; l = l->next)
	{
		callback_data_free((CallbackData*) l->data);
	}

	g_hash_table_destroy(handler->priv->id_lookup);

	for (l = g_hash_table_get_values(handler->priv->action_lookup); l != NULL; l = l->next)
	{
		g_free(l->data);
	}

	g_hash_table_destroy(handler->priv->action_lookup);

	close(signal_pipes[0]);
	close(signal_pipes[1]);

	G_OBJECT_CLASS(mdm_signal_handler_parent_class)->finalize(object);
}

MdmSignalHandler* mdm_signal_handler_new(void)
{
	if (signal_handler_object != NULL)
	{
		g_object_ref(signal_handler_object);
	}
	else
	{
		signal_handler_object = g_object_new(MDM_TYPE_SIGNAL_HANDLER, NULL);
		g_object_add_weak_pointer(signal_handler_object, (gpointer*) &signal_handler_object);
	}

	return MDM_SIGNAL_HANDLER(signal_handler_object);
}