/*
 * Copyright (C) the libgit2 contributors. All rights reserved.
 *
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
 */

#include <stdio.h>
#include <sys/wait.h>
#include <signal.h>
#include <git2.h>

#include "git2_util.h"
#include "vector.h"
#include "futils.h"
#include "process.h"
#include "strlist.h"

#ifdef __APPLE__
	#include <crt_externs.h>
	#define environ (*_NSGetEnviron())
#else
	extern char **environ;
#endif

struct git_process {
	char **args;
	char **env;

	char *cwd;

	unsigned int use_shell   : 1,
	             capture_in  : 1,
	             capture_out : 1,
	             capture_err : 1;

	pid_t pid;

	int child_in;
	int child_out;
	int child_err;
	git_process_result_status status;
};

GIT_INLINE(bool) is_delete_env(const char *env)
{
	const char *c = strchr(env, '=');

	if (c == NULL)
		return false;

	return *(c+1) == '\0';
}

GIT_INLINE(int) insert_dup(git_vector *v, const char *s)
{
	char *dup = git__strdup(s);

	GIT_ERROR_CHECK_ALLOC(dup);

	return git_vector_insert(v, dup);
}

static int setup_args(
	char ***out,
	const char **args,
	size_t args_len,
	bool use_shell)
{
	git_vector prefixed = GIT_VECTOR_INIT;
	git_str first = GIT_STR_INIT;
	size_t cnt;

	GIT_ASSERT(args && args_len);

	if (use_shell) {
		if (git_str_puts(&first, args[0]) < 0 ||
		    git_str_puts(&first, " \"$@\"") < 0 ||
		    insert_dup(&prefixed, "/bin/sh") < 0 ||
		    insert_dup(&prefixed, "-c") < 0 ||
		    git_vector_insert(&prefixed, git_str_detach(&first)) < 0)
			goto on_error;
	}

	for (cnt = 0; args && cnt < args_len; cnt++) {
		if (insert_dup(&prefixed, args[cnt]) < 0)
			goto on_error;
	}

	git_vector_insert(&prefixed, NULL);

	*out = (char **)prefixed.contents;

	return 0;

on_error:
	git_str_dispose(&first);
	git_vector_dispose_deep(&prefixed);
	return -1;
}

static int merge_env(
	char ***out,
	const char **env,
	size_t env_len,
	bool exclude_env)
{
	git_vector merged = GIT_VECTOR_INIT;
	char **kv;
	size_t max, cnt;
	int error = 0;

	for (max = env_len, kv = environ; !exclude_env && *kv; kv++)
		max++;

	if ((error = git_vector_init(&merged, max, NULL)) < 0)
		goto on_error;

	for (cnt = 0; env && cnt < env_len; cnt++) {
		if (is_delete_env(env[cnt]))
			continue;

		if (insert_dup(&merged, env[cnt]) < 0)
			goto on_error;
	}

	if (!exclude_env) {
		for (kv = environ; *kv; kv++) {
			if (env && git_strlist_contains_key(env, env_len, *kv, '='))
				continue;

			if (insert_dup(&merged, *kv) < 0)
				goto on_error;
		}
	}

	if (merged.length == 0) {
		*out = NULL;
		error = 0;
		goto on_error;
	}

	git_vector_insert(&merged, NULL);

	*out = (char **)merged.contents;

	return 0;

on_error:
	git_vector_dispose_deep(&merged);
	return error;
}

int git_process_new(
	git_process **out,
	const char **args,
	size_t args_len,
	const char **env,
	size_t env_len,
	git_process_options *opts)
{
	git_process *process;

	GIT_ASSERT_ARG(out && args && args_len > 0);

	*out = NULL;

	process = git__calloc(sizeof(git_process), 1);
	GIT_ERROR_CHECK_ALLOC(process);

	if (setup_args(&process->args, args, args_len, opts ? opts->use_shell : false) < 0 ||
	    merge_env(&process->env, env, env_len, opts ? opts->exclude_env : false) < 0) {
		git_process_free(process);
		return -1;
	}

	if (opts) {
		process->use_shell = opts->use_shell;
		process->capture_in = opts->capture_in;
		process->capture_out = opts->capture_out;
		process->capture_err = opts->capture_err;

		if (opts->cwd) {
			process->cwd = git__strdup(opts->cwd);
			GIT_ERROR_CHECK_ALLOC(process->cwd);
		}
	}

	process->child_in  = -1;
	process->child_out = -1;
	process->child_err = -1;
	process->status    = -1;

	*out = process;
	return 0;
}

