/*
 * bootlogd.c	Store output from the console during bootup into a file.
 *		The file is usually located on the /var partition, and
 *		gets written (and fsynced) as soon as possible.
 *
 * Version:	@(#)bootlogd  2.86pre  12-Jan-2004  miquels@cistron.nl
 *
 * Bugs:	Uses openpty(), only available in glibc. Sorry.
 *
 *		This file is part of the sysvinit suite,
 *		Copyright (C) 1991-2004 Miquel van Smoorenburg.
 *
 *		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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 */

#include <sys/types.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/utsname.h>
#include <time.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <getopt.h>
#include <dirent.h>
#include <fcntl.h>
#ifdef __linux__
#include <pty.h>
#endif

#if defined (__linux__) || defined(__GNU__)
#include <sys/sysmacros.h>
#endif

#ifdef __FreeBSD__
#include <termios.h>
#include <libutil.h>
#endif

#include <ctype.h>
#ifdef __linux__
#include <sys/mount.h>
#endif
#include "bootlogd.h"

#define MAX_CONSOLES 16
#define KERNEL_COMMAND_LENGTH 4096

char ringbuf[32768];
char *endptr = ringbuf + sizeof(ringbuf);
char *inptr  = ringbuf;
char *outptr = ringbuf;

int got_signal = 0;
int didnl = 1;
int createlogfile = 0;
int syncalot = 0;

struct real_cons {
	char name[1024];
	int fd;
};

/*
 *	Console devices as listed on the kernel command line and
 *	the mapping to actual devices in /dev
 */
struct consdev {
	char	*cmdline;
	char	*dev1;
	char	*dev2;
} consdev[] = {
	{ "ttyB",	"/dev/ttyB%s",		NULL		},
	{ "ttySC",	"/dev/ttySC%s",		"/dev/ttsc/%s"	},
	{ "ttyS",	"/dev/ttyS%s",		"/dev/tts/%s"	},
	{ "tty",	"/dev/tty%s",		"/dev/vc/%s"	},
	{ "hvc",	"/dev/hvc%s",		"/dev/hvc/%s"	},
	{ NULL,		NULL,			NULL		},
};

/*
 *	Devices to try as console if not found on kernel command line.
 *	Tried from left to right (as opposed to kernel cmdline).
 */
char *defcons[] = { "tty0", "hvc0", "ttyS0", "ttySC0", "ttyB0", NULL };

/*
 *	Catch signals.
 */
void handler(int sig)
{
	got_signal = sig;
}


/*
 *	chdir with error message on fail.
 */
static int chdir_int(const char *path)
{
	int ret;

	if ((ret = chdir(path)) != 0) {
		const char *msgprefix = "bootlogd: %s";
		char msg[PATH_MAX + sizeof(msgprefix)];
		snprintf(msg, sizeof(msg), msgprefix, path);
 	        perror(msg);
	}

	return ret;
}
 

/*
 *	Scan /dev and find the device name.
 */
static int findtty(char *res, const char *startdir, int rlen, dev_t dev)
{
	DIR		*dir;
	struct dirent	*ent;
	struct stat	st;
	int		r = -1;
	char *olddir = getcwd(NULL, 0);

	if (chdir_int(startdir) < 0 || (dir = opendir(".")) == NULL) {
		chdir_int(olddir);
		return -1;
	}
	while ((ent = readdir(dir)) != NULL) {
		if (lstat(ent->d_name, &st) != 0)
			continue;
		if (S_ISDIR(st.st_mode)
		    && 0 != strcmp(".", ent->d_name)
		    && 0 != strcmp("..", ent->d_name)) {
			char *path = malloc(rlen);
			snprintf(path, rlen, "%s/%s", startdir, ent->d_name);
			r = findtty(res, path, rlen, dev);
			free(path);
			if (0 == r) { 
				closedir(dir);
				chdir_int(olddir);
				return 0;
			}
			continue;
		}
		if (!S_ISCHR(st.st_mode))
			continue;
		if (st.st_rdev == dev) {
			if ( (int) (strlen(ent->d_name) + strlen(startdir) + 1) >= rlen) {
				fprintf(stderr, "bootlogd: console device name too long\n");
				closedir(dir);
				chdir_int(olddir);
				return -1;
			} else {
				snprintf(res, rlen, "%s/%s", startdir, ent->d_name);
				closedir(dir);
				chdir_int(olddir);
				return 0;
			}
		}
	}
	closedir(dir);

	chdir_int(olddir);
	return r;
}



