/*
 * DF-5537 — guest-side driver.
 *
 * Boots the VKERNEL64 kernel.debug with its console on a pty slave that is
 * its CONTROLLING TERMINAL, so that this driver can inject an arbitrarily
 * fast SIGWINCH storm into the vkernel by alternating TIOCSWINSZ on the
 * pty master (each *change* makes the guest tty layer SIGWINCH the pty's
 * foreground process group == the vkernel process).
 *
 *   vconswinchsig (console.c:243-263) -> signalintr(3) ->
 *   vconswinch_intr (console.c:272-289, INTR_MPSAFE, NO tty token) ->
 *   pgsignal(tp->t_pgrp, SIGWINCH, 1)     <-- racy load
 *
 * usage: vkwinch_drv <mode> <secs>
 *   mode: full    = start in-vkernel racer + storm
 *         churn   = racer only, no storm   (control)
 *         storm   = storm only, no racer   (control)
 *         plain   = neither (boot sanity)
 * env:   VKCON=/tmp/vkconsole.log  VKARGS (default "-m 512m -r /vkroot.img -n 2 -e init_path=/bin/vkinit")
 *
 * exit: 0 = vkernel survived the window; 2 = vkernel crashed/panicked/ddb;
 *       3 = boot never completed (rig failure); 4 = driver error.
 */
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <time.h>

static const char *badstr[] = {
	"Fatal trap",
	"panic:",
	"db> ",
	"Assertion failed",
	"corrupt",
	"double free",
	"freeing free",
	NULL
};

static char *logbuf;
static size_t loglen, logcap;

static void
logappend(const char *buf, ssize_t n, int confd)
{
	if (loglen + (size_t)n + 1 > logcap) {
		logcap = (loglen + n + 1) * 2 + 65536;
		logbuf = realloc(logbuf, logcap);
	}
	memcpy(logbuf + loglen, buf, n);
	loglen += n;
	logbuf[loglen] = 0;
	if (confd >= 0)
		write(confd, buf, n);
}

static int
logcontains(const char *s)
{
	return logbuf && memmem(logbuf, loglen, s, strlen(s)) != NULL;
}

/* read from fd, append to log; returns bytes read (0 on EOF) */
static ssize_t
drain(int fd, int confd)
{
	char buf[16384];
	ssize_t n = read(fd, buf, sizeof(buf));
	if (n > 0)
		logappend(buf, n, confd);
	return n;
}

/* select-gated non-blocking drain: never blocks on an idle console */
static void
drain_nb(int fd, int confd)
{
	fd_set rfds;
	struct timeval tv;

	for (;;) {
		tv.tv_sec = 0;
		tv.tv_usec = 0;
		FD_ZERO(&rfds);
		FD_SET(fd, &rfds);
		if (select(fd + 1, &rfds, NULL, NULL, &tv) <= 0)
			return;
		if (drain(fd, confd) <= 0)
			return;
	}
}

static int
waitfor(int mfd, int confd, const char *s, int timeout_sec)
{
	time_t deadline = time(NULL) + timeout_sec;

	while (time(NULL) < deadline) {
		fd_set rfds;
		struct timeval tv;

		if (logcontains(s))
			return 1;
		FD_ZERO(&rfds);
		FD_SET(mfd, &rfds);
		tv.tv_sec = 1;
		tv.tv_usec = 0;
		if (select(mfd + 1, &rfds, NULL, NULL, &tv) > 0) {
			if (drain(mfd, confd) == 0)
				return -1;	/* EOF: vkernel died */
		}
	}
	return logcontains(s) ? 1 : 0;
}

