#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include "proc_open.h"

#ifdef WIN32
#include <io.h>
#include <fcntl.h>
#else
#include <sys/wait.h>
#include <unistd.h>
#endif


#ifdef _WIN32
/* {{{ win32 stuff */
# define SHELLENV "ComSpec"
# define SECURITY_DC , SECURITY_ATTRIBUTES *security
# define SECURITY_CC , security
# define pipe(pair) (CreatePipe(&pair[0], &pair[1], security, 2048L) ? 0 : -1)
static HANDLE dup_handle(HANDLE src, BOOL inherit, BOOL closeorig)
{
	HANDLE copy, self = GetCurrentProcess();

	if (!DuplicateHandle(self, src, self, &copy, 0, inherit, DUPLICATE_SAME_ACCESS |
				(closeorig ? DUPLICATE_CLOSE_SOURCE : 0)))
		return NULL;
	return copy;
}
# define close_descriptor(fd) CloseHandle(fd)
static void pipe_close_parent(pipe_t *p) {
	/* don't let the child inherit the parent side of the pipe */
	p->parent = dup_handle(p->parent, FALSE, TRUE);
}
static void pipe_close_child(pipe_t *p) {
	close_descriptor(p->child);
	p->fd = _open_osfhandle((long)p->parent,
			(p->fd == 0 ? O_RDONLY : O_WRONLY)|O_BINARY);
}
/* }}} */
#else /* WIN32 */
/* {{{ unix way */
# define SHELLENV "SHELL"
# define SECURITY_DC
# define SECURITY_CC
# define close_descriptor(fd) close(fd)
static void pipe_close_parent(pipe_t *p) {
	/* don't close stdin */
	close_descriptor(p->parent);
	if (dup2(p->child, p->fd) != p->fd) {
		perror("pipe_child dup2");
	} else {
		close_descriptor(p->child);
		p->child = p->fd;
	}
}
static void pipe_close_child(pipe_t *p) {
	close_descriptor(p->child);
	p->fd = p->parent;
}
/* }}} */
#endif /* WIN32 */

/* {{{ pipe_close */
static void pipe_close(pipe_t *p) {
	close_descriptor(p->parent);
	close_descriptor(p->child);
#ifdef WIN32
	close(p->fd);
#endif
}
/* }}} */
/* {{{ pipe_open */
static int pipe_open(pipe_t *p, int fd SECURITY_DC) {
	descriptor_t newpipe[2];

	if (0 != pipe(newpipe)) {
		fprintf(stderr, "can't open pipe");
		return -1;
	}
	if (0 == fd) {
		p->parent = newpipe[1]; /* write */
		p->child  = newpipe[0]; /* read */
	} else {
		p->parent = newpipe[0]; /* read */
		p->child  = newpipe[1]; /* write */
	}
	p->fd = fd;

	return 0;
}
/* }}} */

/* {{{ proc_open_pipes */
static int proc_open_pipes(proc_handler_t *proc SECURITY_DC) {
	if (pipe_open(&(proc->in), 0 SECURITY_CC) != 0) {
		return -1;
	}
	if (pipe_open(&(proc->out), 1 SECURITY_CC) != 0) {
		return -1;
	}
	if (pipe_open(&(proc->err), 2 SECURITY_CC) != 0) {
		return -1;
	}
	return 0;
}
/* }}} */
/* {{{ proc_close_pipes */
static void proc_close_pipes(proc_handler_t *proc) {
	pipe_close(&proc->in);
	pipe_close(&proc->out);
	pipe_close(&proc->err);
}
/* }}} */
/* {{{ proc_close_parents */
static void proc_close_parents(proc_handler_t *proc) {
	pipe_close_parent(&proc->in);
	pipe_close_parent(&proc->out);
	pipe_close_parent(&proc->err);
}
/* }}} */
/* {{{ proc_close_childs */
static void proc_close_childs(proc_handler_t *proc) {
	pipe_close_child(&proc->in);
	pipe_close_child(&proc->out);
	pipe_close_child(&proc->err);
}
/* }}} */

