/* ----------------------------------------------------------------------------
 * $Id: kickd.c,v 1.1 2005/12/23 09:58:24 stephan Exp $
 * ----------------------------------------------------------------------------
 * kickd: watch a directory for updates and if one occurs, have a kicker 
 *        application perform some actions
 * ----------------------------------------------------------------------------
 * $Log: kickd.c,v $
 * Revision 1.1  2005/12/23 09:58:24  stephan
 * Initial revision
 *
 * ----------------------------------------------------------------------------
 * Copyright (C) 2006 Stephan Leemburg <stephan@jvc.nl> All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ----------------------------------------------------------------------------
 */
#include <string.h>
#include <limits.h>
#include <stdlib.h>
#include <sys/types.h>

#ifdef __APPLE__ /* compiles, not tested however */
#define KQUEUE
#endif
#ifdef __FreeBSD__
#define KQUEUE
#endif
#ifdef __linux__
#define DNOTIFY
#define _GNU_SOURCE
#endif

#ifdef KQUEUE	/* *BSD */
#include <sys/event.h>
#endif

#ifdef DNOTIFY /* Linux */
#define __USE_GNU
#endif

#include <fcntl.h>

#ifdef DNOTIFY /* Linux */
#undef __USE_GNU
#endif

#include <sys/param.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#include <errno.h>
#include <dirent.h>
#include <syslog.h>

#define MYNAME		"kickd"
#define MYVERSION	"0.1"
#define PIDFMT		"/tmp/" MYNAME "-%ld-%ld.pid"
#define PIDLEN		128 /* string size of pid */

#define STARTING	0
#define RUNNING		1
#define STOPPING	2

#define TIMER		1800 /* logmark every 15 minutes */
#define MAX_PENDING	5
#define SECSTOLINGER	0
#define NSECSTOLINGER	500000000 /* nano -> 10^-9, micro 10^-6 */

#define OPT_BAILOUT	(1<<0)
#define OPT_ONESHOT	(1<<1)
#define OPT_NODAEMON	(1<<2)

static int mylock = -1;
static char pidfile[FILENAME_MAX+1] = { 0 };
static volatile int mystate = STARTING;

static char *checkkicker(char *);
static char *findpath(char *);
static int monitor(char*, char*, int);
static int kick(char*, char*);
static int watch(int);

#ifdef KQUEUE
static int kqueuewatch(int);
#endif
#ifdef DNOTIFY
static int dnotifywatch(int);
#endif

static void sighandler(int);
#ifdef DNOTIFY
static void sigiohandler(int);
#endif
static void setuphandler(int, void (*handler)(int));
static int startservice(char*, int);
static int stopservice(int);
static void usage(char*, char *);

int main(int argc, char **argv)
{
	char *base, *kicker, basepath[PATH_MAX+1];
	int options, c;

	base = kicker = 0;
	options = 0;

	openlog(MYNAME, LOG_CONS, LOG_DAEMON);

	while((c=getopt(argc, argv, "b:fk:ox")) >= 0)
		switch(c)
		{
		case 'b':
			base = optarg;
			break;
		case 'f':
			options |= OPT_NODAEMON;
			break;
		case 'k':
			kicker = optarg;
			break;
		case 'o':
			options |= OPT_ONESHOT;
			break;
		case 'x':
			options |= OPT_BAILOUT;
			break;
		default:
			usage(MYNAME, MYVERSION);
			return EXIT_FAILURE;
		}

	argc -= optind;

	if(argc)
		syslog(LOG_ERR, "stray arguments on commandline (ignoring)");

	if(!base)
	{
		syslog(LOG_ALERT, "no base supplied, using \".\" as base");
		base = ".";
	}

	if(!realpath(base, basepath))
	{
		syslog(LOG_ERR, "cannot determine realpath of base (%m)");
		return EXIT_FAILURE;
	}

	if(!(kicker = checkkicker(kicker)))
		return EXIT_FAILURE;

	if(startservice(basepath, options) != EXIT_SUCCESS)
		return EXIT_FAILURE;

	return stopservice(monitor(basepath, kicker, options));
}

static char *checkkicker(char *kicker)
{
	char *path;
	struct stat sb;

	if(!kicker || !*kicker)
	{
		syslog(LOG_ERR, "no kicker supplied");
		return 0;
	}

	path = findpath(kicker);
	if(!path || stat(path, &sb))
	{
		syslog(LOG_ERR, "cannot stat(%s) (%m)", kicker);
		return 0;
	}
	
	if(sb.st_uid != geteuid())
	{
		syslog(LOG_ERR, "kicker(%s) is not owned by %d", 
			kicker, geteuid());
		return 0;
	}

	if(!(sb.st_mode & S_IFREG) || !(sb.st_mode & 0100))
	{
		syslog(LOG_ERR, "kicker(%s) is not executable", kicker);
		return 0;
	}

	if(sb.st_mode & 0022)
	{
		syslog(LOG_ERR, "kicker(%s) is writeable by group/others", 
			kicker);
		return 0;
	}
	return strdup(path);
}

