/* ttywatch.c -- log output from TTYs, once loosely based on robin.c from
 * Linux Application Development, by Michael K. Johnson and Erik W. Troan
 *
 * Copyright © 2000-2004 Michael K. Johnson <a1237+ttywatch@danlj.org>
 *
 * 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/poll.h>
#include <errno.h>
#include <fcntl.h>
#include <popt.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>             /* for strerror() */
#include <unistd.h>
#if defined(HAVE_LOCKDEV)
#include <lockdev.h>
#endif

#include "ttywatch.h"
#include "logfile.h"
#include "errors.h"
#include "telnet.h"
#include "socket.h"

/* configuration */
static char *logpath;
static char *modname;
static char *configfile;
static char *pidfile;
static int intarg;
static char *chararg;
static int do_daemon;
#if defined(HAVE_LOCKDEV)
static int do_lock = 1;
#else
static int do_lock = 0;
#endif

static struct poptOption optionsTable[] = {
    { "config",  'c', POPT_ARG_STRING, &configfile, 'c',
      "config file to read before parsing other options", "path" },
    { "pidfile",  'P', POPT_ARG_STRING, &pidfile, 0,
      "file in which to store the pid", "path" },
    { "name", 'n', POPT_ARG_STRING, &chararg, 'n',
      "name of machine to monitor", "machinename" },
    { "port", 'p', POPT_ARG_STRING, &chararg, 'p',
      "pathname for port for current machinename", "/dev/<foo>" },
    { "ipport", 'i', POPT_ARG_STRING, &chararg, 'i',
      "port number on which to talk to this device", "3000" },
    { "ipaddr", 'I', POPT_ARG_STRING, &chararg, 'I',
      "IP address to which to bind", "10.0.0.1" },
    { "bps",  'b', POPT_ARG_INT, &intarg, 'b',
      "signaling rate for current maching in bps", "<BPS>" },
    { "logpath", 'l', POPT_ARG_STRING, &logpath, 0,
      "directory path for logfiles", "path" },
    { "logspew", 'L', POPT_ARG_INT, &intarg, 'L',
      "characters of logfile data to send", "4000" },
    { "module", 'm', POPT_ARG_STRING, &modname, 'm',
      "path to module for logging this port", "path" },
    { "daemon", 'd', POPT_ARG_NONE, &do_daemon, 0,
      "run in background as a daemon", NULL },
#if defined(HAVE_LOCKDEV)
    { "nolock", 'N', POPT_ARG_NONE, NULL, 0,
      "do not lock (this port)", NULL},
#endif
      POPT_AUTOHELP
    { NULL, 0, 0, NULL, 0 }
};

static GSList *machines;
static GSList *retry;    /* list of unconnected sockets to retry later */
static GHashTable *machine_names;
static GHashTable *machine_fds;
static GHashTable *accept_fds;
static GHashTable *net_fds;
static GHashTable *waiting_fds;

static socket_type
machine_type(char *name) {
	char *colon, *c;

	if (strchr(name, '/'))
	    return ST_NOTASOCKET;
	colon = strchr(name, ':');
	if (!colon)
	    return ST_NOTASOCKET;

	/* are there only numerics following the colon? */
	for (c = ++colon; *c; c++) {
	    if (*c < '0' || *c > '9')
		return ST_NOTASOCKET;
	}

	return ST_TELNET;
}

