/* watchd.c - file watcher daemon
 *
 * This daemon watches for changes to files named in its config file, and runs
 * handlers when they change. The config file is a sequence of lines of the
 * format:
 *   /path/to/file /path/to/program
 * All paths are relative to the directory watchd is run from, and there is no
 * ~ expansion or PATH expansion. Lines beginning with # are comments.
 * For example:
 *   /home/elly/p/watchd.c /usr/bin/gcc
 * This line will cause watchd to run `/usr/bin/gcc /home/elly/p/watchd.c`
 * whenever /home/elly/p/watchd.c is modified.
 */

#include <err.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

struct watch {
	struct watch *next;
	char *filepath;
	char *prog;
	int infd;
	int dfd;
};

struct watch *watches = NULL;

void usage(const char *progn) {
	printf("Usage: %s <config-file>\n", progn);
	exit(1);
}

void *xmalloc(size_t sz) {
	void *p = malloc(sz);
	if (!p)
		errx(2, "out of memory");
	return p;
}

char *xstrdup(char *str) {
	char *s = strdup(str);
	if (!s)
		errx(2, "out of memory");
	return s;
}

char *xdirname(char *path) {
	static char dnbuf[PATH_MAX + 1];
	char *p;
	if (strlen(path) + 1 > sizeof dnbuf)
		errx(2, "path too long");
	memcpy(dnbuf, path, strlen(path));
	p = dnbuf + strlen(dnbuf);
	while (*p != '/' && p >= dnbuf)
		p--;
	*p = '\0';
	return dnbuf;
}

char *xbasename(char *path) {
	static char bnbuf[PATH_MAX + 1];
	char *p;
	if (strlen(path) + 1 > sizeof bnbuf)
		errx(2, "path too long");
	memcpy(bnbuf, path, strlen(path));
	p = bnbuf + strlen(bnbuf);
	while (*p != '/' && p >= bnbuf)
		p--;
	return p + 1;
}

void loadline(char *buf) {
	char *fp = strtok(buf, " ");
	char *prog = strtok(NULL, "");
	struct watch *w;
	if (!fp || *fp == '#')
		return;
	if (!prog)
		errx(2, "Config error: missing prog near '%s'", fp);
	if (strchr(prog, '\n'))
		*strchr(prog, '\n') = '\0';
	w = xmalloc(sizeof *w);
	w->filepath = xstrdup(fp);
	w->prog = xstrdup(prog);
	w->next = watches;
	w->infd = -1;
	watches = w;
}

int mkwatch(void) {
	int in = inotify_init();
	struct watch *w;
	if (in == -1)
		err(3, "inotify_init()");
	for (w = watches; w; w = w->next) {
		int m = IN_MODIFY;
		int wfd = inotify_add_watch(in, w->filepath, m);
		if (wfd == -1)
			err(3, "inotify_add_watch(%s)", w->filepath);
		w->infd = wfd;
		m = IN_CREATE | IN_DELETE | IN_MOVED_TO;
		wfd = inotify_add_watch(in, xdirname(w->filepath), m);
		if (wfd == -1)
			err(3, "inotify_add_watch(%s)", xdirname(w->filepath));
		w->dfd = wfd;
	}
	return in;
}

size_t readevt(char *p, size_t len, struct inotify_event **evt) {
	*evt = (struct inotify_event *)p;
	size_t used = sizeof **evt;
	if (len < sizeof **evt)
		return len;
	if ((*evt)->len != 0)
		used += (*evt)->len;
	return used;
}

struct watch *findwatch(int wd) {
	struct watch *w;
	for (w = watches; w; w = w->next)
		if (w->infd == wd)
			return w;
	return NULL;
}

void dowatch(struct watch *w) {
	char *progn[3];
	int pid;
	int status;
	progn[0] = w->prog;
	progn[1] = w->filepath;
	progn[2] = NULL;

	pid = fork();
	if (pid < 0)
		err(2, "fork()");
	else if (pid == 0 && execv(w->prog, progn))
		err(2, "execv(%s)", w->prog);
	else if (pid > 0 && waitpid(pid, &status, 0) != pid)
		err(2, "waitpid(%d)", pid);
}

int affectedby(struct watch *w, struct inotify_event *evt) {
	if (w->dfd != evt->wd)
		return 0;
	if (strcmp(xbasename(w->filepath), evt->name))
		return 0;
	return 1;
}

void renew(int infd, struct watch *w) {
	if (w->infd != -1 && inotify_rm_watch(infd, w->infd) == -1)
		err(4, "inotify_rm_watch()");
	w->infd = inotify_add_watch(infd, w->filepath, IN_MODIFY);
	if (w->infd == -1)
		err(4, "inotify_add_watch()");
}

void track(int infd, struct inotify_event *evt) {
	struct watch *w;
	for (w = watches; w; w = w->next) {
		if (affectedby(w, evt)) {
			renew(infd, w);
			if (evt->mask & (IN_CREATE | IN_MOVED_TO))
				dowatch(w);
		}
	}
}

void watch(void) {
	int infd = mkwatch();
	char buf[4096];
	ssize_t len;
	while ((len = read(infd, buf, sizeof buf)) > 0) {
		char *p = buf;
		while (len > 0) {
			struct inotify_event *evt;
			size_t used = readevt(p, len, &evt);
			struct watch *w;
			p += used;
			len -= used;
			if ((w = findwatch(evt->wd)))
				dowatch(w);
			else
				track(infd, evt);
		}
	}
	close(infd);
}

void loadconf(const char *confname) {
	FILE *f = fopen(confname, "r");
	char buf[1024];
	if (!f)
		err(2, "fopen(%s)", confname);
	while (fgets(buf, sizeof buf, f))
		loadline(buf);
	if (!feof(f))
		err(2, "fgets(%s)", confname);
	fclose(f);
}

int main(int argc, char *argv[]) {
	if (argc != 2)
		usage(argv[0]);
	loadconf(argv[1]);
	watch();
	return 1;
}
