/*
 * This is pseudo-terminal container for child process where parent creates a
 * proxy between the current std{in,out,etrr} and the child's pty. Advantages:
 *
 * - child has no access to parent's terminal (e.g. su --pty)
 * - parent can log all traffic between user and child's terminall (e.g. script(1))
 * - it's possible to start commands on terminal although parent has no terminal
 *
 * This code is in the public domain; do with it what you wish.
 *
 * Written by Karel Zak <kzak@redhat.com> in Jul 2019
 */
#include <stdio.h>
#include <stdlib.h>
#include <pty.h>
#include <poll.h>
#include <sys/signalfd.h>
#include <paths.h>
#include <sys/types.h>
#include <sys/wait.h>

#include "c.h"
#include "all-io.h"
#include "ttyutils.h"
#include "pty-session.h"
#include "monotonic.h"
#include "debug.h"

static UL_DEBUG_DEFINE_MASK(ulpty);
UL_DEBUG_DEFINE_MASKNAMES(ulpty) = UL_DEBUG_EMPTY_MASKNAMES;

#define ULPTY_DEBUG_INIT	(1 << 1)
#define ULPTY_DEBUG_SETUP	(1 << 2)
#define ULPTY_DEBUG_SIG		(1 << 3)
#define ULPTY_DEBUG_IO		(1 << 4)
#define ULPTY_DEBUG_DONE	(1 << 5)
#define ULPTY_DEBUG_ALL		0xFFFF

#define DBG(m, x)       __UL_DBG(ulpty, ULPTY_DEBUG_, m, x)
#define ON_DBG(m, x)    __UL_DBG_CALL(ulpty, ULPTY_DEBUG_, m, x)

#define UL_DEBUG_CURRENT_MASK   UL_DEBUG_MASK(ulpty)
#include "debugobj.h"

void ul_pty_init_debug(int mask)
{
	if (ulpty_debug_mask)
		return;
	__UL_INIT_DEBUG_FROM_ENV(ulpty, ULPTY_DEBUG_, mask, ULPTY_DEBUG);
}

struct ul_pty *ul_new_pty(int is_stdin_tty)
{
	struct ul_pty *pty = calloc(1, sizeof(*pty));

	if (!pty)
		return NULL;

	DBG(SETUP, ul_debugobj(pty, "alloc handler"));
	pty->isterm = is_stdin_tty;
	pty->master = -1;
	pty->slave = -1;
	pty->sigfd = -1;
	pty->child = (pid_t) -1;

	return pty;
}

void ul_free_pty(struct ul_pty *pty)
{
	free(pty);
}

void ul_pty_slave_echo(struct ul_pty *pty, int enable)
{
	assert(pty);
	pty->slave_echo = enable ? 1 : 0;
}

int ul_pty_get_delivered_signal(struct ul_pty *pty)
{
	assert(pty);
	return pty->delivered_signal;
}

struct ul_pty_callbacks *ul_pty_get_callbacks(struct ul_pty *pty)
{
	assert(pty);
	return &pty->callbacks;
}

void ul_pty_set_callback_data(struct ul_pty *pty, void *data)
{
	assert(pty);
	pty->callback_data = data;
}

void ul_pty_set_child(struct ul_pty *pty, pid_t child)
{
	assert(pty);
	pty->child = child;
}

int ul_pty_get_childfd(struct ul_pty *pty)
{
	assert(pty);
	return pty->master;
}

pid_t ul_pty_get_child(struct ul_pty *pty)
{
	assert(pty);
	return pty->child;
}

/* it's active when signals are redurected to sigfd */
int ul_pty_is_running(struct ul_pty *pty)
{
	assert(pty);
	return pty->sigfd >= 0;
}

void ul_pty_set_mainloop_time(struct ul_pty *pty, struct timeval *tv)
{
	assert(pty);
	if (!tv) {
		DBG(IO, ul_debugobj(pty, "mainloop time: clear"));
		timerclear(&pty->next_callback_time);
	} else {
		pty->next_callback_time.tv_sec = tv->tv_sec;
		pty->next_callback_time.tv_usec = tv->tv_usec;
		DBG(IO, ul_debugobj(pty, "mainloop time: %ld.%06ld", tv->tv_sec, tv->tv_usec));
	}
}

static void pty_signals_cleanup(struct ul_pty *pty)
{
	if (pty->sigfd != -1)
		close(pty->sigfd);
	pty->sigfd = -1;

	/* restore original setting */
	sigprocmask(SIG_SETMASK, &pty->orgsig, NULL);
}