/*
 *	For some reason, openpty() in glibc sometimes doesn't
 *	work at boot-time. It must be a bug with old-style pty
 *	names, as new-style (/dev/pts) is not available at that
 *	point. So, we find a pty/tty pair ourself if openpty()
 *	fails for whatever reason.
 */
int findpty(int *master, int *slave, char *name)
{
	char	pty[16];
	char	tty[16];
	int	i, j;
	int	found;

	if (openpty(master, slave, name, NULL, NULL) >= 0)
		return 0;

	found = 0;

	for (i = 'p'; i <= 'z'; i++) {
		for (j = '0'; j <= 'f'; j++) {
			if (j == '9' + 1) j = 'a';
			sprintf(pty, "/dev/pty%c%c", i, j);
			sprintf(tty, "/dev/tty%c%c", i, j);
			if ((*master = open(pty, O_RDWR|O_NOCTTY)) >= 0) {
				*slave = open(tty, O_RDWR|O_NOCTTY);
				if (*slave >= 0) {
					found = 1;
					break;
				}
			}
		}
		if (found) break;
	}
	if (!found) return -1;

	if (name) strcpy(name, tty);

	return 0;
}

static int istty(const char *dev)
{
	int fd, ret;

	fd = open(dev, O_RDONLY|O_NONBLOCK);
	if (fd < 0)
		return 0;

	ret = isatty(fd);

	close(fd);

	return ret;
}

/*
 *	See if a console taken from the kernel command line maps
 *	to a character device we know about, and if we can open it.
 */
int isconsole(char *s, char *res, int rlen)
{
	struct consdev	*c;
	int		l, sl, i, fd;
	char		*p, *q;

	sl = strlen(s);

	for (c = consdev; c->cmdline; c++) {
		l = strlen(c->cmdline);
		if (sl <= l) continue;
		p = s + l;
		if (strncmp(s, c->cmdline, l) != 0)
			continue;
		for (i = 0; i < 2; i++) {
			snprintf(res, rlen, i ? c->dev1 : c->dev2, p);
			if ((q = strchr(res, ',')) != NULL) *q = 0;
			if ((fd = open(res, O_RDONLY|O_NONBLOCK)) >= 0) {
				close(fd);
				return 1;
			}
		}
	}

	/* Fallback: accept any TTY device */
	snprintf(res, rlen, "/dev/%s", s);
	if ((q = strchr(res, ',')) != NULL) *q = 0;
	if (istty(res))
		return 1;

	return 0;
}

/*
 *	Find out the _real_ console(s). Assume that stdin is connected to
 *	the console device (/dev/console).
 */
