/* bug-buddy bug submitting program
 *
 * Copyright (C) 2007 Fernando Herrera
 *
 * Authors: Fernando Herrera  <fherrera@onirica.com>
 *          Cosimo Cecchi     <cosimoc@gnome.org>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General Public
 * License 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cassert>
#include <cstring>

#include <libelf.h>
#include <gelf.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>

#include <glib.h>
#include <glib/gstdio.h>

#include <gdk/gdk.h>
#include <gdk/gdkx.h>
#include <X11/Xlib.h>

#include <config.h>

#ifdef ENABLE_GOOGLE_BREAKPAD
#include "client/linux/handler/exception_handler.h"

using namespace google_breakpad;
#else
#include <string.h>
#endif

extern "C" const char *g_module_check_init (GModule *module);
extern "C" int gtk_module_init (int *argc, char** argv[]);
static bool    run_bug_buddy   (const gchar *appname, pid_t pid, const gchar *minidump_path);
static void    check_if_gdb    ();

static gchar *bugbuddy;
static GLogFunc old_handler = NULL;

typedef struct {
	int start;
	int next_free;
	int size;
	char **strings;
} CircBuff;

static CircBuff * log_buff = NULL;
#define LOG_BUFFER_SIZE 15

#ifdef ENABLE_GOOGLE_BREAKPAD
static ExceptionHandler * handler;

/* Callback when minidump written. */
static bool MinidumpCallback(const char *dump_path,
                             const char *minidump_id,
                             void *context,
                             bool succeeded) {
  gchar *appname;
  gchar *minidump_file;

	
  printf("%s is dumped\n", minidump_id);
  appname = g_get_prgname ();
  minidump_file = g_strdup_printf ("/tmp/%s.dmp", minidump_id);
  run_bug_buddy (appname, 0, minidump_file);

  g_unlink (minidump_file);
  g_free (minidump_file);
  _exit(0);
}
#endif

static void
circ_buff_free (CircBuff *buff)
{
	if (!buff) {
		return;
	}

	g_strfreev (buff->strings);
	g_free (buff);

	buff = NULL;
}

static CircBuff *
circ_buff_new (int size)
{
	CircBuff *retval;

	retval = g_new0 (CircBuff, 1);
	retval->strings = g_new0 (char *, size + 1);
	retval->strings[size + 1] = NULL;
	retval->size = size;

	return retval;
}

static void
circ_add (CircBuff *buff, char * str)
{
	if (buff->strings[buff->next_free]) {
		g_free (buff->strings[buff->next_free]);
		buff->start = (buff->start + 1) % buff->size;
	}
	
	buff->strings[buff->next_free] = str;
	buff->next_free = (buff->next_free + 1) % buff->size;
}

static char *
circ_buff_to_file (CircBuff *buff)
{
	int tmp_fd, i;
	char *actual_name = NULL;
	GError *error = NULL;
	const char *header;

	if (!buff) {
		return NULL;
	}

	tmp_fd = g_file_open_tmp ("bug-buddy-XXXXXX", &actual_name, &error);

	if (error) {
		g_warning ("Unable to create the warnings temp file: %s", error->message);
		g_error_free (error);
		return NULL;
	}

	header = "\n\n---- Critical and fatal warnings logged during execution ----\n\n";
	write (tmp_fd, header, strlen (header));

	i = buff->start;

	while (buff->strings[i] != NULL) {
		/* write to file */
		write (tmp_fd, buff->strings[i], strlen (buff->strings[i]));

		/* increase and check for overflow */
		i = (i + 1) % buff->size;

		if (i == buff->start)
			break;
	}

	close (tmp_fd);

	return actual_name;
}

static void
bug_buddy_log_handler (const gchar *log_domain,
		       GLogLevelFlags log_level,
		       const gchar *message,
		       gpointer user_data)
{
	/* forward the message to the real handler and use the default GLib
	 * handler if that's NULL (should never happen, but who knows).
	 */
	if (old_handler) {
		old_handler (log_domain, log_level, message, user_data);
	} else {
		g_log_default_handler (log_domain, log_level, message, user_data);
	}

	if (log_level & 
	    (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_FLAG_FATAL)) 
	{
		/* log the message into memory */
		if (!log_buff) {
			log_buff = circ_buff_new (LOG_BUFFER_SIZE);
		}

		/* if the log domain is NULL, assume it's a message coming from the app,
		 * see the docs for G_LOG_DOMAIN.
		 */
		circ_add (log_buff, g_strdup_printf ("** %s **: %s \n", log_domain ? log_domain : g_get_prgname (), message));
	}
}