/* call me before fork() */
int ul_pty_setup(struct ul_pty *pty)
{
	struct termios slave_attrs;
	sigset_t ourset;
	int rc = 0;

	assert(pty->sigfd == -1);

	/* save the current signals setting */
	sigprocmask(0, NULL, &pty->orgsig);

	if (pty->isterm) {
	        DBG(SETUP, ul_debugobj(pty, "create for terminal"));

		/* original setting of the current terminal */
		if (tcgetattr(STDIN_FILENO, &pty->stdin_attrs) != 0) {
			rc = -errno;
			goto done;
		}
		ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&pty->win);
		/* create master+slave */
		rc = openpty(&pty->master, &pty->slave, NULL, &pty->stdin_attrs, &pty->win);
		if (rc)
			goto done;

		/* set the current terminal to raw mode; pty_cleanup() reverses this change on exit */
		slave_attrs = pty->stdin_attrs;
		cfmakeraw(&slave_attrs);

		if (pty->slave_echo)
			slave_attrs.c_lflag |= ECHO;
		else
			slave_attrs.c_lflag &= ~ECHO;

		tcsetattr(STDIN_FILENO, TCSANOW, &slave_attrs);
	} else {
	        DBG(SETUP, ul_debugobj(pty, "create for non-terminal"));

		rc = openpty(&pty->master, &pty->slave, NULL, NULL, NULL);
		if (rc)
			goto done;

		tcgetattr(pty->slave, &slave_attrs);

		if (pty->slave_echo)
			slave_attrs.c_lflag |= ECHO;
		else
			slave_attrs.c_lflag &= ~ECHO;

		tcsetattr(pty->slave, TCSANOW, &slave_attrs);
	}

	sigfillset(&ourset);
	if (sigprocmask(SIG_BLOCK, &ourset, NULL)) {
		rc = -errno;
		goto done;
	}

	sigemptyset(&ourset);
	sigaddset(&ourset, SIGCHLD);
	sigaddset(&ourset, SIGWINCH);
	sigaddset(&ourset, SIGALRM);
	sigaddset(&ourset, SIGTERM);
	sigaddset(&ourset, SIGINT);
	sigaddset(&ourset, SIGQUIT);

	if (pty->callbacks.flush_logs)
		sigaddset(&ourset, SIGUSR1);

	if ((pty->sigfd = signalfd(-1, &ourset, SFD_CLOEXEC)) < 0)
		rc = -errno;
done:
	if (rc)
		ul_pty_cleanup(pty);

	DBG(SETUP, ul_debugobj(pty, "pty setup done [master=%d, slave=%d, rc=%d]",
				pty->master, pty->slave, rc));
	return rc;
}

/* cleanup in parent process */
void ul_pty_cleanup(struct ul_pty *pty)
{
	struct termios rtt;

	pty_signals_cleanup(pty);

	if (pty->master == -1 || !pty->isterm)
		return;

	DBG(DONE, ul_debugobj(pty, "cleanup"));
	rtt = pty->stdin_attrs;
	tcsetattr(STDIN_FILENO, TCSADRAIN, &rtt);
}

/* call me in child process */
void ul_pty_init_slave(struct ul_pty *pty)
{
	DBG(SETUP, ul_debugobj(pty, "initialize slave"));

	setsid();

	ioctl(pty->slave, TIOCSCTTY, 1);
	close(pty->master);

	dup2(pty->slave, STDIN_FILENO);
	dup2(pty->slave, STDOUT_FILENO);
	dup2(pty->slave, STDERR_FILENO);

	close(pty->slave);

	if (pty->sigfd >= 0)
		close(pty->sigfd);

	pty->slave = -1;
	pty->master = -1;
	pty->sigfd = -1;

	sigprocmask(SIG_SETMASK, &pty->orgsig, NULL);

	DBG(SETUP, ul_debugobj(pty, "... initialize slave done"));
}

static int write_output(char *obuf, ssize_t bytes)
{
	DBG(IO, ul_debug(" writing output"));

	if (write_all(STDOUT_FILENO, obuf, bytes)) {
		DBG(IO, ul_debug("  writing output *failed*"));
		return -errno;
	}

	return 0;
}

static int write_to_child(struct ul_pty *pty, char *buf, size_t bufsz)
{
	return write_all(pty->master, buf, bufsz);
}

/*
 * The pty is usually faster than shell, so it's a good idea to wait until
 * the previous message has been already read by shell from slave before we
 * write to master. This is necessary especially for EOF situation when we can
 * send EOF to master before shell is fully initialized, to workaround this
 * problem we wait until slave is empty. For example:
 *
 *   echo "date" | su --pty
 *
 * Unfortunately, the child (usually shell) can ignore stdin at all, so we
 * don't wait forever to avoid dead locks...
 *
 * Note that su --pty is primarily designed for interactive sessions as it
 * maintains master+slave tty stuff within the session. Use pipe to write to
 * pty and assume non-interactive (tee-like) behavior is NOT well supported.
 */
