/*
 * Copyright 2001, 2002, 2003, 2004, 2013 Adam Sampson <ats@offog.org>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/wait.h>
#include <time.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/select.h>
#include "svstat.h"
#include "iolib.h"
#include "freedt.h"
#include "config.h"
const char *progname = "supervise";
const char *proghelp =
	"Usage: supervise [OPTIONS] servicedir\n"
	"Supervise a service directory.\n\n";

struct svstat svstatus;
int die_with_child = 0;
int restart_on_death = 1;
int statusfd, control, lockfd;
int selfpipe[2];

void sigchld_handler(int value) {
	/* We ignore errors here -- the only relevant error is EAGAIN
	   for the pipe being full, and that doesn't matter. */
	int saved_errno = errno;
	write(selfpipe[1], "x", 1);
	errno = saved_errno;
}

void start_child() {
	if (svstatus.pid != -1) return;

	svstatus.pid = fork();
	if (svstatus.pid < 0)
		die("fork failed");
	else if (svstatus.pid == 0) {
		sigset_t empty;

		sigemptyset(&empty);
		if (sigprocmask(SIG_SETMASK, &empty, NULL) < 0)
			_exit(111);

		if (setsid() < 0)
			_exit(111);

		execl("./run", "run", NULL);
		_exit(111);
	}
	svstatus.up = 1;
	svstatus.starttime = time(NULL);

	reliable_sleep(1);
}

void kill_child(int signal) {
	if (svstatus.pid == -1) return;

	if (kill(svstatus.pid, signal) < 0) {
		if (errno == EPERM) {
			warn("unable to send signal to process (EPERM)");
		} else if (errno == ESRCH) {
			/* The process has exited -- no problem. */
		} else {
			die("unable to send signal to process");
		}
	}
}

void write_status() {
	if (lseek(statusfd, 0, SEEK_SET) < 0)
		die("unable to seek status file");
	if (write(statusfd, &svstatus, sizeof svstatus) < 0)
		die("unable to write to status file");
}

int main(int argc, char **argv) {
	struct sigaction sa;
	sigset_t sig_chld;
	int fd;
	const char *name;

	get_default_args(argc, argv);
	if ((argc - optind) != 1)
		help();

	name = argv[optind];
	svstatus.pid = -1;
	svstatus.starttime = 0;
	svstatus.up = 0;

	if (chdir(name) < 0)
		die2(name, "unable to chdir to service directory");

	if (mkdir("supervise", 02700) < 0 && errno != EEXIST)
		die2(name, "unable to create supervise directory");

	lockfd = open("supervise/lock", O_WRONLY | O_CREAT, 0600);
	if (lockfd < 0)
		die2(name, "unable to open supervise/lock");
	if (lock_fd(lockfd, 0) < 0)
		die2(name, "unable to obtain lock on supervise/lock");
	set_fd_cloexec(lockfd);

	if (mkfifo("supervise/control", 0600) < 0 
		&& errno != EEXIST)
		die2(name, "unable to create fifo supervise/control");
	control = open("supervise/control", O_RDWR);
	if (control < 0)
		die2(name, "unable to open fifo supervise/control");
	set_fd_cloexec(control);

	statusfd = open("supervise/status", O_WRONLY | O_CREAT, 0644);
	if (statusfd < 0)
		die2(name, "unable to open supervise/status");
	set_fd_cloexec(statusfd);

	if (pipe(selfpipe) < 0)
		die("cannot create self-pipe");
	change_fd_flags(selfpipe[1], O_NONBLOCK, 0);
	set_fd_cloexec(selfpipe[0]);
	set_fd_cloexec(selfpipe[1]);

	sigemptyset(&sig_chld);
	sigaddset(&sig_chld, SIGCHLD);
	if (sigprocmask(SIG_BLOCK, &sig_chld, NULL) < 0)
		die("unable to block SIGCHLD");

	sa.sa_handler = sigchld_handler;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_RESTART;
	if (sigaction(SIGCHLD, &sa, NULL) < 0)
		die("unable to install SIGCHLD handler");

	fd = open("down", O_RDONLY);
	if (fd < 0) {
		start_child();
	} else {
		svstatus.starttime = time(NULL);
		close(fd);
	}

	write_status();

	while (1) {
		int rc;
		fd_set fds;
		sigset_t old_sigs;

		FD_ZERO(&fds);
		FD_SET(control, &fds);
		FD_SET(selfpipe[0], &fds);

		if (sigprocmask(SIG_UNBLOCK, &sig_chld, &old_sigs) < 0)
			die("unable to unblock SIGCHLD");

		do {
			rc = select(MAX(control, selfpipe[0]) + 1,
				&fds, NULL, NULL, NULL);
		} while (rc < 0 && errno == EINTR);
		if (rc < 0)
			die("select failed");

		if (sigprocmask(SIG_SETMASK, &old_sigs, NULL) < 0)
			die("unable to restore signal mask");

		if (FD_ISSET(control, &fds)) {
			/* Command from svc. */
			char c;

			if (read(control, &c, 1) < 0)
				die("read from control failed");

			switch (c) {
			case 'u':
				restart_on_death = 1;
				start_child();
				write_status();
				break;
			case 'o':
				restart_on_death = 0;
				start_child();
				write_status();
				break;
			case 'd':
				restart_on_death = 0;
				kill_child(SIGTERM);
				kill_child(SIGCONT);
				break;
			case 'p':
				kill_child(SIGSTOP);
				break;
			case 'c':
				kill_child(SIGCONT);
				break;
			case 'h':
				kill_child(SIGHUP);
				break;
			case 'a':
				kill_child(SIGALRM);
				break;
			case 'i':
				kill_child(SIGINT);
				break;
			case 't':
				kill_child(SIGTERM);
				break;
			case 'k':
				kill_child(SIGKILL);
				break;
			case 'X':
				kill_child(SIGTERM);
				kill_child(SIGCONT);
				exit(0);
			case 'x':
				if (svstatus.up == 1)
					die_with_child = 1;
				else
					exit(0);
				break;
			}
		}
	
		if (FD_ISSET(selfpipe[0], &fds)) {	
			/* SIGCHLD */
			char c;
			pid_t wrc;

			if (read(selfpipe[0], &c, 1) < 0)
				die("read from self-pipe failed");

			wrc = waitpid(svstatus.pid, NULL, WNOHANG);
			if (wrc < 0) {
				die("waitpid failed");
			} else if (wrc == 0) {
				/* The child has been SIGSTOPped, which
 				 * we don't care about. */
			} else {
				/* The child has exited. */
				svstatus.pid = -1;
				if (die_with_child)
					exit(0);
				if (restart_on_death)
					start_child();
				else
					svstatus.up = 0;
	
				svstatus.starttime = time(NULL);
				write_status();
			}
		}
	}

	return 0; /* NOTREACHED */
}

