[geany/geany] 826f65: Merge pull request #441 from zhekov/spawn

Colomban Wendling git-noreply at xxxxx
Fri May 15 16:54:23 UTC 2015


Branch:      refs/heads/master
Author:      Colomban Wendling <ban at herbesfolles.org>
Committer:   Colomban Wendling <ban at herbesfolles.org>
Date:        Fri, 15 May 2015 16:54:23 UTC
Commit:      826f6516d36ec7a7bed35116fda32d42449c1e5d
             https://github.com/geany/geany/commit/826f6516d36ec7a7bed35116fda32d42449c1e5d

Log Message:
-----------
Merge pull request #441 from zhekov/spawn

Add a spawn module for Geany

Thanks a lot for Dimitar's hard work, commitment and endless patience!

Closes #274, #441, and https://sourceforge.net/p/geany/bugs/943/
Should also fix https://sourceforge.net/p/geany/bugs/898/


Modified Paths:
--------------
    src/Makefile.am
    src/build.c
    src/callbacks.c
    src/keyfile.c
    src/makefile.win32
    src/printing.c
    src/search.c
    src/spawn.c
    src/spawn.h
    src/templates.c
    src/tools.c
    src/utils.c
    src/win32.c
    src/win32.h
    wscript

Modified: src/Makefile.am
1 lines changed, 1 insertions(+), 0 deletions(-)
===================================================================
@@ -89,6 +89,7 @@ libgeany_la_SOURCES = \
 	sciwrappers.c sciwrappers.h \
 	search.c search.h \
 	socket.c socket.h \
+	spawn.c spawn.h \
 	stash.c stash.h \
 	support.h \
 	symbols.c symbols.h \


Modified: src/build.c
291 lines changed, 47 insertions(+), 244 deletions(-)
===================================================================
@@ -44,6 +44,7 @@
 #include "prefs.h"
 #include "projectprivate.h"
 #include "sciwrappers.h"
+#include "spawn.h"
 #include "support.h"
 #include "toolbar.h"
 #include "ui_utils.h"
@@ -60,20 +61,8 @@
 #include <errno.h>
 #include <glib/gstdio.h>
 
-#ifdef G_OS_UNIX
-# include <sys/types.h>
-# include <sys/wait.h>
-# include <signal.h>
-#else
-# include <windows.h>
-#endif
 
 
-/* g_spawn_async_with_pipes doesn't work on Windows */
-#ifdef G_OS_WIN32
-#define SYNC_SPAWN
-#endif
-
 /* Number of editor indicators to draw - limited as this can affect performance */
 #define GEANY_BUILD_ERR_HIGHLIGHT_MAX 50
 
@@ -124,12 +113,10 @@ widgets;
 static guint build_groups_count[GEANY_GBG_COUNT] = { 3, 4, 2 };
 static guint build_items_count = 9;
 
-#ifndef SYNC_SPAWN
-static void build_exit_cb(GPid child_pid, gint status, gpointer user_data);
-static gboolean build_iofunc(GIOChannel *ioc, GIOCondition cond, gpointer data);
-#endif
+static void build_exit_cb(GPid pid, gint status, gpointer user_data);
+static void build_iofunc(GString *string, GIOCondition condition, gpointer data);
 static gchar *build_create_shellscript(const gchar *working_dir, const gchar *cmd, gboolean autoclose, GError **error);
-static GPid build_spawn_cmd(GeanyDocument *doc, const gchar *cmd, const gchar *dir);
+static void build_spawn_cmd(GeanyDocument *doc, const gchar *cmd, const gchar *dir);
 static void set_stop_button(gboolean stop);
 static void run_exit_cb(GPid child_pid, gint status, gpointer user_data);
 static void on_set_build_commands_activate(GtkWidget *w, gpointer u);
@@ -137,7 +124,7 @@ static void on_build_next_error(GtkWidget *menuitem, gpointer user_data);
 static void on_build_previous_error(GtkWidget *menuitem, gpointer user_data);
 static void kill_process(GPid *pid);
 static void show_build_result_message(gboolean failure);
-static void process_build_output_line(const gchar *line, gint color);
+static void process_build_output_line(gchar *msg, gint color);
 static void show_build_commands_dialog(void);
 static void on_build_menu_item(GtkWidget *w, gpointer user_data);
 
@@ -675,47 +662,6 @@ static void clear_all_errors(void)
 }
 
 
-#ifdef SYNC_SPAWN
-static void parse_build_output(const gchar **output, gint status)
-{
-	guint x, i, len;
-	gchar *line, **lines;
-
-	for (x = 0; x < 2; x++)
-	{
-		if (!EMPTY(output[x]))
-		{
-			lines = g_strsplit_set(output[x], "\r\n", -1);
-			len = g_strv_length(lines);
-
-			for (i = 0; i < len; i++)
-			{
-				if (!EMPTY(lines[i]))
-				{
-					line = lines[i];
-					while (*line != '\0')
-					{	/* replace any control characters in the output */
-						if (*line < 32)
-							*line = 32;
-						line++;
-					}
-					process_build_output_line(lines[i], COLOR_BLACK);
-				}
-			}
-			g_strfreev(lines);
-		}
-	}
-
-	show_build_result_message(status != 0);
-	utils_beep();
-
-	build_info.pid = 0;
-	/* enable build items again */
-	build_menu_update(NULL);
-}
-#endif
-
-
 /* Replaces occurrences of %e and %p with the appropriate filenames and
  * %l with current line number. %d and %p replacements should be in UTF8 */
 static gchar *build_replace_placeholder(const GeanyDocument *doc, const gchar *src)