int consolenames(struct real_cons *cons, int max_consoles)
{
#ifdef TIOCGDEV
	unsigned int	kdev;
#endif
	struct stat	st, st2;
	char		buf[KERNEL_COMMAND_LENGTH];
	char		*p;
	int		didmount = 0;
	int		n;
	int		fd;
	int		considx, num_consoles = 0;

#ifdef __linux__
	/*
	 *	Read /proc/cmdline.
	 */
	stat("/", &st);
	if (stat("/proc", &st2) < 0) {
		perror("bootlogd: /proc");
		return 0;
	}
	if (st.st_dev == st2.st_dev) {
		if (mount("proc", "/proc", "proc", 0, NULL) < 0) {
			perror("bootlogd: mount /proc");
			return -1;
		}
		didmount = 1;
	}

	n = -1;
	if ((fd = open("/proc/cmdline", O_RDONLY)) < 0) {
		perror("bootlogd: /proc/cmdline");
	} else {
		buf[0] = 0;
		if ((n = read(fd, buf, KERNEL_COMMAND_LENGTH - 1)) < 0)
			perror("bootlogd: /proc/cmdline");
		close(fd);
	}
	if (didmount) umount("/proc");
                
	if (n < 0) return 0;

	/*
	 *	OK, so find console= in /proc/cmdline.
	 *	Parse in reverse, opening as we go.
	 */
	p = buf + n;
	*p-- = 0;
	while (p >= buf) {
		if (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') {
			*p-- = 0;
			continue;
		}
		if (strncmp(p, "console=", 8) == 0 &&
			isconsole(p + 8, cons[num_consoles].name, sizeof(cons[num_consoles].name))) {
				/*
				 *	Suppress duplicates
				 */
				for (considx = 0; considx < num_consoles; considx++) {
					if (!strcmp(cons[num_consoles].name, cons[considx].name)) {
						goto dontuse;
					}
				}
			
			num_consoles++;
			if (num_consoles >= max_consoles) {
				break;
			}
		}
dontuse:
		p--;
	}

	if (num_consoles > 0) return num_consoles;
#endif
	fstat(0, &st);
	if (major(st.st_rdev) != 5 || minor(st.st_rdev) != 1) {
		/*
		 *	Old kernel, can find real device easily.
		 */
		int r = findtty(cons[num_consoles].name, "/dev", 
                                sizeof(cons[num_consoles].name), st.st_rdev);
		if (!r)
			num_consoles++;
	}

	if (num_consoles > 0) return num_consoles;

#ifdef TIOCGDEV
# ifndef  ENOIOCTLCMD
#  define ENOIOCTLCMD	515
# endif
	if (ioctl(0, TIOCGDEV, &kdev) == 0) {
		int r = findtty(cons[num_consoles].name, "/dev", 
                                sizeof(cons[num_consoles].name), (dev_t)kdev);
		if (!r)
			num_consoles++;
	}

	if (num_consoles > 0) return num_consoles;
#endif

	/*
	 *	Okay, no console on the command line -
	 *	guess the default console.
	 */
	for (n = 0; defcons[n]; n++)
		if (isconsole(defcons[n], cons[0].name, sizeof(cons[0].name))) 
			return 1;

	fprintf(stderr, "bootlogd: cannot deduce real console device\n");

	return 0;
}


/*
 *	Write data and make sure it's on disk.
 */
void writelog(FILE *fp, unsigned char *ptr, int len, int print_escape_characters)
{
	int dosync = 0;
	int i;
	static int first_run = 1;
	static int inside_esc = 0;

	for (i = 0; i < len; i++) {
		int ignore = 0;

		/* prepend date to every line */
		if (*(ptr-1) == '\n' || first_run) {
			time_t t;
			char *s;
			time(&t);
			s = ctime(&t);
			fprintf(fp, "%.24s: ", s);
			dosync = 1;
			first_run = 0;
		}

		/* remove escape sequences, but do it in a way that allows us to stop
		 * in the middle in case the string was cut off */
                if (! print_escape_characters)
                {
                    if (inside_esc == 1) {
			/* first '[' is special because if we encounter it again, it should be considered the final byte */
			if (*ptr == '[') {
				/* multi char sequence */
				ignore = 1;
				inside_esc = 2;
			} else {
				/* single char sequence */
				if (*ptr >= 64 && *ptr <= 95) {
					ignore = 1;
				}
				inside_esc = 0;
			}
                    } else if (inside_esc == 2) {
			switch (*ptr) {
				case '0' ... '9': /* intermediate chars of escape sequence */
				case ';':
				case 32 ... 47:
					if (inside_esc) {
						ignore = 1;
					}
					break;
				case 64 ... 126: /* final char of escape sequence */
					if (inside_esc) {
						ignore = 1;
						inside_esc = 0;
					}
					break;
                        }
		     } else {
			switch (*ptr) {
				case '\r':
					ignore = 1;
					break;
				case 27: /* ESC */
					ignore = 1;
					inside_esc = 1;
					break;
			}
		     }
                }     /* end of if we should filter escape characters */

		if (!ignore) {
			fwrite(ptr, sizeof(char), 1, fp);
		}

		ptr++;
	}
	if (dosync) {
		fflush(fp);
		if (syncalot) {
			fdatasync(fileno(fp));
		}
	}

	outptr += len;
	if (outptr >= endptr)
		outptr = ringbuf;

}


/*
 *	Print usage message and exit.
 */
void usage(void)
{
	fprintf(stderr, "Usage: bootlogd [-v] [-r] [-d] [-e] [-s] [-c] [-p pidfile] [-l logfile]\n");
	exit(1);
}