/* get machine* for machine name, creating a new machine* if necessary */
static machine *
get_machine(char *mach) {
	machine *m;

	m = g_hash_table_lookup(machine_names, mach);
	if (m) return m;

	m = calloc(sizeof(machine), 1);
	if (!m) abort();

	m->dev_nc.fd = -1;
	m->dev_nc.m = m;
	m->dev_nc.ts = TS_DATA;

	m->accept_fd = -1;
	m->name = strdup(mach);
	m->speed = B115200;
	m->logspew = 80*50; /* up to at least two screen pages */

	m->st = machine_type(mach);

	/* defaults to devpath = /dev/<name> for symlinks unless netname */
	if (m->st == ST_NOTASOCKET) {
	    m->devpath = malloc(strlen(mach)+6);
	    strcpy(m->devpath, "/dev/");
	    strcat(m->devpath, mach);
#if defined(HAVE_LOCKDEV)
	    m->lock = 1;
#endif
	} else {
	    m->devpath = strdup(mach);
	}

	g_hash_table_insert(machine_names, m->name, m);
	machines = g_slist_prepend(machines, m);

	return m;
}

static speed_t
symbolic_speed(int speednum) {
   if (speednum >= 460800) return B460800;
   if (speednum >= 230400) return B230400;
   if (speednum >= 115200) return B115200;
   if (speednum >= 57600) return B57600;
   if (speednum >= 38400) return B38400;
   if (speednum >= 19200) return B19200;
   if (speednum >= 9600) return B9600;
   if (speednum >= 4800) return B4800;
   if (speednum >= 2400) return B2400;
   if (speednum >= 1800) return B1800;
   if (speednum >= 1200) return B1200;
   if (speednum >= 600) return B600;
   if (speednum >= 300) return B300;
   if (speednum >= 200) return B200;
   if (speednum >= 150) return B150;
   if (speednum >= 134) return B134;
   if (speednum >= 110) return B110;
   if (speednum >= 75) return B75;
   return B50;
}

static void
setup_pidfile(void) {
	if (pidfile) {
		char pid[32]; /* more than enough for an int... */
		int pidfd;

		sprintf(pid, "%d\n", getpid());
		pidfd = open(pidfile, O_RDWR|O_CREAT|O_TRUNC, 0644);
		write(pidfd, pid, strlen(pid));
		close(pidfd);
	}
}

static void
delete_pidfile(void) {
	if (pidfile) {
		unlink(pidfile);
	}
}

static void
add_to_retry_list(machine *m) {
    m->ss = SS_TIMEOUT;
    m->timeout_expires = time(NULL) + 1;
    retry = g_slist_append(retry, m);
}

/* send a BREAK down the line if indicated */
static void
send_break_if_set (net_conn *nc, int fd) {
    if (nc->flags & TS_FLAG_BREAK) {
	tcsendbreak(fd, 0);
	nc->flags &= ~TS_FLAG_BREAK;
    }
}

/* open and set termios settings for a single port, storing original settings
 * also set up ip port listening if requested
 */