static void
bugbuddy_segv_handle(int signum)
{
	static int in_segv = 0;
        struct sigaction sa;
	pid_t pid;
	
	sa.sa_handler = NULL;
	in_segv++;

        if (in_segv > 2) {
                /* The fprintf() was segfaulting, we are just totally hosed */
                _exit(1);
        } else if (in_segv > 1) {
                /* dialog display isn't working out */
                fprintf(stderr, "Multiple segmentation faults occurred; can't display error dialog\n");
                _exit(1);
        }

	check_if_gdb ();

#ifdef ENABLE_GOOGLE_BREAKPAD
	/* If we are here is because there are no debug symbols or gdb couldn't be run.
	 * Write the minidump anyway.
	 */

	handler->WriteMinidump ("/tmp", MinidumpCallback, NULL);
#endif

        _exit(1);
}

#define N_TRIES 3

static gboolean 
find_in_debug_path (const char *filename, const char *debug_filename)
{
	char *dir;
	char *tries[N_TRIES];
	gboolean result = false;
	int i;

	dir = g_path_get_dirname (filename);
	tries[0] = g_build_filename (dir, debug_filename, NULL);
	tries[1] = g_build_filename (dir, ".debug", debug_filename, NULL);
	tries[2] = g_build_filename ("/usr", "lib", "debug", dir, debug_filename, NULL);

	g_free (dir);

	for (i = 0; i < N_TRIES; ++i) {
		if (g_file_test (tries[i], G_FILE_TEST_EXISTS)) {
			result = true;
			break;
		}
	}

	for (i = 0; i < N_TRIES; i++)
		g_free (tries [i]);

	return result;
}


static gboolean
elf_has_debug_symbols (const char *filename)
{
       int fd;
       Elf *elf;
       GElf_Ehdr elf_header;
       Elf_Scn *section = 0;
       int number = 0;

       if (elf_version(EV_CURRENT) == EV_NONE ) {
               fprintf(stderr, "Elf library out of date!n");
                return false;
       }

       fd = open(filename, O_RDONLY);

       if ((elf = elf_begin(fd, ELF_C_READ, NULL)) == NULL){
               close (fd);
               return false;
       }
       gelf_getehdr (elf, &elf_header);
       while ((section = elf_nextscn(elf, section)) != 0) {
               char *name = 0;
               GElf_Shdr shdr;

		/* Check for stabs debug information */
               if (gelf_getshdr (section, &shdr) != 0) {
                       if (shdr.sh_type == SHT_SYMTAB) {
                               elf_end (elf);
                               return true;
                       }
               }

		/* Check for .gnu_debuglink separete debug file */
               if (shdr.sh_type == SHT_PROGBITS) {
                 char *name = elf_strptr(elf, elf_header.e_shstrndx, shdr.sh_name);
		 if (strcmp (name, ".gnu_debuglink") == 0) {
			Elf_Data *edata;

			edata = elf_getdata(section, NULL);
			if (edata != NULL && find_in_debug_path (filename, (const char*) edata->d_buf)) {
				elf_end (elf);
				return true;
			}
		}
              }
       }

       /* no symtab neither debug file present */
       elf_end (elf);
       return false;
}




static bool
release_grabs (void)
{
        /* Make sure we release grabs */
        gdk_pointer_ungrab(GDK_CURRENT_TIME);
        gdk_keyboard_ungrab(GDK_CURRENT_TIME);

        /* TODO: can we replace this with gdk_x11_ungrab_server ()?
         * see http://bugzilla.gnome.org/show_bug.cgi?id=550135.
         */
	XUngrabServer (GDK_DISPLAY ());

        gdk_flush();

	return true;
}