void ul_pty_write_eof_to_child(struct ul_pty *pty)
{
	unsigned int tries = 0;
	struct pollfd fds[] = {
	           { .fd = pty->slave, .events = POLLIN }
	};
	char c = DEF_EOF;

	DBG(IO, ul_debugobj(pty, " waiting for empty slave"));
	while (poll(fds, 1, 10) == 1 && tries < 8) {
		DBG(IO, ul_debugobj(pty, "   slave is not empty"));
		xusleep(250000);
		tries++;
	}
	if (tries < 8)
		DBG(IO, ul_debugobj(pty, "   slave is empty now"));

	DBG(IO, ul_debugobj(pty, " sending EOF to master"));
	write_to_child(pty, &c, sizeof(char));
}

static int mainloop_callback(struct ul_pty *pty)
{
	int rc;

	if (!pty->callbacks.mainloop)
		return 0;

	DBG(IO, ul_debugobj(pty, "calling mainloop callback"));
	rc = pty->callbacks.mainloop(pty->callback_data);

	DBG(IO, ul_debugobj(pty, " callback done [rc=%d]", rc));
	return rc;
}

static int handle_io(struct ul_pty *pty, int fd, int *eof)
{
	char buf[BUFSIZ];
	ssize_t bytes;
	int rc = 0;

	DBG(IO, ul_debugobj(pty, " handle I/O on fd=%d", fd));
	*eof = 0;

	/* read from active FD */
	bytes = read(fd, buf, sizeof(buf));
	if (bytes < 0) {
		if (errno == EAGAIN || errno == EINTR)
			return 0;
		return -errno;
	}

	if (bytes == 0) {
		*eof = 1;
		return 0;
	}

	/* from stdin (user) to command */
	if (fd == STDIN_FILENO) {
		DBG(IO, ul_debugobj(pty, " stdin --> master %zd bytes", bytes));

		if (write_to_child(pty, buf, bytes))
			return -errno;

		/* without sync write_output() will write both input &
		 * shell output that looks like double echoing */
		fdatasync(pty->master);

	/* from command (master) to stdout */
	} else if (fd == pty->master) {
		DBG(IO, ul_debugobj(pty, " master --> stdout %zd bytes", bytes));
		write_output(buf, bytes);
	}

	if (pty->callbacks.log_stream_activity)
		rc = pty->callbacks.log_stream_activity(
					pty->callback_data, fd, buf, bytes);

	return rc;
}

void ul_pty_wait_for_child(struct ul_pty *pty)
{
	int status;
	pid_t pid;
	int options = 0;

	if (pty->child == (pid_t) -1)
		return;

	DBG(SIG, ul_debug("waiting for child [child=%d]", (int) pty->child));

	if (ul_pty_is_running(pty)) {
		/* wait for specific child */
		options = WNOHANG;
		for (;;) {
			pid = waitpid(pty->child, &status, options);
			DBG(SIG, ul_debug(" waitpid done [rc=%d]", (int) pid));
			if (pid != (pid_t) - 1) {
				if (pty->callbacks.child_die)
					pty->callbacks.child_die(
							pty->callback_data,
							pty->child, status);
				ul_pty_set_child(pty, (pid_t) -1);
			} else
				break;
		}
	} else {
		/* final wait */
		while ((pid = wait3(&status, options, NULL)) > 0) {
			DBG(SIG, ul_debug(" wait3 done [rc=%d]", (int) pid));
			if (pid == pty->child) {
				if (pty->callbacks.child_die)
					pty->callbacks.child_die(
							pty->callback_data,
							pty->child, status);
				ul_pty_set_child(pty, (pid_t) -1);
			}
		}
	}
}