@@ -784,43 +730,26 @@ static gchar *build_replace_placeholder(const GeanyDocument *doc, const gchar *s
 
 /* dir is the UTF-8 working directory to run cmd in. It can be NULL to use the
  * idx document directory */
-static GPid build_spawn_cmd(GeanyDocument *doc, const gchar *cmd, const gchar *dir)
+static void build_spawn_cmd(GeanyDocument *doc, const gchar *cmd, const gchar *dir)
 {
 	GError *error = NULL;
-	gchar **argv;
+	gchar *argv[] = { "/bin/sh", "-c", (gchar *) cmd, NULL };
 	gchar *working_dir;
 	gchar *utf8_working_dir;
 	gchar *utf8_cmd_string;
-#ifdef SYNC_SPAWN
-	gchar *output[2];
-	gint status;
-#else
-	gint stdout_fd;
-	gint stderr_fd;
-#endif
 
-	g_return_val_if_fail(doc == NULL || doc->is_valid, (GPid) -1);
+	g_return_if_fail(doc == NULL || doc->is_valid);
 
-	if (!((doc != NULL && !EMPTY(doc->file_name)) || !EMPTY(dir)))
+	if ((doc == NULL || EMPTY(doc->file_name)) && EMPTY(dir))
 	{
 		geany_debug("Failed to run command with no working directory");
 		ui_set_statusbar(TRUE, _("Process failed, no working directory"));
-		return (GPid) 1;
+		return;
 	}
 
 	clear_all_errors();
 	SETPTR(current_dir_entered, NULL);
 
-#ifdef G_OS_WIN32
-	argv = g_strsplit(cmd, " ", 0);
-#else
-	argv = g_new0(gchar *, 4);
-	argv[0] = g_strdup("/bin/sh");
-	argv[1] = g_strdup("-c");
-	argv[2] = g_strdup(cmd);
-	argv[3] = NULL;
-#endif
-
 	utf8_cmd_string = utils_get_utf8_from_locale(cmd);
 	utf8_working_dir = !EMPTY(dir) ? g_strdup(dir) : g_path_get_dirname(doc->file_name);
 	working_dir = utils_get_locale_from_utf8(utf8_working_dir);
@@ -831,53 +760,28 @@ static GPid build_spawn_cmd(GeanyDocument *doc, const gchar *cmd, const gchar *d
 	g_free(utf8_working_dir);
 	g_free(utf8_cmd_string);
 
+#ifdef G_OS_UNIX
+	cmd = NULL;  /* under Unix, use argv to start cmd via sh for compatibility */
+#else
+	argv[0] = NULL;  /* under Windows, run cmd directly */
+#endif
+
 	/* set the build info for the message window */
 	g_free(build_info.dir);
 	build_info.dir = g_strdup(working_dir);
 	build_info.file_type_id = (doc == NULL) ? GEANY_FILETYPES_NONE : doc->file_type->id;
 	build_info.message_count = 0;
 
-#ifdef SYNC_SPAWN
-	if (! utils_spawn_sync(working_dir, argv, NULL, G_SPAWN_SEARCH_PATH,
-			NULL, NULL, &output[0], &output[1], &status, &error))
-#else
-	if (! g_spawn_async_with_pipes(working_dir, argv, NULL,
-			G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL,
-			&(build_info.pid), NULL, &stdout_fd, &stderr_fd, &error))
-#endif
+	if (!spawn_with_callbacks(working_dir, cmd, argv, NULL, 0, NULL, NULL, build_iofunc,
+		GINT_TO_POINTER(0), 0, build_iofunc, GINT_TO_POINTER(1), 0, build_exit_cb, NULL,
+		&build_info.pid, &error))
 	{
 		geany_debug("build command spawning failed: %s", error->message);
 		ui_set_statusbar(TRUE, _("Process failed (%s)"), error->message);
-		g_strfreev(argv);
 		g_error_free(error);
-		g_free(working_dir);
-		error = NULL;
-		return (GPid) 0;
-	}
-
-#ifdef SYNC_SPAWN
-	parse_build_output((const gchar**) output, status);
-	g_free(output[0]);
-	g_free(output[1]);
-#else
-	if (build_info.pid != 0)
-	{
-		g_child_watch_add(build_info.pid, (GChildWatchFunc) build_exit_cb, NULL);
-		build_menu_update(doc);
-		ui_progress_bar_start(NULL);
 	}
 
-	/* use GIOChannels to monitor stdout and stderr */
-	utils_set_up_io_channel(stdout_fd, G_IO_IN | G_IO_PRI | G_IO_ERR | G_IO_HUP | G_IO_NVAL,
-		TRUE, build_iofunc, GINT_TO_POINTER(0));
-	utils_set_up_io_channel(stderr_fd, G_IO_IN | G_IO_PRI | G_IO_ERR | G_IO_HUP | G_IO_NVAL,
-		TRUE, build_iofunc, GINT_TO_POINTER(1));
-#endif
-
-	g_strfreev(argv);
 	g_free(working_dir);
-
-	return build_info.pid;
 }
 
 
@@ -903,13 +807,10 @@ static gchar *prepare_run_cmd(GeanyDocument *doc, gchar **working_dir, guint cmd
 	working_dir_utf8 = build_replace_placeholder(doc, cmd_working_dir);
 	*working_dir = utils_get_locale_from_utf8(working_dir_utf8);
 
-	/* only test whether working dir exists, don't change it or else Windows support will break
-	 * (gspawn-win32-helper.exe is used by GLib and must be in $PATH which means current working
-	 *  dir where geany.exe was started from, so we can't change it) */
 	if (EMPTY(*working_dir) || ! g_file_test(*working_dir, G_FILE_TEST_EXISTS) ||
 		! g_file_test(*working_dir, G_FILE_TEST_IS_DIR))
 	{
-		ui_set_statusbar(TRUE, _("Failed to change the working directory to \"%s\""),
+		ui_set_statusbar(TRUE, _("Invalid working directory \"%s\""),
 				!EMPTY(working_dir_utf8) ? working_dir_utf8 : "<NULL>" );
 		utils_free_pointers(3, cmd_string_utf8, working_dir_utf8, *working_dir, NULL);
 		return NULL;
@@ -945,18 +846,17 @@ static gchar *prepare_run_cmd(GeanyDocument *doc, gchar **working_dir, guint cmd
 }
 
 
-static GPid build_run_cmd(GeanyDocument *doc, guint cmdindex)
+static void build_run_cmd(GeanyDocument *doc, guint cmdindex)
 {
 	gchar *working_dir;
 	gchar *run_cmd = NULL;
-	GError *error = NULL;
 
 	if (! DOC_VALID(doc) || doc->file_name == NULL)
-		return (GPid) 0;
+		return;
 
 	run_cmd = prepare_run_cmd(doc, &working_dir, cmdindex);
 	if (run_cmd == NULL)
-		return (GPid) 0;
+		return;
 
 	run_info[cmdindex].file_type_id = doc->file_type->id;
 
@@ -990,97 +890,48 @@ static GPid build_run_cmd(GeanyDocument *doc, guint cmdindex)
 		msgwin_show_hide(TRUE);
 
 		run_info[cmdindex].pid = 1;
-
 		g_free(vte_cmd);
 	}
 	else
 #endif
 	{
-		gchar *locale_term_cmd = NULL;
-		gint argv_len, i;
-		gchar **argv = NULL;
-		gchar *script_path = NULL;
+		gchar *locale_term_cmd = utils_get_locale_from_utf8(tool_prefs.term_cmd);
+		GError *error = NULL;
 
-		/* get the terminal path */
-		locale_term_cmd = utils_get_locale_from_utf8(tool_prefs.term_cmd);
-		/* split the term_cmd, so arguments will work too */
-		if (!g_shell_parse_argv(locale_term_cmd, &argv_len, &argv, NULL))
-		{
-			ui_set_statusbar(TRUE,
-				_("Could not parse terminal command \"%s\" "
-					"(check Terminal tool setting in Preferences)"), tool_prefs.term_cmd);
-			run_info[cmdindex].pid = (GPid) 1;
-			g_unlink(run_cmd);
-			goto free_strings;
-		}
+		utils_str_replace_all(&locale_term_cmd, "%c", run_cmd);
 
-		/* check that terminal exists (to prevent misleading error messages) */
-		if (argv[0] != NULL)
+		if (spawn_async(working_dir, locale_term_cmd, NULL, NULL, &(run_info[cmdindex].pid),
+			&error))
 		{
-			gchar *tmp = argv[0];
-			/* g_find_program_in_path checks whether tmp exists and is executable */
-			argv[0] = g_find_program_in_path(tmp);
-			g_free(tmp);
-		}
-		if (argv[0] == NULL)
-		{
-			ui_set_statusbar(TRUE,
-				_("Could not find terminal \"%s\" "
-					"(check path for Terminal tool setting in Preferences)"), tool_prefs.term_cmd);
-			run_info[cmdindex].pid = (GPid) 1;
-			g_unlink(run_cmd);
-			goto free_strings;
-		}
-
-		for (i = 0; i < argv_len; i++)
-		{
-			utils_str_replace_all(&(argv[i]), "%c", run_cmd);
+			g_child_watch_add(run_info[cmdindex].pid, (GChildWatchFunc) run_exit_cb,
+								(gpointer) &(run_info[cmdindex]));
+			build_menu_update(doc);
 		}
-
-		if (! g_spawn_async(working_dir, argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD,
-							NULL, NULL, &(run_info[cmdindex].pid), &error))
+		else
 		{
-			geany_debug("g_spawn_async() failed: %s", error->message);
+			geany_debug("spawn_async() failed: %s", error->message);
 			ui_set_statusbar(TRUE, _("Process failed (%s)"), error->message);
 			g_error_free(error);
 			g_unlink(run_cmd);
-			error = NULL;
 			run_info[cmdindex].pid = (GPid) 0;
 		}
-
-		if (run_info[cmdindex].pid != 0)
-		{
-			g_child_watch_add(run_info[cmdindex].pid, (GChildWatchFunc) run_exit_cb,
-								(gpointer)&(run_info[cmdindex]));
-			build_menu_update(doc);
-		}
-		free_strings:
-		g_strfreev(argv);
-		g_free(locale_term_cmd);
-		g_free(script_path);
 	}
 
 	g_free(working_dir);
 	g_free(run_cmd);
-	return run_info[cmdindex].pid;
 }
 
 
-static void process_build_output_line(const gchar *str, gint color)
+static void process_build_output_line(gchar *msg, gint color)
 {
-	gchar *msg, *tmp;
+	gchar *tmp;
 	gchar *filename;
 	gint line;
 
-	msg = g_strdup(str);
-
 	g_strchomp(msg);
 
 	if (EMPTY(msg))
-	{
-		g_free(msg);
 		return;
-	}
 
 	if (build_parse_make_dir(msg, &tmp))
 	{
@@ -1106,33 +957,17 @@ static void process_build_output_line(const gchar *str, gint color)
 	g_free(filename);
 
 	msgwin_compiler_add_string(color, msg);
-	g_free(msg);
 }
 
 
-#ifndef SYNC_SPAWN
-static gboolean build_iofunc(GIOChannel *ioc, GIOCondition cond, gpointer data)
+static void build_iofunc(GString *string, GIOCondition condition, gpointer data)
 {
-	if (cond & (G_IO_IN | G_IO_PRI))
+	if (condition & (G_IO_IN | G_IO_PRI))
 	{
-		gchar *msg;
-		GIOStatus st;
-
-		while ((st = g_io_channel_read_line(ioc, &msg, NULL, NULL, NULL)) == G_IO_STATUS_NORMAL && msg)
-		{
-			gint color = (GPOINTER_TO_INT(data)) ? COLOR_DARK_RED : COLOR_BLACK;
-
-			process_build_output_line(msg, color);
- 			g_free(msg);
-		}
-		if (st == G_IO_STATUS_ERROR || st == G_IO_STATUS_EOF) return FALSE;
+		process_build_output_line(string->str,
+			(GPOINTER_TO_INT(data)) ? COLOR_DARK_RED : COLOR_BLACK);
 	}
-	if (cond & (G_IO_ERR | G_IO_HUP | G_IO_NVAL))
-		return FALSE;
-
-	return TRUE;
 }
-#endif
 
 
 gboolean build_parse_make_dir(const gchar *string, gchar **prefix)
@@ -1205,40 +1040,16 @@ static void show_build_result_message(gboolean failure)
 }
 
 
-#ifndef SYNC_SPAWN
 static void build_exit_cb(GPid child_pid, gint status, gpointer user_data)
 {
-	gboolean failure = FALSE;
-
-#ifdef G_OS_WIN32
-	failure = status;
-#else
-	if (WIFEXITED(status))
-	{
-		if (WEXITSTATUS(status) != EXIT_SUCCESS)
-			failure = TRUE;
-	}
-	else if (WIFSIGNALED(status))
-	{
-		/* the terminating signal: WTERMSIG (status)); */
-		failure = TRUE;
-	}
-	else
-	{	/* any other failure occurred */
-		failure = TRUE;
-	}
-#endif
-	show_build_result_message(failure);
-
+	show_build_result_message(!WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS);
 	utils_beep();
-	g_spawn_close_pid(child_pid);
 
 	build_info.pid = 0;
 	/* enable build items again */
 	build_menu_update(NULL);
 	ui_progress_bar_stop();
 }
-#endif
 
 
 static void run_exit_cb(GPid child_pid, gint status, gpointer user_data)
@@ -1790,26 +1601,18 @@ static void on_toolbutton_make_activate(GtkWidget *menuitem, gpointer user_data)
 
 static void kill_process(GPid *pid)
 {
-	gint result;
-
-#ifdef G_OS_WIN32
-	g_return_if_fail(*pid != NULL);
-	result = TerminateProcess(*pid, 0);
-	/* TerminateProcess() returns TRUE on success, for the check below we have to convert
-	 * it to FALSE (and vice versa) */
-	result = ! result;
-#else
-	g_return_if_fail(*pid > 1);
-	result = kill(*pid, SIGTERM);
-#endif
+	GError *error = NULL;
 
-	if (result != 0)
-		ui_set_statusbar(TRUE, _("Process could not be stopped (%s)."), g_strerror(errno));
-	else
+	if (spawn_kill_process(*pid, &error))
 	{
 		*pid = 0;
 		build_menu_update(NULL);
 	}
+	else
+	{
+		ui_set_statusbar(TRUE, _("Process could not be stopped (%s)."), error->message);
+		g_error_free(error);
+	}
 }
 
 


Modified: src/callbacks.c
3 lines changed, 2 insertions(+), 1 deletions(-)
===================================================================
@@ -51,6 +51,7 @@
 #include "printing.h"
 #include "sciwrappers.h"
 #include "sidebar.h"
+#include "spawn.h"
 #ifdef HAVE_SOCKET
 # include "socket.h"
 #endif
@@ -1479,7 +1480,7 @@ void on_context_action1_activate(GtkMenuItem *menuitem, gpointer user_data)
 	{
 		utils_str_replace_all(&command, "%s", word);
 
-		if (! g_spawn_command_line_async(command, &error))
+		if (!spawn_async(NULL, command, NULL, NULL, NULL, &error))
 		{
 			ui_set_statusbar(TRUE, "Context action command failed: %s", error->message);
 			g_error_free(error);


Modified: src/keyfile.c
13 lines changed, 7 insertions(+), 6 deletions(-)
===================================================================
@@ -941,19 +941,20 @@ static void load_dialog_prefs(GKeyFile *config)
 
 	/* printing */
 	tmp_string2 = g_find_program_in_path(GEANY_DEFAULT_TOOLS_PRINTCMD);
-#ifdef G_OS_WIN32
+
 	if (!EMPTY(tmp_string2))
 	{
-		/* single quote paths on Win32 for g_spawn_command_line_async */
-		tmp_string = g_strconcat("'", tmp_string2, "' '%f'", NULL);
+	#ifdef G_OS_WIN32
+		tmp_string = g_strconcat(GEANY_DEFAULT_TOOLS_PRINTCMD, " \"%f\"", NULL);
+	#else
+		tmp_string = g_strconcat(GEANY_DEFAULT_TOOLS_PRINTCMD, " '%f'", NULL);
+	#endif
 	}
 	else
 	{
 		tmp_string = g_strdup("");
 	}
-#else
-	tmp_string = g_strconcat(tmp_string2, " %f", NULL);
-#endif
+
 	printing_prefs.external_print_cmd = utils_get_setting_string(config, "printing", "print_cmd", tmp_string);
 	g_free(tmp_string);
 	g_free(tmp_string2);


Modified: src/makefile.win32
2 lines changed, 1 insertions(+), 1 deletions(-)
===================================================================
@@ -67,7 +67,7 @@ OBJS =	about.o build.o callbacks.o dialogs.o document.o editor.o encodings.o fil
 		geanyentryaction.o geanymenubuttonaction.o geanyobject.o geanywraplabel.o highlighting.o \
 		keybindings.o keyfile.o log.o main.o msgwindow.o navqueue.o notebook.o \
 		plugins.o pluginutils.o prefs.o printing.o project.o sciwrappers.o search.o \
-		socket.o stash.o symbols.o templates.o toolbar.o tools.o sidebar.o \
+		socket.o spawn.o stash.o symbols.o templates.o toolbar.o tools.o sidebar.o \
 		ui_utils.o utils.o win32.o
 
 .c.o:


Modified: src/printing.c
19 lines changed, 9 insertions(+), 10 deletions(-)
===================================================================
@@ -39,6 +39,7 @@
 #include "highlighting.h"
 #include "msgwindow.h"
 #include "sciwrappers.h"
+#include "spawn.h"
 #include "support.h"
 #include "utils.h"
 #include "ui_utils.h"
@@ -600,16 +601,15 @@ static void print_external(GeanyDocument *doc)
 			doc->file_name, cmdline))
 	{
 		GError *error = NULL;
-
-#ifdef G_OS_WIN32
-		gchar *tmp_cmdline = g_strdup(cmdline);
-#else
 		/* /bin/sh -c emulates the system() call and makes complex commands possible
-		 * but only needed on non-win32 systems due to the lack of win32's shell capabilities */
-		gchar *tmp_cmdline = g_strconcat("/bin/sh -c \"", cmdline, "\"", NULL);
-#endif
-
-		if (! g_spawn_command_line_async(tmp_cmdline, &error))
+		 * but only on non-win32 systems due to the lack of win32's shell capabilities */
+	#ifdef G_OS_UNIX
+		gchar *argv[] = { "/bin/sh", "-c", cmdline, NULL };
+
+		if (!spawn_async(NULL, NULL, argv, NULL, NULL, &error))
+	#else
+		if (!spawn_async(NULL, cmdline, NULL, NULL, NULL, &error))
+	#endif
 		{
 			dialogs_show_msgbox(GTK_MESSAGE_ERROR,
 				_("Printing of \"%s\" failed (return code: %s)."),
@@ -620,7 +620,6 @@ static void print_external(GeanyDocument *doc)
 		{
 			msgwin_status_add(_("File %s printed."), doc->file_name);
 		}
-		g_free(tmp_cmdline);
 	}
 	g_free(cmdline);
 }


Modified: src/search.c
175 lines changed, 63 insertions(+), 112 deletions(-)
===================================================================
@@ -37,6 +37,7 @@
 #include "msgwindow.h"
 #include "prefs.h"
 #include "sciwrappers.h"
+#include "spawn.h"
 #include "stash.h"
 #include "support.h"
 #include "toolbar.h"
@@ -49,11 +50,6 @@
 #include <string.h>
 #include <ctype.h>
 
-#ifdef G_OS_UNIX
-# include <sys/types.h>
-# include <sys/wait.h>
-#endif
-
 #include <gdk/gdkkeysyms.h>
 
 enum
@@ -149,10 +145,10 @@ static struct
 fif_dlg = {NULL, NULL, NULL, NULL, NULL, NULL, {0, 0}};
 
 
-static gboolean search_read_io(GIOChannel *source, GIOCondition condition, gpointer data);
-static gboolean search_read_io_stderr(GIOChannel *source, GIOCondition condition, gpointer data);
+static void search_read_io(GString *string, GIOCondition condition, gpointer data);
+static void search_read_io_stderr(GString *string, GIOCondition condition, gpointer data);
 
-static void search_close_pid(GPid child_pid, gint status, gpointer user_data);
+static void search_finished(GPid child_pid, gint status, gpointer user_data);
 
 static gchar **search_get_argv(const gchar **argv_prefix, const gchar *dir);
 
@@ -1619,21 +1615,17 @@ on_find_in_files_dialog_response(GtkDialog *dialog, gint response,
 			ui_set_statusbar(FALSE, _("Invalid directory for find in files."));
 		else if (!EMPTY(search_text))
 		{
-			gchar *locale_dir;
 			GString *opts = get_grep_options();
 			const gchar *enc = (enc_idx == GEANY_ENCODING_UTF_8) ? NULL :
 				encodings_get_charset_from_index(enc_idx);
 
-			locale_dir = utils_get_locale_from_utf8(utf8_dir);
-
-			if (search_find_in_files(search_text, locale_dir, opts->str, enc))
+			if (search_find_in_files(search_text, utf8_dir, opts->str, enc))
 			{
 				ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(search_combo), search_text, 0);
 				ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(fif_dlg.files_combo), NULL, 0);
 				ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(dir_combo), utf8_dir, 0);
 				gtk_widget_hide(fif_dlg.dialog);
 			}
-			g_free(locale_dir);
 			g_string_free(opts, TRUE);
 		}
 		else
@@ -1645,36 +1637,26 @@ on_find_in_files_dialog_response(GtkDialog *dialog, gint response,
 
 
 static gboolean
-search_find_in_files(const gchar *utf8_search_text, const gchar *dir, const gchar *opts,
+search_find_in_files(const gchar *utf8_search_text, const gchar *utf8_dir, const gchar *opts,
 	const gchar *enc)
 {
-	gchar **argv_prefix, **argv, **opts_argv;
+	gchar **argv_prefix, **argv;
 	gchar *command_grep;
+	gchar *command_line, *dir;
 	gchar *search_text = NULL;
-	gint opts_argv_len, i;
-	GPid child_pid;
-	gint stdout_fd;
-	gint stderr_fd;
 	GError *error = NULL;
 	gboolean ret = FALSE;
 	gssize utf8_text_len;
 
-	if (EMPTY(utf8_search_text) || ! dir) return TRUE;
+	if (EMPTY(utf8_search_text) || ! utf8_dir) return TRUE;
 
 	command_grep = g_find_program_in_path(tool_prefs.grep_cmd);
 	if (command_grep == NULL)
+		command_line = g_strdup_printf("%s %s --", tool_prefs.grep_cmd, opts);
+	else
 	{
-		ui_set_statusbar(TRUE, _("Cannot execute grep tool '%s';"
-			" check the path setting in Preferences."), tool_prefs.grep_cmd);
-		return FALSE;
-	}
-
-	if (! g_shell_parse_argv(opts, &opts_argv_len, &opts_argv, &error))
-	{
-		ui_set_statusbar(TRUE, _("Cannot parse extra options: %s"), error->message);
-		g_error_free(error);
+		command_line = g_strdup_printf("\"%s\" %s --", command_grep, opts);
 		g_free(command_grep);
-		return FALSE;
 	}
 
 	/* convert the search text in the preferred encoding (if the text is not valid UTF-8. assume
@@ -1687,74 +1669,58 @@ search_find_in_files(const gchar *utf8_search_text, const gchar *dir, const gcha
 	if (search_text == NULL)
 		search_text = g_strdup(utf8_search_text);
 
-	/* set grep command and options */
-	argv_prefix = g_new0(gchar*, 1 + opts_argv_len + 3 + 1);	/* last +1 for recursive arg */
-
-	argv_prefix[0] = command_grep;
-	for (i = 0; i < opts_argv_len; i++)
-	{
-		argv_prefix[i + 1] = g_strdup(opts_argv[i]);
-	}
-	g_strfreev(opts_argv);
-
-	i++;	/* correct for tool_prefs.grep_cmd */
-	argv_prefix[i++] = g_strdup("--");
-	argv_prefix[i++] = search_text;
+	argv_prefix = g_new(gchar*, 3);
+	argv_prefix[0] = search_text;
+	dir = utils_get_locale_from_utf8(utf8_dir);
 
 	/* finally add the arguments(files to be searched) */
-	if (strstr(argv_prefix[1], "r"))	/* recursive option set */
+	if (settings.fif_recursive)	/* recursive option set */
 	{
 		/* Use '.' so we get relative paths in the output */
-		argv_prefix[i++] = g_strdup(".");
-		argv_prefix[i++] = NULL;
+		argv_prefix[1] = g_strdup(".");
+		argv_prefix[2] = NULL;
 		argv = argv_prefix;
 	}
 	else
 	{
-		argv_prefix[i++] = NULL;
+		argv_prefix[1] = NULL;
 		argv = search_get_argv((const gchar**)argv_prefix, dir);
 		g_strfreev(argv_prefix);
-	}
 
-	if (argv == NULL)	/* no files */
-	{
-		return FALSE;
+		if (argv == NULL)	/* no files */
+		{
+			g_free(command_line);
+			return FALSE;
+		}
 	}
 
 	gtk_list_store_clear(msgwindow.store_msg);
 	gtk_notebook_set_current_page(GTK_NOTEBOOK(msgwindow.notebook), MSG_MESSAGE);
 
-	if (! g_spawn_async_with_pipes(dir, (gchar**)argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD,
-		NULL, NULL, &child_pid,
-		NULL, &stdout_fd, &stderr_fd, &error))
-	{
-		geany_debug("%s: g_spawn_async_with_pipes() failed: %s", G_STRFUNC, error->message);
-		ui_set_statusbar(TRUE, _("Process failed (%s)"), error->message);
-		g_error_free(error);
-		ret = FALSE;
+	/* we can pass 'enc' without strdup'ing it here because it's a global const string and
+	 * always exits longer than the lifetime of this function */
+	if (spawn_with_callbacks(dir, command_line, argv, NULL, 0, NULL, NULL, search_read_io,
+		(gpointer) enc, 0, search_read_io_stderr, (gpointer) enc, 0, search_finished, NULL,
+		NULL, &error))
+ 	{
+		gchar *utf8_str;
+ 
+ 		ui_progress_bar_start(_("Searching..."));
+ 		msgwin_set_messages_dir(dir);
+		utf8_str = g_strdup_printf(_("%s %s -- %s (in directory: %s)"),
+			tool_prefs.grep_cmd, opts, utf8_search_text, utf8_dir);
+ 		msgwin_msg_add_string(COLOR_BLUE, -1, NULL, utf8_str);
+		g_free(utf8_str);
+ 		ret = TRUE;
 	}
 	else
 	{
-		gchar *str, *utf8_str;
-
-		ui_progress_bar_start(_("Searching..."));
-
-		msgwin_set_messages_dir(dir);
-		/* we can pass 'enc' without strdup'ing it here because it's a global const string and
-		 * always exits longer than the lifetime of this function */
-		utils_set_up_io_channel(stdout_fd, G_IO_IN | G_IO_PRI | G_IO_ERR | G_IO_HUP | G_IO_NVAL,
-			TRUE, search_read_io, (gpointer) enc);
-		utils_set_up_io_channel(stderr_fd, G_IO_IN | G_IO_PRI | G_IO_ERR | G_IO_HUP | G_IO_NVAL,
-			TRUE, search_read_io_stderr, (gpointer) enc);
-		g_child_watch_add(child_pid, search_close_pid, NULL);
-
-		str = g_strdup_printf(_("%s %s -- %s (in directory: %s)"),
-			tool_prefs.grep_cmd, opts, utf8_search_text, dir);
-		utf8_str = utils_get_utf8_from_locale(str);
-		msgwin_msg_add_string(COLOR_BLUE, -1, NULL, utf8_str);
-		utils_free_pointers(2, str, utf8_str, NULL);
-		ret = TRUE;
+		geany_debug("%s: spawn_with_callbacks() failed: %s", G_STRFUNC, error->message);
+		ui_set_statusbar(TRUE, _("Process failed (%s)"), error->message);
+		g_error_free(error);
 	}
+
+	utils_free_pointers(2, dir, command_line, NULL);
 	g_strfreev(argv);
 	return ret;
 }
@@ -1837,61 +1803,47 @@ static gchar **search_get_argv(const gchar **argv_prefix, const gchar *dir)
 }
 
 
-static gboolean read_fif_io(GIOChannel *source, GIOCondition condition, gchar *enc, gint msg_color)
+static void read_fif_io(gchar *msg, GIOCondition condition, gchar *enc, gint msg_color)
 {
 	if (condition & (G_IO_IN | G_IO_PRI))
 	{
-		gchar *msg, *utf8_msg;
-		GIOStatus st;
+		gchar *utf8_msg = NULL;
 
-		while ((st = g_io_channel_read_line(source, &msg, NULL, NULL, NULL)) != G_IO_STATUS_ERROR &&
-				st != G_IO_STATUS_EOF && msg)
+		g_strstrip(msg);
+		/* enc is NULL when encoding is set to UTF-8, so we can skip any conversion */
+		if (enc != NULL)
 		{
-			utf8_msg = NULL;
-
-			g_strstrip(msg);
-			/* enc is NULL when encoding is set to UTF-8, so we can skip any conversion */
-			if (enc != NULL)
+			if (! g_utf8_validate(msg, -1, NULL))
 			{
-				if (! g_utf8_validate(msg, -1, NULL))
-				{
-					utf8_msg = g_convert(msg, -1, "UTF-8", enc, NULL, NULL, NULL);
-				}
-				if (utf8_msg == NULL)
-					utf8_msg = msg;
+				utf8_msg = g_convert(msg, -1, "UTF-8", enc, NULL, NULL, NULL);
 			}
-			else
+			if (utf8_msg == NULL)
 				utf8_msg = msg;
+		}
+		else
+			utf8_msg = msg;
 
-			msgwin_msg_add_string(msg_color, -1, NULL, utf8_msg);
+		msgwin_msg_add_string(msg_color, -1, NULL, utf8_msg);
 
-			if (utf8_msg != msg)
-				g_free(utf8_msg);
-			g_free(msg);
-		}
-		if (st == G_IO_STATUS_ERROR || st == G_IO_STATUS_EOF)
-			return FALSE;
+		if (utf8_msg != msg)
+			g_free(utf8_msg);
 	}
-	if (condition & (G_IO_ERR | G_IO_HUP | G_IO_NVAL))
-		return FALSE;
-
-	return TRUE;
 }
 
 
-static gboolean search_read_io(GIOChannel *source, GIOCondition condition, gpointer data)
+static void search_read_io(GString *string, GIOCondition condition, gpointer data)
 {
-	return read_fif_io(source, condition, data, COLOR_BLACK);
+	return read_fif_io(string->str, condition, data, COLOR_BLACK);
 }
 
 
-static gboolean search_read_io_stderr(GIOChannel *source, GIOCondition condition, gpointer data)
+static void search_read_io_stderr(GString *string, GIOCondition condition, gpointer data)
 {
-	return read_fif_io(source, condition, data, COLOR_DARK_RED);
+	return read_fif_io(string->str, condition, data, COLOR_DARK_RED);
 }
 
 
-static void search_close_pid(GPid child_pid, gint status, gpointer user_data)
+static void search_finished(GPid child_pid, gint status, gpointer user_data)
 {
 	const gchar *msg = _("Search failed.");
 #ifdef G_OS_UNIX
@@ -1932,7 +1884,6 @@ static void search_close_pid(GPid child_pid, gint status, gpointer user_data)
 			break;
 	}
 	utils_beep();
-	g_spawn_close_pid(child_pid);
 	ui_progress_bar_stop();
 }
 


Modified: src/spawn.c
1341 lines changed, 1341 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,1341 @@
+/*
+ *      spawn.c - this file is part of Geany, a fast and lightweight IDE
+ *
+ *      Copyright 2013 Dimitar Toshkov Zhekov <dimitar(dot)zhekov(at)gmail(dot)com>
+ *
+ *      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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/* An ongoing effort to improve the tool spawning situation under Windows.
+ * In particular:
+ *    - There is no g_shell_parse_argv() for windows. It's not hard to write one,
+ *      but the command line recreated by mscvrt may be wrong.
+ *    - GLib converts the argument vector to UNICODE. For non-UTF8 arguments,
+ *      the result is often "Invalid string in argument vector at %d: %s: Invalid
+ *      byte sequence in conversion input" (YMMV). Our tools (make, grep, gcc, ...)
+ *      are "ANSI", so converting to UNICODE and then back only causes problems.
+ *    - For various reasons, GLib uses an intermediate program to start children
+ *      (see gspawn-win32.c), the result being that the grandchildren output (such
+ *      as make -> gcc) is not captured.
+ *    - With non-blocking pipes, the g_io_add_watch() callbacks are never invoked,
+ *      while with blocking pipes, g_io_channel_read_line() blocks.
+ *    - Some other problems are explained in separate comments below.
+ *
+ * Even under Unix, using g_io_channel_read_line() is not a good idea, since it may
+ * buffer lines of unlimited length.
+ *
+ * This module does not depend on Geany when compiled for testing (-DSPAWN_TEST).
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <errno.h>
+#include <string.h>
+
+#include "spawn.h"
+
+#ifdef G_OS_WIN32
+# include <ctype.h>    /* isspace() */
+# include <fcntl.h>    /* _O_RDONLY, _O_WRONLY */
+# include <io.h>       /* _open_osfhandle, _close */
+# include <windows.h>
+#else  /* G_OS_WIN32 */
+# include <signal.h>
+#endif  /* G_OS_WIN32 */
+
+#ifdef SPAWN_TEST
+# define _
+#else
+# include "support.h"
+#endif
+
+#ifdef G_OS_WIN32
+/* Each 4KB under Windows seem to come in 2 portions, so 2K + 2K is more
+   balanced than 4095 + 1. May be different on the latest Windows/glib? */
+# define DEFAULT_IO_LENGTH 2048
+#else
+# define DEFAULT_IO_LENGTH 4096
+#endif
+
+#define G_IO_FAILURE (G_IO_ERR | G_IO_HUP | G_IO_NVAL)  /* always used together */
+
+
+/**
+ *  Checks whether a command line is syntactically valid and extracts the program name from it.
+ *
+ *  All OS:
+ *     - any leading spaces, tabs and new lines are skipped
+ *     - an empty command is invalid
+ *  Unix:
+ *     - the standard shell quoting and escaping rules are used, see @c g_shell_parse_argv()
+ *     - as a consequence, an unqouted # at the start of an argument comments to the end of line
+ *  Windows:
+ *     - leading carriage returns are skipped too
+ *     - a quoted program name must be entirely inside the quotes. No "C:\Foo\Bar".pdf or
+ *	   "C:\Foo\Bar".bat, which would be executed by Windows as C:\Foo\Bar.exe
+ *     - an unquoted program name may not contain spaces. Foo Bar Qux will not be considered
+ *       "Foo Bar.exe" Qux or "Foo Bar Qux.exe", depending on what executables exist, as
+ *       Windows normally does.
+ *     - the program name must be separated from the arguments by at least one space or tab
+ *     - the standard Windows quoting and escaping rules are used: double quote is escaped with
+ *       backslash, and any literal backslashes before a double quote must be duplicated.
+ *
+ *  @param command_line the command line to check and get the program name from.
+ *  @param error return location for error.
+ *
+ *  @return allocated string with the program name on success, @c NULL on error.
+ *
+ *  @since 1.25
+ **/
+GEANY_API_SYMBOL
+gchar *spawn_get_program_name(const gchar *command_line, GError **error)
+{
+	gchar *program;
+
+#ifdef G_OS_WIN32
+	gboolean open_quote = FALSE;
+	const gchar *s, *arguments;
+
+	g_return_val_if_fail(command_line != NULL, FALSE);
+
+	while (*command_line && strchr(" \t\r\n", *command_line))
+		command_line++;
+
+	if (!*command_line)
+	{
+		/* TL note: from glib */
+		g_set_error(error, G_SHELL_ERROR, G_SHELL_ERROR_EMPTY_STRING,
+			_("Text was empty (or contained only whitespace)"));
+		return FALSE;
+	}
+
+	/* To prevent Windows from doing something weird, we want to be 100% sure that the
+	   character after the program name is a delimiter, so we allow space and tab only. */
+
+	if (*command_line == '"')
+	{
+		command_line++;
+		/* Windows allows "foo.exe, but we may have extra arguments */
+		if ((s = strchr(command_line, '"')) == NULL)
+		{
+			/* TL note: from glib */
+			g_set_error(error, G_SHELL_ERROR, G_SHELL_ERROR_BAD_QUOTING,
+				_("Text ended before matching quote was found for %c."
+				  " (The text was '%s')"), '"', command_line);
+			return FALSE;
+		}
+
+		if (!strchr(" \t", s[1]))  /* strchr() catches s[1] == '\0' */
+		{
+			g_set_error(error, G_SHELL_ERROR, G_SHELL_ERROR_BAD_QUOTING,
+				_("A quoted Windows program name must be entirely inside the quotes"));
+			return FALSE;
+		}
+	}
+	else
+	{
+		const gchar *quote = strchr(command_line, '"');
+
+		/* strchr() catches *s == '\0', and the for body is empty */
+		for (s = command_line; !strchr(" \t", *s); s++);
+
+		if (quote && quote < s)
+		{
+			g_set_error(error, G_SHELL_ERROR, G_SHELL_ERROR_BAD_QUOTING,
+				_("A quoted Windows program name must be entirely inside the quotes"));
+			return FALSE;
+		}
+	}
+
+	program = g_strndup(command_line, s - command_line);
+	arguments = s + (*s == '"');
+
+	for (s = arguments; *s; s++)
+	{
+		if (*s == '"')
+		{
+			const char *slash;
+
+			for (slash = s; slash > arguments && slash[-1] == '\\'; slash--);
+			if ((s - slash) % 2 == 0)
+				open_quote ^= TRUE;
+		}
+	}
+
+	if (open_quote)
+	{
+		/* TL note: from glib */
+		g_set_error(error, G_SHELL_ERROR, G_SHELL_ERROR_BAD_QUOTING,
+			_("Text ended before matching quote was found for %c."
+			  " (The text was '%s')"), '"', command_line);
+		g_free(program);
+		return FALSE;
+	}
+#else  /* G_OS_WIN32 */
+	int argc;
+	char **argv;
+
+	if (!g_shell_parse_argv(command_line, &argc, &argv, error))
+		return FALSE;
+
+	/* empty string results in parse error, so argv[0] is not NULL */
+	program = g_strdup(argv[0]);
+	g_strfreev(argv);
+#endif  /* G_OS_WIN32 */
+
+	return program;
+}
+
+
+/**
+ *  Checks whether a command line is valid.
+ *
+ *  Checks if @a command_line is syntactically valid using @c spawn_get_program_name().
+ *
+ *  If @a execute is TRUE, also checks, using @c g_find_program_in_path(), if the program
+ *  specified in @a command_line exists and is executable.
+ *
+ *  @param command_line the command line to check.
+ *  @param execute whether to check if the command line is really executable.
+ *  @param error return location for error.
+ *
+ *  @return @c TRUE on success, @c FALSE on error.
+ *
+ *  @since 1.25
+ **/
+GEANY_API_SYMBOL
+gboolean spawn_check_command(const gchar *command_line, gboolean execute, GError **error)
+{
+	gchar *program = spawn_get_program_name(command_line, error);
+
+	if (!program)
+		return FALSE;
+
+	if (execute)
+	{
+		gchar *executable = g_find_program_in_path(program);
+
+		if (!executable)
+		{
+			g_set_error(error, G_SHELL_ERROR, G_SHELL_ERROR_FAILED,  /* or SPAWN error? */
+				_("Program '%s' not found"), program);
+			g_free(program);
+			return FALSE;
+		}
+
+		g_free(executable);
+	}
+
+	g_free(program);
+	return TRUE;
+}
+
+
+/**
+ *  Kills a process.
+ *
+ *  @param pid id of the process to kill.
+ *  @param error return location for error.
+ *
+ *  On Unix, sends a SIGTERM to the process.
+ *
+ *  On Windows, terminates the process with exit code 255 (used sometimes as "generic"
+ *  error code, or for programs terminated with Ctrl+C / Ctrl+Break).
+ *
+ *  @return @c TRUE on success, @c FALSE on error.
+ *
+ *  @since 1.25
+ **/
+GEANY_API_SYMBOL
+gboolean spawn_kill_process(GPid pid, GError **error)
+{
+#ifdef G_OS_WIN32
+	if (!TerminateProcess(pid, 255))
+	{
+		gchar *message = g_win32_error_message(GetLastError());
+
+		g_set_error(error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED,
+			_("TerminateProcess() failed: %s"), message);
+		g_free(message);
+		return FALSE;
+	}
+#else
+	if (kill(pid, SIGTERM))
+	{
+		g_set_error(error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED, "%s", g_strerror(errno));
+		return FALSE;
+	}
+#endif
+	return TRUE;
+}
+
+
+#ifdef G_OS_WIN32
+static gchar *spawn_create_process_with_pipes(char *command_line, const char *working_directory,
+	void *environment, HANDLE *hprocess, int *stdin_fd, int *stdout_fd, int *stderr_fd)
+{
+	enum { WRITE_STDIN, READ_STDOUT, READ_STDERR, READ_STDIN, WRITE_STDOUT, WRITE_STDERR };
+	STARTUPINFO startup;
+	PROCESS_INFORMATION process;
+	HANDLE hpipe[6] = { NULL, NULL, NULL, NULL, NULL, NULL };
+	int *fd[3] = { stdin_fd, stdout_fd, stderr_fd };
+	const char *failed;     /* failed WIN32/CRTL function, if any */
+	gchar *message = NULL;  /* glib WIN32/CTRL error message */
+	gchar *failure = NULL;  /* full error text */
+	gboolean pipe_io;
+	int i;
+
+	ZeroMemory(&startup, sizeof startup);
+	startup.cb = sizeof startup;
+	pipe_io = stdin_fd || stdout_fd || stderr_fd;
+
+	if (pipe_io)
+	{
+		startup.dwFlags |= STARTF_USESTDHANDLES;
+
+		/* not all programs accept mixed NULL and non-NULL hStd*, so we create all */
+		for (i = 0; i < 3; i++)
+		{
+			static int pindex[3][2] = { { READ_STDIN, WRITE_STDIN },
+					{ READ_STDOUT, WRITE_STDOUT }, { READ_STDERR, WRITE_STDERR } };
+
+			if (!CreatePipe(&hpipe[pindex[i][0]], &hpipe[pindex[i][1]], NULL, 0))
+			{
+				hpipe[pindex[i][0]] = hpipe[pindex[i][1]] = NULL;
+				failed = "CreatePipe";
+				goto leave;
+			}
+
+			if (fd[i])
+			{
+				static int mode[3] = { _O_WRONLY, _O_RDONLY, _O_RDONLY };
+
+				if ((*fd[i] = _open_osfhandle((intptr_t) hpipe[i], mode[i])) == -1)
+				{
+					failed = "_open_osfhandle";
+					message = g_strdup(g_strerror(errno));
+					goto leave;
+				}
+			}
+			else if (!CloseHandle(hpipe[i]))
+			{
+				failed = "CloseHandle";
+				goto leave;
+			}
+
+			if (!SetHandleInformation(hpipe[i + 3], HANDLE_FLAG_INHERIT,
+				HANDLE_FLAG_INHERIT))
+			{
+				failed = "SetHandleInformation";
+				goto leave;
+			}
+		}
+	}
+
+	startup.hStdInput = hpipe[READ_STDIN];
+	startup.hStdOutput = hpipe[WRITE_STDOUT];
+	startup.hStdError = hpipe[WRITE_STDERR];
+
+	if (!CreateProcess(NULL, command_line, NULL, NULL, TRUE, pipe_io ? CREATE_NO_WINDOW : 0,
+		environment, working_directory, &startup, &process))
+	{
+		failed = "CreateProcess";
+		/* further errors will not be reported */
+	}
+	else
+	{
+		failed = NULL;
+		CloseHandle(process.hThread);  /* we don't need this */
+		*hprocess = process.hProcess;
+	}
+
+leave:
+	if (failed)
+	{
+		if (!message)
+			message = g_win32_error_message(GetLastError());
+
+		failure = g_strdup_printf("%s() failed: %s", failed, message);
+		g_free(message);
+	}
+
+	if (pipe_io)
+	{
+		for (i = 0; i < 3; i++)
+		{
+			if (failed)
+			{
+				if (fd[i] && *fd[i] != -1)
+					_close(*fd[i]);
+				else if (hpipe[i])
+					CloseHandle(hpipe[i]);
+			}
+
+			if (hpipe[i + 3])
+				CloseHandle(hpipe[i + 3]);
+		}
+	}
+
+	return failure;
+}
+
+
+static void spawn_append_argument(GString *command, const char *text)
+{
+	const char *s;
+
+	if (command->len)
+		g_string_append_c(command, ' ');
+
+	for (s = text; *s; s++)
+	{
+		/* g_ascii_isspace() fails for '\v', and locale spaces (if any) will do no harm */
+		if (*s == '"' || isspace(*s))
+			break;
+	}
+
+	if (*text && !*s)
+		g_string_append(command, text);
+	else
+	{
+		g_string_append_c(command, '"');
+
+		for (s = text; *s; s++)
+		{
+			const char *slash;
+
+			for (slash = s; *slash == '\\'; slash++);
+
+			if (slash > s)
+			{
+				g_string_append_len(command, s, slash - s);
+
+				if (!*slash || *slash == '"')
+				{
+					g_string_append_len(command, s, slash - s);
+
+					if (!*slash)
+						break;
+				}
+
+				s = slash;
+			}
+
+			if (*s == '"')
+				g_string_append_c(command, '\\');
+
+			g_string_append_c(command, *s);
+		}
+
+		g_string_append_c(command, '"');
+	}
+}
+
+
+static void spawn_close_pid(GPid pid, G_GNUC_UNUSED gint status, G_GNUC_UNUSED gpointer data)
+{
+	g_spawn_close_pid(pid);
+}
+#endif /* G_OS_WIN32 */
+
+
+/**
+ *  Executes a child program asynchronously and setups pipes.
+ *
+ *  This is the low-level spawning function. Please use @c spawn_with_callbacks() unless
+ *  you need to setup specific event source(s).
+ *
+ *  A command line or an argument vector must be passed. If both are present, the argument
+ *  vector is appended to the command line. An empty command line is not allowed.
+ *
+ *  Under Windows, if the child is a console application, and at least one file descriptor is
+ *  specified, the new child console (if any) will be hidden.
+ *
+ *  If a @a child_pid is passed, it's your responsibility to invoke @c g_spawn_close_pid().
+ *
+ *  @param working_directory child's current working directory, or @c NULL.
+ *  @param command_line child program and arguments, or @c NULL.
+ *  @param argv child's argument vector, or @c NULL.
+ *  @param envp child's environment, or @c NULL.
+ *  @param child_pid return location for child process ID, or @c NULL.
+ *  @param stdin_fd return location for file descriptor to write to child's stdin, or @c NULL.
+ *  @param stdout_fd return location for file descriptor to read child's stdout, or @c NULL.
+ *  @param stderr_fd return location for file descriptor to read child's stderr, or @c NULL.
+ *  @param error return location for error.
+ *
+ *  @return @c TRUE on success, @c FALSE on error.
+ *
+ *  @since 1.25
+ **/
+GEANY_API_SYMBOL
+gboolean spawn_async_with_pipes(const gchar *working_directory, const gchar *command_line,
+	gchar **argv, gchar **envp, GPid *child_pid, gint *stdin_fd, gint *stdout_fd,
+	gint *stderr_fd, GError **error)
+{
+	g_return_val_if_fail(command_line != NULL || argv != NULL, FALSE);
+
+#ifdef G_OS_WIN32
+	GString *command;
+	GArray *environment;
+	GPid pid;
+	gchar *failure;
+
+	if (command_line)
+	{
+		gchar *program = spawn_get_program_name(command_line, error);
+		const gchar *arguments;
+
+		if (!program)
+			return FALSE;
+
+		command = g_string_new(NULL);
+		arguments = strstr(command_line, program) + strlen(program);
+
+		if (*arguments == '"')
+		{
+			g_string_append(command, program);
+			arguments++;
+		}
+		else
+		{
+			/* quote the first token, to avoid Windows attemps to run two or more
+			   unquoted tokens as a program until an existing file name is found */
+			g_string_printf(command, "\"%s\"", program);
+		}
+
+		g_string_append(command, arguments);
+		g_free(program);
+	}
+	else
+		command = g_string_new(NULL);
+
+	environment = g_array_new(TRUE, FALSE, sizeof(char));
+
+	while (argv && *argv)
+		spawn_append_argument(command, *argv++);
+
+#ifdef SPAWN_TEST
+	g_message("full spawn command line: %s\n", command->str);
+#endif
+
+	while (envp && *envp)
+	{
+		g_array_append_vals(environment, *envp, strlen(*envp) + 1);
+		envp++;
+	}
+
+	failure = spawn_create_process_with_pipes(command->str, working_directory,
+		envp ? environment->data : NULL, child_pid ? child_pid : &pid,
+		stdin_fd, stdout_fd, stderr_fd);
+
+	g_string_free(command, TRUE);
+	g_array_free(environment, TRUE);
+
+	if (failure)
+	{
+		g_set_error(error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED, "%s", failure);
+		g_free(failure);
+		return FALSE;
+	}
+	else if (!child_pid)
+		g_child_watch_add(pid, spawn_close_pid, NULL);
+
+	return TRUE;
+#else  /* G_OS_WIN32 */
+	int cl_argc;
+	char **full_argv;
+	gboolean spawned;
+
+	if (command_line)
+	{
+		int argc = 0;
+		char **cl_argv;
+
+		if (!g_shell_parse_argv(command_line, &cl_argc, &cl_argv, error))
+			return FALSE;
+
+		if (argv)
+			for (argc = 0; argv[argc]; argc++);
+
+		full_argv = g_renew(gchar *, cl_argv, cl_argc + argc + 1);
+		memcpy(full_argv + cl_argc, argv, argc * sizeof(gchar *));
+		full_argv[cl_argc + argc] = NULL;
+	}
+	else
+		full_argv = argv;
+
+	spawned = g_spawn_async_with_pipes(working_directory, full_argv, envp,
+		G_SPAWN_SEARCH_PATH | (child_pid ? G_SPAWN_DO_NOT_REAP_CHILD : 0), NULL, NULL,
+		child_pid, stdin_fd, stdout_fd, stderr_fd, error);
+
+	if (full_argv != argv)
+	{
+		full_argv[cl_argc] = NULL;
+		g_strfreev(full_argv);
+	}
+
+	return spawned;
+#endif  /* G_OS_WIN32 */
+}
+
+
+/**
+ *  Executes a child asynchronously.
+ *
+ *  See @c spawn_async_with_pipes() for a full description; this function simply calls
+ *  @c g_spawn_async_with_pipes() without any pipes.
+ *
+ *  @since 1.25
+ **/
+GEANY_API_SYMBOL
+gboolean spawn_async(const gchar *working_directory, const gchar *command_line, gchar **argv,
+	gchar **envp, GPid *child_pid, GError **error)
+{
+	return spawn_async_with_pipes(working_directory, command_line, argv, envp, child_pid,
+		NULL, NULL, NULL, error);
+}
+
+
+/*
+ * Spawn with callbacks - general event sequence:
+ *
+ * - Launch the child.
+ * - Setup any I/O callbacks and a child watch callback.
+ * - On sync execution, run a main loop.
+ * - Wait for the child to terminate.
+ * - Check for active I/O sources. If any, add a timeout source to watch them, they should
+ *   become inactive real soon now that the child is dead. Otherwise, finalize immediately.
+ * - In the timeout source: check for active I/O sources and finalize if none.
+ */
+
+typedef struct _SpawnChannelData
+{
+	GIOChannel *channel;  /* NULL if not created / already destroyed */
+	union
+	{
+		GIOFunc write;
+		SpawnReadFunc read;
+	} cb;
+	gpointer cb_data;
+	/* stdout/stderr only */
+	GString *buffer;       /* NULL if recursive */
+	GString *line_buffer;  /* NULL if char buffered */
+	gsize max_length;
+} SpawnChannelData;
+
+
+static void spawn_destroy_cb(gpointer data)
+{
+	SpawnChannelData *sc = (SpawnChannelData *) data;
+
+	g_io_channel_shutdown(sc->channel, FALSE, NULL);
+	sc->channel = NULL;
+
+	if (sc->buffer)
+		g_string_free(sc->buffer, TRUE);
+
+	if (sc->line_buffer)
+		g_string_free(sc->line_buffer, TRUE);
+}
+
+
+static gboolean spawn_write_cb(GIOChannel *channel, GIOCondition condition, gpointer data)
+{
+	SpawnChannelData *sc = (SpawnChannelData *) data;
+
+	if (!sc->cb.write(channel, condition, sc->cb_data))
+		return FALSE;
+
+	return !(condition & G_IO_FAILURE);
+}
+
+
+static gboolean spawn_read_cb(GIOChannel *channel, GIOCondition condition, gpointer data)
+{
+	SpawnChannelData *sc = (SpawnChannelData *) data;
+	GString *line_buffer = sc->line_buffer;
+	GString *buffer = sc->buffer ? sc->buffer : g_string_sized_new(sc->max_length);
+	GIOCondition input_cond = condition & (G_IO_IN | G_IO_PRI);
+	GIOCondition failure_cond = condition & G_IO_FAILURE;
+	/*
+	 * - Normally, read only once. With IO watches, our data processing must be immediate,
+	 *   which may give the child time to emit more data, and a read loop may combine it into
+	 *   large stdout and stderr portions. Under Windows, looping blocks.
+	 * - On failure, read in a loop. It won't block now, there will be no more data, and the
+	 *   IO watch is not guaranteed to be called again (under Windows this is the last call).
+	 */
+	if (input_cond)
+	{
+		gsize chars_read;
+		GIOStatus status;
+
+		if (line_buffer)
+		{
+			gsize n = line_buffer->len;
+
+			while ((status = g_io_channel_read_chars(channel, line_buffer->str + n,
+				DEFAULT_IO_LENGTH, &chars_read, NULL)) == G_IO_STATUS_NORMAL)
+			{
+				g_string_set_size(line_buffer, n + chars_read);
+
+				while (n < line_buffer->len)
+				{
+					gsize line_len = 0;
+
+					if (n == sc->max_length)
+						line_len = n;
+					else if (strchr("\n", line_buffer->str[n]))  /* '\n' or '\0' */
+						line_len = n + 1;
+					else if (n < line_buffer->len - 1 && line_buffer->str[n] == '\r')
+						line_len = n + 1 + (line_buffer->str[n + 1] == '\n');
+
+					if (!line_len)
+						n++;
+					else
+					{
+						g_string_append_len(buffer, line_buffer->str, line_len);
+						g_string_erase(line_buffer, 0, line_len);
+						/* input only, failures are reported separately below */
+						sc->cb.read(buffer, input_cond, sc->cb_data);
+						g_string_truncate(buffer, 0);
+						n = 0;
+					}
+				}
+
+				if (!failure_cond)
+					break;
+			}
+		}
+		else
+		{
+			while ((status = g_io_channel_read_chars(channel, buffer->str, sc->max_length,
+				&chars_read, NULL)) == G_IO_STATUS_NORMAL)
+			{
+				g_string_set_size(buffer, chars_read);
+				/* input only, failures are reported separately below */
+				sc->cb.read(buffer, input_cond, sc->cb_data);
+
+				if (!failure_cond)
+					break;
+			}
+		}
+
+		/* Under OSX, after child death, the read watches receive input conditions instead
+		   of error conditions, so we convert the termination statuses into conditions.
+		   Should not hurt the other OS. */
+		if (status == G_IO_STATUS_ERROR)
+			failure_cond |= G_IO_ERR;
+		else if (status == G_IO_STATUS_EOF)
+			failure_cond |= G_IO_HUP;
+	}
+
+	if (failure_cond)  /* we must signal the callback */
+	{
+		if (line_buffer && line_buffer->len)  /* flush the line buffer */
+		{
+			g_string_append_len(buffer, line_buffer->str, line_buffer->len);
+			/* all data may be from a previous call */
+			if (!input_cond)
+				input_cond = G_IO_IN;
+		}
+		else
+		{
+			input_cond = 0;
+			g_string_truncate(buffer, 0);
+		}
+
+		sc->cb.read(buffer, input_cond | failure_cond, sc->cb_data);
+	}
+
+	if (buffer != sc->buffer)
+		g_string_free(buffer, TRUE);
+
+	return !failure_cond;
+}
+
+
+typedef struct _SpawnWatcherData
+{
+	SpawnChannelData sc[3];       /* stdin, stdout, stderr */
+	GChildWatchFunc exit_cb;
+	gpointer exit_data;
+	GPid pid;
+	gint exit_status;
+	GMainContext *main_context;  /* NULL if async execution */
+	GMainLoop *main_loop;        /* NULL if async execution */
+} SpawnWatcherData;
+
+
+static void spawn_finalize(SpawnWatcherData *sw)
+{
+	if (sw->exit_cb)
+		sw->exit_cb(sw->pid, sw->exit_status, sw->exit_data);
+
+	if (sw->main_loop)
+	{
+		g_main_loop_quit(sw->main_loop);
+		g_main_loop_unref(sw->main_loop);
+	}
+
+	g_spawn_close_pid(sw->pid);
+	g_slice_free(SpawnWatcherData, sw);
+}
+
+
+static gboolean spawn_timeout_cb(gpointer data)
+{
+	SpawnWatcherData *sw = (SpawnWatcherData *) data;
+	int i;
+
+	for (i = 0; i < 3; i++)
+		if (sw->sc[i].channel)
+			return TRUE;
+
+	spawn_finalize(sw);
+	return FALSE;
+}
+
+
+static void spawn_watch_cb(GPid pid, gint status, gpointer data)
+{
+	SpawnWatcherData *sw = (SpawnWatcherData *) data;
+	int i;
+
+	sw->pid = pid;
+	sw->exit_status = status;
+
+	for (i = 0; i < 3; i++)
+	{
+		if (sw->sc[i].channel)
+		{
+			GSource *source = g_timeout_source_new(50);
+
+			g_source_set_callback(source, spawn_timeout_cb, data, NULL);
+			g_source_attach(source, sw->main_context);
+			g_source_unref(source);
+			return;
+		}
+	}
+
+	spawn_finalize(sw);
+}
+
+
+/**
+ *  Executes a child program and setups callbacks.
+ *
+ *  A command line or an argument vector must be passed. If both are present, the argument
+ *  vector is appended to the command line. An empty command line is not allowed.
+ *
+ *  The synchronous execution may not be combined with recursive callbacks.
+ *
+ *  In line buffered mode, the child input is broken on '\n', "\r\n", '\r', '\0' and max length.
+ *
+ *  All I/O callbacks are guaranteed to be invoked at least once with @c G_IO_ERR, @c G_IO_HUP
+ *  or @c G_IO_NVAL set (except for a @a stdin_cb which returns @c FALSE before that). For the
+ *  non-recursive callbacks, this is guaranteed to be the last call, and may be used to free any
+ *  resources associated with the callback.
+ *
+ *  The @a stdin_cb may write to @c channel only once per invocation, only if @c G_IO_OUT is
+ *  set, and only a non-zero number of characters.
+ *
+ *  @c stdout_cb and @c stderr_cb may modify the received strings in any way, but must not
+ *  free them.
+ *
+ *  The default max lengths are 24K for line buffered stdout, 8K for line buffered stderr,
+ *  4K for unbuffered input under Unix, and 2K for unbuffered input under Windows.
+ *
+ *  @c exit_cb is always invoked last, after all I/O callbacks.
+ *
+ *  The @a child_pid will be closed automatically, after @a exit_cb is invoked.
+ *
+ *  @param working_directory child's current working directory, or @c NULL.
+ *  @param command_line child program and arguments, or @c NULL.
+ *  @param argv child's argument vector, or @c NULL.
+ *  @param envp child's environment, or @c NULL.
+ *  @param spawn_flags flags from SpawnFlags.
+ *  @param stdin_cb callback to send data to childs's stdin, or @c NULL.
+ *  @param stdin_data data to pass to @a stdin_cb.
+ *  @param stdout_cb callback to receive child's stdout, or @c NULL.
+ *  @param stdout_data data to pass to @a stdout_cb.
+ *  @param stdout_max_length maximum data length to pass to stdout_cb, @c 0 = default.
+ *  @param stderr_cb callback to receive child's stderr, or @c NULL.
+ *  @param stderr_data data to pass to @a stderr_cb.
+ *  @param stderr_max_length maximum data length to pass to stderr_cb, @c 0 = default.
+ *  @param exit_cb callback to invoke when the child exits, or @c NULL.
+ *  @param exit_data data to pass to @a exit_cb.
+ *  @param child_pid return location for child process ID, or @c NULL.
+ *  @param error return location for error.
+ *
+ *  @return @c TRUE on success, @c FALSE on error.
+ *
+ *  @since 1.25
+ **/
+GEANY_API_SYMBOL
+gboolean spawn_with_callbacks(const gchar *working_directory, const gchar *command_line,
+	gchar **argv, gchar **envp, SpawnFlags spawn_flags, GIOFunc stdin_cb, gpointer stdin_data,
+	SpawnReadFunc stdout_cb, gpointer stdout_data, gsize stdout_max_length,
+	SpawnReadFunc stderr_cb, gpointer stderr_data, gsize stderr_max_length,
+	GChildWatchFunc exit_cb, gpointer exit_data, GPid *child_pid, GError **error)
+{
+	GPid pid;
+	int pipe[3] = { -1, -1, -1 };
+
+	g_return_val_if_fail(!(spawn_flags & SPAWN_RECURSIVE) || !(spawn_flags & SPAWN_SYNC),
+		FALSE);
+
+	if (spawn_async_with_pipes(working_directory, command_line, argv, envp, &pid,
+		stdin_cb ? &pipe[0] : NULL, stdout_cb ? &pipe[1] : NULL,
+		stderr_cb ? &pipe[2] : NULL, error))
+	{
+		SpawnWatcherData *sw = g_slice_new0(SpawnWatcherData);
+		gpointer cb_data[3] = { stdin_data, stdout_data, stderr_data };
+		GSource *source;
+		int i;
+
+		sw->main_context = spawn_flags & SPAWN_SYNC ? g_main_context_new() : NULL;
+
+		if (child_pid)
+			*child_pid = pid;
+
+		for (i = 0; i < 3; i++)
+		{
+			SpawnChannelData *sc = &sw->sc[i];
+			GIOCondition condition;
+			GSourceFunc callback;
+
+			if (pipe[i] == -1)
+				continue;
+
+		#ifdef G_OS_WIN32
+			sc->channel = g_io_channel_win32_new_fd(pipe[i]);
+		#else
+			sc->channel = g_io_channel_unix_new(pipe[i]);
+			g_io_channel_set_flags(sc->channel, G_IO_FLAG_NONBLOCK, NULL);
+		#endif
+			g_io_channel_set_encoding(sc->channel, NULL, NULL);
+			/* we have our own buffers, and GIO buffering blocks under Windows */
+			g_io_channel_set_buffered(sc->channel, FALSE);
+			sc->cb_data = cb_data[i];
+
+			if (i == 0)
+			{
+				sc->cb.write = stdin_cb;
+				condition = G_IO_OUT | G_IO_FAILURE;
+				callback = (GSourceFunc) spawn_write_cb;
+			}
+			else
+			{
+				gboolean line_buffered = !(spawn_flags &
+					((SPAWN_STDOUT_UNBUFFERED >> 1) << i));
+
+				condition = G_IO_IN | G_IO_PRI | G_IO_FAILURE;
+				callback = (GSourceFunc) spawn_read_cb;
+
+				if (i == 1)
+				{
+					sc->cb.read = stdout_cb;
+					sc->max_length = stdout_max_length ? stdout_max_length :
+						line_buffered ? 24576 : DEFAULT_IO_LENGTH;
+				}
+				else
+				{
+					sc->cb.read = stderr_cb;
+					sc->max_length = stderr_max_length ? stderr_max_length :
+						line_buffered ? 8192 : DEFAULT_IO_LENGTH;
+				}
+
+				if (line_buffered)
+				{
+					sc->line_buffer = g_string_sized_new(sc->max_length +
+						DEFAULT_IO_LENGTH);
+				}
+			}
+
+			source = g_io_create_watch(sc->channel, condition);
+			g_io_channel_unref(sc->channel);
+
+			if (spawn_flags & (SPAWN_STDIN_RECURSIVE << i))
+				g_source_set_can_recurse(source, TRUE);
+			else if (i)  /* to avoid new string on each call */
+				sc->buffer = g_string_sized_new(sc->max_length);
+
+			g_source_set_callback(source, callback, sc, spawn_destroy_cb);
+			g_source_attach(source, sw->main_context);
+			g_source_unref(source);
+		}
+
+		sw->exit_cb = exit_cb;
+		sw->exit_data = exit_data;
+		source = g_child_watch_source_new(pid);
+		g_source_set_callback(source, (GSourceFunc) spawn_watch_cb, sw, NULL);
+		g_source_attach(source, sw->main_context);
+		g_source_unref(source);
+
+		if (spawn_flags & SPAWN_SYNC)
+		{
+			sw->main_loop = g_main_loop_new(sw->main_context, FALSE);
+			g_main_context_unref(sw->main_context);
+			g_main_loop_run(sw->main_loop);
+		}
+
+		return TRUE;
+	}
+
+	return FALSE;
+}
+
+
+/**
+ *  Writes (a portion of) the data pointed by @a data->ptr to the @a channel.
+ *
+ *  If @c G_IO_OUT in @a condition is set, and the @a data->size is > 0, attempts to write
+ *  @a data->ptr (or a portion of it, depending on the size) to the @a channel. On success,
+ *  increases ptr and decreases size with the number of characters written.
+ *
+ *  This function may converted to @c GIOFunc and passed to @c spawn_with_callbacks() as
+ *  @c stdin_cb, together with a @c SpawnWriteData for @c stdin_data. As with any other
+ *  callback data, make sure that @c stdin_data exists while the child is being executed.
+ *  (For example, on asynchronous execution, you can allocate the data in the heap, and free
+ *  it in your @c spawn_with_callbacks() @c exit_cb callback.)
+ *
+ *  @return @c TRUE if the remaining size is > 0 and @a condition does not indicate any error,
+ *  @c FALSE otherwise.
+ *
+ *  @since 1.25
+ **/
+GEANY_API_SYMBOL
+gboolean spawn_write_data(GIOChannel *channel, GIOCondition condition, SpawnWriteData *data)
+{
+	if ((condition & G_IO_OUT) && data->size)
+	{
+		gsize chars_written = 0;
+
+		g_io_channel_write_chars(channel, data->ptr, data->size < DEFAULT_IO_LENGTH ?
+			data->size : DEFAULT_IO_LENGTH, &chars_written, NULL);
+
+		/* "This can be nonzero even if the return value is not G_IO_STATUS_NORMAL." */
+		if (chars_written)
+		{
+			data->ptr += chars_written;
+			data->size -= chars_written;
+		}
+	}
+
+	return data->size > 0 && !(condition & G_IO_FAILURE);
+}
+
+
+static void spawn_append_gstring_cb(GString *string, GIOCondition condition, gpointer data)
+{
+	if (condition & (G_IO_IN | G_IO_PRI))
+		g_string_append_len((GString *) data, string->str, string->len);
+}
+
+
+/**
+ *  Convinience @c GChildWatchFunc callback that copies the child exit status into a gint
+ *  pointed by @a exit_status.
+ *
+ *  @since 1.25
+ **/
+GEANY_API_SYMBOL
+void spawn_get_exit_status_cb(G_GNUC_UNUSED GPid pid, gint status, gpointer exit_status)
+{
+	*(gint *) exit_status = status;
+}
+
+
+/**
+ *  Executes a child synchronously.
+ *
+ *  A command line or an argument vector must be passed. If both are present, the argument
+ *  vector is appended to the command line. An empty command line is not allowed.
+ *
+ *  The @a stdin_data is sent to the child with @c spawn_write_data().
+ *
+ *  All output from the child, including the nul characters, is stored in @a stdout_data and
+ *  @a stderr_data (if non-NULL). Any existing data in these strings will be erased.
+ *
+ *  @param working_directory child's current working directory, or @c NULL.
+ *  @param command_line child program and arguments, or @c NULL.
+ *  @param argv child's argument vector, or @c NULL.
+ *  @param envp child's environment, or @c NULL.
+ *  @param stdin_data data to send to childs's stdin, or @c NULL.
+ *  @param stdout_data GString location to receive the child's stdout, or NULL.
+ *  @param stderr_data GString location to receive the child's stderr, or NULL.
+ *  @param exit_status return location for the child exit code, or NULL.
+ *  @param error return location for error.
+ *
+ *  @return @c TRUE on success, @c FALSE on error.
+ *
+ *  @since 1.25
+ **/
+GEANY_API_SYMBOL
+gboolean spawn_sync(const gchar *working_directory, const gchar *command_line, gchar **argv,
+	gchar **envp, SpawnWriteData *stdin_data, GString *stdout_data, GString *stderr_data,
+	gint *exit_status, GError **error)
+{
+	g_string_truncate(stdout_data, 0);
+	g_string_truncate(stderr_data, 0);
+
+	return spawn_with_callbacks(working_directory, command_line, argv, envp, SPAWN_SYNC |
+		SPAWN_UNBUFFERED, stdin_data ? (GIOFunc) spawn_write_data : NULL, stdin_data,
+		stdout_data ? spawn_append_gstring_cb : NULL, stdout_data, 0,
+		stderr_data ? spawn_append_gstring_cb : NULL, stderr_data, 0,
+		exit_status ? spawn_get_exit_status_cb : NULL, exit_status, NULL, error);
+}
+
+
+/* tests, not part of the API */
+#ifdef SPAWN_TEST
+#include <stdio.h>
+
+
+static gboolean read_line(const char *prompt, char *buffer, size_t size)
+{
+	fputs(prompt, stderr);
+	*buffer = '\0';
+
+	if (fgets(buffer, size, stdin))
+	{
+		char *s = strchr(buffer, '\n');
+
+		if (s)
+			*s = '\0';
+	}
+
+	return *buffer;
+}
+
+
+static GString *read_string(const char *prompt)
+{
+	char buffer[0x1000];  /* larger portions for spawn < file */
+	GString *string = g_string_sized_new(sizeof buffer);
+
+	while (read_line(prompt, buffer, sizeof buffer))
+	{
+		if (string->len)
+			g_string_append_c(string, '\n');
+
+		g_string_append(string, buffer);
+	}
+
+	if (!string->len)
+	{
+		g_string_free(string, TRUE);
+		string = NULL;
+	}
+
+	return string;
+}
+
+
+static void print_cb(GString *string, GIOCondition condition, gpointer data)
+{
+	if (condition & (G_IO_IN | G_IO_PRI))
+	{
+		gsize i;
+
+		printf("%s: ", (const gchar *) data);
+		/*fputs(string->str, stdout);*/
+		for (i = 0; i < string->len; i++)
+		{
+			unsigned char c = (unsigned char) string->str[i];
+			printf(c >= ' ' && c < 0x80 ? "%c" : "\\x%02x", c);
+		}
+		putchar('\n');
+	}
+}
+
+
+static void print_status(gint status)
+{
+	fputs("finished, ", stderr);
+
+	if (WIFEXITED(status))
+		fprintf(stderr, "exit code %d\n", WEXITSTATUS(status));
+	else
+		fputs("abnormal termination\n", stderr);
+}
+
+
+static void exit_cb(GPid pid, gint status, G_GNUC_UNUSED gpointer data)
+{
+	fprintf(stderr, "process %u ", (guint) pid);
+	print_status(status);
+}
+
+
+static void watch_cb(GPid pid, gint status, gpointer data)
+{
+	g_spawn_close_pid(pid);
+	exit_cb(pid, status, NULL);
+	g_main_loop_quit((GMainLoop *) data);
+}
+
+
+int main(int argc, char **argv)
+{
+	char *test_type;
+
+	if (argc != 2)
+	{
+		fputs("usage: spawn <test-type>\n", stderr);
+		return 1;
+	}
+
+	test_type = argv[1];
+
+	if (!strcmp(test_type, "syntax") || !strcmp(test_type, "syntexec"))
+	{
+		char command_line[0x100];
+
+		while (read_line("command line: ", command_line, sizeof command_line))
+		{
+			GError *error = NULL;
+
+			if (spawn_check_command(command_line, argv[1][4] == 'e', &error))
+				fputs("valid\n", stderr);
+			else
+			{
+				fprintf(stderr, "error: %s\n", error->message);
+				g_error_free(error);
+			}
+		}
+	}
+	else if (!strcmp(test_type, "execute"))
+	{
+		char command_line[0x100];
+
+		while (read_line("command line: ", command_line, sizeof command_line))
+		{
+			char working_directory[0x100];
+			char args[4][0x100];
+			char envs[4][0x100];
+			char *argv[] = { args[0], args[1], args[2], args[3], NULL };
+			char *envp[] = { envs[0], envs[1], envs[2], envs[3], NULL };
+			int i;
+			GPid pid;
+			GError *error = NULL;
+
+			read_line("working directory: ", working_directory, sizeof working_directory);
+
+			fputs("up to 4 arguments\n", stderr);
+			for (i = 0; i < 4 && read_line("argument: ", args[i], sizeof args[i]); i++);
+			argv[i] = NULL;
+
+			fputs("up to 4 variables, or empty line for parent environment\n", stderr);
+			for (i = 0; i < 4 && read_line("variable: ", envs[i], sizeof envs[i]); i++);
+			envp[i] = NULL;
+
+			if (spawn_async_with_pipes(*working_directory ? working_directory : NULL,
+				*command_line ? command_line : NULL, argv, i ? envp : NULL, &pid, NULL,
+				NULL, NULL, &error))
+			{
+				GMainLoop *loop = g_main_loop_new(NULL, TRUE);
+
+				g_child_watch_add(pid, watch_cb, loop);
+				g_main_loop_run(loop);
+				g_main_loop_unref(loop);
+			}
+			else
+			{
+				fprintf(stderr, "error: %s\n", error->message);
+				g_error_free(error);
+			}
+		}
+	}
+	else if (!strcmp(test_type, "redirect") || !strcmp(test_type, "redinput"))
+	{
+		char command_line[0x100];
+		gboolean output = test_type[4] == 'r';
+
+		while (read_line("command line: ", command_line, sizeof command_line))
+		{
+			GString *stdin_text = read_string("text to send: ");
+			SpawnWriteData stdin_data;
+			GError *error = NULL;
+
+			if (stdin_text)
+			{
+				stdin_data.ptr = stdin_text->str;
+				stdin_data.size = stdin_text->len;
+			}
+
+			if (!spawn_with_callbacks(NULL, command_line, NULL, NULL, SPAWN_SYNC,
+				stdin_text ? (GIOFunc) spawn_write_data : NULL, &stdin_data,
+				output ? print_cb : NULL, "stdout", 0, output ? print_cb : NULL,
+				"stderr", 0, exit_cb, NULL, NULL, &error))
+			{
+				fprintf(stderr, "error: %s\n", error->message);
+				g_error_free(error);
+			}
+
+			if (stdin_text)
+				g_string_free(stdin_text, TRUE);
+		}
+	}
+	else if (!strcmp(test_type, "capture"))
+	{
+		char command_line[0x100];
+
+		while (read_line("command line: ", command_line, sizeof command_line))
+		{
+			GString *stdin_text = read_string("text to send: ");
+			SpawnWriteData stdin_data = { NULL, 0 };
+			GString *stdout_data = g_string_sized_new(0x10000);  /* may grow */
+			GString *stderr_data = g_string_sized_new(0x1000);   /* may grow */
+			gint exit_status;
+			GError *error = NULL;
+
+			if (stdin_text)
+			{
+				stdin_data.ptr = stdin_text->str;
+				stdin_data.size = stdin_text->len;
+			}
+
+			if (spawn_sync(NULL, command_line, NULL, NULL, &stdin_data, stdout_data,
+				stderr_data, &exit_status, &error))
+			{
+				printf("stdout: %s\n", stdout_data->str);
+				printf("stderr: %s\n", stderr_data->str);
+				print_status(exit_status);
+			}
+			else
+			{
+				fprintf(stderr, "error: %s\n", error->message);
+				g_error_free(error);
+			}
+
+			if (stdin_text)
+				g_string_free(stdin_text, TRUE);
+
+			g_string_free(stdout_data, TRUE);
+			g_string_free(stderr_data, TRUE);
+		}
+	}
+	else
+	{
+		fprintf(stderr, "spawn: unknown test type '%s'", argv[1]);
+		return 1;
+	}
+
+	return 0;
+}
+#endif  /* SPAWN_TEST */


Modified: src/spawn.h
107 lines changed, 107 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,107 @@
+/*
+ *      spawn.h - this file is part of Geany, a fast and lightweight IDE
+ *
+ *      Copyright 2013 Dimitar Toshkov Zhekov <dimitar(dot)zhekov(at)gmail(dot)com>
+ *
+ *      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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+
+#ifndef GEANY_SPAWN_H
+#define GEANY_SPAWN_H 1
+
+#include <glib.h>
+
+#ifdef G_OS_WIN32
+# define WIFEXITED(status) TRUE
+# define WEXITSTATUS(status) (status)
+# define WIFSIGNALLED(status) FALSE
+#else
+# include <sys/types.h>
+# include <sys/wait.h>
+#endif
+
+gchar *spawn_get_program_name(const gchar *command_line, GError **error);
+
+gboolean spawn_check_command(const gchar *command_line, gboolean execute, GError **error);
+
+gboolean spawn_kill_process(GPid pid, GError **error);
+
+gboolean spawn_async_with_pipes(const gchar *working_directory, const gchar *command_line,
+	gchar **argv, gchar **envp, GPid *child_pid, gint *stdin_fd, gint *stdout_fd,
+	gint *stderr_fd, GError **error);
+
+gboolean spawn_async(const gchar *working_directory, const gchar *command_line, gchar **argv,
+	gchar **envp, GPid *child_pid, GError **error);
+
+/** Flags passed to @c spawn_with_callbacks(), which see. */
+typedef enum
+{
+	SPAWN_ASYNC                = 0x00,  /**< Asynchronous execution [default]. */
+	SPAWN_SYNC                 = 0x01,  /**< Synchronous execution. */
+	/* buffering modes */
+	SPAWN_LINE_BUFFERED        = 0x00,  /**< stdout/stderr are line buffered [default]. */
+	SPAWN_STDOUT_UNBUFFERED    = 0x02,  /**< stdout is not buffered. */
+	SPAWN_STDERR_UNBUFFERED    = 0x04,  /**< stderr is not buffered. */
+	SPAWN_UNBUFFERED           = 0x06,  /**< stdout/stderr are not buffered. */
+	/* recursive modes */
+	SPAWN_STDIN_RECURSIVE      = 0x08,  /**< The stdin callback is recursive. */
+	SPAWN_STDOUT_RECURSIVE     = 0x10,  /**< The stdout callback is recursive. */
+	SPAWN_STDERR_RECURSIVE     = 0x20,  /**< The stderr callback is recursive. */
+	SPAWN_RECURSIVE            = 0x38   /**< All callbacks are recursive. */
+} SpawnFlags;
+
+/**
+ *  Specifies the type of function passed to @c spawn_with_callbacks() as stdout or stderr
+ *  callback.
+ *
+ *  In unbuffered mode, the @a string may contain nuls, while in line buffered mode, it may
+ *  contain only a single nul as a line termination character at @a string->len - 1. In all
+ *  cases, the @a string will be terminated with a nul character that is not part of the data
+ *  at @a string->len.
+ *
+ *  If @c G_IO_IN or @c G_IO_PRI are set, the @a string will contain at least one character.
+ *
+ *  @param string contains the child data if @c G_IO_IN or @c G_IO_PRI are set.
+ *  @param condition the I/O condition which has been satisfied.
+ *  @param data the passed to @c spawn_with_callbacks() with the callback.
+ */
+typedef void (*SpawnReadFunc)(GString *string, GIOCondition condition, gpointer data);
+
+gboolean spawn_with_callbacks(const gchar *working_directory, const gchar *command_line,
+	gchar **argv, gchar **envp, SpawnFlags spawn_flags, GIOFunc stdin_cb, gpointer stdin_data,
+	SpawnReadFunc stdout_cb, gpointer stdout_data, gsize stdout_max_length,
+	SpawnReadFunc stderr_cb, gpointer stderr_data, gsize stderr_max_length,
+	GChildWatchFunc exit_cb, gpointer exit_data, GPid *child_pid, GError **error);
+
+/** 
+ *  A simple structure used by @c spawn_write_data() to write data to a channel.
+ *  See @c spawn_write_data() for more information.
+ */
+typedef struct _SpawnWriteData
+{
+	const gchar *ptr;   /**< Pointer to the data. May be NULL if the size is 0. */
+	gsize size;         /**< Size of the data. */
+} SpawnWriteData;
+
+gboolean spawn_write_data(GIOChannel *channel, GIOCondition condition, SpawnWriteData *data);
+
+void spawn_get_exit_status_cb(GPid pid, gint status, gpointer exit_status);
+
+gboolean spawn_sync(const gchar *working_directory, const gchar *command_line, gchar **argv,
+	gchar **envp, SpawnWriteData *stdin_data, GString *stdout_data, GString *stderr_data,
+	gint *exit_status, GError **error);
+
+#endif  /* GEANY_SPAWN_H */


Modified: src/templates.c
44 lines changed, 22 insertions(+), 22 deletions(-)
===================================================================
@@ -37,6 +37,7 @@
 #include "geany.h"
 #include "geanymenubuttonaction.h"
 #include "geanyobject.h"
+#include "spawn.h"
 #include "support.h"
 #include "toolbar.h"
 #include "ui_utils.h"
@@ -595,33 +596,32 @@ static void templates_replace_default_dates(GString *text)
 static gchar *run_command(const gchar *command, const gchar *file_name,
 						  const gchar *file_type, const gchar *func_name)
 {
+	GString *output = g_string_new(NULL);
 	gchar *result = NULL;
-	gchar **argv;
+	GError *error = NULL;
+	gchar **env;
 
-	if (g_shell_parse_argv(command, NULL, &argv, NULL))
-	{
-		GError *error = NULL;
-		gchar **env;
+	file_name = (file_name != NULL) ? file_name : "";
+	file_type = (file_type != NULL) ? file_type : "";
+	func_name = (func_name != NULL) ? func_name : "";
 
-		file_name = (file_name != NULL) ? file_name : "";
-		file_type = (file_type != NULL) ? file_type : "";
-		func_name = (func_name != NULL) ? func_name : "";
+	env = utils_copy_environment(NULL,
+		"GEANY_FILENAME", file_name,
+		"GEANY_FILETYPE", file_type,
+		"GEANY_FUNCNAME", func_name,
+		NULL);
 
-		env = utils_copy_environment(NULL,
-			"GEANY_FILENAME", file_name,
-			"GEANY_FILETYPE", file_type,
-			"GEANY_FUNCNAME", func_name,
-			NULL);
-		if (! utils_spawn_sync(NULL, argv, env, G_SPAWN_SEARCH_PATH,
-				NULL, NULL, &result, NULL, NULL, &error))
-		{
-			g_warning("templates_replace_command: %s", error->message);
-			g_error_free(error);
-			result = NULL;
-		}
-		g_strfreev(argv);
-		g_strfreev(env);
+	if (spawn_sync(NULL, command, NULL, env, NULL, output, NULL, NULL, &error))
+	{
+		result = g_string_free(output, FALSE);
 	}
+	else
+	{
+		g_warning("templates_replace_command: %s", error->message);
+		g_error_free(error);
+	}
+
+	g_strfreev(env);
 	return result;
 }
 


Modified: src/tools.c
264 lines changed, 36 insertions(+), 228 deletions(-)
===================================================================
@@ -33,6 +33,7 @@
 #include "document.h"
 #include "keybindings.h"
 #include "sciwrappers.h"
+#include "spawn.h"
 #include "support.h"
 #include "ui_utils.h"
 #include "utils.h"
@@ -45,12 +46,6 @@
 #include <string.h>
 #include <errno.h>
 
-#ifdef G_OS_UNIX
-# include <sys/types.h>
-# include <sys/wait.h>
-# include <signal.h>
-#endif
-
 
 enum
 {
@@ -76,26 +71,6 @@ struct cc_dialog
 	GtkWidget *button_down;
 };
 
-/* data required by the custom command callbacks */
-struct cc_data
-{
-	const gchar *command;	/* command launched */
-	GeanyDocument *doc;		/* document in which replace the selection */
-	GString *buffer;		/* buffer holding stdout content, or NULL */
-	gboolean error;			/* whether and error occurred */
-	gboolean finished;		/* whether the command has finished */
-};
-
-
-static gboolean cc_exists_command(const gchar *command)
-{
-	gchar *path = g_find_program_in_path(command);
-
-	g_free(path);
-
-	return path != NULL;
-}
-
 
 /* update STATUS and TOOLTIP columns according to cmd */
 static void cc_dialog_update_row_status(GtkListStore *store, GtkTreeIter *iter, const gchar *cmd)
@@ -103,19 +78,9 @@ static void cc_dialog_update_row_status(GtkListStore *store, GtkTreeIter *iter,
 	GError *err = NULL;
 	const gchar *stock_id = GTK_STOCK_NO;
 	gchar *tooltip = NULL;
-	gint argc;
-	gchar **argv;
 
-	if (EMPTY(cmd))
+	if (EMPTY(cmd) || spawn_check_command(cmd, TRUE, &err))
 		stock_id = GTK_STOCK_YES;
-	else if (g_shell_parse_argv(cmd, &argc, &argv, &err))
-	{
-		if (argc > 0 && cc_exists_command(argv[0]))
-			stock_id = GTK_STOCK_YES;
-		else
-			tooltip = g_strdup_printf(_("Invalid command: %s"), _("Command not found"));
-		g_strfreev(argv);
-	}
 	else
 	{
 		tooltip = g_strdup_printf(_("Invalid command: %s"), err->message);
@@ -227,218 +192,61 @@ static void cc_on_dialog_move_down_clicked(GtkButton *button, struct cc_dialog *
 }
 
 
-static gboolean cc_iofunc(GIOChannel *ioc, GIOCondition cond, gpointer user_data)
+/* Executes command (which should include all necessary command line args) and passes the current
+ * selection through the standard input of command. The whole output of command replaces the
+ * current selection. */
+void tools_execute_custom_command(GeanyDocument *doc, const gchar *command)
 {
-	struct cc_data *data = user_data;
-
-	if (cond & (G_IO_IN | G_IO_PRI))
-	{
-		gchar *msg = NULL;
-		GIOStatus rv;
-		GError *err = NULL;
-
-		if (! data->buffer)
-			data->buffer = g_string_sized_new(256);
-
-		do
-		{
-			rv = g_io_channel_read_line(ioc, &msg, NULL, NULL, &err);
-			if (msg != NULL)
-			{
-				g_string_append(data->buffer, msg);
-				g_free(msg);
-			}
-			if (G_UNLIKELY(err != NULL))
-			{
-				geany_debug("%s: %s", G_STRFUNC, err->message);
-				g_error_free(err);
-				err = NULL;
-			}
-		} while (rv == G_IO_STATUS_NORMAL || rv == G_IO_STATUS_AGAIN);
-
-		if (G_UNLIKELY(rv != G_IO_STATUS_EOF))
-		{	/* Something went wrong? */
-			g_warning("%s: %s\n", G_STRFUNC, "Incomplete command output");
-		}
-	}
-	return FALSE;
-}
-
+	GError *error = NULL;
+	gchar *sel;
+	SpawnWriteData input;
+	GString *output;
+	GString *errors;
+	gint status;
+
+	g_return_if_fail(doc != NULL && command != NULL);
+ 
+	if (! sci_has_selection(doc->editor->sci))
+		editor_select_lines(doc->editor, FALSE);
 
-static gboolean cc_iofunc_err(GIOChannel *ioc, GIOCondition cond, gpointer user_data)
-{
-	struct cc_data *data = user_data;
+	sel = sci_get_selection_contents(doc->editor->sci);
+	input.ptr = sel;
+	input.size = strlen(sel);
+	output = g_string_sized_new(256);
+	errors = g_string_new(NULL);
+	ui_set_statusbar(TRUE, _("Passing data and executing custom command: %s"), command);
 
-	if (cond & (G_IO_IN | G_IO_PRI))
+	if (spawn_sync(NULL, command, NULL, NULL, &input, output, errors, &status, &error))
 	{
-		gchar *msg = NULL;
-		GString *str = g_string_sized_new(256);
-		GIOStatus rv;
-
-		do
-		{
-			rv = g_io_channel_read_line(ioc, &msg, NULL, NULL, NULL);
-			if (msg != NULL)
-			{
-				g_string_append(str, msg);
-				g_free(msg);
-			}
-		} while (rv == G_IO_STATUS_NORMAL || rv == G_IO_STATUS_AGAIN);
-
-		if (!EMPTY(str->str))
+		if (errors->len > 0)
 		{
-			g_warning("%s: %s\n", data->command, str->str);
+			g_warning("%s: %s\n", command, errors->str);
 			ui_set_statusbar(TRUE,
 				_("The executed custom command returned an error. "
 				"Your selection was not changed. Error message: %s"),
-				str->str);
-			data->error = TRUE;
-
+				errors->str);
 		}
-		g_string_free(str, TRUE);
-	}
-	data->finished = TRUE;
-	return FALSE;
-}
-
-
-static gboolean cc_replace_sel_cb(gpointer user_data)
-{
-	struct cc_data *data = user_data;
-
-	if (! data->finished)
-	{	/* keep this function in the main loop until cc_iofunc_err() has finished */
-		return TRUE;
-	}
-
-	if (! data->error && data->buffer != NULL && DOC_VALID(data->doc))
-	{	/* Command completed successfully */
-		sci_replace_sel(data->doc->editor->sci, data->buffer->str);
-	}
-
-	if (data->buffer)
-		g_string_free(data->buffer, TRUE);
-	g_slice_free1(sizeof *data, data);
-
-	return FALSE;
-}
-
-
-/* check whether the executed command failed and if so do nothing.
- * If it returned with a successful exit code, replace the selection. */
-static void cc_exit_cb(GPid child_pid, gint status, gpointer user_data)
-{
-	struct cc_data *data = user_data;
-
-	/* if there was already an error, skip further checks */
-	if (! data->error)
-	{
-#ifdef G_OS_UNIX
-		if (WIFEXITED(status))
+		else if (!WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS)
 		{
-			if (WEXITSTATUS(status) != EXIT_SUCCESS)
-				data->error = TRUE;
-		}
-		else if (WIFSIGNALED(status))
-		{	/* the terminating signal: WTERMSIG (status)); */
-			data->error = TRUE;
-		}
-		else
-		{	/* any other failure occurred */
-			data->error = TRUE;
-		}
-#else
-		data->error = ! win32_get_exit_status(child_pid);
-#endif
-
-		if (data->error)
-		{	/* here we are sure data->error was set due to an unsuccessful exit code
-			 * and so we add an error message */
 			/* TODO maybe include the exit code in the error message */
 			ui_set_statusbar(TRUE,
 				_("The executed custom command exited with an unsuccessful exit code."));
 		}
-	}
-
-	g_idle_add(cc_replace_sel_cb, data);
-	g_spawn_close_pid(child_pid);
-}
-
-
-/* Executes command (which should include all necessary command line args) and passes the current
- * selection through the standard input of command. The whole output of command replaces the
- * current selection. */
-void tools_execute_custom_command(GeanyDocument *doc, const gchar *command)
-{
-	GError *error = NULL;
-	GPid pid;
-	gchar **argv;
-	gint stdin_fd;
-	gint stdout_fd;
-	gint stderr_fd;
-
-	g_return_if_fail(DOC_VALID(doc) && command != NULL);
-
-	if (! sci_has_selection(doc->editor->sci))
-		editor_select_lines(doc->editor, FALSE);
-
-	if (!g_shell_parse_argv(command, NULL, &argv, &error))
-	{
-		ui_set_statusbar(TRUE, _("Custom command failed: %s"), error->message);
-		g_error_free(error);
-		return;
-	}
-	ui_set_statusbar(TRUE, _("Passing data and executing custom command: %s"), command);
-
-	if (g_spawn_async_with_pipes(NULL, argv, NULL, G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD,
-						NULL, NULL, &pid, &stdin_fd, &stdout_fd, &stderr_fd, &error))
-	{
-		gchar *sel;
-		gint remaining, wrote;
-		struct cc_data *data = g_slice_alloc(sizeof *data);
-
-		data->error = FALSE;
-		data->finished = FALSE;
-		data->buffer = NULL;
-		data->doc = doc;
-		data->command = command;
-
-		g_child_watch_add(pid, cc_exit_cb, data);
-
-		/* use GIOChannel to monitor stdout */
-		utils_set_up_io_channel(stdout_fd, G_IO_IN | G_IO_PRI | G_IO_ERR | G_IO_HUP | G_IO_NVAL,
-				FALSE, cc_iofunc, data);
-		/* copy program's stderr to Geany's stdout to help error tracking */
-		utils_set_up_io_channel(stderr_fd, G_IO_IN | G_IO_PRI | G_IO_ERR | G_IO_HUP | G_IO_NVAL,
-				FALSE, cc_iofunc_err, data);
-
-		/* get selection */
-		sel = sci_get_selection_contents(doc->editor->sci);
-
-		/* write data to the command */
-		remaining = strlen(sel);
-		do
-		{
-			wrote = write(stdin_fd, sel, remaining);
-			if (G_UNLIKELY(wrote < 0))
-			{
-				g_warning("%s: %s: %s\n", G_STRFUNC, "Failed sending data to command",
-										g_strerror(errno));
-				break;
-			}
-			remaining -= wrote;
-		} while (remaining > 0);
-		close(stdin_fd);
-		g_free(sel);
+		else
+		{   /* Command completed successfully */
+			sci_replace_sel(doc->editor->sci, output->str);
+		}
 	}
 	else
 	{
-		geany_debug("g_spawn_async_with_pipes() failed: %s", error->message);
+		geany_debug("spawn_sync() failed: %s", error->message);
 		ui_set_statusbar(TRUE, _("Custom command failed: %s"), error->message);
 		g_error_free(error);
 	}
 
-	g_strfreev(argv);
+	g_string_free(output, TRUE);
+	g_string_free(errors, TRUE);
+	g_free(sel);
 }
 
 


Modified: src/utils.c
80 lines changed, 24 insertions(+), 56 deletions(-)
===================================================================
@@ -34,6 +34,7 @@
 #include "document.h"
 #include "prefs.h"
 #include "sciwrappers.h"
+#include "spawn.h"
 #include "support.h"
 #include "templates.h"
 #include "ui_utils.h"
@@ -76,29 +77,21 @@ void utils_open_browser(const gchar *uri)
 	g_return_if_fail(uri != NULL);
 	win32_open_browser(uri);
 #else
-	gboolean again = TRUE;
+	gchar *argv[2] = { (gchar *) uri, NULL };
 
 	g_return_if_fail(uri != NULL);
 
-	while (again)
+	while (!spawn_async(NULL, tool_prefs.browser_cmd, argv, NULL, NULL, NULL))
 	{
-		gchar *cmdline = g_strconcat(tool_prefs.browser_cmd, " \"", uri, "\"", NULL);
+		gchar *new_cmd = dialogs_show_input(_("Select Browser"), GTK_WINDOW(main_widgets.window),
+			_("Failed to spawn the configured browser command. "
+			  "Please correct it or enter another one."),
+			tool_prefs.browser_cmd);
 
-		if (g_spawn_command_line_async(cmdline, NULL))
-			again = FALSE;
-		else
-		{
-			gchar *new_cmd = dialogs_show_input(_("Select Browser"), GTK_WINDOW(main_widgets.window),
-				_("Failed to spawn the configured browser command. "
-				  "Please correct it or enter another one."),
-				tool_prefs.browser_cmd);
+		if (new_cmd == NULL) /* user canceled */
+			break;
 
-			if (new_cmd == NULL) /* user canceled */
-				again = FALSE;
-			else
-				SETPTR(tool_prefs.browser_cmd, new_cmd);
-		}
-		g_free(cmdline);
+		SETPTR(tool_prefs.browser_cmd, new_cmd);
 	}
 #endif
 }
@@ -1651,15 +1644,14 @@ const gchar *utils_get_default_dir_utf8(void)
 
 
 /**
- *  Wraps g_spawn_sync() and internally calls this function on Unix-like
- *  systems. On Win32 platforms, it uses the Windows API.
+ *  Wraps @c spawn_sync(), which see.
  *
  *  @param dir The child's current working directory, or @a NULL to inherit parent's.
  *  @param argv The child's argument vector.
  *  @param env The child's environment, or @a NULL to inherit parent's.
- *  @param flags Flags from GSpawnFlags.
- *  @param child_setup A function to run in the child just before exec().
- *  @param user_data The user data for child_setup.
+ *  @param flags Ignored.
+ *  @param child_setup Ignored.
+ *  @param user_data Ignored.
  *  @param std_out The return location for child output, or @a NULL.
  *  @param std_err The return location for child error messages, or @a NULL.
  *  @param exit_status The child exit status, as returned by waitpid(), or @a NULL.
@@ -1672,40 +1664,29 @@ gboolean utils_spawn_sync(const gchar *dir, gchar **argv, gchar **env, GSpawnFla
 						  GSpawnChildSetupFunc child_setup, gpointer user_data, gchar **std_out,
 						  gchar **std_err, gint *exit_status, GError **error)
 {
-	gboolean result;
-
-	if (argv == NULL)
-	{
-		g_set_error(error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED, "argv must not be NULL");
-		return FALSE;
-	}
+	GString *output = std_out ? g_string_new(NULL) : NULL;
+	GString *errors = std_err ? g_string_new(NULL) : NULL;
+	gboolean result = spawn_sync(dir, NULL, argv, env, NULL, output, errors, exit_status, error);
 
 	if (std_out)
-		*std_out = NULL;
+		*std_out = g_string_free(output, !result);
 
 	if (std_err)
-		*std_err = NULL;
-
-#ifdef G_OS_WIN32
-	result = win32_spawn(dir, argv, env, flags, std_out, std_err, exit_status, error);
-#else
-	result = g_spawn_sync(dir, argv, env, flags, NULL, NULL, std_out, std_err, exit_status, error);
-#endif
+		*std_err = g_string_free(errors, !result);
 
 	return result;
 }
 
 
 /**
- *  Wraps g_spawn_async() and internally calls this function on Unix-like
- *  systems. On Win32 platforms, it uses the Windows API.
+ *  Wraps @c spawn_async(), which see.
  *
  *  @param dir The child's current working directory, or @a NULL to inherit parent's.
  *  @param argv The child's argument vector.
  *  @param env The child's environment, or @a NULL to inherit parent's.
- *  @param flags Flags from GSpawnFlags.
- *  @param child_setup A function to run in the child just before exec().
- *  @param user_data The user data for child_setup.
+ *  @param flags Ignored.
+ *  @param child_setup Ignored.
+ *  @param user_data Ignored.
  *  @param child_pid The return location for child process ID, or NULL.
  *  @param error The return location for error or @a NULL.
  *
@@ -1716,20 +1697,7 @@ gboolean utils_spawn_async(const gchar *dir, gchar **argv, gchar **env, GSpawnFl
 						   GSpawnChildSetupFunc child_setup, gpointer user_data, GPid *child_pid,
 						   GError **error)
 {
-	gboolean result;
-
-	if (argv == NULL)
-	{
-		g_set_error(error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED, "argv must not be NULL");
-		return FALSE;
-	}
-
-#ifdef G_OS_WIN32
-	result = win32_spawn(dir, argv, env, flags, NULL, NULL, NULL, error);
-#else
-	result = g_spawn_async(dir, argv, env, flags, NULL, NULL, child_pid, error);
-#endif
-	return result;
+	return spawn_async(dir, NULL, argv, env, child_pid, error);
 }
 
 


Modified: src/win32.c
493 lines changed, 0 insertions(+), 493 deletions(-)
===================================================================
@@ -57,31 +57,6 @@
 #include <glib/gstdio.h>
 #include <gdk/gdkwin32.h>
 
-#define BUFSIZE 4096
-#define CMDSIZE 32768
-
-struct _geany_win32_spawn
-{
-	HANDLE hChildStdinRd;
-	HANDLE hChildStdinWr;
-	HANDLE hChildStdoutRd;
-	HANDLE hChildStdoutWr;
-	HANDLE hChildStderrRd;
-	HANDLE hChildStderrWr;
-	HANDLE hInputFile;
-	HANDLE hStdout;
-	HANDLE hStderr;
-	HANDLE processId;
-	DWORD dwExitCode;
-};
-typedef struct _geany_win32_spawn geany_win32_spawn;
-
-static gboolean GetContentFromHandle(HANDLE hFile, gchar **content, GError **error);
-static HANDLE GetTempFileHandle(GError **error);
-static gboolean CreateChildProcess(geany_win32_spawn *gw_spawn, TCHAR *szCmdline,
-		const TCHAR *dir, GError **error);
-static VOID ReadFromPipe(HANDLE hRead, HANDLE hWrite, HANDLE hFile, GError **error);
-
 
 /* The timer handle used to refresh windows below modal native dialogs. If
  * ever more than one dialog can be shown at a time, this needs to be changed
@@ -837,17 +812,6 @@ void win32_open_browser(const gchar *uri)
 }
 
 
-/* Returns TRUE if the command, which child_pid refers to, returned with a successful exit code,
- * otherwise FALSE. */
-gboolean win32_get_exit_status(GPid child_pid)
-{
-	DWORD exit_code;
-	GetExitCodeProcess(child_pid, &exit_code);
-
-	return (exit_code == 0);
-}
-
-
 static FILE *open_std_handle(DWORD handle, const char *mode)
 {
 	HANDLE lStdHandle;
@@ -929,299 +893,7 @@ void win32_init_debug_code(void)
 		/* create a console window to get log messages on Windows,
 		 * especially useful when generating tags files */
 		debug_setup_console();
-		/* Enable GLib process spawn debug mode when Geany was started with the debug flag */
-		g_setenv("G_SPAWN_WIN32_DEBUG", "1", FALSE);
-	}
-}
-
-
-static gchar *create_temp_file(void)
-{
-	gchar *name;
-	gint fd;
-
-	fd = g_file_open_tmp("tmp_XXXXXX", &name, NULL);
-	if (fd == -1)
-		name = NULL;
-	else
-		close(fd);
-
-	return name;
-}
-
-
-/* Sometimes this blocks for 30s before aborting when there are several
- * pages of (error) output and sometimes hangs - see the FIXME.
- * Also gw_spawn.dwExitCode seems to be not set properly. */
-/* Process spawning implementation for Windows, by Pierre Joye.
- * Don't call this function directly, use utils_spawn_[a]sync() instead. */
-static
-gboolean _broken_win32_spawn(const gchar *dir, gchar **argv, gchar **env, GSpawnFlags flags,
-					 gchar **std_out, gchar **std_err, gint *exit_status, GError **error)
-{
-	TCHAR  buffer[CMDSIZE]=TEXT("");
-	TCHAR  cmdline[CMDSIZE] = TEXT("");
-	TCHAR* lpPart[CMDSIZE]={NULL};
-	DWORD  retval = 0;
-	gint argc = 0, i;
-	gint cmdpos = 0;
-
-	SECURITY_ATTRIBUTES saAttr;
-	BOOL fSuccess;
-	geany_win32_spawn gw_spawn;
-
-	/* Temp file */
-	HANDLE hStdoutTempFile = NULL;
-	HANDLE hStderrTempFile = NULL;
-
-	gchar *stdout_content = NULL;
-	gchar *stderr_content = NULL;
-
-	while (argv[argc])
-	{
-		++argc;
-	}
-	g_return_val_if_fail (std_out == NULL ||
-						!(flags & G_SPAWN_STDOUT_TO_DEV_NULL), FALSE);
-	g_return_val_if_fail (std_err == NULL ||
-						!(flags & G_SPAWN_STDERR_TO_DEV_NULL), FALSE);
-
-	if (flags & G_SPAWN_SEARCH_PATH)
-	{
-		retval = SearchPath(NULL, argv[0], ".exe", sizeof(buffer), buffer, lpPart);
-		if (retval > 0)
-			g_snprintf(cmdline, sizeof(cmdline), "\"%s\"", buffer);
-		else
-			g_strlcpy(cmdline, argv[0], sizeof(cmdline));
-		cmdpos = 1;
-	}
-
-	for (i = cmdpos; i < argc; i++)
-	{
-		g_snprintf(cmdline, sizeof(cmdline), "%s %s", cmdline, argv[i]);
-		/*MessageBox(NULL, cmdline, cmdline, MB_OK);*/
-	}
-
-	if (std_err != NULL)
-	{
-		hStderrTempFile = GetTempFileHandle(error);
-		if (hStderrTempFile == INVALID_HANDLE_VALUE)
-		{
-			gchar *msg = g_win32_error_message(GetLastError());
-			geany_debug("win32_spawn: Second CreateFile failed (%d)", (gint) GetLastError());
-			g_set_error(error, G_SPAWN_ERROR, G_FILE_ERROR, "%s", msg);
-			g_free(msg);
-			return FALSE;
-		}
-	}
-
-	if (std_out != NULL)
-	{
-		hStdoutTempFile = GetTempFileHandle(error);
-		if (hStdoutTempFile == INVALID_HANDLE_VALUE)
-		{
-			gchar *msg = g_win32_error_message(GetLastError());
-			geany_debug("win32_spawn: Second CreateFile failed (%d)", (gint) GetLastError());
-			g_set_error(error, G_SPAWN_ERROR, G_FILE_ERROR, "%s", msg);
-			g_free(msg);
-			return FALSE;
-		}
-	}
-
-	/* Set the bInheritHandle flag so pipe handles are inherited. */
-	saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
-	saAttr.bInheritHandle = TRUE;
-	saAttr.lpSecurityDescriptor = NULL;
-
-	/* Get the handle to the current STDOUT and STDERR. */
-	gw_spawn.hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
-	gw_spawn.hStderr = GetStdHandle(STD_ERROR_HANDLE);
-	gw_spawn.dwExitCode = 0;
-
-	/* Create a pipe for the child process's STDOUT. */
-	if (! CreatePipe(&(gw_spawn.hChildStdoutRd), &(gw_spawn.hChildStdoutWr), &saAttr, 0))
-	{
-		gchar *msg = g_win32_error_message(GetLastError());
-		geany_debug("win32_spawn: Stdout pipe creation failed (%d)", (gint) GetLastError());
-		g_set_error(error, G_SPAWN_ERROR, G_FILE_ERROR_PIPE, "%s", msg);
-		g_free(msg);
-		return FALSE;
-	}
-
-	/* Ensure that the read handle to the child process's pipe for STDOUT is not inherited.*/
-	SetHandleInformation(gw_spawn.hChildStdoutRd, HANDLE_FLAG_INHERIT, 0);
-
-	/* Create a pipe for the child process's STDERR. */
-	if (! CreatePipe(&(gw_spawn.hChildStderrRd), &(gw_spawn.hChildStderrWr), &saAttr, 0))
-	{
-		gchar *msg = g_win32_error_message(GetLastError());
-		geany_debug("win32_spawn: Stderr pipe creation failed");
-		g_set_error(error, G_SPAWN_ERROR, G_FILE_ERROR_PIPE, "%s", msg);
-		g_free(msg);
-		return FALSE;
-	}
-
-	/* Ensure that the read handle to the child process's pipe for STDOUT is not inherited.*/
-	SetHandleInformation(gw_spawn.hChildStderrRd, HANDLE_FLAG_INHERIT, 0);
-
-	/* Create a pipe for the child process's STDIN.  */
-	if (! CreatePipe(&(gw_spawn.hChildStdinRd), &(gw_spawn.hChildStdinWr), &saAttr, 0))
-	{
-		gchar *msg = g_win32_error_message(GetLastError());
-		geany_debug("win32_spawn: Stdin pipe creation failed");
-		g_set_error(error, G_SPAWN_ERROR, G_FILE_ERROR_PIPE, "%s", msg);
-		g_free(msg);
-		return FALSE;
-	}
-
-	/* Ensure that the write handle to the child process's pipe for STDIN is not inherited. */
-	SetHandleInformation(gw_spawn.hChildStdinWr, HANDLE_FLAG_INHERIT, 0);
-
-	/* Now create the child process. */
-	fSuccess = CreateChildProcess(&gw_spawn, cmdline, dir, error);
-	if (exit_status)
-	{
-		*exit_status = gw_spawn.dwExitCode;
-	}
-
-	if (! fSuccess)
-	{
-		geany_debug("win32_spawn: Create process failed");
-		return FALSE;
-	}
-
-	/* Read from pipe that is the standard output for child process. */
-	if (std_out != NULL)
-	{
-		ReadFromPipe(gw_spawn.hChildStdoutRd, gw_spawn.hChildStdoutWr, hStdoutTempFile, error);
-		if (! GetContentFromHandle(hStdoutTempFile, &stdout_content, error))
-		{
-			return FALSE;
-		}
-		*std_out = stdout_content;
-	}
-
-	if (std_err != NULL)
-	{
-		ReadFromPipe(gw_spawn.hChildStderrRd, gw_spawn.hChildStderrWr, hStderrTempFile, error);
-		if (! GetContentFromHandle(hStderrTempFile, &stderr_content, error))
-		{
-			return FALSE;
-		}
-		*std_err = stderr_content;
-	}
-	return TRUE;
-}
-
-
-/* Note: g_spawn is broken for receiving both stdio and stderr e.g. when
- * running make and there are compile errors. See glib/giowin32.c header
- * comment about Windows bugs, e.g. #338943 */
-/* Simple replacement for _broken_win32_spawn().
- * flags is ignored, G_SPAWN_SEARCH_PATH is implied.
- * Don't call this function directly, use utils_spawn_[a]sync() instead.
- * Adapted from tm_workspace_create_global_tags(). */
-gboolean win32_spawn(const gchar *dir, gchar **argv, gchar **env, GSpawnFlags flags,
-					 gchar **std_out, gchar **std_err, gint *exit_status, GError **error)
-{
-	gint ret;
-	gboolean fail;
-	gchar *tmp_file = create_temp_file();
-	gchar *tmp_errfile = create_temp_file();
-	gchar *command;
-	gchar *locale_command;
-
-	if (env != NULL)
-	{
-		return _broken_win32_spawn(dir, argv, env, flags, std_out, std_err,
-			exit_status, error);
-	}
-	if (!tmp_file || !tmp_errfile)
-	{
-		g_warning("%s: Could not create temporary files!", G_STRFUNC);
-		return FALSE;
-	}
-	command = g_strjoinv(" ", argv);
-	SETPTR(command, g_strdup_printf("cmd.exe /S /C \"%s >%s 2>%s\"",
-		command, tmp_file, tmp_errfile));
-	locale_command = g_locale_from_utf8(command, -1, NULL, NULL, NULL);
-	if (! locale_command)
-		locale_command = g_strdup(command);
-	geany_debug("WIN32: actually running command:\n%s", command);
-	g_chdir(dir);
-	errno = 0;
-	ret = system(locale_command);
-	/* the command can return -1 as an exit code, so check errno also */
-	fail = ret == -1 && errno;
-	if (!fail)
-	{
-		if (std_out != NULL)
-			g_file_get_contents(tmp_file, std_out, NULL, NULL);
-		if (std_err != NULL)
-			g_file_get_contents(tmp_errfile, std_err, NULL, NULL);
-	}
-	else if (error)
-		g_set_error_literal(error, G_SPAWN_ERROR, errno, g_strerror(errno));
-
-	g_free(command);
-	g_free(locale_command);
-	g_unlink(tmp_file);
-	g_free(tmp_file);
-	g_unlink(tmp_errfile);
-	g_free(tmp_errfile);
-	if (exit_status)
-		*exit_status = ret;
-
-	return !fail;
-}
-
-
-static gboolean GetContentFromHandle(HANDLE hFile, gchar **content, GError **error)
-{
-	DWORD filesize;
-	gchar * buffer;
-	DWORD dwRead;
-
-	filesize = GetFileSize(hFile, NULL);
-	if (filesize < 1)
-	{
-		*content = NULL;
-		return TRUE;
-	}
-
-	buffer = g_malloc(filesize + 1);
-	if (! buffer)
-	{
-		gchar *msg = g_win32_error_message(GetLastError());
-		geany_debug("GetContentFromHandle: Alloc failed");
-		g_set_error(error, G_SPAWN_ERROR, G_SPAWN_ERROR, "%s", msg);
-		g_free(msg);
-		return FALSE;
-	}
-
-	SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
-	if (! ReadFile(hFile, buffer, filesize, &dwRead, NULL) || dwRead == 0)
-	{
-		gchar *msg = g_win32_error_message(GetLastError());
-		geany_debug("GetContentFromHandle: Cannot read tempfile");
-		g_set_error(error, G_SPAWN_ERROR, G_FILE_ERROR_FAILED, "%s", msg);
-		g_free(msg);
-		return FALSE;
-	}
-
-	if (! CloseHandle(hFile))
-	{
-		gchar *msg = g_win32_error_message(GetLastError());
-		geany_debug("GetContentFromHandle: CloseHandle failed (%d)", (gint) GetLastError());
-		g_set_error(error, G_SPAWN_ERROR, G_FILE_ERROR_FAILED, "%s", msg);
-		g_free(msg);
-		g_free(buffer);
-		*content = NULL;
-		return FALSE;
 	}
-	buffer[filesize] = '\0';
-	*content = buffer;
-	return TRUE;
 }
 
 
@@ -1236,171 +908,6 @@ gchar *win32_expand_environment_variables(const gchar *str)
 }
 
 
-static gboolean CreateChildProcess(geany_win32_spawn *gw_spawn, TCHAR *szCmdline,
-		const TCHAR *dir, GError **error)
-{
-	PROCESS_INFORMATION piProcInfo;
-	STARTUPINFOW siStartInfo;
-	BOOL bFuncRetn = FALSE;
-	gchar *expandedCmdline;
-	wchar_t w_commandline[CMDSIZE];
-	wchar_t w_dir[MAX_PATH];
-
-	/* Set up members of the PROCESS_INFORMATION structure. */
-	ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION));
-
-	/* Set up members of the STARTUPINFO structure.*/
-	ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
-
-	siStartInfo.cb         = sizeof(STARTUPINFO);
-	siStartInfo.hStdError  = gw_spawn->hChildStderrWr;
-	siStartInfo.hStdOutput = gw_spawn->hChildStdoutWr;
-	siStartInfo.hStdInput  = gw_spawn->hChildStdinRd;
-	siStartInfo.dwFlags   |= STARTF_USESTDHANDLES;
-
-	/* Expand environment variables like %blah%. */
-	expandedCmdline = win32_expand_environment_variables(szCmdline);
-
-	MultiByteToWideChar(CP_UTF8, 0, expandedCmdline, -1, w_commandline, G_N_ELEMENTS(w_commandline));
-	MultiByteToWideChar(CP_UTF8, 0, dir, -1, w_dir, G_N_ELEMENTS(w_dir));
-
-	/* Create the child process. */
-	bFuncRetn = CreateProcessW(NULL,
-		w_commandline,             /* command line */
-		NULL,          /* process sec@@ Diff output truncated at 100000 characters. @@

--------------
This E-Mail was brought to you by github_commit_mail.py (Source: https://github.com/geany/infrastructure).


More information about the Commits mailing list