/*-------------------------------------------------------------------------
 *
 * execute_pipe.c
 *	   popen() replacement with stderr
 *
 * NOTES:
 *
 * COPY and external table can execute arbitary command. Upstream uses
 * OpenPipeStream(), which uses popen to create the new process. But
 * popen can't catch stderr, so when something goes wrong only exit code
 * is reported. This is a replacement that also catches stderr as well,
 * so we can get detail messages from child process if provided.
 *
 * Portions Copyright (C) 2020-Present VMware, Inc.
 *
 *
 * IDENTIFICATION
 *	    src/backend/storage/file/execute_pipe.c
 *
 *-------------------------------------------------------------------------
 */

#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

#include "postgres.h"
#include "storage/execute_pipe.h"
#include "cdb/cdbvars.h"
#include "utils/faultinjector.h"
#include "postmaster/fork_process.h"

#define EXEC_DATA_P 0 /* index to data pipe */
#define EXEC_ERR_P 1 /* index to error pipe  */

static void read_err_msg(int fid, StringInfo sinfo);

/*
 * popen_with_stderr
 *
 * standard popen doesn't redirect stderr from the child process.
 * we need stderr in order to display the error that child process
 * encountered and show it to the user. This is, therefore, a wrapper
 * around a set of file descriptor redirections and a fork.
 *
 * if 'forwrite' is set then we set the data pipe write side on the
 * parent. otherwise, we set the read side on the parent.
 *
 * This function is used to replace OpenPipeStream in COPY TO/FROM
 * PROGRAM command. With OpenPipeStream, COPY command can only report
 * exit code.
 *
 * XXX: Even though we have the ability to extract the child's stderr, we do
 * lose out on updates/protections from upstream (such as the niceties with
 * fd.c and SIGPIPE handling that are available with OpenPipeStream()). So we
 * should consider augmenting OpenPipeStream() and using that instead.
 */
int
popen_with_stderr(int *pipes, const char *exe, bool forwrite)
{
	int data[2];	/* pipe to send data child <--> parent */
	int err[2];		/* pipe to send errors child --> parent */
	int pid = -1;

	const int READ = 0;
	const int WRITE = 1;

	if (pipe(data) < 0)
		return -1;

	if (pipe(err) < 0)
	{
		close(data[READ]);
		close(data[WRITE]);
		return -1;
	}
#ifndef WIN32

	pid = fork_process();

	if (pid > 0) /* parent */
	{

		if (forwrite)
		{
			/* parent writes to child */
			close(data[READ]);
			pipes[EXEC_DATA_P] = data[WRITE];
		}
		else
		{
			/* parent reads from child */
			close(data[WRITE]);
			pipes[EXEC_DATA_P] = data[READ];
		}

		close(err[WRITE]);
		pipes[EXEC_ERR_P] = err[READ];

		return pid;
	}
	else if (pid == 0) /* child */
	{

		SIMPLE_FAULT_INJECTOR("popen_with_stderr_in_child");

		/*
		 * set up the data pipe
		 */
		if (forwrite)
		{
			close(data[WRITE]);
			close(fileno(stdin));

			if (dup2(data[READ], fileno(stdin)) < 0)
			{
				perror("could not redirect stdin to PROGRAM");
				_exit(EXIT_FAILURE);
			}

			/* no longer needed after the duplication */
			close(data[READ]);
		}
		else
		{
			close(data[READ]);
			close(fileno(stdout));

			if (dup2(data[WRITE], fileno(stdout)) < 0)
			{
				perror("could not redirect stdout to PROGRAM");
				_exit(EXIT_FAILURE);
			}

			/* no longer needed after the duplication */
			close(data[WRITE]);
		}

		/*
		 * now set up the error pipe
		 */
		close(err[READ]);
		close(fileno(stderr));

		if (dup2(err[WRITE], fileno(stderr)) < 0)
		{
			if(forwrite)
				close(data[WRITE]);
			else
				close(data[READ]);

			_exit(EXIT_FAILURE);
		}

		close(err[WRITE]);

		/* go ahead and execute the user command */
		execl("/bin/sh", "sh", "-c", exe, NULL);

		/* if we're here an error occurred */
		_exit(EXIT_FAILURE);
	}
	else
	{
		if(forwrite)
		{
			close(data[WRITE]);
			close(data[READ]);
		}
		else
		{
			close(data[READ]);
			close(data[WRITE]);
		}
		close(err[READ]);
		close(err[WRITE]);

		return -1;
	}
#endif

	return pid;
}

/*
 * pclose_with_stderr
 *
 * close our data and error pipes and return the child process
 * termination status. if child terminated with error, 'buf' will
 * point to the error string retrieved from child's stderr.
 */
int
pclose_with_stderr(int pid, int *pipes, StringInfo sinfo)
{
	int status = 0;

	/* close the data pipe. we can now read from error pipe without being blocked */
	close(pipes[EXEC_DATA_P]);

	read_err_msg(pipes[EXEC_ERR_P], sinfo);

	close(pipes[EXEC_ERR_P]);

	if (kill(pid, 0) == 0) /* process exists */
	{
	#ifndef WIN32
		waitpid(pid, &status, 0);
	#else
		status = -1;
	#endif
	}

	return status;
}


/*
 * read err msg from err pipe
 */
static void
read_err_msg(int fid, StringInfo sinfo)
{
	char ebuf[512];
	int ebuflen = 512;

	while (true)
	{
		int nread = read(fid, ebuf, ebuflen);

		if(nread == 0)
		{
			break;
		}
		else if(nread > 0)
		{
			appendBinaryStringInfo(sinfo, ebuf, nread);
		}
		else
		{
			appendStringInfoString(sinfo, "error string unavailable due to read error");
			break;
		}
	}

	if (sinfo->len > 0){
		write_log("read err msg from pipe, len:%d msg:%s", sinfo->len, sinfo->data);
	}
}
