/*\
 *  Copyright (C) International Business Machines  Corp., 2005
 *  Author(s): Anthony Liguori <aliguori@us.ibm.com>
 *
 *  Xen Console Daemon
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; under version 2 of the License.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program; If not, see <http://www.gnu.org/licenses/>.
\*/

#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <termios.h>
#include <signal.h>
#include <getopt.h>
#include <sys/select.h>
#include <err.h>
#include <string.h>
#ifdef __sun__
#include <sys/stropts.h>
#endif

#include <xenstore.h>
#include "xenctrl.h"
#include "_paths.h"

#define ESCAPE_CHARACTER 0x1d

static volatile sig_atomic_t received_signal = 0;
static char lockfile[sizeof (XEN_LOCK_DIR "/xenconsole.") + 8] = { 0 };
static int lockfd = -1;

static void sighandler(int signum)
{
	received_signal = 1;
}

static bool write_sync(int fd, const void *data, size_t size)
{
	size_t offset = 0;
	ssize_t len;

	while (offset < size) {
		len = write(fd, data + offset, size - offset);
		if (len < 1) {
			return false;
		}
		offset += len;
	}

	return true;
}

static void usage(const char *program) {
	printf("Usage: %s [OPTION] DOMID\n"
	       "Attaches to a virtual domain console\n"
	       "\n"
	       "  -h, --help       display this help and exit\n"
	       "  -n, --num N      use console number N\n"
	       "  --type TYPE      console type. must be 'pv' or 'serial'\n"
	       "  --start-notify-fd N file descriptor used to notify parent\n"
	       , program);
}

#ifdef	__sun__
void cfmakeraw(struct termios *termios_p)
{
	termios_p->c_iflag &=
	    ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON);
	termios_p->c_oflag &= ~OPOST;
	termios_p->c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
	termios_p->c_cflag &= ~(CSIZE|PARENB);
	termios_p->c_cflag |= CS8;

	termios_p->c_cc[VMIN] = 0;
	termios_p->c_cc[VTIME] = 0;
}
#endif

static int get_pty_fd(struct xs_handle *xs, char *path, int seconds)
/* Check for a pty in xenstore, open it and return its fd.
 * Assumes there is already a watch set in the store for this path. */
{
	struct timeval tv;
	fd_set watch_fdset;
	int xs_fd = xs_fileno(xs), pty_fd = -1;
	int start, now;
	unsigned int len = 0;
	char *pty_path, **watch_paths;

	start = now = time(NULL);
	do {
		tv.tv_usec = 0;
		tv.tv_sec = (start + seconds) - now;
		FD_ZERO(&watch_fdset);
		FD_SET(xs_fd, &watch_fdset);
		if (select(xs_fd + 1, &watch_fdset, NULL, NULL, &tv)) {
			/* Read the watch to drain the buffer */
			watch_paths = xs_read_watch(xs, &len);
			free(watch_paths);
			/* We only watch for one thing, so no need to 
			 * disambiguate: just read the pty path */
			pty_path = xs_read(xs, XBT_NULL, path, &len);
			if (pty_path != NULL && pty_path[0] != '\0') {
				pty_fd = open(pty_path, O_RDWR | O_NOCTTY);
				if (pty_fd == -1)
					warn("Could not open tty `%s'", pty_path);
			}
			free(pty_path);
		}
	} while (pty_fd == -1 && (now = time(NULL)) < start + seconds);

#ifdef __sun__
	if (pty_fd != -1) {
		struct termios term;

		/*
		 * The pty may come from either xend (with pygrub) or
		 * xenconsoled.  It may have tty semantics set up, or not.
		 * While it isn't strictly necessary to have those
		 * semantics here, it is good to have a consistent
		 * state that is the same as under Linux.
		 *
		 * If tcgetattr fails, they have not been set up,
		 * so go ahead and set them up now, by pushing the
		 * ptem and ldterm streams modules.
		 */
		if (tcgetattr(pty_fd, &term) < 0) {
			ioctl(pty_fd, I_PUSH, "ptem");
			ioctl(pty_fd, I_PUSH, "ldterm");
		}
	}
#endif

	return pty_fd;
}


/* don't worry too much if setting terminal attributes fail */
static void init_term(int fd, struct termios *old)
{
	struct termios new_term;

	if (tcgetattr(fd, old) == -1)
		return;

	new_term = *old;
	cfmakeraw(&new_term);

	tcsetattr(fd, TCSANOW, &new_term);
}

static void restore_term(int fd, struct termios *old)
{
	tcsetattr(fd, TCSANOW, old);
}

