/*
 * kmscon - Pseudo Terminal Handling
 *
 * Copyright (c) 2012 Ran Benita <ran234@gmail.com>
 *
 * 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 <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <pthread.h>
#include <pty.h>
#include <signal.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/signalfd.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#include "pty.h"
#include "shl/eloop.h"
#include "shl/log.h"
#include "shl/misc.h"
#include "shl/ring.h"

#define LOG_SUBSYSTEM "pty"

#define KMSCON_NREAD 16384

#define MAX_RETRY_TIME 2
#define MAX_RETRY_COUNT 5

struct kmscon_pty {
	unsigned long ref;
	struct ev_eloop *eloop;

	int fd;
	pid_t child;
	struct ev_fd *efd;
	struct shl_ring *msgbuf;
	char io_buf[KMSCON_NREAD];

	kmscon_pty_input_cb input_cb;
	void *data;

	char *term;
	char *colorterm;
	char **argv;
	char *seat;
	char *vtnr;
	bool env_reset;
	bool backspace_delete;

	time_t last_spawn_time;
	int retry_count;
};

int kmscon_pty_new(struct kmscon_pty **out, kmscon_pty_input_cb input_cb, void *data)
{
	struct kmscon_pty *pty;
	int ret;

	if (!out || !input_cb)
		return -EINVAL;

	pty = malloc(sizeof(*pty));
	if (!pty)
		return -ENOMEM;

	memset(pty, 0, sizeof(*pty));
	pty->fd = -1;
	pty->ref = 1;
	pty->input_cb = input_cb;
	pty->last_spawn_time = time(NULL);
	pty->retry_count = 0;
	pty->data = data;

	ret = ev_eloop_new(&pty->eloop);
	if (ret)
		goto err_free;

	ret = shl_ring_new(&pty->msgbuf);
	if (ret)
		goto err_eloop;

	log_debug("new pty object");
	*out = pty;
	return 0;

err_eloop:
	ev_eloop_unref(pty->eloop);
err_free:
	free(pty);
	return ret;
}

void kmscon_pty_ref(struct kmscon_pty *pty)
{
	if (!pty)
		return;

	pty->ref++;
}

void kmscon_pty_unref(struct kmscon_pty *pty)
{
	if (!pty || !pty->ref || --pty->ref)
		return;

	log_debug("free pty object");
	kmscon_pty_close(pty);
	free(pty->vtnr);
	free(pty->seat);
	free(pty->argv);
	free(pty->colorterm);
	free(pty->term);
	shl_ring_free(pty->msgbuf);
	ev_eloop_unref(pty->eloop);
	free(pty);
}

int kmscon_pty_set_conf(struct kmscon_pty *pty, const char *term, const char *colorterm,
			char **argv, const char *seat, unsigned int vtnr, bool do_reset,
			bool backspace)
{
	char *vt;

	if (!pty)
		return -EINVAL;

	pty->env_reset = do_reset;
	pty->backspace_delete = backspace;

	if (term) {
		pty->term = strdup(term);
		if (!pty->term)
			return -ENOMEM;
	}
	if (colorterm) {
		pty->colorterm = strdup(colorterm);
		if (!pty->colorterm)
			return -ENOMEM;
	}
	if (seat) {
		pty->seat = strdup(seat);
		if (!pty->seat)
			return -ENOMEM;
	}
	if (vtnr) {
		if (asprintf(&vt, "%u", vtnr) < 0)
			return -ENOMEM;
		pty->vtnr = vt;
	}
	if (argv && *argv && **argv)
		return shl_dup_array(&pty->argv, argv);
	return 0;
}

int kmscon_pty_get_fd(struct kmscon_pty *pty)
{
	if (!pty)
		return -EINVAL;

	return ev_eloop_get_fd(pty->eloop);
}

void kmscon_pty_dispatch(struct kmscon_pty *pty)
{
	if (!pty)
		return;

	ev_eloop_dispatch(pty->eloop, 0);
}

static bool pty_is_open(struct kmscon_pty *pty)
{
	return pty->fd >= 0;
}

static void __attribute__((noreturn)) exec_child(const char *term, const char *colorterm,
						 char **argv, const char *seat, const char *vtnr,
						 bool env_reset, bool drm)
{
	char **env;
	char **def_argv;
	char **argv_parsed;
	int ret;

	if (env_reset) {
		env = malloc(sizeof(char *));
		if (!env) {
			log_error("cannot allocate memory for environment (%d): %m", errno);
			exit(EXIT_FAILURE);
		}

		memset(env, 0, sizeof(char *));
		environ = env;

		def_argv = (char *[]){"/bin/login", "-p", NULL};
	} else {
		def_argv = (char *[]){"/bin/login", NULL};
	}

	if (!term)
		term = "vt220";
	if (!argv)
		argv = def_argv;

	setenv("TERM", term, 1);
	if (colorterm)
		setenv("COLORTERM", colorterm, 1);
	if (seat)
		setenv("XDG_SEAT", seat, 1);
	if (vtnr)
		setenv("XDG_VTNR", vtnr, 1);

	if (drm) {
		setenv("KMS_START_SCRIPT", "kmscon-launch-gui", 1);
		setenv("TERM_SESSION_TYPE", "kms", 1);
	} else {
		setenv("TERM_SESSION_TYPE", "fb", 1);
	}
	ret = shl_replace_array_with_env(&argv_parsed, argv);
	if (ret)
		argv_parsed = argv;

	execve(argv_parsed[0], argv_parsed, environ);

	log_err("failed to exec child %s: %m", argv[0]);

	exit(EXIT_FAILURE);
}

static void setup_child(int master, bool backspace_delete, struct winsize *ws)
{
	int ret;
	sigset_t sigset;
	pid_t pid;
	char slave_name[128];
	int slave = -1, i;
	struct termios attr;

	/* The child should not inherit our signal mask. */
	sigemptyset(&sigset);
	ret = pthread_sigmask(SIG_SETMASK, &sigset, NULL);
	if (ret)
		log_warn("cannot reset blocked signals: %m");

	for (i = 1; i < SIGSYS; ++i)
		signal(i, SIG_DFL);

	ret = grantpt(master);
	if (ret < 0) {
		log_err("grantpt failed: %m");
		goto err_out;
	}

	ret = unlockpt(master);
	if (ret < 0) {
		log_err("cannot unlock pty: %m");
		goto err_out;
	}

	ret = ptsname_r(master, slave_name, sizeof(slave_name));
	if (ret) {
		log_err("cannot find slave name: %m");
		goto err_out;
	}

	/* This also loses our controlling tty. */
	pid = setsid();
	if (pid < 0) {
		log_err("cannot start a new session: %m");
		goto err_out;
	}

	/* And the slave pty becomes our controlling tty. */
	slave = open(slave_name, O_RDWR | O_CLOEXEC);
	if (slave < 0) {
		log_err("cannot open slave: %m");
		goto err_out;
	}

	/* get terminal attributes */
	if (tcgetattr(slave, &attr) < 0) {
		log_err("cannot get terminal attributes: %m");
		goto err_out;
	}

	if (backspace_delete) {
		/* erase character should be delete */
		attr.c_cc[VERASE] = 0177;
	} else {
		/* erase character should be normal backspace */
		attr.c_cc[VERASE] = 010;
	}

	/* set changed terminal attributes */
	if (tcsetattr(slave, TCSANOW, &attr) < 0) {
		log_warn("cannot set terminal attributes: %m");
		goto err_out;
	}

	if (ws) {
		ret = ioctl(slave, TIOCSWINSZ, ws);
		if (ret)
			log_warn("cannot set slave window size: %m");
	}

	if (dup2(slave, STDIN_FILENO) != STDIN_FILENO ||
	    dup2(slave, STDOUT_FILENO) != STDOUT_FILENO ||
	    dup2(slave, STDERR_FILENO) != STDERR_FILENO) {
		log_err("cannot duplicate slave: %m");
		goto err_out;
	}

	close(master);
	close(slave);
	return;

