/*
 * Copyright 2001, 2002, 2003, 2009 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 <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <dirent.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include "iolib.h"
#include "freedt.h"
#include "config.h"
const char *progname = "svscan";
const char *proghelp =
	"Usage: svscan [OPTIONS] [dir]\n"
	"Manage several supervise processes for a directory of services.\n\n";

struct service {
	char *dir;
	int logpipe[2];
	int found;
	struct service *next;
};

struct service *services = NULL;
int numservices = 0;
int startdir;

void sigchld_handler(int value) {
	int saved_errno = errno;
	while (waitpid(0, NULL, WNOHANG) > 0)
		/* nothing */;
	errno = saved_errno;
}

/* Check whether a service is running in dir. Returns 1 if it is, 0 if
   it isn't, and -1 if dir isn't a service directory or another error
   occurred. */
int service_running(const char *dir) {
	int fd;
	buffer b = BUFFER;

	bformat(&b, "@c/supervise/control", dir);
	fd = open(bstr(&b), O_WRONLY | O_NONBLOCK);
	bfree(&b);

	if (fd < 0) {
		if (errno == ENXIO || errno == ENOENT) {
			/* No supervise is listening, or no
			   supervise dir or pipe exists. */
			return 0;
		} else if (errno == ENOTDIR) {
			/* What we thought was a service directory is
			   actually a file or a non-service. */
			return -1;
		} else {
			warn2(dir, "unable to open control pipe");
			return -1;
		}
	}

	close(fd);
	return 1;
}

/* Check that a supervise process is running happily for dir, and 
   start one if it isn't. Return 0 on success, -1 if dir isn't
   actually a service or something else goes wrong. */
int check_supervise(const char *dir, int in, int err) {
	pid_t pid;
	int running = service_running(dir);

	if (running == -1)
		return -1;
	else if (running == 1)
		return 0;

	pid = fork();
	if (pid < 0) {
		warn2(dir, "fork failed");
		return -1;
	}

	if (pid == 0) {
		if (in != -1) {
			if (dup2(in, STDIN_FILENO) < 0)
				_exit(111);
		}
		if (err != -1) {
			if (dup2(err, STDOUT_FILENO) < 0)
				_exit(111);
		}

		execlp("supervise", "supervise", dir, NULL);
		_exit(111);
	}

	return 0;
}

/* Check that a supervise or pair of supervises is running for service
   serv, and start them if not. Return 0 on success, or -1 if we
   shouldn't supervise this service any more. */
int check_service(struct service *serv) {
	if (check_supervise(serv->dir, -1, serv->logpipe[1]) < 0)
		return -1;

	if (serv->logpipe[0] != -1) {
		buffer b = BUFFER;
		int rc;

		bformat(&b, "@c/log", serv->dir);
		rc = check_supervise(bstr(&b), serv->logpipe[0], -1);
		bfree(&b);

		if (rc < 0)
			return -1;
	}

	return 0;
}

void remove_service(struct service *this) {
	struct service *prev;

	if (services == this) {
		services = this->next;
	} else {
		for (prev = services; prev; prev = prev->next)
			if (prev->next == this)
				break;
		if (prev == NULL)
			die("service list corrupt");
		prev->next = this->next;
	}
	if (this->logpipe[0] != -1) {
		close(this->logpipe[0]);
		close(this->logpipe[1]);
	}
	free(this->dir);
	free(this);
}

struct service *add_service(const char *dir) {
	struct service *svc;

	if (chdir(dir) < 0) {
		if (errno != ENOTDIR)
			warn2(dir, "unable to change into service directory");
		return NULL;
	}

	svc = malloc(sizeof *svc);
	if (svc == NULL) {
		warn("out of memory");
		goto out;
	}

	if (chdir("log") == 0) {
		if (pipe(svc->logpipe) < 0) {
			warn("cannot create pipe");
			free(svc);
			svc = NULL;
			goto out;
		}
		set_fd_cloexec(svc->logpipe[0]);
		set_fd_cloexec(svc->logpipe[1]);
	} else {
		svc->logpipe[0] = -1;
		svc->logpipe[1] = -1;
	}

	svc->dir = strdup(dir);
	svc->next = NULL;
	if (services == NULL) {
		services = svc;
	} else {
		struct service *last;
		for (last = services; last->next; last = last->next)
			;
		last->next = svc;
	}

out:
	if (fchdir(startdir) < 0)
		die("unable to change back to start directory");

	return svc;
}

struct service *find_service(const char *dir) {
	struct service *svc;

	for (svc = services; svc; svc = svc->next) {
		if (strcmp(svc->dir, dir) == 0)
			return svc;
	}
	return NULL;
}

int main(int argc, char **argv) {
	struct sigaction sa;
	DIR *dir;

	get_default_args(argc, argv);
	if ((argc - optind) == 1) {
		if (chdir(argv[optind]) < 0)
			die("unable to chdir to specified directory");
	} else if (argc == optind) {
		/* scan current directory */
	} else {
		help();
	}

	startdir = open(".", O_RDONLY);
	if (startdir < 0)
		die("unable to open specified directory");
	set_fd_cloexec(startdir);

	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");

	while (1) {
		struct service *svc;

		dir = opendir(".");
		if (dir == NULL) {
			warn("unable to list directory");
			goto next;
		}

		for (svc = services; svc; svc = svc->next)
			svc->found = 0;

		while (1) {
			struct dirent *entry = readdir(dir);

			if (entry == NULL)
				break;
			if (entry->d_name[0] == '.')
				continue;

			svc = find_service(entry->d_name);
			if (svc == NULL)
				svc = add_service(entry->d_name);

			if (svc != NULL) {
				if (check_service(svc) < 0)
					svc->found = 0;
				else
					svc->found = 1;
			}
		}

		for (svc = services; svc;) {
			struct service *next = svc->next;
			if (svc->found == 0)
				remove_service(svc);
			svc = next;
		}

		closedir(dir);
	next:
		reliable_sleep(5);
	}

	return 0; /* NOTREACHED */
}