extern int git_process_new_from_cmdline(
	git_process **out,
	const char *cmdline,
	const char **env,
	size_t env_len,
	git_process_options *opts)
{
	git_process_options merged_opts = {0};

	memcpy(&merged_opts, opts, sizeof(git_process_options));
	merged_opts.use_shell = 1;

	return git_process_new(out, &cmdline, 1, env, env_len, &merged_opts);
}

#define CLOSE_FD(fd) \
	if (fd >= 0) {     \
		close(fd); \
		fd = -1;   \
	}

static int try_read_status(size_t *out, int fd, void *buf, size_t len)
{
	size_t read_len = 0;
	int ret = -1;

	while (ret && read_len < len) {
		ret = read(fd, buf + read_len, len - read_len);

		if (ret < 0 && errno != EAGAIN && errno != EINTR) {
			git_error_set(GIT_ERROR_OS, "could not read child status");
			return -1;
		}

		read_len += ret;
	}

	*out = read_len;
	return 0;
}


static int read_status(int fd)
{
	size_t status_len = sizeof(int) * 3, read_len = 0;
	char buffer[status_len], fn[128];
	int error, fn_error, os_error, fn_len = 0;

	if ((error = try_read_status(&read_len, fd, buffer, status_len)) < 0)
		return error;

	/* Immediate EOF indicates the exec succeeded. */
	if (read_len == 0)
		return 0;

	if (read_len < status_len) {
		git_error_set(GIT_ERROR_INVALID, "child status truncated");
		return -1;
	}

	memcpy(&fn_error, &buffer[0], sizeof(int));
	memcpy(&os_error, &buffer[sizeof(int)], sizeof(int));
	memcpy(&fn_len, &buffer[sizeof(int) * 2], sizeof(int));

	if (fn_len > 0) {
		fn_len = min(fn_len, (int)(ARRAY_SIZE(fn) - 1));

		if ((error = try_read_status(&read_len, fd, fn, fn_len)) < 0)
			return error;

		fn[fn_len] = '\0';
	} else {
		fn[0] = '\0';
	}

	if (fn_error) {
		errno = os_error;
		git_error_set(GIT_ERROR_OS, "could not %s", fn[0] ? fn : "(unknown)");
	}

	return fn_error;
}

static bool try_write_status(int fd, const void *buf, size_t len)
{
	size_t write_len;
	int ret;

	for (write_len = 0; write_len < len; ) {
		ret = write(fd, buf + write_len, len - write_len);

		if (ret <= 0)
			break;

		write_len += ret;
	}

	return (len == write_len);
}

static void write_status(int fd, const char *fn, int error, int os_error)
{
	size_t status_len = sizeof(int) * 3, fn_len;
	char buffer[status_len];

	fn_len = strlen(fn);

	if (fn_len > INT_MAX)
		fn_len = INT_MAX;

	memcpy(&buffer[0], &error, sizeof(int));
	memcpy(&buffer[sizeof(int)], &os_error, sizeof(int));
	memcpy(&buffer[sizeof(int) * 2], &fn_len, sizeof(int));

	/* Do our best effort to write all the status. */
	if (!try_write_status(fd, buffer, status_len))
		return;

	if (fn_len)
		try_write_status(fd, fn, fn_len);
}

static int resolve_path(git_process *process)
{
	git_str full_path = GIT_STR_INIT;
	int error = 0;

	/* The shell will resolve the path for us */
	if (process->use_shell)
		goto done;

	error = git_fs_path_find_executable(&full_path, process->args[0]);

	if (error == GIT_ENOTFOUND) {
		git_error_set(GIT_ERROR_SSH, "cannot run %s: No such file or directory", process->args[0]);
		error = -1;
	}

	if (error)
		goto done;

	git__free(process->args[0]);
	process->args[0] = git_str_detach(&full_path);

done:
	git_str_dispose(&full_path);
	return error;
}