static int handle_signal(struct ul_pty *pty, int fd)
{
	struct signalfd_siginfo info;
	ssize_t bytes;
	int rc = 0;

	DBG(SIG, ul_debugobj(pty, " handle signal on fd=%d", fd));

	bytes = read(fd, &info, sizeof(info));
	if (bytes != sizeof(info)) {
		if (bytes < 0 && (errno == EAGAIN || errno == EINTR))
			return 0;
		return -errno;
	}

	switch (info.ssi_signo) {
	case SIGCHLD:
		DBG(SIG, ul_debugobj(pty, " get signal SIGCHLD"));

		if (info.ssi_code == CLD_EXITED
		    || info.ssi_code == CLD_KILLED
		    || info.ssi_code == CLD_DUMPED) {

			if (pty->callbacks.child_wait)
				pty->callbacks.child_wait(pty->callback_data,
							  pty->child);
			else
				ul_pty_wait_for_child(pty);

		} else if (info.ssi_status == SIGSTOP && pty->child > 0)
			pty->callbacks.child_sigstop(pty->callback_data,
						     pty->child);

		if (pty->child <= 0) {
			DBG(SIG, ul_debugobj(pty, " no child, setting leaving timeout"));
			pty->poll_timeout = 10;
			timerclear(&pty->next_callback_time);
		}
		return 0;
	case SIGWINCH:
		DBG(SIG, ul_debugobj(pty, " get signal SIGWINCH"));
		if (pty->isterm) {
			ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&pty->win);
			ioctl(pty->slave, TIOCSWINSZ, (char *)&pty->win);

			if (pty->callbacks.log_signal)
				rc = pty->callbacks.log_signal(pty->callback_data,
							&info, (void *) &pty->win);
		}
		break;
	case SIGTERM:
		/* fallthrough */
	case SIGINT:
		/* fallthrough */
	case SIGQUIT:
		DBG(SIG, ul_debugobj(pty, " get signal SIG{TERM,INT,QUIT}"));
		pty->delivered_signal = info.ssi_signo;
                /* Child termination is going to generate SIGCHILD (see above) */
		if (pty->child > 0)
	                kill(pty->child, SIGTERM);

		if (pty->callbacks.log_signal)
			rc = pty->callbacks.log_signal(pty->callback_data,
					&info, (void *) &pty->win);
		break;
	case SIGUSR1:
		DBG(SIG, ul_debugobj(pty, " get signal SIGUSR1"));
		if (pty->callbacks.flush_logs)
			rc = pty->callbacks.flush_logs(pty->callback_data);
		break;
	default:
		abort();
	}

	return rc;
}

/* loop in parent */
int ul_pty_proxy_master(struct ul_pty *pty)
{
	int rc = 0, ret, eof = 0;
	enum {
		POLLFD_SIGNAL = 0,
		POLLFD_MASTER,
		POLLFD_STDIN

	};
	struct pollfd pfd[] = {
		[POLLFD_SIGNAL] = { .fd = -1,		.events = POLLIN | POLLERR | POLLHUP },
		[POLLFD_MASTER] = { .fd = pty->master,  .events = POLLIN | POLLERR | POLLHUP },
		[POLLFD_STDIN]	= { .fd = STDIN_FILENO, .events = POLLIN | POLLERR | POLLHUP }
	};

	/* We use signalfd and standard signals by handlers are blocked
	 * at all
	 */
	assert(pty->sigfd >= 0);

	pfd[POLLFD_SIGNAL].fd = pty->sigfd;
	pty->poll_timeout = -1;

	while (!pty->delivered_signal) {
		size_t i;
		int errsv, timeout;

		DBG(IO, ul_debugobj(pty, "--poll() loop--"));

		/* note, callback usually updates @next_callback_time */
		if (timerisset(&pty->next_callback_time)) {
			struct timeval now;

			DBG(IO, ul_debugobj(pty, " callback requested"));
			gettime_monotonic(&now);
			if (timercmp(&now, &pty->next_callback_time, >)) {
				rc = mainloop_callback(pty);
				if (rc)
					break;
			}
		}

		/* set timeout */
		if (timerisset(&pty->next_callback_time)) {
			struct timeval now, rest;

			gettime_monotonic(&now);
			timersub(&pty->next_callback_time, &now, &rest);
			timeout = (rest.tv_sec * 1000) +  (rest.tv_usec / 1000);
		} else
			timeout = pty->poll_timeout;

		/* wait for input, signal or timeout */
		DBG(IO, ul_debugobj(pty, "calling poll() [timeout=%dms]", timeout));
		ret = poll(pfd, ARRAY_SIZE(pfd), timeout);

		errsv = errno;
		DBG(IO, ul_debugobj(pty, "poll() rc=%d", ret));

		/* error */
		if (ret < 0) {
			if (errsv == EAGAIN)
				continue;
			rc = -errno;
			break;
		}

		/* timeout */
		if (ret == 0) {
			if (timerisset(&pty->next_callback_time)) {
				rc = mainloop_callback(pty);
				if (rc == 0)
					continue;
			} else
				rc = 0;

			DBG(IO, ul_debugobj(pty, "leaving poll() loop [timeout=%d, rc=%d]", timeout, rc));
			break;
		}
		/* event */
		for (i = 0; i < ARRAY_SIZE(pfd); i++) {
			rc = 0;

			if (pfd[i].revents == 0)
				continue;

			DBG(IO, ul_debugobj(pty, " active pfd[%s].fd=%d %s %s %s %s",
						i == POLLFD_STDIN  ? "stdin" :
						i == POLLFD_MASTER ? "master" :
						i == POLLFD_SIGNAL ? "signal" : "???",
						pfd[i].fd,
						pfd[i].revents & POLLIN  ? "POLLIN" : "",
						pfd[i].revents & POLLHUP ? "POLLHUP" : "",
						pfd[i].revents & POLLERR ? "POLLERR" : "",
						pfd[i].revents & POLLNVAL ? "POLLNVAL" : ""));

			switch (i) {
			case POLLFD_STDIN:
			case POLLFD_MASTER:
				/* data */
				if (pfd[i].revents & POLLIN)
					rc = handle_io(pty, pfd[i].fd, &eof);
				/* EOF maybe detected by two ways:
				 *	A) poll() return POLLHUP event after close()
				 *	B) read() returns 0 (no data)
				 *
				 * POLLNVAL means that fd is closed.
				 */
				if ((pfd[i].revents & POLLHUP) || (pfd[i].revents & POLLNVAL) || eof) {
					DBG(IO, ul_debugobj(pty, " ignore FD"));
					pfd[i].fd = -1;
					if (i == POLLFD_STDIN) {
						ul_pty_write_eof_to_child(pty);
						DBG(IO, ul_debugobj(pty, "  ignore STDIN"));
					}
				}
				continue;
			case POLLFD_SIGNAL:
				rc = handle_signal(pty, pfd[i].fd);
				break;
			}
			if (rc)
				break;
		}
	}

	pty_signals_cleanup(pty);

	DBG(IO, ul_debug("poll() done [signal=%d, rc=%d]", pty->delivered_signal, rc));
	return rc;
}