static int console_loop(int fd, struct xs_handle *xs, char *pty_path,
		        bool interactive)
{
	int ret, xs_fd = xs_fileno(xs), max_fd = -1;

	do {
		fd_set fds;

		FD_ZERO(&fds);
		if (interactive) {
			FD_SET(STDIN_FILENO, &fds);
			max_fd = STDIN_FILENO;
		}
		FD_SET(xs_fd, &fds);
		if (xs_fd > max_fd) max_fd = xs_fd;
		if (fd != -1) FD_SET(fd, &fds);
		if (fd > max_fd) max_fd = fd;

		ret = select(max_fd + 1, &fds, NULL, NULL, NULL);
		if (ret == -1) {
			if (errno == EINTR || errno == EAGAIN) {
				continue;
			}
			return -1;
		}

		if (FD_ISSET(xs_fileno(xs), &fds)) {
			int newfd = get_pty_fd(xs, pty_path, 0);
			if (fd != -1)
				close(fd);
                        if (newfd == -1) 
				/* Console PTY has become invalid */
				return 0;
			fd = newfd;
			continue;
		}

		if (FD_ISSET(STDIN_FILENO, &fds)) {
			ssize_t len;
			char msg[60];

			len = read(STDIN_FILENO, msg, sizeof(msg));
			if (len == 1 && msg[0] == ESCAPE_CHARACTER) {
				return 0;
			} 

			if (len == 0 || len == -1) {
				if (len == -1 &&
				    (errno == EINTR || errno == EAGAIN)) {
					continue;
				}
				return -1;
			}

			if (!write_sync(fd, msg, len)) {
				close(fd);
				fd = -1;
				continue;
			}
		}

		if (fd != -1 && FD_ISSET(fd, &fds)) {
			ssize_t len;
			char msg[512];

			len = read(fd, msg, sizeof(msg));
			if (len == 0 || len == -1) {
				if (len == -1 &&
				    (errno == EINTR || errno == EAGAIN)) {
					continue;
				}
				close(fd);
				fd = -1;
				continue;
			}

			if (!write_sync(STDOUT_FILENO, msg, len)) {
				perror("write() failed");
				return -1;
			}
		}
	} while (received_signal == 0);

	return 0;
}

typedef enum {
       CONSOLE_INVAL,
       CONSOLE_PV,
       CONSOLE_SERIAL,
} console_type;

static struct termios stdin_old_attr;

static void restore_term_stdin(void)
{
	restore_term(STDIN_FILENO, &stdin_old_attr);
}

/* The following locking strategy is based on that from
 * libxl__domain_userdata_lock(), with the difference that we want to fail if we
 * cannot acquire the lock rather than wait indefinitely.
 */
