/* bug-buddy bug submitting program
 *
 * Copyright (C) Jacob Berkman
 *
 * Author:  Jacob Berkman  <jacob@bug-buddy.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 <config.h>

#include <stdio.h>

#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <math.h>
#include <string.h>

#include <glib/gi18n.h>
#include <gtk/gtk.h>

#include "gdb-buddy.h"

#define d(x)

typedef struct {
	gint         pid;
	GIOChannel  *ioc;
	GString     *stacktrace;
	gpointer     user_data;
	GdbCallback  callback;
} GdbData;

static void
gdb_stop (GIOChannel *ioc, int pid)
{
	if (!ioc) {
		d(g_message (_("gdb has already exited")));
		return;
	}
	
	g_io_channel_shutdown (ioc, 1, NULL);
	
	kill (pid, SIGTERM);
	/* i don't think we need to SIGKILL it */
	/*kill (druid_data.gdb_pid, SIGKILL);*/
	waitpid (pid, NULL, 0);
	
	pid = 0;

#if 0
	/* sometimes gdb doesn't restart the old app... */
	if (druid_data.app_pid) {
		kill (druid_data.app_pid, SIGCONT);
		druid_data.app_pid = 0;
	}
#endif
}

static gboolean
gdb_handle_input (GIOChannel *ioc, GIOCondition condition, gpointer data)
{	
	gboolean retval = FALSE;
	gchar buf[1024];
	gsize len;
	GIOStatus io_status;
	GdbData *gdb_data = (GdbData *)data;

	while (gtk_events_pending ())
		gtk_main_iteration ();

 gdb_try_read:
	io_status = g_io_channel_read_chars (ioc, buf, 1024, &len, NULL);

	switch (io_status) {
	case G_IO_STATUS_AGAIN:
		goto gdb_try_read;
	case G_IO_STATUS_ERROR:
		d(g_warning (_("Error on read... aborting")));
		break;
	case G_IO_STATUS_NORMAL:
		retval = TRUE;
		break;
	default:
		break;
	}

	if (len > 0) {
		char *utftext;
		gsize localelen;
		gsize utflen;

		/* gdb charset is ISO-8859-1 */
		utftext = g_convert_with_fallback (buf, len, "UTF-8", "ISO-8859-1", NULL, &localelen, &utflen, NULL);
		gdb_data->stacktrace = g_string_append (gdb_data->stacktrace, utftext);
		g_free (utftext);
	}

	if (!retval || io_status == G_IO_STATUS_EOF) {
		gdb_stop (ioc, gdb_data->pid);
		gdb_data->pid = 0;
		gdb_data->ioc = NULL;

		/* call the user specified callback function with the string containing
		 * the stacktrace, and the user specified data */
		(*(gdb_data->callback))(gdb_data->stacktrace->str, gdb_data->user_data);
		return FALSE;
	}

	return retval;
}

static void
gdb_destroy (gpointer user_data)
{
	GdbData *gdb_data;

	g_return_if_fail (user_data != NULL);

	d (g_print ("entering gdb_destroy\n"));

	gdb_data = (GdbData *)user_data;

	if (gdb_data->pid != 0 && gdb_data->ioc != NULL) {
		d (g_print ("stopping gdb, pid = %d\n", gdb_data->pid));
		gdb_stop (gdb_data->ioc, gdb_data->pid);
	}

	if (gdb_data->pid != 0)
		gdb_data->pid = 0;
	
	if (gdb_data->ioc != NULL)
		gdb_data->ioc = NULL;

	if (gdb_data->stacktrace != NULL)
		g_string_free (gdb_data->stacktrace, TRUE);

	g_free (gdb_data);

	return;
}

GQuark
gdb_buddy_error_quark (void)
{
	  return g_quark_from_static_string ("gdb_buddy_error");
}

static char *
get_process_executable (int pid)
{
	char *link, *exe;
	GError *error = NULL;

#if defined (__linux__)
	link = g_strdup_printf ("/proc/%d/exe", pid);
#elif defined (sun) && defined (__SVR4)
	link = g_strdup_printf ("/proc/%d/path/a.out", pid);
#else
	/* if someone knows how to do this on BSD, please send a patch */
	return NULL;
#endif
	exe = g_file_read_link (link, &error);

	if (error) {
		g_warning ("Could not read %s: %s\n", link, error->message);
		g_error_free (error);
	}

	return exe;
}