static void
setup_one_device(gpointer data, gpointer ignore) {
	machine *m = data;

	open_one_logfile(m);

	if (m->st == ST_TELNET || m->st == ST_RAWTCP) {
	    /* connection may not be available, but we keep trying */
	    m->dev_nc.fd = socket_connect(m);
	    if (m->dev_nc.fd > -1 && m->ss == SS_INPROGRESS) {
		g_hash_table_insert(waiting_fds, &m->dev_nc.fd, m);
	    }
	} else {
	    if (do_lock && m->lock) {
#if defined(HAVE_LOCKDEV)
		int lock_pid;
		errno = 0;
		lock_pid = dev_lock(m->devpath);
		if (lock_pid > 0) {
		    warn(m, "device %s is locked by process %d, ignoring",
			 m->devpath, lock_pid);
		    m->dev_nc.fd = -1; /* remove from the list */
		} else {
		    if (lock_pid == -1)
			warn(m, "device %s could not be locked", m->devpath);
		    m->dev_nc.fd = open(m->devpath, O_RDWR|O_NOCTTY);
		}
#endif
	    } else {
		/* ignore locking */
		m->dev_nc.fd = open(m->devpath, O_RDWR|O_NOCTTY);
	    }
	}
	if (m->dev_nc.fd < 0) {
	    if (errno)
		warn(m, strerror(errno));
	    g_hash_table_remove(machine_names, m->name);
	    machines = g_slist_remove(machines, m);
	    free(m->devpath);
	    free(m->name);
	    free(m);
	    return;
	}
	g_hash_table_insert(machine_fds, &m->dev_nc.fd, m);

	if (m->ss == SS_NOTASOCKET) {
	    /* modify the port configuration */
	    tcgetattr(m->dev_nc.fd, &m->ots);
	    m->ts = m->ots;
	    /* lots of random junk... */
	    m->ts.c_lflag &= ~ICANON;
	    m->ts.c_lflag &= ~(ECHO | ECHOCTL | ECHONL);
	    m->ts.c_cflag |= HUPCL;
	    m->ts.c_cflag &= ~CRTSCTS;
	    m->ts.c_iflag &= ~(IXON | IXOFF | IXANY);
	    m->ts.c_cc[VMIN] = 1;
	    m->ts.c_cc[VTIME] = 0;

	    /* Do not convert \r input characters to \n */
	    m->ts.c_iflag &= ~ICRNL;

	    /* set ALL the speeds */
	    cfsetospeed(&m->ts, m->speed);
	    cfsetispeed(&m->ts, m->speed);

	    /* Now set the modified termios settings */
	    tcsetattr(m->dev_nc.fd, TCSANOW, &m->ts);
	}

	/* Are we supposed to export this port via the network? */
	if (m->ipport) {
	    m->accept_fd = socket_listen(m->ipport, m->ipaddr);
	    if (m->accept_fd > -1) {
		g_hash_table_insert(accept_fds, &m->accept_fd, m);
	    }
	}
}

/* set termios settings for all ports */
static void
setup_devices(void) {
	g_slist_foreach(machines, setup_one_device, NULL);
}

/* restore termios settings for a single port, then close all fds */
static void
cleanup_one_device(gpointer data, gpointer ignore) {
	machine *m = data;

	if (m->ss == SS_NOTASOCKET) {
	    tcsetattr(m->dev_nc.fd, TCSANOW, &m->ots);
	}
	close(m->dev_nc.fd);
#if defined(HAVE_DEVLOCK)
	if (do_lock && m->lock) {
	    dev_unlock(m->devpath, -1);
	}
#endif
	/* no logfile if a module is being used */
	if (m->log_fd > -1) close(m->log_fd);
	if (m->accept_fd > -1) close(m->accept_fd);
	if (m->net_conns) {
	    GSList *l;
	    for (l = m->net_conns; l; l = g_slist_next(l)) {
		net_conn *nc = l->data;

		close(nc->fd);
		g_hash_table_remove(net_fds, &nc->fd);
		free(nc);
	    }
	    g_slist_free(m->net_conns);
	}
}

static void
reopen_logfiles(int signo) {
    g_slist_foreach(machines, reopen_one_logfile, NULL);
}

/* restore all original terminal settings on exit */
static void
cleanup_devices_and_exit(int signal) {
	g_slist_foreach(machines, cleanup_one_device, NULL);
	delete_pidfile();
	exit(0);
}

static struct pollfd *
prepare_poll(int *nfds /*OUT*/, struct pollfd *p) {
	GSList *l, *n;
	machine *m;
	int i;

	*nfds=0;

	for (l = machines; l; l = g_slist_next(l)) {
	    m = l->data;
	    if (m->dev_nc.fd > -1)
		(*nfds)++; /* one for the port */
	    if (m->accept_fd > -1)
		(*nfds)++; /* one for the accept fd */
	    if (m->net_conns)
		for (n = m->net_conns; n; n = g_slist_next(n))
		    (*nfds)++; /* one for each listening client */
	}
	if (p) free (p);
	p = calloc(sizeof(struct pollfd), *nfds);

	for (l = machines, i=0; l; l = g_slist_next(l)) {
	    m = l->data;
	    if (m->dev_nc.fd > -1) {
		p[i].fd = m->dev_nc.fd;
		D("adding dev_nc.fd %d\n", p[i].fd);
		if (m->ss == SS_INPROGRESS) {
		    /* wait for the connection */
		    p[i++].events = POLLOUT;
		} else  {
		    /* wait for data */
		    p[i++].events = POLLIN;
		}
	    }
	    if (m->accept_fd > -1) {
		p[i].fd = m->accept_fd;
		D("adding accept_fd %d\n", p[i].fd);
		p[i++].events = POLLIN;
	    }
	    if (m->net_conns) {
		for (n = m->net_conns; n; n = g_slist_next(n)) {
		    net_conn *nc = n->data;

		    p[i].fd = nc->fd;
		    D("adding net_conn fd %d\n", p[i].fd);
		    p[i++].events = POLLIN;
		}
	    }
	}

	return p;
}