char *findpath(char *kicker)
{
	char *p, *path; 
	static char exe[PATH_MAX+1];
	static char try[PATH_MAX+1];
	struct stat sb;

	if(!stat(kicker, &sb) && realpath(kicker, exe))
		return exe;

	p = getenv("PATH");
	if(!p)
		return 0;

	path = strdup(p);
	if(!path)
		return 0;
	
	for(p = strtok(path, ":"); p; p = strtok(0, ":"))
	{
		snprintf(try, sizeof(exe), "%s/%s", p, kicker);
		if(stat(try, &sb))
			continue;
	
		p = realpath(try, exe);
		break;
	}
	free(path);
	return p;
}

static int monitor(char *base, char *kicker, int options)
{
	int fd = -1;

	syslog(LOG_INFO, "%smonitoring base %s with kicker %s", 
		options & OPT_ONESHOT ? "oneshot " : "", base, kicker);

	fd = open(base, O_RDONLY);
	if(fd < 0)
	{
		syslog(LOG_ERR, "cannot open '%s' (%m)", base);
		return EXIT_FAILURE;
	}
	
	while(mystate != STOPPING)
	{
		if(watch(fd) == EXIT_FAILURE)
			continue;
		
		/* 
		 * start kicker on the signalled change, but don't
		 * queue other changes while it is running as the
		 * kicker may be the cause of these changes itself
		 */
	
		if(kick(base, kicker) != EXIT_SUCCESS && (options & OPT_BAILOUT))
			break;

		if(options & OPT_ONESHOT)
			mystate = STOPPING;
	}
	if(fd >= 0)
		close(fd);

	return mystate == STOPPING ? EXIT_SUCCESS : EXIT_FAILURE;
}

static int kick(char *base, char *kicker)
{
	pid_t pid;
	int status, tries;
	char *service = strrchr(kicker, '/');
	if(!*service)
		service = kicker;

	syslog(LOG_INFO, "kicking '%s' on base '%s'", kicker, base);

	if((pid = fork()) < 0)
	{
		syslog(LOG_ERR, "cannot fork (%m)");
		return EXIT_FAILURE;
	}
	
	if(!pid) /* child */
	{
		/* parent is already in the path of base */

		execl(kicker, service, base, NULL);
		syslog(LOG_ERR, "exec of kicker '%s' failed (%m)", kicker);
		_exit(EXIT_FAILURE);
	} 

	tries = 0;

	while(1)
	{
		if(waitpid(pid, &status, 0) < 0)
		{ 
			if(errno == EINTR && mystate == STOPPING)
			{
				syslog(LOG_INFO, "killing child before stopping");
				kill(pid, SIGTERM);
				if(++tries > 2)
					break;
			}

			/* 
			 * sigio received, there's more work to do but we
			 * need to wait for the current job to finish in
			 * order to serialize jobs
			 */
			continue;
		}

		if(WIFEXITED(status))
		{
			if(status)
				syslog(LOG_INFO, "kicker exited with status (%d)", 
					WEXITSTATUS(status));
		}
		else if(WIFSIGNALED(status))
			syslog(LOG_ERR, "kicker terminated with signal (%d)", 
				WTERMSIG(status));
		else
			syslog(LOG_ERR, "kicker seems to have stopped");

		break;
	}

	return EXIT_SUCCESS; /* continue to monitor */
}

static int watch(int fd)
{
#ifdef KQUEUE
	return kqueuewatch(fd);
#endif
#ifdef DNOTIFY
	return dnotifywatch(fd);
#endif
}

#ifdef KQUEUE /* *BSD */
#error TODO Watch a directory
static int kqueuewatch(int fd)
{
	int fired;
	static int kq = -1;
	struct kevent watch;
	struct kevent event;
	struct timespec timer;

	bzero(&watch, sizeof(struct kevent));
	bzero(&event, sizeof(struct kevent));

	watch.ident = fd;
	watch.filter = EVFILT_VNODE;
	watch.flags = EV_ADD | EV_CLEAR;
	watch.fflags = NOTE_DELETE|NOTE_WRITE|NOTE_EXTEND|NOTE_ATTRIB|NOTE_LINK;
	watch.data = 0;
	watch.udata = 0;

	if(kq < 0)
		kq = kqueue();

	if(kq < 0) 
	{
		syslog(LOG_ERR, "cannot create kqueue: (%m)");
		return EXIT_FAILURE;
	}
	kevent(kq, &watch, 1, NULL, 0, NULL);

	timer.tv_sec = TIMER;
	timer.tv_nsec = 0;

	for(fired=0;;) 
	{
		int rc = kevent(kq, NULL, 0, &event, 1, &timer);
		if(rc < 0)
		{
			syslog(LOG_ERR, "kevent error: (%m)");
			return EXIT_FAILURE;
		}

		if(rc == 0) 
		{
			/* and kick in if event(s) occured */
			if(fired)
				break;

			timer.tv_sec = TIMER;
			timer.tv_nsec = 0;
			/*syslog(LOG_NOTICE, "Still watching.");*/
			continue;
		}
		if(++fired > MAX_PENDING)
			break;

		/* wait a second for another event */

		timer.tv_sec = SECSTOLINGER;
		timer.tv_nsec = NSECSTOLINGER;
		fired++;
	}
	syslog(LOG_INFO, "kicking in for pending %d events", fired);

	return EXIT_SUCCESS;
}
#endif