#ifdef TEST_PROGRAM_PTY
/*
 * $ make test_pty
 * $ ./test_pty
 *
 * ... and see for example tty(1) or "ps afu"
 */
static void child_sigstop(void *data __attribute__((__unused__)), pid_t child)
{
	kill(getpid(), SIGSTOP);
	kill(child, SIGCONT);
}

int main(int argc, char *argv[])
{
	struct ul_pty_callbacks *cb;
	const char *shell, *command = NULL, *shname = NULL;
	int caught_signal = 0;
	pid_t child;
	struct ul_pty *pty;

	shell = getenv("SHELL");
	if (shell == NULL)
		shell = _PATH_BSHELL;
	if (argc == 2)
		command = argv[1];

	ul_pty_init_debug(0);

	pty = ul_new_pty(isatty(STDIN_FILENO));
	if (!pty)
		err(EXIT_FAILURE, "failed to allocate PTY handler");

	cb = ul_pty_get_callbacks(pty);
	cb->child_sigstop = child_sigstop;

	if (ul_pty_setup(pty))
		err(EXIT_FAILURE, "failed to create pseudo-terminal");

	fflush(stdout);			/* ??? */

	switch ((int) (child = fork())) {
	case -1: /* error */
		ul_pty_cleanup(pty);
		err(EXIT_FAILURE, "cannot create child process");
		break;

	case 0: /* child */
		ul_pty_init_slave(pty);

		signal(SIGTERM, SIG_DFL); /* because /etc/csh.login */

		shname = strrchr(shell, '/');
		shname = shname ? shname + 1 : shell;

		if (command)
			execl(shell, shname, "-c", command, NULL);
		else
			execl(shell, shname, "-i", NULL);
		err(EXIT_FAILURE, "failed to execute %s", shell);
		break;

	default:
		break;
	}

	/* parent */
	ul_pty_set_child(pty, child);

	/* this is the main loop */
	ul_pty_proxy_master(pty);

	/* all done; cleanup and kill */
	caught_signal = ul_pty_get_delivered_signal(pty);

	if (!caught_signal && ul_pty_get_child(pty) != (pid_t)-1)
		ul_pty_wait_for_child(pty);	/* final wait */

	if (caught_signal && ul_pty_get_child(pty) != (pid_t)-1) {
		fprintf(stderr, "\nSession terminated, killing shell...");
		kill(child, SIGTERM);
		sleep(2);
		kill(child, SIGKILL);
		fprintf(stderr, " ...killed.\n");
	}

	ul_pty_cleanup(pty);
	ul_free_pty(pty);
	return EXIT_SUCCESS;
}

#endif /* TEST_PROGRAM */