int git_process_start(git_process *process)
{
	int in[2] = { -1, -1 }, out[2] = { -1, -1 },
	    err[2] = { -1, -1 }, status[2] = { -1, -1 };
	int fdflags, state, error;
	pid_t pid;

	/* Locate the path (unless we're letting the shell do it for us) */
	if ((error = resolve_path(process)) < 0)
		goto on_error;

	/* Set up the pipes to read from/write to the process */
	if ((process->capture_in && pipe(in) < 0) ||
	    (process->capture_out && pipe(out) < 0) ||
	    (process->capture_err && pipe(err) < 0)) {
		git_error_set(GIT_ERROR_OS, "could not create pipe");
		goto on_error;
	}

	/* Set up a self-pipe for status from the forked process. */
	if (pipe(status) < 0 ||
	    (fdflags = fcntl(status[1], F_GETFD)) < 0 ||
	    fcntl(status[1], F_SETFD, fdflags | FD_CLOEXEC) < 0) {
		git_error_set(GIT_ERROR_OS, "could not create pipe");
		goto on_error;
	}

	switch (pid = fork()) {
	case -1:
		git_error_set(GIT_ERROR_OS, "could not fork");
		goto on_error;

	/* Child: start the process. */
	case 0:
		/* Close the opposing side of the pipes */
		CLOSE_FD(status[0]);

		if (process->capture_in) {
			CLOSE_FD(in[1]);
			dup2(in[0],  STDIN_FILENO);
		}

		if (process->capture_out) {
			CLOSE_FD(out[0]);
			dup2(out[1], STDOUT_FILENO);
		}

		if (process->capture_err) {
			CLOSE_FD(err[0]);
			dup2(err[1], STDERR_FILENO);
		}

		if (process->cwd && (error = chdir(process->cwd)) < 0) {
			write_status(status[1], "chdir", error, errno);
			exit(0);
		}

		/*
		 * Exec the process and write the results back if the
		 * call fails.  If it succeeds, we'll close the status
		 * pipe (via CLOEXEC) and the parent will know.
		 */
		error = execve(process->args[0],
		               process->args,
			       process->env);

		write_status(status[1], "execve", error, errno);
		exit(0);

	/* Parent: make sure the child process exec'd correctly. */
	default:
		/* Close the opposing side of the pipes */
		CLOSE_FD(status[1]);

		if (process->capture_in) {
			CLOSE_FD(in[0]);
			process->child_in  = in[1];
		}

		if (process->capture_out) {
			CLOSE_FD(out[1]);
			process->child_out = out[0];
		}

		if (process->capture_err) {
			CLOSE_FD(err[1]);
			process->child_err = err[0];
		}

		/* Try to read the status */
		process->status = status[0];
		if ((error = read_status(status[0])) < 0) {
			waitpid(process->pid, &state, 0);
			goto on_error;
		}

		process->pid = pid;
		return 0;
	}

on_error:
	CLOSE_FD(in[0]);     CLOSE_FD(in[1]);
	CLOSE_FD(out[0]);    CLOSE_FD(out[1]);
	CLOSE_FD(err[0]);    CLOSE_FD(err[1]);
	CLOSE_FD(status[0]); CLOSE_FD(status[1]);
	return -1;
}

int git_process_id(p_pid_t *out, git_process *process)
{
	GIT_ASSERT(out && process);

	if (!process->pid) {
		git_error_set(GIT_ERROR_INVALID, "process not running");
		return -1;
	}

	*out = process->pid;
	return 0;
}

static ssize_t process_read(int fd, void *buf, size_t count)
{
	ssize_t ret;

	if (count > SSIZE_MAX)
		count = SSIZE_MAX;

	if ((ret = read(fd, buf, count)) < 0) {
		git_error_set(GIT_ERROR_OS, "could not read from child process");
		return -1;
	}

	return ret;
}

ssize_t git_process_read(git_process *process, void *buf, size_t count)
{
	GIT_ASSERT_ARG(process);
	GIT_ASSERT(process->capture_out);

	return process_read(process->child_out, buf, count);
}

ssize_t git_process_read_err(git_process *process, void *buf, size_t count)
{
	GIT_ASSERT_ARG(process);
	GIT_ASSERT(process->capture_err);

	return process_read(process->child_err, buf, count);
}

#ifdef GIT_THREADS

# define signal_state sigset_t

/*
 * Since signal-handling is process-wide, we cannot simply use
 * SIG_IGN to avoid SIGPIPE. Instead: http://www.microhowto.info:80/howto/ignore_sigpipe_without_affecting_other_threads_in_a_process.html
 */

GIT_INLINE(int) disable_signals(sigset_t *saved_mask)
{
	sigset_t sigpipe_mask;

	sigemptyset(&sigpipe_mask);
	sigaddset(&sigpipe_mask, SIGPIPE);

	if (pthread_sigmask(SIG_BLOCK, &sigpipe_mask, saved_mask) < 0) {
		git_error_set(GIT_ERROR_OS, "could not configure signal mask");
		return -1;
	}

	return 0;
}