err_out:
	ret = -errno;
	if (slave >= 0)
		close(slave);
	close(master);
	exit(EXIT_FAILURE);
}

/*
 * This is functionally equivalent to forkpty(3). We do it manually to obtain
 * a little bit more control of the process, and as a bonus avoid linking to
 * the libutil library in glibc.
 */
static int pty_spawn(struct kmscon_pty *pty, int master, unsigned short width,
		     unsigned short height, bool drm)
{
	pid_t pid;
	struct winsize ws;

	memset(&ws, 0, sizeof(ws));
	ws.ws_col = width;
	ws.ws_row = height;

	pid = fork();
	switch (pid) {
	case -1:
		log_err("cannot fork: %m");
		return -errno;
	case 0:
		setup_child(master, pty->backspace_delete, &ws);
		exec_child(pty->term, pty->colorterm, pty->argv, pty->seat, pty->vtnr,
			   pty->env_reset, drm);
		exit(EXIT_FAILURE);
	default:
		log_debug("forking child %d", pid);
		pty->fd = master;
		pty->child = pid;
		break;
	}

	return 0;
}

static int send_buf(struct kmscon_pty *pty)
{
	const char *buf;
	size_t len;
	int ret;

	while ((buf = shl_ring_peek(pty->msgbuf, &len, 0))) {
		ret = write(pty->fd, buf, len);
		if (ret > 0) {
			shl_ring_drop(pty->msgbuf, ret);
			continue;
		}

		if (ret < 0 && errno != EWOULDBLOCK) {
			log_warn("cannot write to child process (%d): %m", errno);
			return ret;
		}

		/* EWOULDBLOCK */
		return 0;
	}

	ev_fd_update(pty->efd, EV_READABLE | EV_ET);
	return 0;
}