#ifdef DNOTIFY /* Linux */
static int dnotifywatch(int fd)
{
	int fired;
	struct timeval timer;
	static int registered = 0;

	timer.tv_sec = TIMER;
	timer.tv_usec = 0;

	if(!registered)
	{
		if(fcntl(fd, F_NOTIFY, DN_MULTISHOT|DN_DELETE|DN_MODIFY|DN_CREATE))
		{
			syslog(LOG_ERR, "cannot set DNOTIFY (%m)");
			return EXIT_FAILURE;
		}
		registered = 1;
	}

	for(fired=0;;) 
	{
		int rc = select(0, 0, 0, 0, &timer);
		if(!rc) /* timer expired */
		{
			/* and kick in if event(s) occured */
			if(fired)
				break;

			timer.tv_sec = TIMER;
			timer.tv_usec = 0;
			continue;
		}
		if(errno == EINTR && mystate == STOPPING)
			return EXIT_FAILURE;

		if(++fired > MAX_PENDING)
			break;

		/* wait a second for another event */

		timer.tv_sec = SECSTOLINGER;
		timer.tv_usec = (NSECSTOLINGER / 1000); /* 10^-9 to 10^-6 */
	}
	syslog(LOG_INFO, "kicking in for pending %d events", fired);

	return EXIT_SUCCESS;
}
#endif

#ifdef DNOTIFY
static void sigiohandler(int sig)
{
	;
}
#endif

static void sighandler(int sig)
{
	switch(sig)
	{
	default:
		mystate = STOPPING;
		break;
	}
}

static void setuphandler(int sig, void (*handler)(int))
{
	struct sigaction sa;

	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = handler;

	sigaction(sig, &sa, 0);
}

static int startservice(char *base, int options)
{
	char pid[PIDLEN];
	struct flock sf;
	struct stat sb;
	char path[PATH_MAX+1];

	if(stat(base, &sb))
	{
		syslog(LOG_ERR, "cannot stat(%s) (%m)", base);
		return EXIT_FAILURE;
	}

	snprintf(path, sizeof(path), "%s", base);
	if(!S_ISDIR(sb.st_mode))
	{
		dirname(path);

		syslog(LOG_ERR, "file monitoring is not yet supported");
		return EXIT_FAILURE;
	}

	if(chdir(path))
	{
		syslog(LOG_ERR, "cannot chdir(%s) (%m)", path);
		return EXIT_FAILURE;
	}

	if(!(options & OPT_NODAEMON) && daemon(1, 0))
	{
		syslog(LOG_ERR, "cannot daemonize (%m)");
		return EXIT_FAILURE;
	}

	snprintf(pidfile, FILENAME_MAX, PIDFMT, (long)sb.st_dev, sb.st_ino);

	mylock = open(pidfile, O_RDWR|O_CREAT, 0644);
	if(mylock < 0)
	{
		syslog(LOG_ERR, "cannot open(%s) (%m)", pidfile);
		return EXIT_FAILURE;
	}

	sf.l_type = F_WRLCK;
	sf.l_whence = sf.l_start = sf.l_len = 0;
	sf.l_pid = 0;

	if(fcntl(mylock, F_SETLK, &sf) < 0)
	{
		syslog(LOG_ERR, "cannot lock(" MYNAME ") (%m)");
		return EXIT_FAILURE;
	}

	snprintf(pid, sizeof(pid), "%d\n", getpid());

	if(ftruncate(mylock, 0) 
	|| write(mylock, pid, strlen(pid)) != strlen(pid))
	{
		syslog(LOG_ERR, "cannot write(" MYNAME ") (%m)");
		return EXIT_FAILURE;
	}

	mystate = RUNNING;

	setuphandler(SIGINT, &sighandler);
	setuphandler(SIGQUIT, &sighandler);
	setuphandler(SIGTERM, &sighandler);
#ifdef DNOTIFY
	setuphandler(SIGIO, &sigiohandler);
#endif

	return EXIT_SUCCESS;
}

static int stopservice(int status)
{
	syslog(LOG_INFO, "stopping service " MYNAME " %s" ,
		status == EXIT_FAILURE ? "with an error" : "normally");

	if(mystate != STARTING)
	{
		unlink(pidfile);
		close(mylock); /* and release fcntl lock */
	}
	return status;
}

static void usage(char *name, char *vs)
{
	fprintf(stderr, "usage: %s -b <base> -k <kicker> [-f][-o][-x]\n"
		"where:\n"
		" -b <base>   : use <base> as the nis repository basename\n"
		" -k <kicker> : use <kicker> as the change kicker\n"
		" -f          : do not daemonize but stay in the foreground\n"
		" -o          : oneshot, exit after one sucessful call of the"
			      " kicker\n"
		" -x          : exit when the kicker ends with an error\n"
		"Version: %s\n",
		name,
		vs
	);
}