/* A network client has gone away */
static void
remove_client (net_conn *nc, struct pollfd **ufds, int *nfds) {
	machine *m;

	close(nc->fd);
	m = nc->m;
	m->net_conns = g_slist_remove(m->net_conns, nc);
	g_hash_table_remove(net_fds, &nc->fd);
	free(nc);
	*ufds = prepare_poll(nfds, *ufds);
}


static machine *
parse_argument (char c, machine *m) {
	int	conf_fd = 0;
	struct stat sb;

	switch (c) {
	case 'n':
		m = get_machine(chararg);
		break;
	case 'i':
		if (!m) die ("--name or -n must come before --ipport or -i\n");
		if (m->ipport) free(m->ipport);
		m->ipport = strdup(chararg);
		break;
	case 'I':
		if (!m) die ("--name or -n must come before --ipaddr or -I\n");
		if (m->ipaddr) free(m->ipaddr);
		m->ipaddr = strdup(chararg);
		break;
	case 'p':
		if (!m) die ("--name or -n must come before --port or -p\n");
		if (m->devpath) free(m->devpath);
		m->devpath = strdup(chararg);
		break;
	case 'b':
		if (!m) die ("--name or -n must come before --bps or -b\n");
		m->speed = symbolic_speed(intarg);
		break;
	case 'L':
		if (!m) die ("--name or -n must come before --logspew or -L\n");
		m->logspew = intarg;
		break;
	case 'm':
		if (!m) die ("--name or -n must come before --module or -m\n");
		if (m->modhandle) dlclose(m->modhandle);
		m->modhandle = dlopen(chararg, RTLD_NOW);
		m->process_line = dlsym(m->modhandle, "process_line");
		break;
	case 'N':
		if (!m)
		    do_lock = 0;
		else
		    m->lock = 0;
	case 'c':
		/* read the whole config file in at once */
		conf_fd = open(configfile, O_RDONLY);
		if (conf_fd >= 0) {
			poptContext optCon;
			char	*conftext;
			char	*confline;
			char	*confnext;
			int	confc;
			char	**confv;

			fstat(conf_fd, &sb);
			conftext = malloc(sb.st_size+1);
			/* pretend read always works, since this is from disk :-) */
			read(conf_fd, conftext, sb.st_size);
			close(conf_fd);
			conftext[sb.st_size] = '\0';
			
			/* read the config file line by line */
			for (confline = conftext;
			     confline;
			     confline = confnext) {
				confnext = strchr(confline, '\n');
				if (confnext) *confnext++ = '\0';

				if (!confline[0] || confline[0] == '#')
				    continue;

				poptParseArgvString(confline, &confc, (const char ***)&confv);
				optCon = poptGetContext("ttywatch", confc, (const char **)confv,
							optionsTable, POPT_CONTEXT_KEEP_FIRST);
				while ((c = poptGetNextOpt(optCon)) >= 0)
					m = parse_argument (c, m);
				poptFreeContext(optCon);
			}
			free(conftext);
		}
	}

	return m;
}

