/*
 * Spawning processes.
 *
 * Author:
 *   Gonzalo Paniagua Javier (gonzalo@novell.com
 *
 * (C) 2006 Novell, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>

#include <glib.h>

#ifdef HAVE_UNISTD_H
#ifndef __USE_GNU
#define __USE_GNU
#endif
#include <unistd.h>
#endif

#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif

#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif

#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif

#ifdef HAVE_SYS_RESOURCE_H
#  include <sys/resource.h>
#endif

#ifdef G_OS_WIN32
#include <io.h>
#include <winsock2.h>
#define open _open
#define close _close
#define read _read
#define write _write
/* windows pipe api details: http://msdn2.microsoft.com/en-us/library/edze9h7e(VS.80).aspx */
#define pipe(x) _pipe(x, 256, 0)
#endif

#define set_error(msg, ...) do { if (gerror != NULL) *gerror = g_error_new (G_LOG_DOMAIN, 1, msg, __VA_ARGS__); } while (0)
#define set_error_cond(cond,msg, ...) do { if ((cond) && gerror != NULL) *gerror = g_error_new (G_LOG_DOMAIN, 1, msg, __VA_ARGS__); } while (0)
#define set_error_status(status,msg, ...) do { if (gerror != NULL) *gerror = g_error_new (G_LOG_DOMAIN, status, msg, __VA_ARGS__); } while (0)
#define NO_INTR(var,cmd) do { (var) = (cmd); } while ((var) == -1 && errno == EINTR)
#define CLOSE_PIPE(p) do { close (p [0]); close (p [1]); } while (0)

#if defined(__APPLE__)
#if defined (TARGET_OSX)
/* Apple defines this in crt_externs.h but doesn't provide that header for 
 * arm-apple-darwin9.  We'll manually define the symbol on Apple as it does
 * in fact exist on all implementations (so far) 
 */
G_BEGIN_DECLS
gchar ***_NSGetEnviron(void);
G_END_DECLS
#define environ (*_NSGetEnviron())
#else
static char *mono_environ[1] = { NULL };
#define environ mono_environ
#endif /* defined (TARGET_OSX) */
#elif defined(_MSC_VER)
/* MS defines this in stdlib.h */
#else
G_BEGIN_DECLS
extern char **environ;
G_END_DECLS
#endif

#if !defined (G_OS_WIN32) && defined (HAVE_FORK) && defined (HAVE_EXECV)
static int
safe_read (int fd, gchar *buffer, gint count, GError **gerror)
{
	int res;

	NO_INTR (res, read (fd, buffer, count));
	set_error_cond (res == -1, "%s", "Error reading from pipe.");
	return res;
}

static int
read_pipes (int outfd, gchar **out_str, int errfd, gchar **err_str, GError **gerror)
{
	fd_set rfds;
	int res;
	gboolean out_closed;
	gboolean err_closed;
	GString *out = NULL;
	GString *err = NULL;
	gchar *buffer = NULL;
	gint nread;

	out_closed = (outfd < 0);
	err_closed = (errfd < 0);
	if (out_str) {
		*out_str = NULL;
		out = g_string_new ("");
	}	

	if (err_str) {
		*err_str = NULL;
		err = g_string_new ("");
	}	

	do {
		if (out_closed && err_closed)
			break;

		FD_ZERO (&rfds);
		if (!out_closed && outfd >= 0)
			FD_SET (outfd, &rfds);
		if (!err_closed && errfd >= 0)
			FD_SET (errfd, &rfds);

		res = select (MAX (outfd, errfd) + 1, &rfds, NULL, NULL, NULL);
		if (res > 0) {
			if (buffer == NULL)
				buffer = g_malloc (1024);
			if (!out_closed && FD_ISSET (outfd, &rfds)) {
				nread = safe_read (outfd, buffer, 1024, gerror);
				if (nread < 0) {
					close (errfd);
					close (outfd);
					return -1;
				}
				g_string_append_len (out, buffer, nread);
				if (nread <= 0) {
					out_closed = TRUE;
					close (outfd);
				}
			}

			if (!err_closed && FD_ISSET (errfd, &rfds)) {
				nread = safe_read (errfd, buffer, 1024, gerror);
				if (nread < 0) {
					close (errfd);
					close (outfd);
					return -1;
				}
				g_string_append_len (err, buffer, nread);
				if (nread <= 0) {
					err_closed = TRUE;
					close (errfd);
				}
			}
		}
	} while (res > 0 || (res == -1 && errno == EINTR));

	g_free (buffer);
	if (out_str)
		*out_str = g_string_free (out, FALSE);

	if (err_str)
		*err_str = g_string_free (err, FALSE);

	return 0;
}

static gboolean
create_pipe (int *fds, GError **gerror)
{
	if (pipe (fds) == -1) {
		set_error ("%s", "Error creating pipe.");
		return FALSE;
	}
	return TRUE;
}