GIT_INLINE(int) restore_signals(sigset_t *saved_mask)
{
	sigset_t sigpipe_mask, pending;
	int signal;

	sigemptyset(&sigpipe_mask);
	sigaddset(&sigpipe_mask, SIGPIPE);

	if (sigpending(&pending) < 0) {
		git_error_set(GIT_ERROR_OS, "could not examine pending signals");
		return -1;
	}

	if (sigismember(&pending, SIGPIPE) == 1 &&
	    sigwait(&sigpipe_mask, &signal) < 0) {
		git_error_set(GIT_ERROR_OS, "could not wait for (blocking) signal delivery");
		return -1;
	}

	if (pthread_sigmask(SIG_SETMASK, saved_mask, 0) < 0) {
		git_error_set(GIT_ERROR_OS, "could not configure signal mask");
		return -1;
	}

	return 0;
}

#else

# define signal_state struct sigaction

GIT_INLINE(int) disable_signals(struct sigaction *saved_handler)
{
	struct sigaction ign_handler = { 0 };

	ign_handler.sa_handler = SIG_IGN;

	if (sigaction(SIGPIPE, &ign_handler, saved_handler) < 0) {
		git_error_set(GIT_ERROR_OS, "could not configure signal handler");
		return -1;
	}

	return 0;
}

GIT_INLINE(int) restore_signals(struct sigaction *saved_handler)
{
	if (sigaction(SIGPIPE, saved_handler, NULL) < 0) {
		git_error_set(GIT_ERROR_OS, "could not configure signal handler");
		return -1;
	}

	return 0;
}

#endif

ssize_t git_process_write(git_process *process, const void *buf, size_t count)
{
	signal_state saved_signal;
	ssize_t ret;

	GIT_ASSERT_ARG(process);
	GIT_ASSERT(process->capture_in);

	if (count > SSIZE_MAX)
		count = SSIZE_MAX;

	if (disable_signals(&saved_signal) < 0)
		return -1;

	if ((ret = write(process->child_in, buf, count)) < 0)
		git_error_set(GIT_ERROR_OS, "could not write to child process");

	if (restore_signals(&saved_signal) < 0)
		return -1;

	return (ret < 0) ? -1 : ret;
}

int git_process_close_in(git_process *process)
{
	if (!process->capture_in) {
		git_error_set(GIT_ERROR_INVALID, "input is not open");
		return -1;
	}

	CLOSE_FD(process->child_in);
	return 0;
}

int git_process_close_out(git_process *process)
{
	if (!process->capture_out) {
		git_error_set(GIT_ERROR_INVALID, "output is not open");
		return -1;
	}

	CLOSE_FD(process->child_out);
	return 0;
}

int git_process_close_err(git_process *process)
{
	if (!process->capture_err) {
		git_error_set(GIT_ERROR_INVALID, "error is not open");
		return -1;
	}

	CLOSE_FD(process->child_err);
	return 0;
}

int git_process_close(git_process *process)
{
	CLOSE_FD(process->child_in);
	CLOSE_FD(process->child_out);
	CLOSE_FD(process->child_err);

	return 0;
}

int git_process_wait(git_process_result *result, git_process *process)
{
	int state;

	if (result)
		memset(result, 0, sizeof(git_process_result));

	if (!process->pid) {
		git_error_set(GIT_ERROR_INVALID, "process is stopped");
		return -1;
	}

	if (waitpid(process->pid, &state, 0) < 0) {
		git_error_set(GIT_ERROR_OS, "could not wait for child");
		return -1;
	}

	process->pid = 0;

	if (result) {
		if (WIFEXITED(state)) {
			result->status = GIT_PROCESS_STATUS_NORMAL;
			result->exitcode = WEXITSTATUS(state);
		} else if (WIFSIGNALED(state)) {
			result->status = GIT_PROCESS_STATUS_ERROR;
			result->signal = WTERMSIG(state);
		} else {
			result->status = GIT_PROCESS_STATUS_ERROR;
		}
	}

	return 0;
}

int git_process_result_msg(git_str *out, git_process_result *result)
{
	if (result->status == GIT_PROCESS_STATUS_NONE) {
		return git_str_puts(out, "process not started");
	} else if (result->status == GIT_PROCESS_STATUS_NORMAL) {
		return git_str_printf(out, "process exited with code %d",
		                      result->exitcode);
	} else if (result->signal) {
		return git_str_printf(out, "process exited on signal %d",
		                      result->signal);
	}

	return git_str_puts(out, "unknown error");
}

void git_process_free(git_process *process)
{
	if (!process)
		return;

	if (process->pid)
		git_process_close(process);

	git__free(process->cwd);
	git_strlist_free_with_null(process->args);
	git_strlist_free_with_null(process->env);
	git__free(process);
}