#ifdef WIN32
/* {{{ proc_close */
int proc_close(proc_handler_t *proc) {
	proc_pid_t child = proc->child;
	DWORD wstatus;

	proc_close_pipes(proc);
	WaitForSingleObject(child, INFINITE);
	GetExitCodeProcess(child, &wstatus);
	CloseHandle(child);

	return wstatus;
}
/* }}} */
/* {{{ proc_open */
int proc_open(proc_handler_t *proc, const char *command) {
	PROCESS_INFORMATION pi;
	STARTUPINFO si;
	BOOL procok;
	SECURITY_ATTRIBUTES security;
	const char *shell = NULL;
	const char *windir = NULL;
	buffer *cmdline;

	if (NULL == (shell = getenv(SHELLENV)) &&
			NULL == (windir = getenv("SystemRoot")) &&
			NULL == (windir = getenv("windir"))) {
		fprintf(stderr, "One of %s,%%SystemRoot,%%windir is required", SHELLENV);
		return -1;
	}

	/* we use this to allow the child to inherit handles */
	memset(&security, 0, sizeof(security));
	security.nLength = sizeof(security);
	security.bInheritHandle = TRUE;
	security.lpSecurityDescriptor = NULL;

	if (proc_open_pipes(proc, &security) != 0) {
		return -1;
	}
	proc_close_parents(proc);

	memset(&si, 0, sizeof(si));
	si.cb = sizeof(si);
	si.dwFlags = STARTF_USESTDHANDLES;
	si.hStdInput = proc->in.child;
	si.hStdOutput = proc->out.child;
	si.hStdError = proc->err.child;

	memset(&pi, 0, sizeof(pi));

	cmdline = buffer_init();
	if (shell) {
		buffer_append_string(cmdline, shell);
	} else {
		buffer_append_string(cmdline, windir);
		buffer_append_string(cmdline, "\\system32\\cmd.exe");
	}
	buffer_append_string_len(cmdline, CONST_STR_LEN(" /c "));
	buffer_append_string(cmdline, command);
	procok = CreateProcess(NULL, cmdline->ptr, &security, &security, TRUE,
			NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi);

	if (FALSE == procok) {
		fprintf(stderr, "failed to CreateProcess: %s", cmdline->ptr);
		buffer_free(cmdline);
		return -1;
	}
	buffer_free(cmdline);

	proc->child = pi.hProcess;
	CloseHandle(pi.hThread);

	proc_close_childs(proc);

	return 0;
}
/* }}} */
#else /* WIN32 */
/* {{{ proc_close */
int proc_close(proc_handler_t *proc) {
	pid_t child = proc->child;
	int wstatus;
	pid_t wait_pid;

	proc_close_pipes(proc);

	do {
		wait_pid = waitpid(child, &wstatus, 0);
	} while (wait_pid == -1 && errno == EINTR);

	if (wait_pid == -1) {
		return -1;
	} else {
		if (WIFEXITED(wstatus))
			wstatus = WEXITSTATUS(wstatus);
	}

	return wstatus;
}
/* }}} */
/* {{{ proc_open */
int proc_open(proc_handler_t *proc, const char *command) {
	pid_t child;
	const char *shell;

	if (NULL == (shell = getenv(SHELLENV))) {
		shell = "/bin/sh";
	}

	if (proc_open_pipes(proc) != 0) {
		return -1;
	}

	/* the unix way */

	child = fork();

	if (child == 0) {
		/* this is the child process */

		/* close those descriptors that we just opened for the parent stuff,
		 * dup new descriptors into required descriptors and close the original
		 * cruft
		 */
		proc_close_parents(proc);

		execl(shell, shell, "-c", command, (char *)NULL);
		_exit(127);

	} else if (child < 0) {
		fprintf(stderr, "failed to forking");
		proc_close(proc);
		return -1;

	} else {
		proc->child = child;
		proc_close_childs(proc);
		return 0;
	}
}
/* }}} */
#endif /* _WIN32 */

/* {{{ proc_read_fd_to_buffer */
static void proc_read_fd_to_buffer(int fd, buffer *b) {
	int s; /* win32 has not ssize_t */

	for (;;) {
		buffer_prepare_append(b, 512);
		if ((s = read(fd, (void *)(b->ptr + b->used), 512 - 1)) <= 0) {
			break;
		}
		b->used += s;
	}
	b->ptr[b->used] = '\0';
}
/* }}} */
/* {{{ proc_open_buffer */
int proc_open_buffer(proc_handler_t *proc, const char *command, buffer *in, buffer *out, buffer *err) {

	UNUSED(err);

	if (proc_open(proc, command) != 0) {
		return -1;
	}

	if (in) {
		if (write(proc->in.fd, (void *)in->ptr, in->used) < 0) {
			perror("error writing pipe");
			return -1;
		}
	}
	pipe_close(&proc->in);

	if (out) {
		proc_read_fd_to_buffer(proc->out.fd, out);
	}
	pipe_close(&proc->out);

	if (err) {
		proc_read_fd_to_buffer(proc->err.fd, err);
	}
	pipe_close(&proc->err);

	return 0;
}
/* }}} */

/* {{{ test */
#ifdef DEBUG_PROC_OPEN
int main() {
	proc_handler_t proc;
	buffer *in = buffer_init(), *out = buffer_init(), *err = buffer_init();
	int wstatus;

#define FREE() do { \
	buffer_free(in); \
	buffer_free(out); \
	buffer_free(err); \
} while (0)

#define RESET() do { \
	buffer_reset(in); \
	buffer_reset(out); \
	buffer_reset(err); \
	wstatus = proc_close(&proc); \
	if (0&&wstatus != 0) { \
		fprintf(stdout, "exitstatus %d\n", wstatus); \
		return __LINE__ - 200; \
	} \
} while (0)

#define ERROR_OUT() do { \
	fprintf(stdout, "failed opening proc\n"); \
	wstatus = proc_close(&proc); \
	fprintf(stdout, "exitstatus %d\n", wstatus); \
	FREE(); \
	return __LINE__ - 300; \
} while (0)

#ifdef WIN32
#define CMD_CAT "pause"
#else
#define CMD_CAT "cat"
#endif

	do {
		fprintf(stdout, "test: echo 123 without read\n");
		if (proc_open(&proc, "echo 321") != 0) {
			ERROR_OUT();
		}
		close_descriptor(proc.in.parent);
		close_descriptor(proc.out.parent);
		close_descriptor(proc.err.parent);
		RESET();

		fprintf(stdout, "test: echo 321 with read\n"); fflush(stdout);
		if (proc_open_buffer(&proc, "echo 321", NULL, out, err) != 0) {
			ERROR_OUT();
		}
		fprintf(stdout, "result: ->%s<-\n\n", out->ptr); fflush(stdout);
		RESET();

		fprintf(stdout, "test: echo 123 | " CMD_CAT "\n"); fflush(stdout);
		buffer_copy_string_len(in, CONST_STR_LEN("123\n"));
		if (proc_open_buffer(&proc, CMD_CAT, in, out, err) != 0) {
			ERROR_OUT();
		}
		fprintf(stdout, "result: ->%s<-\n\n", out->ptr); fflush(stdout);
		RESET();
	} while (0);

#undef RESET
#undef ERROR_OUT

	fprintf(stdout, "ok\n");

	FREE();
	return 0;
}
#endif /* DEBUG_PROC_OPEN */
/* }}} */