int open_nb(char *buf)
{
	int	fd, n;

	if ((fd = open(buf, O_WRONLY|O_NONBLOCK|O_NOCTTY)) < 0)
		return -1;
	n = fcntl(fd, F_GETFL);
	n &= ~(O_NONBLOCK);
	fcntl(fd, F_SETFL, n);

	return fd;
}

/*
 *	We got a write error on the real console. If its an EIO,
 *	somebody hung up our filedescriptor, so try to re-open it.
 */
int write_err(int pts, int realfd, char *realcons, int e)
{
	int	fd;

	if (e != EIO) {
werr:
		close(pts);
		fprintf(stderr, "bootlogd: writing to console: %s\n",
			strerror(e));
		return -1;
	}
	close(realfd);
	if ((fd = open_nb(realcons)) < 0)
		goto werr;

	return fd;
}

int main(int argc, char **argv)
{
	FILE		*fp;
	struct timeval	tv;
	fd_set		fds;
	char		buf[1024];
	char		*p;
	char		*logfile;
	char		*pidfile;
	int		rotate;
	int		dontfork;
	int		ptm, pts;
	/* int		realfd;   -- this is now unused */
	int		n, m, i;
	int		todo;
#ifndef __linux__	/* BSD-style ioctl needs an argument. */
	int		on = 1;
#endif
	int		considx;
	struct real_cons cons[MAX_CONSOLES];
	int		num_consoles, consoles_left;
        int             print_escape_sequence = 0;
	fp = NULL;
	logfile = LOGFILE;
	pidfile = NULL;
	rotate = 0;
	dontfork = 0;

	while ((i = getopt(argc, argv, "cdesl:p:rv")) != EOF) switch(i) {
		case 'l':
			logfile = optarg;
			break;
		case 'r':
			rotate = 1;
			break;
		case 'v':
			printf("bootlogd - %s\n", VERSION);
			exit(0);
			break;
		case 'p':
			pidfile = optarg;
			break;
		case 'c':
			createlogfile = 1;
			break;
		case 'd':
			dontfork = 1;
			break;
                case 'e':
                        print_escape_sequence = 1;
                        break;
		case 's':
			syncalot = 1;
			break;
		default:
			usage();
			break;
	}
	if (optind < argc) usage();

	signal(SIGTERM, handler);
	signal(SIGQUIT, handler);
	signal(SIGINT,  handler);
	signal(SIGTTIN,  SIG_IGN);
	signal(SIGTTOU,  SIG_IGN);
	signal(SIGTSTP,  SIG_IGN);

	/*
	 *	Open console device directly.
	 */
        /*
	if (consolename(realcons, sizeof(realcons)) < 0)
		return 1;

	if (strcmp(realcons, "/dev/tty0") == 0)
		strcpy(realcons, "/dev/tty1");
	if (strcmp(realcons, "/dev/vc/0") == 0)
		strcpy(realcons, "/dev/vc/1");

	if ((realfd = open_nb(realcons)) < 0) {
		fprintf(stderr, "bootlogd: %s: %s\n", realcons, strerror(errno));
		return 1;
	}
        */
        if ((num_consoles = consolenames(cons, MAX_CONSOLES)) <= 0)
                return 1;
        consoles_left = num_consoles;
        for (considx = 0; considx < num_consoles; considx++) {
               if (strcmp(cons[considx].name, "/dev/tty0") == 0)
                       strcpy(cons[considx].name, "/dev/tty1");
               if (strcmp(cons[considx].name, "/dev/vc/0") == 0)
                       strcpy(cons[considx].name, "/dev/vc/1");

               if ((cons[considx].fd = open_nb(cons[considx].name)) < 0) {
                       fprintf(stderr, "bootlogd: %s: %s\n", 
                                cons[considx].name, strerror(errno));
                       consoles_left--;
               }
        }
        if (!consoles_left)
               return 1;


	/*
	 *	Grab a pty, and redirect console messages to it.
	 */
	ptm = -1;
	pts = -1;
	buf[0] = 0;
	if (findpty(&ptm, &pts, buf) < 0) {
		fprintf(stderr,
			"bootlogd: cannot allocate pseudo tty: %s\n",
			strerror(errno));
		return 1;
	}

#ifdef __linux__
	(void)ioctl(0, TIOCCONS, NULL);
	/* Work around bug in 2.1/2.2 kernels. Fixed in 2.2.13 and 2.3.18 */
	if ((n = open("/dev/tty0", O_RDWR)) >= 0) {
		(void)ioctl(n, TIOCCONS, NULL);
		close(n);
	}
#endif
#ifdef __linux__
	if (ioctl(pts, TIOCCONS, NULL) < 0)
#else	/* BSD usage of ioctl TIOCCONS. */
	if (ioctl(pts, TIOCCONS, &on) < 0)
#endif
	{
		fprintf(stderr, "bootlogd: ioctl(%s, TIOCCONS): %s\n",
			buf, strerror(errno));
		return 1;
	}

	/*
	 *	Fork and write pidfile if needed.
	 */
	if (!dontfork) {
		pid_t child_pid = fork();
		switch (child_pid) {
		case -1: /* I am parent and the attempt to create a child failed */
			fprintf(stderr, "bootlogd: fork failed: %s\n",
				strerror(errno));
			exit(1);
			break;
		case 0: /* I am the child */
			break;
		default: /* I am parent and got child's pid */
			exit(0);
			break;
		}
		setsid();
	}
	if (pidfile) {
		unlink(pidfile);
		if ((fp = fopen(pidfile, "w")) != NULL) {
			fprintf(fp, "%d\n", (int)getpid());
			fclose(fp);
		}
		fp = NULL;
	}

	/*
	 *	Read the console messages from the pty, and write
	 *	to the real console and the logfile.
	 */
	while (!got_signal) {

		/*
		 *	We timeout after 5 seconds if we still need to
		 *	open the logfile. There might be buffered messages
		 *	we want to write.
		 */
		tv.tv_sec = 0;
		tv.tv_usec = 500000;
		FD_ZERO(&fds);
		FD_SET(ptm, &fds);
		if (select(ptm + 1, &fds, NULL, NULL, &tv) == 1) {
			/*
			 *	See how much space there is left, read.
			 */
			if ((n = read(ptm, inptr, endptr - inptr)) >= 0) {
				/*
				 *	Write data (in chunks if needed)
				 *	to the real output devices.
				 */
				for (considx = 0; considx < num_consoles; considx++) {
					if (cons[considx].fd < 0) continue;
					m = n;
					p = inptr;
					while (m > 0) {
						i = write(cons[considx].fd, p, m);
						if (i >= 0) {
							m -= i;
							p += i;
							continue;
						}
						/*
						 *	Handle EIO (somebody hung
						 *	up our filedescriptor)
						 */
						cons[considx].fd = write_err(pts,
							cons[considx].fd,
							cons[considx].name, errno);
						if (cons[considx].fd >= 0) continue;
						/*	
						 *	If this was the last console,
						 *	generate a fake signal
						 */
						if (--consoles_left <= 0) got_signal = 1;
						break;
 					}   /* end of while */
				}     /* end of going through all consoles */

				/*
				 *	Increment buffer position. Handle
				 *	wraps, and also drag output pointer
				 *	along if we cross it.
				 */
				inptr += n;
				if (inptr - n < outptr && inptr > outptr)
					outptr = inptr;
				if (inptr >= endptr)
					inptr = ringbuf;
				if (outptr >= endptr)
					outptr = ringbuf;
			}       /* end of got data from read */
		}      /* end of checking select for new data */

		/*
		 *	Perhaps we need to open the logfile.
		 */
		if (fp == NULL && access(logfile, F_OK) == 0) {
			if (rotate) {
				snprintf(buf, sizeof(buf), "%s~", logfile);
				rename(logfile, buf);
			}
			fp = fopen(logfile, "a");
		}
		if (fp == NULL && createlogfile)
			fp = fopen(logfile, "a");

		if (inptr >= outptr)
			todo = inptr - outptr;
		else
			todo = endptr - outptr;
		if (fp && todo)
			writelog(fp, (unsigned char *)outptr, todo, print_escape_sequence);
	}    /* end of while waiting for signal */

	if (fp) {
		if (!didnl) fputc('\n', fp);
		fclose(fp);
	}

	close(pts);
	close(ptm);
	for (considx = 0; considx < num_consoles; considx++) {
		close(cons[considx].fd);
	}

	return 0;
}