static bool
run_bug_buddy (const gchar *appname, pid_t pid, const gchar *minidump_path)
{
	gboolean res;
	char *warning_file, *exec_str;
	GString *args_str;
	GError *error = NULL;

	if (pid == 0 && minidump_path == NULL)
		return false;

	warning_file = circ_buff_to_file (log_buff);
	circ_buff_free (log_buff);

	args_str = g_string_new ("bug-buddy ");
	g_string_append_printf (args_str, "--appname=\"%s\" ", appname);

	if (warning_file) {
		g_string_append_printf (args_str, "--include=\"%s\" --unlink-tempfile ", warning_file);
		g_free (warning_file);
	}

	if (pid != 0)
		g_string_append_printf (args_str,  "--pid=%d",(int) pid);
	else if (minidump_path != NULL)
		g_string_append_printf (args_str, "--minidump=%s", minidump_path);

	exec_str = g_string_free (args_str, FALSE);
	res = g_spawn_command_line_sync (exec_str, NULL, NULL,
					 NULL, &error);
	g_free (exec_str);
	if (!res) {
		g_warning ("Couldn't run bug-buddy\n");
		return false;
	}

	return true;
}

static bool
run_gdb (const gchar *appname, pid_t pid)
{
	gchar *exec_str;
	gchar *title;
	gboolean res;
	GError *error = NULL;

	title = g_strdup_printf ("Debugging %s", appname);

	exec_str = g_strdup_printf("gnome-terminal "
				 "--title=\"%s\" "
				 "--disable-factory "
				 "--command=\"gdb %s %d\"",
				title, appname, (int)pid);
				g_free (title);
	res = g_spawn_command_line_sync (exec_str, NULL, NULL,
					 NULL, &error);
	g_free(exec_str);
	if (!res) {
		g_warning("Couldn't run debugger\n");
		return false;
	}

	return true;
}

static void
check_if_gdb ()
{
	char *mypath;
	gchar *gdb;
	bool has_debug_symbols = false;
	char *filename;
	gchar *appname;
	pid_t pid;
	gboolean res;

	release_grabs ();

	if (g_getenv ("GNOME_DISABLE_CRASH_DIALOG"))
		_exit(0);

	appname = g_get_prgname ();
	pid = getpid ();
	gdb = g_find_program_in_path ("gdb");

	if (gdb && g_getenv("GNOME_HACKER")) {
		res = run_gdb (appname, pid);
		if (!res)
			_exit (1);
		_exit(0);
	}
	
	mypath = g_file_read_link ("/proc/self/exe", NULL);
	if (mypath)
		has_debug_symbols = elf_has_debug_symbols (mypath);

	g_free (mypath);

	if (bugbuddy && gdb && has_debug_symbols) {
		res = run_bug_buddy (appname, pid, NULL);
		if (!res)
			_exit (1);
		_exit(0);
	}
}
		
int
gtk_module_init (int *argc, char** argv[])
{
	bugbuddy = g_find_program_in_path ("bug-buddy");
	
	if (bugbuddy && !g_getenv ("GNOME_DISABLE_CRASH_DIALOG")) {
#ifdef ENABLE_GOOGLE_BREAKPAD
		/* create the handler for minidumps */
  		handler = new ExceptionHandler ("/tmp", NULL,
						NULL, NULL, false);
#endif

        	static struct sigaction *setptr;
        	static struct sigaction old_action;
        	struct sigaction sa;
                memset(&sa, 0, sizeof(sa));
                setptr = &sa;

                sa.sa_handler = bugbuddy_segv_handle;

                sigaction(SIGSEGV, NULL, &old_action);
		if (old_action.sa_handler == SIG_DFL)
                	sigaction(SIGSEGV, setptr, NULL);

                sigaction(SIGABRT, NULL, &old_action);
		if (old_action.sa_handler == SIG_DFL)
                	sigaction(SIGABRT, setptr, NULL);

                sigaction(SIGTRAP, NULL, &old_action);
		if (old_action.sa_handler == SIG_DFL)
                	sigaction(SIGTRAP, setptr, NULL);

                sigaction(SIGFPE, NULL, &old_action);
		if (old_action.sa_handler == SIG_DFL)
                	sigaction(SIGFPE, setptr, NULL);

                sigaction(SIGBUS, NULL, &old_action);
		if (old_action.sa_handler == SIG_DFL)
                	sigaction(SIGBUS, setptr, NULL);

		old_handler = g_log_set_default_handler (bug_buddy_log_handler, NULL);
	}
	return 0;
}

const char *
g_module_check_init (GModule *module)
{
        g_module_make_resident (module);

        return NULL;
}