static int read_buf(struct kmscon_pty *pty)
{
	ssize_t len, num;
	int mask;

	/* Use a maximum of 50 steps to avoid staying here forever.
	 * TODO: recheck where else a user might flush our queues and try to
	 * install an explicit policy. */
	num = 50;
	do {
		len = read(pty->fd, pty->io_buf, sizeof(pty->io_buf));
		if (len > 0) {
			if (pty->input_cb)
				pty->input_cb(pty, pty->io_buf, len, pty->data);
		} else if (len == 0) {
			log_debug("HUP during read on pty of child %d", pty->child);
			break;
		} else if (errno != EWOULDBLOCK) {
			log_debug("cannot read from pty of child %d (%d): %m", pty->child, errno);
			break;
		}
	} while (len > 0 && --num);

	if (!num) {
		log_debug("cannot read application data fast enough");

		/* We are edge-triggered so update the mask to get the
		 * EV_READABLE event again next round. */
		mask = EV_READABLE | EV_ET;
		if (!shl_ring_is_empty(pty->msgbuf))
			mask |= EV_WRITEABLE;
		ev_fd_update(pty->efd, mask);
	}

	return 0;
}

static void pty_input(struct ev_fd *fd, int mask, void *data)
{
	struct kmscon_pty *pty = data;

	/* Programs like /bin/login tend to perform a vhangup() on their TTY
	 * before running the login procedure. This also causes the pty master
	 * to get a EV_HUP event as long as no client has the TTY opened.
	 * This means, we cannot use the TTY connection as reliable way to track
	 * the client. Instead, we _must_ rely on the PID of the client to track
	 * them.
	 * However, this has the side effect that if the client forks and the
	 * parent exits, we loose them and restart the client. But this seems to
	 * be the expected behavior so we implement it here.
	 * Unfortunately, epoll always polls for EPOLLHUP so as long as the
	 * vhangup() is ongoing, we will _always_ get EPOLLHUP and cannot sleep.
	 * This gets worse if the client closes the TTY but doesn't exit.
	 * Therefore, we set the fd as edge-triggered in the epoll-set so we
	 * only get the events once they change. This has to be taken into
	 * account at all places of kmscon_pty to avoid missing events. */

	if (mask & EV_ERR)
		log_warn("error on pty socket of child %d", pty->child);
	if (mask & EV_HUP)
		log_debug("HUP on pty of child %d", pty->child);
	if (mask & EV_WRITEABLE)
		send_buf(pty);
	if (mask & EV_READABLE)
		read_buf(pty);
}