int
main(int argc, char **argv) {
	char    c;            /* used for argument parsing */
	struct sigaction sact;/* used to initialize the signal handler */
	poptContext optCon;   /* context for parsing command-line options */

	machine *m = NULL;

	struct pollfd *ufds;
	int nfds;

	machine_names = g_hash_table_new(g_str_hash, g_str_equal);
	machine_fds = g_hash_table_new(g_int_hash, g_int_equal);
	accept_fds = g_hash_table_new(g_int_hash, g_int_equal);
	net_fds = g_hash_table_new(g_int_hash, g_int_equal);
	waiting_fds = g_hash_table_new(g_int_hash, g_int_equal);

	if (getuid())
		logpath = ".";
	else
		logpath = "/var/log/ttywatch";

	optCon = poptGetContext("ttywatch", argc, (const char **)argv,
				optionsTable, 0);
	while ((c = poptGetNextOpt(optCon)) >= 0)
		m = parse_argument (c, m);

	if (c < -1) {
		/* an error occurred during option processing */
		fprintf(stderr, "%s: %s\n", 
			poptBadOption(optCon, POPT_BADOPTION_NOALIAS),
			poptStrerror(c));
		return 1;
	}
	poptFreeContext(optCon);

	/* set everything up -- daemon() after setup to report problems */
	setup_logpath(logpath);
	setup_pidfile();
	setup_devices();
	if (do_daemon) {
		daemon(1, 0);
		setup_pidfile(); /* daemon() has changed pid ... */
	}

	/* set the signal handler to restore the old
	 * termios handler, etc. */
	sact.sa_handler = cleanup_devices_and_exit;
	sigaction(SIGINT, &sact, NULL);
	sigaction(SIGTERM, &sact, NULL);
	sact.sa_handler = reopen_logfiles;
	sigaction(SIGHUP, &sact, NULL);
	sact.sa_handler = SIG_IGN;
	sigaction(SIGPIPE, &sact, NULL);

	ufds = prepare_poll(&nfds, NULL);

	do {
	    int i, n, r;
	    machine *m;
	    net_conn *nc;
	    time_t now;

	    r = poll(ufds, nfds, retry ? 1000 : -1);
	    if ((r < 0) && (errno != EINTR)) die("poll failed unexpectedly\n");

	    if (retry) {
		now = time(NULL);
		while (retry) {
		    m = retry->data;
		    if (m->timeout_expires < now) {
			m->dev_nc.fd = -1;
			socket_connect(m);
			if (m->dev_nc.fd > -1) {
			    if (m->ss == SS_INPROGRESS) {
				g_hash_table_insert(waiting_fds, &m->dev_nc.fd, m);
			    }
			    retry = g_slist_remove(retry, m);
			} else {
			    /* try again significantly later */
			    m->timeout_expires = time(NULL) + 10;
			}
		    }
		}
	    }

	    for (n = 0; n < nfds; n++) {
#		define TTYWATCH_BUFSIZE 1024
		char buf[TTYWATCH_BUFSIZE];

		if (!ufds[n].revents) continue;

		if ((ufds[n].revents & POLLHUP) ||
		    (ufds[n].revents & POLLERR) ||
		    (ufds[n].revents & POLLNVAL)) {
		    if ((nc = g_hash_table_lookup(net_fds, &ufds[n].fd))) {
			remove_client (nc, &ufds, &nfds);
			break;
		    } else {
			if ((m = g_hash_table_lookup(machine_fds, &ufds[n].fd))) {
			    /* a network data source has closed/died */
			    close(m->dev_nc.fd);
			    m->dev_nc.fd = -1;
			    add_to_retry_list(m);
			    ufds = prepare_poll(&nfds, ufds); break;
			}

			/* should not happen, but if it does, we cannot recover;
			 * we would go into an infinite poll loop instead.
			 */
			die("Unexpected HUP|ERR|NVAL poll event 0x%x on fd %d\n",
			    ufds[n].revents, ufds[n].fd);
		    }
		}

		if (ufds[n].revents & POLLOUT) {
		    /* must be a finished non-blocking connect */
		    if (!(m = g_hash_table_lookup(waiting_fds, &ufds[n].fd))) {
			die("Socket on fd %d for POLLOUT DNE\n", ufds[n].fd);
		    }
		    g_hash_table_remove(waiting_fds, &ufds[n].fd);

		    socket_continue_connect(m);
		    switch (m->ss) {
		    case SS_CONNECTED:
			break;
		    case SS_TIMEOUT:
			/* APPEND to list of timed out sockets to retry later */
			m->timeout_expires = time(NULL) + 1;
			retry = g_slist_append(retry, m);
			break;
		    default:
			die("illegal state %d for %s\n", m->ss, m->name);
		    }
		    ufds = prepare_poll(&nfds, ufds); break;
		}

		if (ufds[n].revents & POLLIN) {
		    /* is it incoming data from a port? */
		    if ((m = g_hash_table_lookup(machine_fds, &ufds[n].fd))) {
			i = read(m->dev_nc.fd, buf, TTYWATCH_BUFSIZE-1);
			if (i >= 1) {
			    buf[i+1] = '\0';
			    if (m->ss != SS_NOTASOCKET) {
				i = telnet_process_input(&m->dev_nc, buf, i);
			    }
			    logfile_write(m->log_fd, buf, i);
			    if (m->process_line)
				m->process_line(m, buf);
			    if (m->net_conns) {
				GSList *l;

				for (l = m->net_conns; l; l = g_slist_next(l)) {
				    net_conn *nc = l->data;

				    telnet_send_output(nc, buf, i);
				}
			    }
			}

		    /* is it an incoming connection request? */
		    } else if ((m = g_hash_table_lookup(accept_fds, &ufds[n].fd))) {
			int conn = socket_accept(ufds[n].fd);
			if (conn) {
			    net_conn *nc = calloc(sizeof(net_conn), 1);
			    if (!nc) {
				close(conn);
				warn(m, "out of memory for new connection");
				break;
			    }
			    nc->m = m;
			    nc->fd = conn;

			    m->net_conns = g_slist_prepend(m->net_conns, nc);
			    g_hash_table_insert(net_fds, &nc->fd, nc);
			    telnet_negotiate_server(nc->fd);
			    /* if available, send some existing logfile data */
			    logfile_spew(nc);
			    ufds = prepare_poll(&nfds, ufds); break;
			}

		    /* is it incoming typing on a client socket? */
		    } else if ((nc = g_hash_table_lookup(net_fds, &ufds[n].fd))) {
			m = nc->m;
			i = read(ufds[n].fd, buf, TTYWATCH_BUFSIZE-1);
			if (!i) {
			    /* client connection closed (POLLHUP only for ttys, <sigh>) */
			    remove_client (nc, &ufds, &nfds);
			    break;
			}

			if (m->ss == SS_NOTASOCKET) {
			    i = telnet_process_input(nc, buf, i);
			    send_break_if_set (nc, m->dev_nc.fd);
			    write(m->dev_nc.fd, buf, i);
			} else {
			    /* pass telnet info, including BREAK, on */
			    telnet_send_output(&m->dev_nc, buf, i);
			}
			/* write(STDERR_FILENO, buf, i); */

		    } else {
			/* what are we missing? */
			i = read(ufds[n].fd, buf, TTYWATCH_BUFSIZE-1);
			warn(NULL, "error: throwing away %d bytes from unknown fd %d", i, ufds[n].fd);
		    }

		} else if (ufds[n].revents) {
		    /* should not happen, but if it does, we cannot recover;
		     * we would go into an infinite poll loop instead.
		     */
		    die("Unexpected poll event 0x%x on fd %d\n",
		        ufds[n].revents, ufds[n].fd);
		}
	    }
	} while (1); /* exits through signal handler */
}