static int
write_all (int fd, const void *vbuf, size_t n)
{
	const char *buf = (const char *) vbuf;
	size_t nwritten = 0;
	int w;
	
	do {
		do {
			w = write (fd, buf + nwritten, n - nwritten);
		} while (w == -1 && errno == EINTR);
		
		if (w == -1)
			return -1;
		
		nwritten += w;
	} while (nwritten < n);
	
	return nwritten;
}
#endif /* !defined (G_OS_WIN32) && defined (HAVE_FORK) && defined (HAVE_EXECV) */

#if !defined(G_OS_WIN32) && defined(HAVE_GETDTABLESIZE)
int
eg_getdtablesize (void)
{
#ifdef HAVE_GETRLIMIT
	struct rlimit limit;
	int res;

	res = getrlimit (RLIMIT_NOFILE, &limit);
	g_assert (res == 0);
	return limit.rlim_cur;
#else
	return getdtablesize ();
#endif
}
#else
int
eg_getdtablesize (void)
{
	g_error ("Should not be called");
}
#endif

gboolean
g_spawn_command_line_sync (const gchar *command_line,
				gchar **standard_output,
				gchar **standard_error,
				gint *exit_status,
				GError **gerror)
{
#ifdef G_OS_WIN32
	return TRUE;
#elif !defined (HAVE_FORK) || !defined (HAVE_EXECV)
	fprintf (stderr, "g_spawn_command_line_sync not supported on this platform\n");
	return FALSE;
#else
	pid_t pid;
	gchar **argv;
	gint argc;
	int stdout_pipe [2] = { -1, -1 };
	int stderr_pipe [2] = { -1, -1 };
	int status;
	int res;
	
	if (!g_shell_parse_argv (command_line, &argc, &argv, gerror))
		return FALSE;

	if (standard_output && !create_pipe (stdout_pipe, gerror))
		return FALSE;

	if (standard_error && !create_pipe (stderr_pipe, gerror)) {
		if (standard_output) {
			CLOSE_PIPE (stdout_pipe);
		}
		return FALSE;
	}

	pid = fork ();
	if (pid == 0) {
		gint i;

		if (standard_output) {
			close (stdout_pipe [0]);
			dup2 (stdout_pipe [1], STDOUT_FILENO);
		}

		if (standard_error) {
			close (stderr_pipe [0]);
			dup2 (stderr_pipe [1], STDERR_FILENO);
		}
		for (i = eg_getdtablesize () - 1; i >= 3; i--)
			close (i);

		/* G_SPAWN_SEARCH_PATH is always enabled for g_spawn_command_line_sync */
		if (!g_path_is_absolute (argv [0])) {
			gchar *arg0;

			arg0 = g_find_program_in_path (argv [0]);
			if (arg0 == NULL) {
				exit (1);
			}
			//g_free (argv [0]);
			argv [0] = arg0;
		}
		execv (argv [0], argv);
		exit (1); /* TODO: What now? */
	}

	g_strfreev (argv);
	if (standard_output)
		close (stdout_pipe [1]);

	if (standard_error)
		close (stderr_pipe [1]);

	if (standard_output || standard_error) {
		res = read_pipes (stdout_pipe [0], standard_output, stderr_pipe [0], standard_error, gerror);
		if (res) {
			waitpid (pid, &status, WNOHANG); /* avoid zombie */
			return FALSE;
		}
	}

	NO_INTR (res, waitpid (pid, &status, 0));

	/* TODO: What if error? */
	if (WIFEXITED (status) && exit_status) {
		*exit_status = WEXITSTATUS (status);
	}
	return TRUE;
#endif
}