int
main(int argc, char **argv)
{
	const char *mode = argc > 1 ? argv[1] : "full";
	int secs = argc > 2 ? atoi(argv[2]) : 120;
	const char *conlog = getenv("VKCON") ? getenv("VKCON") : "/tmp/vkconsole.log";
	const char *vkdir = "/usr/obj/usr/src/sys/VKERNEL64";
	const char *vkargs = getenv("VKARGS") ? getenv("VKARGS")
	    : "-m 512m -r /vkroot.img -n 2 -e init_path=/bin/vkinit";
	int mfd, sfd, confd;
	pid_t vk;
	struct winsize wz;
	unsigned long storms = 0;
	int i, rc = 0, died = 0;
	signal(SIGWINCH, SIG_IGN);
	signal(SIGCHLD, SIG_DFL);

	confd = open(conlog, O_WRONLY | O_CREAT | O_TRUNC, 0644);
	if (confd < 0) {
		perror(conlog);
		return 4;
	}

	/* pty pair */
	mfd = posix_openpt(O_RDWR | O_NOCTTY);
	if (mfd < 0 || grantpt(mfd) < 0 || unlockpt(mfd) < 0) {
		perror("posix_openpt");
		return 4;
	}
	sfd = open(ptsname(mfd), O_RDWR);
	if (sfd < 0) {
		perror("open pty slave");
		return 4;
	}

	vk = fork();
	if (vk == 0) {
		char *args[32];
		char *tmp, *tok;
		int n = 0;
		setsid();
		if (ioctl(sfd, TIOCSCTTY, 0) < 0)
			perror("TIOCSCTTY");
		dup2(sfd, 0);
		dup2(sfd, 1);
		dup2(sfd, 2);
		if (sfd > 2)
			close(sfd);
		if (mfd > 2)
			close(mfd);
		chdir(vkdir);
		setenv("SYSROOT", "/", 1);
		args[n++] = (char *)"kernel.debug";
		tmp = strdup(vkargs);
		while ((tok = strsep(&tmp, " \t")) != NULL) {
			if (*tok)
				args[n++] = tok;
		}
		args[n] = NULL;
		execv("./kernel.debug", args);
		perror("execv kernel.debug");
		_exit(99);
	}
	close(sfd);

	printf("[drv] vkernel pid=%d mode=%s secs=%d\n", vk, mode, secs);

	/*
	 * The vkernel's get_line() (sys/kern/vfs_conf.c:566-583) treats an
	 * empty non-blocking cncheckc() poll as EOF and panics, so the root
	 * spec must ALWAYS be queued on the console when the mountroot
	 * prompt starts reading.  Pre-feed continuously (vconsinit's
	 * TCSAFLUSH, console.c:460, eats feeds that land too early);
	 * surplus lines land in the sh as harmless "not found" commands.
	 */
	for (i = 0; i < 1200; i++) {
		if (write(mfd, "ufs:vkd0\n", 9) < 0)
			perror("feed rootspec");
		usleep(100000);
		{
			fd_set rfds;
			struct timeval tv0 = {0, 0};
			FD_ZERO(&rfds);
			FD_SET(mfd, &rfds);
			if (select(mfd + 1, &rfds, NULL, NULL, &tv0) > 0) {
				if (drain(mfd, confd) == 0) {
					fprintf(stderr, "[drv] EOF during feed\n");
					kill(vk, SIGKILL);
					return 3;
				}
			}
		}
		if (logcontains("Mounting root from ufs:vkd0\r"))
			break;
	}

	rc = waitfor(mfd, confd, "Mounting root from ufs:vkd0\r", 180);
	if (rc <= 0) {
		fprintf(stderr, "[drv] root mount failed (rc=%d)\n", rc);
		kill(vk, SIGKILL);
		return 3;
	}
	printf("[drv] root mounted\n");
	waitfor(mfd, confd, "# ", 90);
	sleep(3);
	drain_nb(mfd, confd);

	if (strcmp(mode, "full") == 0 || strcmp(mode, "churn") == 0) {
		char cmd[128];
		snprintf(cmd, sizeof(cmd), "/root/vkwinch %d\n", secs);
		if (write(mfd, cmd, strlen(cmd)) < 0)
			perror("write cmd");
		printf("[drv] racer started (%s)\n", cmd);
		if (waitfor(mfd, confd, "[race] uid=", 60) != 1) {
			fprintf(stderr, "[drv] racer did not start\n");
			kill(vk, SIGKILL);
			return 3;
		}
		printf("[drv] racer running as uid 9\n");
		/* let the churn spin up */
		sleep(2);
		drain_nb(mfd, confd);
	}

	if (strcmp(mode, "full") == 0 || strcmp(mode, "storm") == 0 ||
	    strcmp(mode, "plain") == 0) {
		/* baseline probe: what does the console tty think its size is */
		if (strcmp(mode, "plain") == 0) {
			write(mfd, "\nstty size\n", 11);
			sleep(3);
			drain_nb(mfd, confd);
			printf("[drv] baseline stty probed\n");
		}
		struct timeval tv;
		time_t end = time(NULL) + secs;
		int row = 100;
		printf("[drv] SIGWINCH storm begin\n");
		while (time(NULL) < end) {
			wz.ws_row = row;
			wz.ws_col = 80;
			wz.ws_xpixel = 640;
			wz.ws_ypixel = row * 10;
			if (ioctl(mfd, TIOCSWINSZ, &wz) < 0) {
				if (errno == EINTR)
					continue;
				perror("TIOCSWINSZ");
				break;
			}
			row = (row == 100) ? 101 : 100;
			storms++;
			/* service the console */
			{
				fd_set rfds;
				struct timeval tv0;
				tv0.tv_sec = 0;
				tv0.tv_usec = 0;
				FD_ZERO(&rfds);
				FD_SET(mfd, &rfds);
				if (select(mfd + 1, &rfds, NULL, NULL, &tv0) > 0) {
					if (drain(mfd, confd) == 0) {
						died = 1;
						break;
					}
				}
			}
			for (i = 0; badstr[i]; i++) {
				if (logcontains(badstr[i])) {
					printf("[drv] MATCH <%s>\n", badstr[i]);
					died = 2;
					break;
				}
			}
			if (died)
				break;
			if ((storms & 0x3FFF) == 0)
				usleep(200);	/* pacing */
		}
		printf("[drv] storm end: %lu TIOCSWINSZ ops\n", storms);
		if (strcmp(mode, "plain") == 0) {
			write(mfd, "\nstty size\n", 11);
			sleep(3);
			drain_nb(mfd, confd);
			printf("[drv] post-storm stty probed\n");
		}
	}

	/* post-window observation */
	for (i = 0; i < 30 && !died; i++) {
		fd_set rfds;
		struct timeval tvo;
		FD_ZERO(&rfds);
		FD_SET(mfd, &rfds);
		tvo.tv_sec = 1;
		tvo.tv_usec = 0;
		if (select(mfd + 1, &rfds, NULL, NULL, &tvo) > 0) {
			if (drain(mfd, confd) == 0) {
				died = 1;
				break;
			}
		}
		for (int j = 0; badstr[j]; j++) {
			if (logcontains(badstr[j])) {
				died = 2;
				break;
			}
		}
		if (kill(vk, 0) < 0 && errno == ESRCH) {
			died = 1;
			break;
		}
	}

	if (kill(vk, 0) == 0) {
		printf("[drv] VKRESULT: ALIVE\n");
		rc = 0;
	} else {
		int st;
		waitpid(vk, &st, 0);
		printf("[drv] VKRESULT: DEAD (status=%d)\n", st);
		rc = 2;
	}
	if (died == 2)
		rc = 2;

	/* dump the tail of the console for the caller */
	{
		size_t off = loglen > 8000 ? loglen - 8000 : 0;
		printf("=== console tail ===\n%.*s\n=== end tail ===\n",
		    (int)(loglen - off), logbuf + off);
	}
	kill(vk, SIGKILL);
	waitpid(vk, NULL, 0);
	close(confd);
	return rc;
}