/**
 * @app: the executable name of the program that crashed
 * @pid: the process id of the application that crashed
 * @gdb_pid: the pid of the GDB process that is collecting the stacktrace
 * @user_data: a pointer that will be passed to the gdb_finish function
 * @gdb_finish: A callback function that is called after gdb finishes getting the stack trace
 * @err: if the function returns NULL, then *err is populated with a GError
 *
 * This function calls the gdb_finish callback function with backtrace obtained from gdb
 * and the pointer user_data, for a given process id and application name.  If there is an error
 * during processing, *err will be populated with a GError message.
 *
 * This function returns the event source id that is added to the main loop, or zero if a failure
 * occurs in the function.  If you need to terminate the application before the gdb_finish callback 
 * has been called, then use g_source_remove().  This will cleanup the GIOChannel and stop any 
 * running gdb processes. 
 */
guint
gdb_get_trace (const gchar *app, int pid, gpointer user_data, GdbCallback gdb_finish, GError **err)
{
	char *s;
	char *long_app;
	int gdb_pid;
	int fd;
	guint source_id;
	GIOChannel *ioc;
	GError *error = NULL;
	GdbData *gdb_data = NULL;
	char *args[] = { "gdb",
			 "--batch", 
			 "--quiet",
			 "--command=" BUDDY_DATADIR "/gdb-cmd",
			 NULL, NULL, NULL };

	g_return_val_if_fail (app != NULL, 0);
	g_return_val_if_fail (*app != '\0', 0);
	g_return_val_if_fail (pid != 0, 0);
	g_return_val_if_fail (gdb_finish != NULL, 0);
	g_return_val_if_fail (pid != 0, 0);
	g_return_val_if_fail (err == NULL || *err == NULL, 0);

	d (g_print ("app=%s\n", app));

	/* apply a SIGCONT to the process */
	kill (pid, SIGCONT);

	/* to get the application path do the following:
 	 * - if the path is absolute, it has been provided by the user, so just use it.
 	 * - otherwise look for it in /proc based on its pid.
 	 * - if that fails, look for the program in path.
 	 * - finally, use $libexecdir as fallback.
	 */

	if (g_path_is_absolute (app)) {
		long_app = g_strdup (app);
	} else {
		long_app = get_process_executable (pid);
	}

	if (!long_app) {
		long_app = g_find_program_in_path (app);
		if (!long_app) {
			/* this should never fail */
			long_app = g_strconcat (GNOMELIBEXECDIR,"/", app, NULL);
		}
	}

	args[0] = g_find_program_in_path ("gdb");
	args[4] = long_app;

	if (args[0] == NULL) {
		d(g_message ("Path: %s", getenv ("PATH")));
		g_free (long_app);
		g_set_error (err, GDB_BUDDY_ERROR, GDB_BUDDY_GDB_NOT_FOUND, 
			     _("GDB could not be found on your system. "
			       "Debugging information will not be obtained."));
		return 0;
	} 
	
	d(g_message ("About to debug '%s'", long_app));
	
	if (!g_file_test (BUDDY_DATADIR "/gdb-cmd", G_FILE_TEST_EXISTS)) {
		g_set_error (err, GDB_BUDDY_ERROR, GDB_BUDDY_GDB_CMD_NOT_FOUND, 
			     _("Could not find the gdb-cmd file.\n"
			       "Please try reinstalling Bug Buddy."));
		g_free (args[0]);
		g_free (long_app);
		return 0;
	}
	
	args[5] = g_strdup_printf ("%d", pid);
	
	if (!g_spawn_async_with_pipes (NULL, args, NULL, 0, NULL, NULL,
				       &gdb_pid,
				       NULL, 
				       &fd, 
				       NULL, &error)) {
		g_set_error (err, GDB_BUDDY_ERROR, GDB_BUDDY_GDB_ERROR, 
			     _("There was an error running gdb:\n\n%s"),
			     error->message);
		g_error_free (error);
		g_free (args[0]);
		g_free (args[5]);
		g_free (long_app);
		return 0;
	} 

	ioc = g_io_channel_unix_new (fd);
	g_io_channel_set_encoding (ioc, NULL, NULL);
	g_io_channel_set_flags (ioc, G_IO_FLAG_NONBLOCK, &error);

	gdb_data = g_new0 (GdbData, 1);

	s = g_strdup_printf ("Backtrace was generated from '%s'\n\n", long_app);
	gdb_data->stacktrace = g_string_new (s);
	g_free (s);
	
	gdb_data->pid = gdb_pid;
	gdb_data->ioc = ioc;
	gdb_data->user_data = user_data;
	gdb_data->callback = gdb_finish;

	source_id = g_io_add_watch_full (ioc, 100, G_IO_IN | G_IO_HUP, 
	                                 gdb_handle_input, gdb_data, gdb_destroy);
	g_io_channel_unref (ioc);

	g_free (args[0]);
	g_free (args[5]);
	g_free (long_app);

	return source_id;
}