/*
 * This is the only use we have in mono/metadata
!g_spawn_async_with_pipes (NULL, (char**)addr_argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &child_pid, &ch_in, &ch_out, NULL, NULL)
*/
gboolean
g_spawn_async_with_pipes (const gchar *working_directory,
			gchar **argv,
			gchar **envp,
			GSpawnFlags flags,
			GSpawnChildSetupFunc child_setup,
			gpointer user_data,
			GPid *child_pid,
			gint *standard_input,
			gint *standard_output,
			gint *standard_error,
			GError **gerror)
{
#ifdef G_OS_WIN32
	return TRUE;
#elif !defined (HAVE_FORK) || !defined (HAVE_EXECVE)
	fprintf (stderr, "g_spawn_async_with_pipes is not supported on this platform\n");
	return FALSE;
#else
	pid_t pid;
	int info_pipe [2];
	int in_pipe [2] = { -1, -1 };
	int out_pipe [2] = { -1, -1 };
	int err_pipe [2] = { -1, -1 };
	int status;

	g_return_val_if_fail (argv != NULL, FALSE); /* Only mandatory arg */

	if (!create_pipe (info_pipe, gerror))
		return FALSE;

	if (standard_output && !create_pipe (out_pipe, gerror)) {
		CLOSE_PIPE (info_pipe);
		return FALSE;
	}

	if (standard_error && !create_pipe (err_pipe, gerror)) {
		CLOSE_PIPE (info_pipe);
		CLOSE_PIPE (out_pipe);
		return FALSE;
	}

	if (standard_input && !create_pipe (in_pipe, gerror)) {
		CLOSE_PIPE (info_pipe);
		CLOSE_PIPE (out_pipe);
		CLOSE_PIPE (err_pipe);
		return FALSE;
	}

	pid = fork ();
	if (pid == -1) {
		CLOSE_PIPE (info_pipe);
		CLOSE_PIPE (out_pipe);
		CLOSE_PIPE (err_pipe);
		CLOSE_PIPE (in_pipe);
		set_error ("%s", "Error in fork ()");
		return FALSE;
	}

	if (pid == 0) {
		/* No zombie left behind */
		if ((flags & G_SPAWN_DO_NOT_REAP_CHILD) == 0) {
			pid = fork ();
		}

		if (pid != 0) {
			exit (pid == -1 ? 1 : 0);
		}  else {
			gint i;
			int fd;
			gchar *arg0;
			gchar **actual_args;
			gint unused;

			close (info_pipe [0]);
			close (in_pipe [1]);
			close (out_pipe [0]);
			close (err_pipe [0]);

			/* when exec* succeeds, we want to close this fd, which will return
			 * a 0 read on the parent. We're not supposed to keep it open forever.
			 * If exec fails, we still can write the error to it before closing.
			 */
			fcntl (info_pipe [1], F_SETFD, FD_CLOEXEC);

			if ((flags & G_SPAWN_DO_NOT_REAP_CHILD) == 0) {
				pid = getpid ();
				NO_INTR (unused, write_all (info_pipe [1], &pid, sizeof (pid_t)));
			}

			if (working_directory && chdir (working_directory) == -1) {
				int err = errno;
				NO_INTR (unused, write_all (info_pipe [1], &err, sizeof (int)));
				exit (0);
			}

			if (standard_output) {
				dup2 (out_pipe [1], STDOUT_FILENO);
			} else if ((flags & G_SPAWN_STDOUT_TO_DEV_NULL) != 0) {
				fd = open ("/dev/null", O_WRONLY);
				dup2 (fd, STDOUT_FILENO);
			}

			if (standard_error) {
				dup2 (err_pipe [1], STDERR_FILENO);
			} else if ((flags & G_SPAWN_STDERR_TO_DEV_NULL) != 0) {
				fd = open ("/dev/null", O_WRONLY);
				dup2 (fd, STDERR_FILENO);
			}

			if (standard_input) {
				dup2 (in_pipe [0], STDIN_FILENO);
			} else if ((flags & G_SPAWN_CHILD_INHERITS_STDIN) == 0) {
				fd = open ("/dev/null", O_RDONLY);
				dup2 (fd, STDIN_FILENO);
			}

			if ((flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN) != 0) {
				for (i = eg_getdtablesize () - 1; i >= 3; i--)
					close (i);
			}

			actual_args = ((flags & G_SPAWN_FILE_AND_ARGV_ZERO) == 0) ? argv : argv + 1;
			if (envp == NULL)
				envp = environ;

			if (child_setup)
				child_setup (user_data);

			arg0 = argv [0];
			if (!g_path_is_absolute (arg0) || (flags & G_SPAWN_SEARCH_PATH) != 0) {
				arg0 = g_find_program_in_path (argv [0]);
				if (arg0 == NULL) {
					int err = ENOENT;
					write_all (info_pipe [1], &err, sizeof (int));
					exit (0);
				}
			}

			execve (arg0, actual_args, envp);
			int const err = errno;
			write_all (info_pipe [1], &err, sizeof (int));
			exit (0);
		}
	} else if ((flags & G_SPAWN_DO_NOT_REAP_CHILD) == 0) {
		int w;
		/* Wait for the first child if two are created */
		NO_INTR (w, waitpid (pid, &status, 0));
		if (status == 1 || w == -1) {
			CLOSE_PIPE (info_pipe);
			CLOSE_PIPE (out_pipe);
			CLOSE_PIPE (err_pipe);
			CLOSE_PIPE (in_pipe);
			set_error ("Error in fork (): %d", status);
			return FALSE;
		}
	}
	close (info_pipe [1]);
	close (in_pipe [0]);
	close (out_pipe [1]);
	close (err_pipe [1]);

	if ((flags & G_SPAWN_DO_NOT_REAP_CHILD) == 0) {
		int x;
		NO_INTR (x, read (info_pipe [0], &pid, sizeof (pid_t))); /* if we read < sizeof (pid_t)... */
	}

	if (child_pid) {
		*child_pid = pid;
	}

	if (read (info_pipe [0], &status, sizeof (int)) != 0) {
		close (info_pipe [0]);
		close (in_pipe [0]);
		close (out_pipe [1]);
		close (err_pipe [1]);
		set_error_status (status, "Error in exec (%d -> %s)", status, strerror (status));
		return FALSE;
	}

	close (info_pipe [0]);
	if (standard_input)
		*standard_input = in_pipe [1];
	if (standard_output)
		*standard_output = out_pipe [0];
	if (standard_error)
		*standard_error = err_pipe [0];
	return TRUE;
#endif
}
