/** 
 * @file linux_updater.cpp
 * @author Kyle Ambroff <ambroff@lindenlab.com>, Tofu Linden
 * @brief Viewer update program for unix platforms that support GTK+
 *
 * $LicenseInfo:firstyear=2008&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include <unistd.h>
#include <signal.h>
#include <errno.h>

#include "linden_common.h"
#include "llerrorcontrol.h"
#include "llfile.h"
#include "lldir.h"
#include "llxmlnode.h"
#include "lltrans.h"

#include <curl/curl.h>

extern "C" {
#include <gtk/gtk.h>
}

const guint UPDATE_PROGRESS_TIMEOUT = 100;
const guint UPDATE_PROGRESS_TEXT_TIMEOUT = 1000;
const guint ROTATE_IMAGE_TIMEOUT = 8000;

typedef struct _updater_app_state {
	std::string app_name;
	std::string url;
	std::string image_dir;
	std::string dest_dir;
	std::string strings_dirs;
	std::string strings_file;

	GtkWidget *window;
	GtkWidget *progress_bar;
	GtkWidget *image;

	double progress_value;
	bool activity_mode;

	guint image_rotation_timeout_id;
	guint progress_update_timeout_id;
	guint update_progress_text_timeout_id;

	bool failure;
} UpdaterAppState;

// List of entries from strings.xml to always replace
static std::set<std::string> default_trans_args;
void init_default_trans_args()
{
        default_trans_args.insert("SECOND_LIFE"); // World
        default_trans_args.insert("APP_NAME");
        default_trans_args.insert("SECOND_LIFE_GRID");
        default_trans_args.insert("SUPPORT_SITE");
}

bool translate_init(std::string comma_delim_path_list,
		    std::string base_xml_name)
{
	init_default_trans_args();

	// extract paths string vector from comma-delimited flat string
	std::vector<std::string> paths;
	LLStringUtil::getTokens(comma_delim_path_list, paths, ","); // split over ','

	// suck the translation xml files into memory
	LLXMLNodePtr root;
	bool success = LLXMLNode::getLayeredXMLNode(base_xml_name, root, paths);
	if (!success)
	{
		// couldn't load string table XML
		return false;
	}
	else
	{
		// get those strings out of the XML
		LLTrans::parseStrings(root, default_trans_args);
		return true;
	}
}


void updater_app_ui_init(void);
void updater_app_quit(UpdaterAppState *app_state);
void parse_args_and_init(int argc, char **argv, UpdaterAppState *app_state);
std::string next_image_filename(std::string& image_path);
void display_error(GtkWidget *parent, std::string title, std::string message);
BOOL install_package(std::string package_file, std::string destination);
BOOL spawn_viewer(UpdaterAppState *app_state);

extern "C" {
	void on_window_closed(GtkWidget *sender, gpointer state);
	gpointer worker_thread_cb(gpointer *data);
	int download_progress_cb(gpointer data, double t, double d, double utotal, double ulnow);
	gboolean rotate_image_cb(gpointer data);
	gboolean progress_update_timeout(gpointer data);
	gboolean update_progress_text_timeout(gpointer data);
}

void updater_app_ui_init(UpdaterAppState *app_state)
{
	GtkWidget *vbox;
	GtkWidget *summary_label;
	GtkWidget *description_label;
	GtkWidget *frame;

	llassert(app_state != NULL);

	// set up window and main container
	std::string window_title = LLTrans::getString("UpdaterWindowTitle");
	app_state->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
	gtk_window_set_title(GTK_WINDOW(app_state->window),
			     window_title.c_str());
	gtk_window_set_resizable(GTK_WINDOW(app_state->window), FALSE);
	gtk_window_set_position(GTK_WINDOW(app_state->window),
				GTK_WIN_POS_CENTER_ALWAYS);

	gtk_container_set_border_width(GTK_CONTAINER(app_state->window), 12);
	g_signal_connect(G_OBJECT(app_state->window), "delete-event", 
			 G_CALLBACK(on_window_closed), app_state);

	vbox = gtk_vbox_new(FALSE, 6);
	gtk_container_add(GTK_CONTAINER(app_state->window), vbox);

	// set top label
	std::ostringstream label_ostr;
	label_ostr << "<big><b>"
		   << LLTrans::getString("UpdaterNowUpdating")
		   << "</b></big>";

	summary_label = gtk_label_new(NULL);
	gtk_label_set_use_markup(GTK_LABEL(summary_label), TRUE);
	gtk_label_set_markup(GTK_LABEL(summary_label), 
			     label_ostr.str().c_str());
	gtk_misc_set_alignment(GTK_MISC(summary_label), 0, 0.5);
	gtk_box_pack_start(GTK_BOX(vbox), summary_label, FALSE, FALSE, 0);

	// create the description label
	description_label = gtk_label_new(LLTrans::getString("UpdaterUpdatingDescriptive").c_str());
	gtk_label_set_line_wrap(GTK_LABEL(description_label), TRUE);
	gtk_misc_set_alignment(GTK_MISC(description_label), 0, 0.5);
	gtk_box_pack_start(GTK_BOX(vbox), description_label, FALSE, FALSE, 0);

	// If an image path has been set, load the background images
	if (!app_state->image_dir.empty()) {
		frame = gtk_frame_new(NULL);
		gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
		gtk_box_pack_start(GTK_BOX(vbox), frame, TRUE, TRUE, 0);

		// load the first image
		app_state->image = gtk_image_new_from_file
			(next_image_filename(app_state->image_dir).c_str());
		gtk_widget_set_size_request(app_state->image, 340, 310);
		gtk_container_add(GTK_CONTAINER(frame), app_state->image);

		// rotate the images every 5 seconds
		app_state->image_rotation_timeout_id = g_timeout_add
			(ROTATE_IMAGE_TIMEOUT, rotate_image_cb, app_state);
	}

	// set up progress bar, and update it roughly every 1/10 of a second
	app_state->progress_bar = gtk_progress_bar_new();
	gtk_progress_bar_set_text(GTK_PROGRESS_BAR(app_state->progress_bar), 
				  LLTrans::getString("UpdaterProgressBarTextWithEllipses").c_str());
	gtk_box_pack_start(GTK_BOX(vbox), 
			   app_state->progress_bar, FALSE, TRUE, 0);
	app_state->progress_update_timeout_id = g_timeout_add
		(UPDATE_PROGRESS_TIMEOUT, progress_update_timeout, app_state);
	app_state->update_progress_text_timeout_id = g_timeout_add
		(UPDATE_PROGRESS_TEXT_TIMEOUT, update_progress_text_timeout, app_state);

	gtk_widget_show_all(app_state->window);
}

gboolean rotate_image_cb(gpointer data)
{
	UpdaterAppState *app_state;
	std::string filename;

	llassert(data != NULL);
	app_state = (UpdaterAppState *) data;

	filename = next_image_filename(app_state->image_dir);

	gdk_threads_enter();
	gtk_image_set_from_file(GTK_IMAGE(app_state->image), filename.c_str());
	gdk_threads_leave();

	return TRUE;
}

std::string next_image_filename(std::string& image_path)
{
	std::string image_filename;
	gDirUtilp->getNextFileInDir(image_path, "/*.jpg", image_filename, true);
	return image_path + "/" + image_filename;
}

void on_window_closed(GtkWidget *sender, gpointer data)
{
	UpdaterAppState *app_state;

	llassert(data != NULL);
	app_state = (UpdaterAppState *) data;

	updater_app_quit(app_state);
}

void updater_app_quit(UpdaterAppState *app_state)
{
	if (app_state != NULL)
	{
		g_source_remove(app_state->progress_update_timeout_id);

		if (!app_state->image_dir.empty())
		{
			g_source_remove(app_state->image_rotation_timeout_id);
		}
	}

	gtk_main_quit();
}

void display_error(GtkWidget *parent, std::string title, std::string message)
{
	GtkWidget *dialog;

	dialog = gtk_message_dialog_new(GTK_WINDOW(parent),
					GTK_DIALOG_DESTROY_WITH_PARENT,
					GTK_MESSAGE_ERROR,
					GTK_BUTTONS_OK,
					"%s", message.c_str());
	gtk_window_set_title(GTK_WINDOW(dialog), title.c_str());
	gtk_dialog_run(GTK_DIALOG(dialog));
	gtk_widget_destroy(dialog);
}

gpointer worker_thread_cb(gpointer data)
{
	UpdaterAppState *app_state;
	CURL *curl;
	CURLcode result;
	FILE *package_file;
	GError *error = NULL;
	char *tmp_filename = NULL;
	int fd;

	//g_return_val_if_fail (data != NULL, NULL);
	app_state = (UpdaterAppState *) data;

	try {
		// create temporary file to store the package.
		fd = g_file_open_tmp
			("secondlife-update-XXXXXX", &tmp_filename, &error);
		if (error != NULL)
		{
			llerrs << "Unable to create temporary file: "
			       << error->message
			       << llendl;

			g_error_free(error);
			throw 0;
		}

		package_file = fdopen(fd, "wb");
		if (package_file == NULL)
		{
			llerrs << "Failed to create temporary file: "
			       << tmp_filename
			       << llendl;

			gdk_threads_enter();
			display_error(app_state->window,
				      LLTrans::getString("UpdaterFailDownloadTitle"),
				      LLTrans::getString("UpdaterFailUpdateDescriptive"));
			gdk_threads_leave();
			throw 0;
		}

		// initialize curl and start downloading the package
		llinfos << "Downloading package: " << app_state->url << llendl;

		curl = curl_easy_init();
		if (curl == NULL)
		{
			llerrs << "Failed to initialize libcurl" << llendl;

			gdk_threads_enter();
			display_error(app_state->window,
				      LLTrans::getString("UpdaterFailDownloadTitle"),
				      LLTrans::getString("UpdaterFailUpdateDescriptive"));
			gdk_threads_leave();
			throw 0;
		}

		curl_easy_setopt(curl, CURLOPT_URL, app_state->url.c_str());
		curl_easy_setopt(curl, CURLOPT_NOSIGNAL, TRUE);
		curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, TRUE);
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, package_file);
		curl_easy_setopt(curl, CURLOPT_NOPROGRESS, FALSE);
		curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, 
				 &download_progress_cb);
		curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, app_state);

		result = curl_easy_perform(curl);
		fclose(package_file);
		curl_easy_cleanup(curl);

		if (result)
		{
			llerrs << "Failed to download update: " 
			       << app_state->url 
			       << llendl;

			gdk_threads_enter();
			display_error(app_state->window,
				      LLTrans::getString("UpdaterFailDownloadTitle"),
				      LLTrans::getString("UpdaterFailUpdateDescriptive"));
			gdk_threads_leave();

			throw 0;
		}

		// now pulse the progres bar back and forth while the package is
		// being unpacked
		gdk_threads_enter();
		std::string installing_msg = LLTrans::getString("UpdaterNowInstalling");
		gtk_progress_bar_set_text(
			GTK_PROGRESS_BAR(app_state->progress_bar),
			installing_msg.c_str());
		app_state->activity_mode = TRUE;
		gdk_threads_leave();

		// *TODO: if the destination is not writable, terminate this
		// thread and show file chooser?
		if (!install_package(tmp_filename, app_state->dest_dir))
		{
			llwarns << "Failed to install package to destination: "
				<< app_state->dest_dir
				<< llendl;

			gdk_threads_enter();
			display_error(app_state->window,
				      LLTrans::getString("UpdaterFailInstallTitle"),
				      LLTrans::getString("UpdaterFailUpdateDescriptive"));
			//"Failed to update " + app_state->app_name,
			gdk_threads_leave();
			throw 0;
		}

		// try to spawn the new viewer
		if (!spawn_viewer(app_state))
		{
			llwarns << "Viewer was not installed properly in : "
				<< app_state->dest_dir
				<< llendl;

			gdk_threads_enter();
			display_error(app_state->window,
				      LLTrans::getString("UpdaterFailStartTitle"),
				      LLTrans::getString("UpdaterFailUpdateDescriptive"));
			gdk_threads_leave();
			throw 0;
		}
	}
	catch (...)
	{
		app_state->failure = TRUE;
	}

	// FIXME: delete package file also if delete-event is raised on window
	if (tmp_filename != NULL)
	{
		if (gDirUtilp->fileExists(tmp_filename))
		{
			LLFile::remove(tmp_filename);
		}
	}

	gdk_threads_enter();
	updater_app_quit(app_state);
	gdk_threads_leave();

	return NULL;
}


gboolean less_anal_gspawnsync(gchar **argv,
			      gchar **stderr_output,
			      gint *child_exit_status,
			      GError **spawn_error)
{
	// store current SIGCHLD handler if there is one, replace with default
	// handler to make glib happy
	struct sigaction sigchld_backup;
	struct sigaction sigchld_appease_glib;
	sigchld_appease_glib.sa_handler = SIG_DFL;
	sigemptyset(&sigchld_appease_glib.sa_mask);
	sigchld_appease_glib.sa_flags = 0;
	sigaction(SIGCHLD, &sigchld_appease_glib, &sigchld_backup);

	gboolean rtn = g_spawn_sync(NULL,
				    argv,
				    NULL,
				    (GSpawnFlags) (G_SPAWN_STDOUT_TO_DEV_NULL),
				    NULL,
				    NULL,
				    NULL,
				    stderr_output,
				    child_exit_status,
				    spawn_error);

	// restore SIGCHLD handler
	sigaction(SIGCHLD, &sigchld_backup, NULL);
	
	return rtn;
}


// perform a rename, or perform a (prompted) root rename if that fails
int
rename_with_sudo_fallback(const std::string& filename, const std::string& newname)
{
	int rtncode = ::rename(filename.c_str(), newname.c_str());
	lldebugs << "rename result is: " << rtncode << " / " << errno << llendl;
	if (rtncode && (EACCES == errno || EPERM == errno || EXDEV == errno))
	{
		llinfos << "Permission problem in rename, or moving between different mount points.  Retrying as a mv under a sudo." << llendl;
		// failed due to permissions, try again as a gksudo or kdesu mv wrapper hack
		char *sudo_cmd = NULL;
		sudo_cmd = g_find_program_in_path("gksudo");
		if (!sudo_cmd)
		{
			sudo_cmd = g_find_program_in_path("kdesu");
		}
		if (sudo_cmd)
		{
			char *mv_cmd = NULL;
			mv_cmd = g_find_program_in_path("mv");
			if (mv_cmd)
			{
				char *src_string_copy = g_strdup(filename.c_str());
				char *dst_string_copy = g_strdup(newname.c_str());
				char* argv[] = 
					{
						sudo_cmd,
						mv_cmd,
						src_string_copy,
						dst_string_copy,
						NULL
					};

				gchar *stderr_output = NULL;
				gint child_exit_status = 0;
				GError *spawn_error = NULL;
				if (!less_anal_gspawnsync(argv, &stderr_output,
							  &child_exit_status, &spawn_error))
				{
					llwarns << "Failed to spawn child process: " 
						<< spawn_error->message 
						<< llendl;
				}
				else if (child_exit_status)
				{
					llwarns << "mv command failed: "
						<< (stderr_output ? stderr_output : "(no reason given)")
						<< llendl;
				}
				else
				{
					// everything looks good, clear the error code
					rtncode = 0;
				}				

				g_free(src_string_copy);
				g_free(dst_string_copy);
				if (spawn_error) g_error_free(spawn_error);
			}
		}
	}
	return rtncode;
}

gboolean install_package(std::string package_file, std::string destination)
{
	char *tar_cmd = NULL;
	std::ostringstream command;

	// Find the absolute path to the 'tar' command.
	tar_cmd = g_find_program_in_path("tar");
	if (!tar_cmd)
	{
		llerrs << "`tar' was not found in $PATH" << llendl;
		return FALSE;
	}
	llinfos << "Found tar command: " << tar_cmd << llendl;

	// Unpack the tarball in a temporary place first, then move it to 
	// its final destination
	std::string tmp_dest_dir = gDirUtilp->getTempFilename();
	if (LLFile::mkdir(tmp_dest_dir, 0744))
	{
		llerrs << "Failed to create directory: "
		       << destination
		       << llendl;

		return FALSE;
	}

	char *package_file_string_copy = g_strdup(package_file.c_str());
	char *tmp_dest_dir_string_copy = g_strdup(tmp_dest_dir.c_str());
	gchar *argv[8] = {
		tar_cmd,
		const_cast<gchar*>("--strip"), const_cast<gchar*>("1"),
		const_cast<gchar*>("-xjf"),
		package_file_string_copy,
		const_cast<gchar*>("-C"), tmp_dest_dir_string_copy,
		NULL,
	};

	llinfos << "Untarring package: " << package_file << llendl;

	// store current SIGCHLD handler if there is one, replace with default
	// handler to make glib happy
	struct sigaction sigchld_backup;
	struct sigaction sigchld_appease_glib;
	sigchld_appease_glib.sa_handler = SIG_DFL;
	sigemptyset(&sigchld_appease_glib.sa_mask);
	sigchld_appease_glib.sa_flags = 0;
	sigaction(SIGCHLD, &sigchld_appease_glib, &sigchld_backup);

	gchar *stderr_output = NULL;
	gint child_exit_status = 0;
	GError *untar_error = NULL;
	if (!less_anal_gspawnsync(argv, &stderr_output,
				  &child_exit_status, &untar_error))
	{
		llwarns << "Failed to spawn child process: " 
			<< untar_error->message 
			<< llendl;
		return FALSE;
	}

	if (child_exit_status)
	{
	 	llwarns << "Untar command failed: "
			<< (stderr_output ? stderr_output : "(no reason given)")
			<< llendl;
		return FALSE;
	}

	g_free(tar_cmd);
	g_free(package_file_string_copy);
	g_free(tmp_dest_dir_string_copy);
	g_free(stderr_output);
	if (untar_error) g_error_free(untar_error);

	// move the existing package out of the way if it exists
	if (gDirUtilp->fileExists(destination))
	{
		std::string backup_dir = destination + ".backup";
		int oldcounter = 1;
		while (gDirUtilp->fileExists(backup_dir))
		{
			// find a foo.backup.N folder name that isn't taken yet
			backup_dir = destination + ".backup." + llformat("%d", oldcounter);
			++oldcounter;
		}

		if (rename_with_sudo_fallback(destination, backup_dir))
		{
			llwarns << "Failed to move directory: '" 
				<< destination << "' -> '" << backup_dir 
				<< llendl;
			return FALSE;
		}
	}

	// The package has been unpacked in a staging directory, now we just
	// need to move it to its destination.
	if (rename_with_sudo_fallback(tmp_dest_dir, destination))
	{
		llwarns << "Failed to move installation to the destination: "
			<< destination 
			<< llendl;
		return FALSE;
	}

	// \0/ Success!
	return TRUE;
}

gboolean progress_update_timeout(gpointer data)
{
	UpdaterAppState *app_state;

	llassert(data != NULL);

	app_state = (UpdaterAppState *) data;

	gdk_threads_enter();
	if (app_state->activity_mode)
	{
		gtk_progress_bar_pulse
			(GTK_PROGRESS_BAR(app_state->progress_bar));
	}
	else
	{
		gtk_progress_set_value(GTK_PROGRESS(app_state->progress_bar),
				       app_state->progress_value);
	}
	gdk_threads_leave();

	return TRUE;
}

gboolean update_progress_text_timeout(gpointer data)
{
	UpdaterAppState *app_state;

	llassert(data != NULL);
	app_state = (UpdaterAppState *) data;

	if (app_state->activity_mode == TRUE)
	{
		// We no longer need this timeout, it will be removed.
		return FALSE;
	}

	if (!app_state->progress_value)
	{
		return TRUE;
	}

	std::string progress_text = llformat((LLTrans::getString("UpdaterProgressBarText")+" (%.0f%%)").c_str(), app_state->progress_value);

	gdk_threads_enter();
	gtk_progress_bar_set_text(GTK_PROGRESS_BAR(app_state->progress_bar),
				  progress_text.c_str());
	gdk_threads_leave();

	return TRUE;
}

int download_progress_cb(gpointer data,
			 double t,
			 double d,
			 double utotal,
			 double ulnow)
{
	UpdaterAppState *app_state;

	llassert(data != NULL);
	app_state = (UpdaterAppState *) data;

	if (t <= 0.0)
	{
		app_state->progress_value = 0;
	}
	else
	{
		app_state->progress_value = d * 100.0 / t;
	}
	return 0;
}

BOOL spawn_viewer(UpdaterAppState *app_state)
{
	llassert(app_state != NULL);

	std::string cmd = app_state->dest_dir + "/secondlife";
	GError *error = NULL;

	// We want to spawn the Viewer on the same display as the updater app
	gboolean success = gdk_spawn_command_line_on_screen
		(gtk_widget_get_screen(app_state->window), cmd.c_str(), &error);

	if (!success)
	{
		llwarns << "Failed to launch viewer: " << error->message 
			<< llendl;
	}

	if (error) g_error_free(error);

	return success;
}

void show_usage_and_exit()
{
	std::cout << "Usage: linux-updater --url URL --name NAME --dest PATH --stringsdir PATH1,PATH2 --stringsfile FILE"
		  << "[--image-dir PATH]"
		  << std::endl;
	exit(1);
}

void parse_args_and_init(int argc, char **argv, UpdaterAppState *app_state)
{
	int i;

	for (i = 1; i < argc; i++)
	{
		if ((!strcmp(argv[i], "--url")) && (++i < argc))
		{
			app_state->url = argv[i];
		}
		else if ((!strcmp(argv[i], "--name")) && (++i < argc))
		{
			app_state->app_name = argv[i];
		}
		else if ((!strcmp(argv[i], "--image-dir")) && (++i < argc))
		{
			app_state->image_dir = argv[i];
		}
		else if ((!strcmp(argv[i], "--dest")) && (++i < argc))
		{
			app_state->dest_dir = argv[i];
		}
		else if ((!strcmp(argv[i], "--stringsdir")) && (++i < argc))
		{
			app_state->strings_dirs = argv[i];
		}
		else if ((!strcmp(argv[i], "--stringsfile")) && (++i < argc))
		{
			app_state->strings_file = argv[i];
		}
		else
		{
			// show usage, an invalid option was given.
			show_usage_and_exit();
		}
	}

	if (app_state->app_name.empty() 
	    || app_state->url.empty() 
	    || app_state->dest_dir.empty())
	{
		show_usage_and_exit();
	}

	app_state->progress_value = 0.0;
	app_state->activity_mode = FALSE;
	app_state->failure = FALSE;

	translate_init(app_state->strings_dirs, app_state->strings_file);
}

int main(int argc, char **argv)
{
	UpdaterAppState app_state;
	GThread *worker_thread;

	parse_args_and_init(argc, argv, &app_state);

	// Initialize logger, and rename old log file
	gDirUtilp->initAppDirs("SecondLife");
	LLError::initForApplication
		(gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, ""));
	std::string old_log_file = gDirUtilp->getExpandedFilename
		(LL_PATH_LOGS, "updater.log.old");
	std::string log_file = 
		gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "updater.log");
	LLFile::rename(log_file, old_log_file);
	LLError::logToFile(log_file);

	// initialize gthreads and gtk+
	if (!g_thread_supported())
	{
		g_thread_init(NULL);
		gdk_threads_init();
	}

	gtk_init(&argc, &argv);

	// create UI
	updater_app_ui_init(&app_state);

	//llinfos << "SAMPLE TRANSLATION IS: " << LLTrans::getString("LoginInProgress") << llendl;

	// create download thread
	worker_thread = g_thread_create
		(GThreadFunc(worker_thread_cb), &app_state, FALSE, NULL);

	gdk_threads_enter();
	gtk_main();
	gdk_threads_leave();

	return (app_state.failure == FALSE) ? 0 : 1;
}