static void sig_child(struct ev_eloop *eloop, struct ev_child_data *chld, void *data)
{
	struct kmscon_pty *pty = data;
	time_t current_time;

	if (chld->pid != pty->child)
		return;

	log_info("child exited: pid: %u status: %d", chld->pid, chld->status);

	if (pty->retry_count == MAX_RETRY_COUNT) {
		log_err("reached max retry attempts for login process");
		return;
	}

	current_time = time(NULL);

	if (current_time - pty->last_spawn_time < MAX_RETRY_TIME) {
		pty->retry_count++;
		pty->last_spawn_time = current_time;
	} else {
		pty->retry_count = 0;
	}

	pty->input_cb(pty, NULL, 0, pty->data);
}

int kmscon_pty_open(struct kmscon_pty *pty, unsigned short width, unsigned short height, bool drm)
{
	int ret;
	int master;

	if (!pty)
		return -EINVAL;

	if (pty_is_open(pty))
		return -EALREADY;

	master = posix_openpt(O_RDWR | O_NOCTTY | O_CLOEXEC | O_NONBLOCK);
	if (master < 0) {
		log_err("cannot open master: %m");
		return -errno;
	}

	ret = ev_eloop_new_fd(pty->eloop, &pty->efd, master, EV_ET | EV_READABLE, pty_input, pty);
	if (ret)
		goto err_master;

	ret = ev_eloop_register_child_cb(pty->eloop, sig_child, pty);
	if (ret)
		goto err_fd;

	ret = pty_spawn(pty, master, width, height, drm);
	if (ret)
		goto err_sig;

	return 0;

err_sig:
	ev_eloop_unregister_child_cb(pty->eloop, sig_child, pty);
err_fd:
	ev_eloop_rm_fd(pty->efd);
	pty->efd = NULL;
err_master:
	close(master);
	return ret;
}

void kmscon_pty_close(struct kmscon_pty *pty)
{
	if (!pty || !pty_is_open(pty))
		return;

	ev_eloop_rm_fd(pty->efd);
	pty->efd = NULL;
	ev_eloop_unregister_child_cb(pty->eloop, sig_child, pty);
	close(pty->fd);
	pty->fd = -1;
}

int kmscon_pty_write(struct kmscon_pty *pty, const char *u8, size_t len)
{
	int ret;

	if (!pty || !pty_is_open(pty) || !u8 || !len)
		return -EINVAL;

	if (!shl_ring_is_empty(pty->msgbuf))
		goto buf;

	ret = write(pty->fd, u8, len);
	if (ret < 0) {
		if (errno != EWOULDBLOCK) {
			log_warn("cannot write to child process");
			return ret;
		}
	} else if (ret >= len) {
		return 0;
	} else if (ret > 0) {
		len -= ret;
		u8 = &u8[ret];
	}

	ev_fd_update(pty->efd, EV_READABLE | EV_WRITEABLE | EV_ET);

buf:
	ret = shl_ring_write(pty->msgbuf, u8, len);
	if (ret)
		log_warn("cannot allocate buffer; dropping output");

	return 0;
}

void kmscon_pty_signal(struct kmscon_pty *pty, int signum)
{
	int ret;

	if (!pty || !pty_is_open(pty) || signum < 0)
		return;

	ret = ioctl(pty->fd, TIOCSIG, signum);
	if (ret) {
		log_warn("cannot send signal %d to child", signum);
		return;
	}

	log_debug("send signal %d to child", signum);
}

int kmscon_pty_get_slave_name(struct kmscon_pty *pty, char *buf, size_t len)
{
	if (!pty || !buf || !len || !pty_is_open(pty))
		return -EINVAL;

	return ptsname_r(pty->fd, buf, len);
}

void kmscon_pty_resize(struct kmscon_pty *pty, unsigned short width, unsigned short height)
{
	int ret;
	struct winsize ws;

	if (!pty || !pty_is_open(pty))
		return;

	memset(&ws, 0, sizeof(ws));
	ws.ws_col = width;
	ws.ws_row = height;

	/*
	 * This will send SIGWINCH to the pty slave foreground process group.
	 * We will also get one, but we don't need it.
	 */
	ret = ioctl(pty->fd, TIOCSWINSZ, &ws);
	if (ret) {
		log_warn("cannot set window size");
		return;
	}
}