static void console_lock(int domid)
{
	struct stat stab, fstab;
	int fd;

	snprintf(lockfile, sizeof lockfile, "%s%d", XEN_LOCK_DIR "/xenconsole.", domid);

	while (true) {
		fd = open(lockfile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
		if (fd < 0)
			err(errno, "Could not open %s", lockfile);

		while (flock(fd, LOCK_EX | LOCK_NB)) {
			if (errno == EINTR)
				continue;
			else
				err(errno, "Could not lock %s", lockfile);
		}
		if (fstat(fd, &fstab))
			err(errno, "Could not fstat %s", lockfile);
		if (stat(lockfile, &stab)) {
			if (errno != ENOENT)
				err(errno, "Could not stat %s", lockfile);
		} else {
			if (stab.st_dev == fstab.st_dev && stab.st_ino == fstab.st_ino)
				break;
		}

		close(fd);
	}

	lockfd = fd;
	return;
}

static void console_unlock(void)
{
	if (lockfile[0] && lockfd != -1) {
		unlink(lockfile);
		close(lockfd);
	}
}

int main(int argc, char **argv)
{
	struct termios attr;
	int domid;
	char *sopt = "hn:";
	int ch;
	unsigned int num = 0;
	int opt_ind=0;
	int start_notify_fd = -1;
	struct option lopt[] = {
		{ "type",     1, 0, 't' },
		{ "num",     1, 0, 'n' },
		{ "help",    0, 0, 'h' },
		{ "start-notify-fd", 1, 0, 's' },
		{ 0 },

	};
	char *dom_path = NULL, *path = NULL, *test = NULL;
	int spty, xsfd;
	struct xs_handle *xs;
	char *end;
	console_type type = CONSOLE_INVAL;
	bool interactive = 0;

	if (isatty(STDIN_FILENO) && isatty(STDOUT_FILENO))
		interactive = 1;

	while((ch = getopt_long(argc, argv, sopt, lopt, &opt_ind)) != -1) {
		switch(ch) {
		case 'h':
			usage(argv[0]);
			exit(0);
			break;
		case 'n':
			num = atoi(optarg);
			break;
		case 't':
			if (!strcmp(optarg, "serial"))
				type = CONSOLE_SERIAL;
			else if (!strcmp(optarg, "pv"))
				type = CONSOLE_PV;
			else {
				fprintf(stderr, "Invalid type argument\n");
				fprintf(stderr, "Console types supported are: serial, pv\n");
				exit(EINVAL);
			}
			break;
		case 's':
			start_notify_fd = atoi(optarg);
			break;
		default:
			fprintf(stderr, "Invalid argument\n");
			fprintf(stderr, "Try `%s --help' for more information.\n", 
					argv[0]);
			exit(EINVAL);
		}
	}

	if (optind >= argc) {
		fprintf(stderr, "DOMID should be specified\n");
		fprintf(stderr, "Try `%s --help' for more information.\n",
			argv[0]);
		exit(EINVAL);
	}
	domid = strtol(argv[optind], &end, 10);
	if (end && *end) {
		fprintf(stderr, "Invalid DOMID `%s'\n", argv[optind]);
		fprintf(stderr, "Try `%s --help' for more information.\n",
			argv[0]);
		exit(EINVAL);
	}

	xs = xs_daemon_open();
	if (xs == NULL) {
		err(errno, "Could not contact XenStore");
	}

	signal(SIGTERM, sighandler);

	dom_path = xs_get_domain_path(xs, domid);
	if (dom_path == NULL)
		err(errno, "xs_get_domain_path()");
	if (type == CONSOLE_INVAL) {
		xc_dominfo_t xcinfo;
		xc_interface *xc_handle = xc_interface_open(0,0,0);
		if (xc_handle == NULL)
			err(errno, "Could not open xc interface");
		if ( (xc_domain_getinfo(xc_handle, domid, 1, &xcinfo) != 1) ||
		     (xcinfo.domid != domid) ) {
			xc_interface_close(xc_handle);
			err(errno, "Failed to get domain information");
		}
		/* default to pv console for pv guests and serial for hvm guests */
		if (xcinfo.hvm)
			type = CONSOLE_SERIAL;
		else
			type = CONSOLE_PV;
		xc_interface_close(xc_handle);
	}
	path = malloc(strlen(dom_path) + strlen("/device/console/0/tty") + 5);
	if (path == NULL)
		err(ENOMEM, "malloc");
	if (type == CONSOLE_SERIAL) {
		snprintf(path, strlen(dom_path) + strlen("/serial/0/tty") + 5, "%s/serial/%d/tty", dom_path, num);
		test = xs_read(xs, XBT_NULL, path, NULL);
		free(test);
		if (test == NULL)
			type = CONSOLE_PV;
	}
	if (type == CONSOLE_PV) {

		if (num == 0)
			snprintf(path, strlen(dom_path) + strlen("/console/tty") + 1, "%s/console/tty", dom_path);
		else
			snprintf(path, strlen(dom_path) + strlen("/device/console/%d/tty") + 5, "%s/device/console/%d/tty", dom_path, num);
	}

	/* FIXME consoled currently does not assume domain-0 doesn't have a
	   console which is good when we break domain-0 up.  To keep us
	   user friendly, we'll bail out here since no data will ever show
	   up on domain-0. */
	if (domid == 0) {
		fprintf(stderr, "Can't specify Domain-0\n");
		exit(EINVAL);
	}

	console_lock(domid);
	atexit(console_unlock);

	/* Set a watch on this domain's console pty */
	if (!xs_watch(xs, path, ""))
		err(errno, "Can't set watch for console pty");
	xsfd = xs_fileno(xs);

	/* Wait a little bit for tty to appear.  There is a race
	   condition that occurs after xend creates a domain.  This code
	   might be running before consoled has noticed the new domain
	   and setup a pty for it. */ 
        spty = get_pty_fd(xs, path, 5);
	if (spty == -1) {
		err(errno, "Could not read tty from store");
	}

	init_term(spty, &attr);
	if (interactive) {
		init_term(STDIN_FILENO, &stdin_old_attr);
		atexit(restore_term_stdin); /* if this fails, oh dear */
	}

	if (start_notify_fd != -1) {
		/* Write 0x00 to notify parent about client's readiness */
		static const char msg[] = { 0x00 };
		int r;

		do {
			r = write(start_notify_fd, msg, 1);
		} while ((r == -1 && errno == EINTR) || r == 0);

		if (r == -1)
			err(errno, "Could not notify parent with fd %d",
			    start_notify_fd);
		close(start_notify_fd);
	}

	console_loop(spty, xs, path, interactive);

	free(path);
	free(dom_path);
	return 0;
 }
