commit 9fcdef164f2fae4b5e96d77a3ff56ea427288240
Author: Natasha Kerensikova <natacha@instinctive.eu>
Date:   Tue, 30 Jul 2013 23:38:09 +0200
Initial commit
Diffstat:
| A | LICENSE |  |  | 13 | +++++++++++++ | 
| A | Makefile |  |  | 63 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | README.md |  |  | 73 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | TODO |  |  | 6 | ++++++ | 
| A | filewatcherd.c |  |  | 322 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | log.c |  |  | 285 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | log.h |  |  | 168 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | run.c |  |  | 92 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | run.h |  |  | 30 | ++++++++++++++++++++++++++++++ | 
| A | watchtab.c |  |  | 666 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | watchtab.h |  |  | 113 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
11 files changed, 1831 insertions(+), 0 deletions(-)
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2013, Natacha Porté
+
+Permission to use, copy, modify, and 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.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,63 @@
+# Makefile
+
+# Copyright (c) 2009-2013, Natacha Porté
+#
+# Permission to use, copy, modify, and 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.
+
+DEPDIR=depends
+ALLDEPS=$(DEPDIR)/all
+CFLAGS=-g -O3 -Wall -Wextra -Werror
+LDFLAGS=-g -O3 -Wall -Wextra -Werror
+CC=gcc
+
+all:		filewatcherd
+
+.PHONY:		all clean
+
+
+# executables
+
+filewatcherd:	filewatcherd.o log.o run.o watchtab.o
+	$(CC) $(LDFLAGS) $(.ALLSRC) -o $(.TARGET)
+
+
+# Housekeeping
+
+clean:
+	rm -f *.o
+	rm -f filewatcherd
+	rm -rf $(DEPDIR)
+
+
+# dependencies
+
+.sinclude "$(ALLDEPS)"
+
+
+# generic object compilations
+
+.c.o:
+	@mkdir -p $(DEPDIR)
+	@touch $(ALLDEPS)
+	@$(CC) -MM $(.IMPSRC) > $(DEPDIR)/$(.PREFIX).d
+	@grep -q "$(.PREFIX).d" $(ALLDEPS) \
+			|| echo ".include \"$(.PREFIX).d\"" >> $(ALLDEPS)
+	$(CC) -c $(CFLAGS) -o $(.TARGET) $(.IMPSRC)
+
+.m.o:
+	@mkdir -p $(DEPDIR)
+	@touch $(ALLDEPS)
+	@$(CC) -MM $(.IMPSRC) > depends/$(.PREFIX).d
+	@grep -q "$(.PREFIX).d" $(ALLDEPS) \
+			|| echo ".include \"$(.PREFIX).d\"" >> $(ALLDEPS)
+	$(CC) -c $(CFLAGS) -o $(.TARGET) $(.IMPSRC)
diff --git a/README.md b/README.md
@@ -0,0 +1,73 @@
+# Overview
+
+`filewatcherd` is a daemon inspired by cron, that run commands based on
+file changes instead of time.
+
+In principle it is similar to `incron`, but it's simpler, more limited,
+and does not depend on anything outside of FreeBSD base.
+
+# Watchtab
+
+Usage of `filewatcherd` is quite straightforward: the daemon has a few
+basic command-line options, and takes a _watchtab_ file as main input.
+
+The watchtab is heavily inspired from `crontab`. Blank lines are ignored,
+leading and trailing blanks in line are ignored, line starting with a
+hash sign (`#`) are ignored as comments.
+
+Environment lines are defined as having an equal sign (`=`) before any
+backslash (`\\`) or tabulation character. They represent environment
+variables available for commands, and only affect the entries below them.
+
+Entry lines consist of 3 to 6 tabulation-separated fields. A complete line
+contains the following fields in respective order:
+
+1. Path of the file to watch
+2. Event set to consider
+3. Delay between the first triggering event and command run
+4. User, and optionally group, to set for the command
+5. `chroot` to set for the command
+6. The command itself
+
+When only 5 fields are present, `chroot` is skipped. When there are only
+4 fields, user is also skipped. When there are only 3 field, delay is
+considered 0.
+
+In path, `chroot` and command fields, backslashes (`\\`) act as an escape
+character, allowing to embed tabulations, backslashes and/or equal signs
+in those fields without misinterpretation.
+
+The event set can be a single star sign (`*`) to mean all available event,
+or a list of any number of event names separated by a single non-letter
+byte. The available events are `delete`, `write`, `extend`, `attrib`,
+`link`, `rename` and `revoke`, with semantics matching those of
+similar-named `fflags` for vnode filter.
+
+The delay is given in seconds and can be fractional, up to the nanosecond
+(though most system probably do not have such a resolution in
+`nanosleep(3)`).
+
+The user can be a login string or a numeric id, and is optionally followed
+by a group string or numeric id after a colon (`:`). When specified, those
+must exist and have a matching `passwd` or `group` entry.
+
+The command is run in a clean environment, containing only variables
+explicitly declared in the watchtab file, and `SHELL`, `PATH`, `HOME`,
+`TRIGGER`, `USER`, `LOGNAME`.
+
+  * `SHELL` and `PATH` default respectively to `/bin/sh` and
+`/usr/bin:/bin`, but they can be overwritten in the watchtab.
+  * `HOME` default to the home directory of the user running the command,
+but can be overwritten in the watchtab.
+  * `USER` and `LOGNAME` are both forced to the login name of the user
+running the command, and values provided in the watchtab are ignored.
+  * `TRIGGER` is forced to the path of the file triggering the event
+(seen from outside the `chroot`), ignoring any value provided in the
+watchtab.
+
+The watchtab is automatically watched by `filewatcherd` itself, and is
+automatically reloaded when it changes.
+
+# Internals
+
+Coming soon.
diff --git a/TODO b/TODO
@@ -0,0 +1,6 @@
+  * document the internals
+  * fix implicit HOME in first entry considered as explicit in following entries
+  * deal with command output
+  * check how signals interfere with current code
+  * think about how to handle multiple watchtabs
+  * support locking watchtabs to specific users, to make a safe multiuser system daemon
diff --git a/filewatcherd.c b/filewatcherd.c
@@ -0,0 +1,322 @@
+/* filewatcherd.c - main function for file watcher daemon */
+
+/*
+ * Copyright (c) 2013, Natacha Porté
+ *
+ * Permission to use, copy, modify, and 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 <fcntl.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <syslog.h>
+
+#include <sys/types.h>
+#include <sys/event.h>
+#include <sys/time.h>
+
+#include "log.h"
+#include "run.h"
+#include "watchtab.h"
+
+/* insert_entry - wait for an event described by the given watchtab entry */
+static int
+insert_entry(int kq, struct watch_entry *wentry) {
+	struct kevent event;
+	wentry->fd = open(wentry->path, O_RDONLY | O_CLOEXEC);
+	if (wentry->fd < 0) {
+		log_open_entry(wentry->path);
+		wentry->fd = -1;
+		return -1;
+	}
+	EV_SET(&event, wentry->fd,
+	    EVFILT_VNODE,
+	    EV_ADD | EV_ONESHOT,
+	    wentry->events,
+	    0,
+	    wentry);
+	if (kevent(kq, &event, 1, 0, 0, 0) < 0) {
+		log_kevent_entry(wentry->path);
+		close(wentry->fd);
+		wentry->fd = -1;
+		return -1;
+	}
+
+	log_entry_wait(wentry);
+	return 0;
+}
+
+
+
+int
+main(int argc, char **argv) {
+	int kq;			/* file descriptor for the kernel queue */
+	int argerr = 0;		/* whether arguments are invalid */
+	int help = 0;		/* whether help text should be displayed */
+	int daemonize = 1;	/* whether fork to background and use syslog */
+	const char *tabpath = 0;/* path to the watchtab file */
+	int tab_fd;		/* file descriptor of watchtab */
+	FILE *tab_f;		/* file stream of watchtab */
+	struct watchtab wtab;	/* current watchtab data */
+	intptr_t delay = 100;	/* delay in ms before reloading watchtab */
+	int wtab_error = 0;	/* whether watchtab can't be opened */
+
+	struct option longopts[] = {
+	    { "foreground", no_argument,       0, 'd' },
+	    { "help",       no_argument,       0, 'h' },
+	    { "wait",       required_argument, 0, 'w' },
+	    { 0,            0,                 0,  0 }
+	};
+
+	/* Temporary variables */
+	struct kevent event;
+	struct watch_entry *wentry;
+	pid_t pid;
+	char c;
+	char *s;
+
+
+	/***************************
+	 * COMMAND LINE PROCESSING *
+	 ***************************/
+
+	/* Process options */
+	while (!argerr
+	    && (c = getopt_long(argc, argv, "dhw:", longopts, 0)) != -1) {
+		switch (c) {
+		    case 'd':
+			daemonize = 0;
+			break;
+		    case 'h':
+			help = 1;
+			break;
+		    case 'w':
+			delay = strtol(optarg, &s, 10);
+			if (!s[0]) {
+				log_bad_delay(optarg);
+				argerr = 1;
+			}
+			break;
+		    default:
+			argerr = 1;
+		}
+	}
+
+	/* Use the first argument as watchtab, discard the reset */
+	if (optind < argc)
+		tabpath = argv[optind];
+	else
+		argerr = 1;
+
+	/* Display help text and terminate */
+	if (argerr || help) {
+		fprintf(argerr ? stderr : stdout,
+		    "Usage: %s [-dh] [-f delay_ms] watchtab\n\n"
+		    "\t-d, --foreground\n"
+		    "\t\tDon't fork to background and log to stderr\n"
+		    "\t-h, --help\n"
+		    "\t\tDisplay this help text\n"
+		    "\t-w, --wait delay_ms\n"
+		    "\t\tWait that number of milliseconds after watchtab\n"
+		    "\t\tchanges before reloading it\n",
+		    argv[0]);
+		return argerr ? EXIT_FAILURE : EXIT_SUCCESS;
+	}
+
+
+	/******************
+	 * INITIALIZATION *
+	 ******************/
+
+	/* Try to open and read the watchtab */
+	tab_fd = open(tabpath, O_RDONLY | O_CLOEXEC);
+	if (tab_fd < 0) {
+		log_open_watchtab(tabpath);
+		return EXIT_FAILURE;
+	}
+	tab_f = fdopen(tab_fd, "r");
+	if (!tab_f) {
+		log_open_watchtab(tabpath);
+		return EXIT_FAILURE;
+	}
+	SLIST_INIT(&wtab);
+	if (wtab_readfile(&wtab, tab_f, tabpath) < 0)
+		return EXIT_FAILURE;
+	log_watchtab_loaded(tabpath);
+
+	/* Create a kernel queue */
+	kq = kqueue();
+	if (kq == -1) {
+		log_kqueue();
+		return EXIT_FAILURE;
+	}
+
+	/* Insert config file watcher */
+	EV_SET(&event, tab_fd,
+	    EVFILT_VNODE,
+	    EV_ADD | EV_ONESHOT,
+	    NOTE_DELETE | NOTE_WRITE | NOTE_RENAME | NOTE_REVOKE,
+	    0, 0);
+	if (kevent(kq, &event, 1, 0, 0, 0) < 0) {
+		log_kevent_watchtab(tabpath);
+		return EXIT_FAILURE;
+	}
+
+	/* Fork to background */
+	if (daemonize) {
+		daemon(0, 0);
+		set_report(&syslog);
+	}
+
+	/* Insert initial watchers */
+	SLIST_FOREACH(wentry, &wtab, next) {
+		insert_entry(kq, wentry);
+	}
+
+
+	/*************
+	 * MAIN LOOP *
+	 *************/
+
+	while (1) {
+		/* Wait for a single event */
+		if (kevent(kq, 0, 0, &event, 1, 0) < 0) {
+			log_kevent_wait();
+			break;
+		}
+
+		switch (event.filter) {
+		    case EVFILT_VNODE:
+			if (!event.udata) {
+				/*
+				 * Something happened on the watchtab:
+				 * close everything and start the timer before
+				 * reloading it.
+				 */
+				fclose(tab_f);  /* also closes tab_fd */
+				EV_SET(&event, 42,
+				    EVFILT_TIMER,
+				    EV_ADD,
+				    0,
+				    delay, /* ms */
+				    0);
+				if (kevent(kq, &event, 1, 0, 0, 0) < 0) {
+					log_kevent_timer();
+					exit(EXIT_FAILURE);
+				}
+				break;
+			}
+
+			/* A watchtab entry has been triggered */
+			wentry = event.udata;
+			if (wentry->fd < 0
+			    || (uintptr_t)wentry->fd != event.ident) {
+				LOG_ASSERT("wentry->fd");
+				exit(EXIT_FAILURE);
+			}
+			close(wentry->fd);
+			wentry->fd = -1;
+			pid = run_entry(wentry);
+			if (!pid) break;
+
+			/* Wait for the command to finish */
+			EV_SET(&event, pid,
+			    EVFILT_PROC,
+			    EV_ADD | EV_ONESHOT,
+			    NOTE_EXIT,
+			    0,
+			    wentry);
+			if (kevent(kq, &event, 1, 0, 0, 0) < 0)
+				log_kevent_proc(wentry, pid);
+			break;
+
+		    case EVFILT_PROC:
+			/*
+			 * The command has finished, re-insert the path to
+			 * watch it.
+			 */
+			insert_entry(kq, event.udata);
+			break;
+
+		    case EVFILT_TIMER:
+			/*
+			 * Timer for watchtab reload has expired, try to
+			 * reopen and reload it.
+			 * When open fails, keep the timer around to try
+			 * again after delay (suppressing errors).
+			 * When loading fails, keep the old watchtab but add
+			 * the event filter anyway to try again on next update.
+			 */
+
+			/* Try opening the watchtab file */
+			tab_fd = open(tabpath, O_RDONLY | O_CLOEXEC);
+			if (tab_fd < 0) {
+				if (!wtab_error)
+					log_open_watchtab(tabpath);
+				wtab_error = 1;
+				break;
+			}
+			tab_f = fdopen(tab_fd, "r");
+			if (!tab_f) {
+				if (!wtab_error)
+					log_open_watchtab(tabpath);
+				wtab_error = 1;
+				close(tab_fd);
+				break;
+			}
+
+			/* Delete the timer */
+			event.flags = EV_DELETE;
+			if (kevent(kq, &event, 1, 0, 0, 0) < 0) {
+				log_kevent_timer_off();
+				/* timer is still around, close files */
+				fclose(tab_f);
+				break;
+			}
+
+			/* Watch the file for changes */
+			EV_SET(&event, tab_fd,
+			    EVFILT_VNODE,
+			    EV_ADD | EV_ONESHOT,
+			    NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE
+			      | NOTE_WRITE,
+			    0, 0);
+			if (kevent(kq, &event, 1, 0, 0, 0) < 0)
+				log_kevent_watchtab(tabpath);
+
+			/* Load watchtab contents on a temporary variable */
+			/* local */{
+				struct watchtab new_wtab
+				    = SLIST_HEAD_INITIALIZER(new_wtab);
+
+				if (wtab_readfile(&new_wtab,
+				    tab_f, tabpath) < 0) {
+					wtab_release(&new_wtab);
+					break;
+				}
+
+				wtab_release(&wtab);
+				wtab = new_wtab;
+				SLIST_FOREACH(wentry, &wtab, next) {
+					insert_entry(kq, wentry);
+				}
+			}
+
+			log_watchtab_loaded(tabpath);
+			break;
+		}
+	}
+
+	return EXIT_SUCCESS;
+}
diff --git a/log.c b/log.c
@@ -0,0 +1,285 @@
+/* log.c - report errors to the outside world */
+
+/*
+ * Copyright (c) 2013, Natacha Porté
+ *
+ * Permission to use, copy, modify, and 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 <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <syslog.h>
+
+#include "log.h"
+
+/*************
+ * REPORTING *
+ *************/
+
+/* report - callback actually used by formatting functions */
+static report_fn report = &report_to_stderr;
+
+
+/* set_report - use the given callback for error reporting */
+void
+set_report(report_fn callback) {
+	report = callback;
+}
+
+
+/* report_to_stderr - wrapper to send the message to standard error output */
+void
+report_to_stderr(int priority, const char *message, ...) {
+	va_list ap;
+	(void)priority;
+
+	va_start(ap, message);
+	vfprintf(stderr, message, ap);
+	va_end(ap);
+	fputc('\n', stderr);
+}
+
+
+
+/*******************
+ * ERROR FORMATING *
+ *******************/
+
+/* log_alloc - memory allocation failure */
+void
+log_alloc(const char *subsystem) {
+	if (subsystem)
+		report(LOG_ERR, "Unable to allocate memory for %s", subsystem);
+	else
+		report(LOG_ERR, "Unable to allocate memory");
+}
+
+
+/* log_assert - internal inconsistency */
+void
+log_assert(const char *reason, const char *source, unsigned line) {
+	if (reason)
+		report(LOG_ERR, "Internal inconsistency at %s:%u (%s)",
+		    source, line, reason);
+	else
+		report(LOG_ERR, "Internal inconsistency at %s:%u",
+		    source, line);
+}
+
+
+/* log_bad_delay - invalid string provided for delay value */
+void
+log_bad_delay(const char *opt) {
+	report(LOG_ERR, "Bad value \"%s\" for delay", opt);
+}
+
+
+/* log_chdir - chdir("/") failed after successful chroot() */
+void
+log_chdir(const char *newroot) {
+	report(LOG_ERR, "chdir(\"/\") error after chroot to %s: %s",
+	    newroot, strerror(errno));
+}
+
+
+/* log_chroot - chroot() failed */
+void
+log_chroot(const char *newroot) {
+	report(LOG_ERR, "Unable to chroot to %s: %s",
+	    newroot, strerror(errno));
+}
+
+
+/* log_entry_wait - watchtab entry successfully inserted in the queue */
+void
+log_entry_wait(struct watch_entry *wentry) {
+	report(LOG_INFO, "Waiting for events on \"%s\"", wentry->path);
+}
+
+/* log_exec - execve() failed */
+void
+log_exec(struct watch_entry *wentry) {
+	report(LOG_ERR, "Unable to execute \"%s\": %s",
+	    wentry->command, strerror(errno));
+}
+
+
+/* log_fork - fork() failed */
+void
+log_fork(void) {
+	report(LOG_ERR, "Error in fork(): %s", strerror(errno));
+}
+
+
+/* log_kevent_entry - kevent() failed when adding an event for a file entry */
+void
+log_kevent_entry(const char *path) {
+	report(LOG_ERR, "Unable to queue filter for file \"%s\": %s",
+	    path, strerror(errno));
+}
+
+
+/* log_kevent_proc - kevent() failed when adding a command watcher */
+void
+log_kevent_proc(struct watch_entry *wentry, pid_t pid) {
+	report(LOG_ERR, "Unable to watch command pid %d (\"%s\"): %s",
+	    (int)pid, wentry->command, strerror(errno));
+}
+
+
+/* log_kevent_timer - kevent() failed when adding a timer */
+void
+log_kevent_timer(void) {
+	report(LOG_ERR, "Unable to queue timer for watchtab: %s",
+	    strerror(errno));
+}
+
+
+/* log_kevent_timer_off - kevent() failed when removing a timer */
+void
+log_kevent_timer_off(void) {
+	report(LOG_ERR, "Unable to delete timer for watchtab: %s",
+	    strerror(errno));
+}
+
+
+/* log_kevent_wait - kevent() failed while waiting for an event */
+void
+log_kevent_wait(void) {
+	report(LOG_ERR, "Error while waiting for a kevent: %s",
+	    strerror(errno));
+}
+
+
+/* log_kevent_watchtab - kevent() failed when adding a watchtab event */
+void
+log_kevent_watchtab(const char *path) {
+	report(LOG_ERR, "Unable to queue filter for watchtab \"%s\": %s",
+	    path, strerror(errno));
+}
+
+
+/* log_kqueue - report failure in kqueue() call */
+void
+log_kqueue(void) {
+	report(LOG_ERR, "Error in kqueue(): %s", strerror(errno));
+}
+
+
+/* log_lookup_group - getgrnam() failed */
+void
+log_lookup_group(const char *group) {
+	if (errno)
+		report(LOG_ERR, "Error while lookup group \"%s\": %s",
+		    group, strerror(errno));
+	else
+		report(LOG_ERR, "Unable to find group \"%s\"", group);
+}
+
+/* log_lookup_pw - getpwnam() failed */
+void
+log_lookup_pw(const char *login) {
+	if (errno)
+		report(LOG_ERR, "Error while lookup user \"%s\": %s",
+		    login, strerror(errno));
+	else
+		report(LOG_ERR, "Unable to find user \"%s\"", login);
+}
+
+/* log_lookup_self - getlogin() or getpwnam() failed */
+void
+log_lookup_self(void) {
+	report(LOG_ERR, "Error while trying to lookup current user login");
+}
+
+
+/* log_open_entry - open() failed on watchtab entry file */
+void
+log_open_entry(const char *path) {
+	report(LOG_ERR, "Unable to open watched file \"%s\": %s",
+	    path, strerror(errno));
+}
+
+
+/* log_open_watchtab - watchtab file open() failed */
+void
+log_open_watchtab(const char *path) {
+	report(LOG_ERR, "Unable to open watchtab \"%s\": %s",
+	    path, strerror(errno));
+}
+
+
+/* log_running - a watchtab entry has been triggered */
+void
+log_running(struct watch_entry *wentry) {
+	report(LOG_INFO, "Running \"%s\", triggered by \"%s\"",
+	    wentry->command, wentry->path);
+}
+
+
+/* log_setgid - setgid() failed */
+void
+log_setgid(gid_t gid) {
+	report(LOG_INFO, "Unable to set gID to %d: %s",
+	    (int)gid, strerror(errno));
+}
+
+
+/* log_setuid - setuid() failed */
+void
+log_setuid(uid_t uid) {
+	report(LOG_INFO, "Unable to set uID to %d: %s",
+	    (int)uid, strerror(errno));
+}
+
+
+/* log_watchtab_invalid_action - invalid action line in watchtab */
+void
+log_watchtab_invalid_action(const char *filename, unsigned line_no) {
+	report(LOG_ERR, "Invalid action line at line %s:%u",
+	    filename, line_no);
+}
+
+
+/* log_watchtab_invalid_delay - invalid delay field in watchtab entry */
+void
+log_watchtab_invalid_delay(const char *filename, unsigned line_no,
+    const char *field) {
+	report(LOG_ERR, "Invalid delay field \"%s\" at %s:%u",
+	    field, filename, line_no);
+}
+
+
+/* log_watchtab_invalid_events - parse error in watchtab event set */
+void
+log_watchtab_invalid_events(const char *filename, unsigned line_no,
+    const char *field, size_t len) {
+	report(LOG_ERR, "Invalid event set \"%.*s\" at %s:%u",
+	    (int)len, field, filename, line_no);
+}
+
+
+/* log_watchtab_loaded - watchtab has been successfully loaded */
+void
+log_watchtab_loaded(const char *path) {
+	report(LOG_NOTICE, "Watchtab \"%s\" loaded successfully", path);
+}
+
+
+/* log_watchtab_read - read error on watchtab */
+void
+log_watchtab_read(void) {
+	report(LOG_ERR, "Error while reading from watchtab");
+}
diff --git a/log.h b/log.h
@@ -0,0 +1,168 @@
+/* log.h - report errors to the outside world */
+
+/*
+ * Copyright (c) 2013, Natacha Porté
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+/*
+ * This module gather all error-formating functions, so that all user-facing
+ * strings are gathered in one place.
+ */
+
+#ifndef FILEWATCHER_LOG_H
+#define FILEWATCHER_LOG_H
+
+#include "watchtab.h"
+
+
+/*************
+ * REPORTING *
+ *************/
+
+/* report_fn - report callback, same semantics as syslog() */
+typedef void (*report_fn)(int priority, const char *message, ...)
+    __attribute__((format (printf, 2, 3)));
+
+/* report_to_stderr - wrapper to send the message to standard error output */
+void
+report_to_stderr(int priority, const char *message, ...);
+
+/* set_report - use the given callback for error reporting */
+void
+set_report(report_fn callback);
+
+/*******************
+ * ERROR FORMATING *
+ *******************/
+
+/* log_alloc - memory allocation failure */
+void
+log_alloc(const char *subsystem);
+
+/* log_assert - internal inconsistency */
+void
+log_assert(const char *reason, const char *source, unsigned line);
+#define LOG_ASSERT(m) log_assert((m), __FILE__, __LINE__)
+
+/* log_bad_delay - invalid string provided for delay value */
+void
+log_bad_delay(const char *opt);
+
+/* log_chdir - chdir("/") failed after successful chroot() */
+void
+log_chdir(const char *newroot);
+
+/* log_chroot - chroot() failed */
+void
+log_chroot(const char *newroot);
+
+/* log_entry_wait - watchtab entry successfully inserted in the queue */
+void
+log_entry_wait(struct watch_entry *wentry);
+
+/* log_exec - execve() failed */
+void
+log_exec(struct watch_entry *wentry);
+
+/* log_fork - fork() failed */
+void
+log_fork(void);
+
+/* log_kevent_entry - kevent() failed when adding an event for a file entry */
+void
+log_kevent_entry(const char *path);
+
+/* log_kevent_proc - kevent() failed when adding a command watcher */
+void
+log_kevent_proc(struct watch_entry *wentry, pid_t pid);
+
+/* log_kevent_timer - kevent() failed when adding a timer */
+void
+log_kevent_timer(void);
+
+/* log_kevent_timer_off - kevent() failed when removing a timer */
+void
+log_kevent_timer_off(void);
+
+/* log_kevent_wait - kevent() failed while waiting for an event */
+void
+log_kevent_wait(void);
+
+/* log_kevent_watchtab - kevent() failed when adding a watchtab event */
+void
+log_kevent_watchtab(const char *path);
+
+/* log_kqueue - kqueue() failed */
+void
+log_kqueue(void);
+
+/* log_lookup_group - getgrnam() failed */
+/* WARNING: errno must explicitly be zeroed before calling getgrnam() */
+void
+log_lookup_group(const char *group);
+
+/* log_lookup_pw - getpwnam() failed */
+/* WARNING: errno must explicitly be zeroed before calling getpwnam() */
+void
+log_lookup_pw(const char *login);
+
+/* log_lookup_self - getlogin() or getpwnam() failed */
+/* WARNING: errno must explicitly be zeroed before calling getpwnam() */
+void
+log_lookup_self(void);
+
+/* log_open_entry - open() failed on watchtab entry file */
+void
+log_open_entry(const char *path);
+
+/* log_open_watchtab - watchtab file open() failed */
+void
+log_open_watchtab(const char *path);
+
+/* log_running - a watchtab entry has been triggered */
+void
+log_running(struct watch_entry *wentry);
+
+/* log_setgid - setgid() failed */
+void
+log_setgid(gid_t gid);
+
+/* log_setuid - setuid() failed */
+void
+log_setuid(uid_t uid);
+
+/* log_watchtab_invalid_action - invalid action line in watchtab */
+void
+log_watchtab_invalid_action(const char *filename, unsigned line_no);
+
+/* log_watchtab_invalid_delay - invalid delay field in watchtab entry */
+void
+log_watchtab_invalid_delay(const char *filename, unsigned line_no,
+    const char *field);
+
+/* log_watchtab_invalid_events - parse error in watchtab event set */
+void
+log_watchtab_invalid_events(const char *filename, unsigned line_no,
+    const char *field, size_t len);
+
+/* log_watchtab_loaded - watchtab has been successfully loaded */
+void
+log_watchtab_loaded(const char *path);
+
+/* log_watchtab_read - read error on watchtab */
+void
+log_watchtab_read(void);
+
+#endif /* ndef FILEWATCHER_LOG_H */
diff --git a/run.c b/run.c
@@ -0,0 +1,92 @@
+/* run.c - command execution */
+
+/*
+ * Copyright (c) 2013, Natacha Porté
+ *
+ * Permission to use, copy, modify, and 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 <spawn.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "log.h"
+#include "run.h"
+
+/* run_entry - start the command associated with the given entry */
+pid_t
+run_entry(struct watch_entry *wentry) {
+	char *argv[4];
+	size_t i = 0;
+	pid_t result;
+	int has_delay = (wentry->delay.tv_sec || wentry->delay.tv_nsec);
+
+	/* Create a child process and hand control back to parent */
+	result = has_delay ? fork() : vfork();
+	if (result == -1) {
+		log_fork();
+		return 0;
+	} else if (result != 0) {
+	 	return result;
+	}
+
+	/* chroot if requested */
+	if (wentry->chroot) {
+		if (chroot(wentry->chroot) < 0) {
+			log_chroot(wentry->chroot);
+			_exit(EXIT_FAILURE);
+		}
+		if (chdir("/") < 0) {
+			log_chdir(wentry->chroot);
+			_exit(EXIT_FAILURE);
+		}
+	}
+
+	/* Set gid and uid if requested */
+	if (wentry->gid && setgid(wentry->gid) < 0) {
+		log_setgid(wentry->gid);
+		_exit(EXIT_FAILURE);
+	}
+	if (wentry->uid && setuid(wentry->uid) < 0) {
+		log_setuid(wentry->uid);
+		_exit(EXIT_FAILURE);
+	}
+
+	/* Wait for some time if requested */
+	if (has_delay)
+		nanosleep(&wentry->delay, 0);
+
+	/* Lookup SHELL environment variable */
+	argv[0] = 0;
+	for (i = 0; wentry->envp[i]; i++) {
+		if (strncmp(wentry->envp[i], "SHELL=", 6) == 0) {
+			argv[0] = wentry->envp[i] + 6;
+			break;
+		}
+	}
+
+	/* Build argument list */
+	if (!argv[0]) argv[0] = "/bin/sh";
+	argv[1] = "-c";
+	argv[2] = (char *)wentry->command;
+	argv[3] = 0;
+
+	/* Handover control to the command */
+	execve(argv[0], argv, wentry->envp);
+
+	/* Report error */
+	log_exec(wentry);
+	_exit(EXIT_FAILURE);
+}
diff --git a/run.h b/run.h
@@ -0,0 +1,30 @@
+/* run.h - command execution */
+
+/*
+ * Copyright (c) 2013, Natacha Porté
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#ifndef FILEWATCHER_RUN_H
+#define FILEWATCHER_RUN_H
+
+#include <sys/types.h>
+
+#include "watchtab.h"
+
+/* run_entry - start the command associated with the given entry */
+pid_t
+run_entry(struct watch_entry *wentry);
+
+#endif /* ndef FILEWATCHER_RUN_H */
diff --git a/watchtab.c b/watchtab.c
@@ -0,0 +1,666 @@
+/* watchtab.c - configuration tables for file watches */
+
+/*
+ * Copyright (c) 2013, Natacha Porté
+ *
+ * Permission to use, copy, modify, and 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 <errno.h>
+#include <grp.h>
+#include <pwd.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sys/types.h>
+#include <sys/event.h>
+
+#include "log.h"
+#include "watchtab.h"
+
+/* number of pointers allocated at once in watch_env */
+#define WENV_ALLOC_UNIT 16;
+
+/*********************
+ * LOCAL SUBPROGRAMS *
+ *********************/
+
+/* parse_events - process a configuration string into fflags vnode events */
+static u_int
+parse_events(const char *line, size_t len) {
+	u_int result = 0;
+	size_t i = 0;
+
+	/* Check wildcard */
+	if (len == 1 && line[0] == '*')
+		return NOTE_DELETE | NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB
+		    | NOTE_LINK | NOTE_RENAME | NOTE_REVOKE;
+
+	/*
+	 * Parse entry as a list of token separated by a single
+	 * non-letter byte.
+	 */
+	while (i < len) {
+		if (strncmp(line + i, "delete", 6) == 0
+		    || strncmp(line + i, "DELETE", 6) == 0) {
+			result |= NOTE_DELETE;
+			i += 6;
+		}
+		else if (strncmp(line + i, "write", 5) == 0
+		    || strncmp(line + i, "WRITE", 5) == 0) {
+			result |= NOTE_WRITE;
+			i += 5;
+		}
+		else if (strncmp(line + i, "extend", 6) == 0
+		    || strncmp(line + i, "EXTEND", 6) == 0) {
+			result |= NOTE_EXTEND;
+			i += 6;
+		}
+		else if (strncmp(line + i, "attrib", 6) == 0
+		    || strncmp(line + i, "ATTRIB", 6) == 0) {
+			result |= NOTE_ATTRIB;
+			i += 6;
+		}
+		else if (strncmp(line + i, "link", 4) == 0
+		    || strncmp(line + i, "LINK", 4) == 0) {
+			result |= NOTE_LINK;
+			i += 4;
+		}
+		else if (strncmp(line + i, "rename", 6) == 0
+		    || strncmp(line + i, "RENAME", 6) == 0) {
+			result |= NOTE_RENAME;
+			i += 6;
+		}
+		else if (strncmp(line + i, "revoke", 6) == 0
+		    || strncmp(line + i, "REVOKE", 6) == 0) {
+			result |= NOTE_REVOKE;
+			i += 6;
+		}
+		else return 0;
+
+		if (i < len && ((line[i] >= 'a' && line[i] <= 'z')
+		    || (line[i] >= 'A' && line[i] <= 'Z')))
+			return 0;
+		else
+			i++;
+	}
+
+	return result;
+}
+
+
+/* strdupesc - duplicate and unescape an input string */
+static char *
+strdupesc(const char *src, size_t len) {
+	size_t s = 0, d = 0;
+	char *dest = malloc(len + 1);
+
+	if (!dest) {
+		log_alloc("watchtab entry internal string");
+		return 0;
+	}
+
+	while (src[s] && s < len) {
+		if (src[s] != '\\' || (s > 0 && src[s-1] == '\\'))
+			dest[d++] = src[s];
+		s++;
+	}
+	dest[d] = 0;
+	return dest;
+}
+
+/* wenv_resize - preallocate enough storage for new_size pointers */
+static int
+wenv_resize(struct watch_env *wenv, size_t new_size) {
+	size_t new_cap, i;
+	const char **new_env;
+
+	/* Initialize if needed */
+	if (!wenv->environ && wenv_init(wenv) < 0)
+		return -1;
+
+	/* Don't realloc when enough capacity is available */
+	if (new_size <= wenv->capacity) return 0;
+
+	/* Actually resize */
+	new_cap = wenv->capacity;
+	while (new_cap < new_size) new_cap += WENV_ALLOC_UNIT;
+	new_env = realloc(wenv->environ, sizeof *wenv->environ * new_cap);
+	if (!new_env) {
+		log_alloc("environment variables");
+		return -1;
+	}
+
+	/* Clear new pointers */
+	for (i = wenv->capacity; i < new_cap; i++)
+		new_env[i] = 0;
+
+	/* Update watch_env */
+	wenv->capacity = new_cap;
+	wenv->environ = new_env;
+	return 0;
+}
+
+
+
+/********************
+ * PUBLIC INTERFACE *
+ ********************/
+
+/* wentry_init - initialize a watch_entry with null values */
+void
+wentry_init(struct watch_entry *wentry) {
+	if (!wentry) return;
+
+	wentry->path = 0;
+	wentry->events = 0;
+	wentry->delay.tv_sec = 0;
+	wentry->delay.tv_nsec = 0;
+	wentry->uid = 0;
+	wentry->gid = 0;
+	wentry->chroot = 0;
+	wentry->command = 0;
+	wentry->envp = 0;
+	wentry->fd = -1;
+}
+
+
+/* wentry_release - free internal objects from a watch_entry */
+void
+wentry_release(struct watch_entry *wentry) {
+	if (!wentry) return;
+
+	free((void *)(wentry->path));
+	free((void *)(wentry->chroot));
+	free((void *)(wentry->command));
+	wentry->path = 0;
+	wentry->chroot = 0;
+	wentry->command = 0;
+
+	if (wentry->envp) {
+		size_t i = 0;
+		while (wentry->envp[i])
+			free(wentry->envp[i++]);
+		free(wentry->envp);
+	}
+	wentry->envp = 0;
+
+	if (wentry->fd != -1)
+		close(wentry->fd);
+	wentry->fd = -1;
+}
+
+
+/* wentry_free - free a watch_entry and the string it contains */
+void
+wentry_free(struct watch_entry *wentry) {
+	if (!wentry) return;
+
+	wentry_release(wentry);
+	free(wentry);
+}
+
+
+/* wentry_readline - parse a config file line and fill a struct watch_entry */
+/*   Return 0 on success or -1 on failure. */
+int
+wentry_readline(struct watch_entry *dest, char *line,
+    struct watch_env *base_env, const char *filename, unsigned line_no) {
+	size_t path_len = 0;
+	size_t event_first = 0, event_len = 0;
+	size_t delay_first = 0, delay_len = 0;
+	size_t user_first = 0, user_len = 0;
+	size_t chroot_first = 0, chroot_len = 0;
+	size_t cmd_first = 0, cmd_len = 0;
+	struct passwd *pw = 0;
+	struct group *grp = 0;
+	size_t i = 1;
+
+	/* Sanity checks */
+	if (!line || line[0] == 0 || line[0] == '\t') {
+		LOG_ASSERT(0);
+		return -1;
+	}
+
+	/* Look for fields boundaries */
+	while (line[i] != 0 && (line[i] != '\t' || line[i-1] == '\\')) i++;
+	path_len = i;
+	while (line[i] == '\t') i++;
+	event_first = i;
+	while (line[i] != 0 && (line[i] != '\t' || line[i-1] == '\\')) i++;
+	event_len = i - event_first;
+	while (line[i] == '\t') i++;
+	delay_first = i;
+	while (line[i] != 0 && (line[i] != '\t' || line[i-1] == '\\')) i++;
+	delay_len = i - delay_first;
+	while (line[i] == '\t') i++;
+	user_first = i;
+	while (line[i] != 0 && (line[i] != '\t' || line[i-1] == '\\')) i++;
+	user_len = i - user_first;
+	while (line[i] == '\t') i++;
+	chroot_first = i;
+	while (line[i] != 0 && (line[i] != '\t' || line[i-1] == '\\')) i++;
+	chroot_len = i - chroot_first;
+	while (line[i] == '\t') i++;
+	cmd_first = i;
+	while (line[i] != 0) i++;
+	cmd_len = i - cmd_first;
+
+	/* Less than 3 fields found is a parse error */
+	if (line[delay_first] == 0) {
+		log_watchtab_invalid_action(filename, line_no);
+		return -1;
+	}
+
+	/* Adjust offsets depending on which fields are omitted */
+	if (line[user_first] == 0) {
+		/* 3-field line: path, events, command */
+		cmd_first = delay_first;
+		cmd_len = delay_len;
+		delay_first = delay_len = 0;
+		user_first = user_len = 0;
+		chroot_first = chroot_len = 0;
+	}
+	else if (line[chroot_first] == 0) {
+		/* 4-field line: path, events, delay, command */
+		cmd_first = user_first;
+		cmd_len = user_len;
+		user_first = user_len = chroot_first = chroot_len = 0;
+	}
+	else if (line[cmd_first] == 0) {
+		/* 5-field line: path, events, delay, user, command */
+		cmd_first = chroot_first;
+		cmd_len = chroot_len;
+		chroot_first = chroot_len = 0;
+	}
+
+	/* Parse event set */
+	dest->events = parse_events(line + event_first, event_len);
+	if (dest->events == 0) {
+		log_watchtab_invalid_events(filename, line_no,
+		    line + event_first, event_len);
+		return -1;
+	}
+
+	/* Parse delay */
+	dest->delay.tv_sec = 0;
+	dest->delay.tv_nsec = 0;
+	if (delay_len > 0
+	    && !(delay_len == 1 && line[delay_first] == '*')) {
+		char *s;
+
+		/* Decode integer part */
+		dest->delay.tv_sec = strtol(line + delay_first, &s, 10);
+
+		/* Decode fractional part if any */
+		if (*s == '.') {
+			char *ns;
+			dest->delay.tv_nsec = strtol(s + 1, &ns, 10);
+			while (ns - s <= 9) {
+				dest->delay.tv_nsec *= 10;
+				s--;
+			}
+			s = ns;
+		}
+
+		/* Check trailing non-digits */
+		if (s < line + delay_first + delay_len) {
+			line[delay_first + delay_len] = 0;
+			log_watchtab_invalid_delay(filename, line_no,
+			    line + delay_first);
+			return -1;
+		}
+	}
+
+	/* Process user name and optional group name */
+	if (user_len > 0) {
+		char *login = line + user_first;
+		char *group = 0;
+
+		line[user_len] = 0;
+
+		/* Process group */
+		group = strchr(login, ':');
+		if (group) {
+			*group = 0;
+			group++;
+			for (i = 0; group[i] >= '0' && group[i] <= '9'; i++);
+			errno = 0;
+			grp = group[i]
+			    ? getgrnam(group)
+			    : getgrgid(strtol(group, 0, 10));
+			if (!grp) {
+				log_lookup_group(group);
+				return -1;
+			}
+		}
+
+		/* Lookup user name */
+		for (i = 0; login[i] >= '0' && login[i] <= '9'; i++);
+		errno = 0;
+		pw = login[i]
+		    ? getpwnam(login)
+		    : getpwuid(strtol(login, 0, 10));
+		if (!pw) {
+			log_lookup_pw(login);
+			return -1;
+		}
+	}
+
+	/* Store numeric ids */
+	dest->uid = pw ? pw->pw_uid : 0;
+	dest->gid = grp ? grp->gr_gid : (pw ? pw->pw_gid : 0);
+
+	/* Lookup self name if not overridden */
+	if (!pw) {
+		char *login;
+		errno = 0;
+		login = getlogin();
+		pw = login ? getpwnam(login) : 0;
+		if (!pw) {
+			log_lookup_self();
+			return -1;
+		}
+	}
+
+	/* At this point, no parse error can occur, filling in data */
+
+	/* Clean up destination */
+	wentry_release(dest);
+
+	/* Copy string parameters */
+	dest->path = strdupesc(line, path_len);
+	dest->command = strdupesc(line + cmd_first, cmd_len);
+
+	if (chroot_len > 0)
+		dest->chroot = strdupesc(line + chroot_first, chroot_len);
+	else
+		dest->chroot = 0;
+
+	/* Setup environment */
+	wenv_set(base_env, "LOGNAME", pw->pw_name, 1);
+	wenv_set(base_env, "USER", pw->pw_name, 1);
+	wenv_set(base_env, "HOME", pw->pw_dir, 0);
+	wenv_set(base_env, "TRIGGER", dest->path, 1);
+	dest->envp = wenv_dup(base_env);
+
+	return 0;
+}
+
+
+
+/***********************
+ * WATCH_ENV INTERFACE *
+ ***********************/
+
+/* wenv_init - create an empty environment list */
+int
+wenv_init(struct watch_env *wenv) {
+	if (!wenv) {
+		LOG_ASSERT(0);
+		return -1;
+	}
+
+	wenv->capacity = WENV_ALLOC_UNIT;
+	wenv->size = 0;
+	wenv->environ = malloc(sizeof *wenv->environ * wenv->capacity);
+	if (!wenv->environ) {
+		log_alloc("initial environment variables");
+		return -1;
+	}
+
+	return 0;
+}
+
+
+/* wenv_release - free string memory in a struct watch_env but not the struct*/
+void
+wenv_release(struct watch_env *wenv) {
+	free(wenv->environ);
+	wenv->size = 0;
+	wenv->environ = 0;
+}
+
+
+/* wenv_add - append a string to an existing struct watch_env */
+int
+wenv_add(struct watch_env *wenv, const char *env_str) {
+	if (!wenv || !env_str) {
+		LOG_ASSERT(0);
+		return -1;
+	}
+
+	/* Increase array size if needed */
+	if (wenv_resize(wenv, wenv->size + 2) < 0) return -1;
+
+	/* Store a copy of the provided string */
+	wenv->environ[wenv->size] = strdup(env_str);
+	wenv->size++;
+	return 0;
+}
+
+/* wenv_set - insert or reset an environment variable */
+int
+wenv_set(struct watch_env *wenv, const char *name, const char *value,
+    int overwrite) {
+	size_t namelen, linelen, i;
+	char *line;
+
+	if (!wenv || !name || !value) return -1;
+
+	/* Initialize if needed */
+	if (!wenv->environ && wenv_init(wenv) < 0)
+		return -1;
+
+	/* Build the environment line */
+	namelen = strlen(name);
+	linelen = namelen + 1 + strlen(value);
+	line = malloc(linelen + 1);
+	if (!line) {
+		log_alloc("environment variable entry");
+		return -1;
+	}
+	strncpy(line, name, namelen);
+	line[namelen] = '=';
+	strncpy(line + namelen + 1, value, linelen - (namelen + 1));
+	line[linelen] = 0;
+
+	/* Look for an existing entry for the name */
+	i = 0;
+	while (wenv->environ[i]) {
+		if (strncmp(wenv->environ[i], line, namelen + 1) == 0)
+			break;
+		i++;
+	}
+
+	/* If not found, insert the crafted line */
+	if (wenv->environ[i] == 0) {
+		if (wenv_resize(wenv, wenv->size + 2) < 0) {
+			free(line);
+			return -1;
+		}
+		wenv->environ[wenv->size] = line;
+		wenv->size++;
+		return 0;
+	}
+
+	/* Exit when environment variable exist but overwriting is forbidden */
+	if (!overwrite) return 0;
+
+	/* Replace found variable with new environment line */
+	free((void *)wenv->environ[i]);
+	wenv->environ[i] = line;
+	return 0;
+}
+
+/* wenv_get - lookup environment variable */
+const char *
+wenv_get(struct watch_env *wenv, const char *name) {
+	size_t namelen, i;
+
+	if (!wenv || !wenv->environ || !name) {
+		LOG_ASSERT(0);
+		return 0;
+	}
+	namelen = strlen(name);
+
+	for (i = 0; wenv->environ[i]; i++) {
+		if (strncmp(wenv->environ[i], name, namelen) == 0
+		    && wenv->environ[i][namelen] == '=')
+			return wenv->environ[i] + (namelen + 1);
+	}
+
+	return 0;
+}
+
+
+/* wenv_dup - deep copy environment strings */
+char **
+wenv_dup(struct watch_env *wenv) {
+	char **result;
+	size_t len, i;
+	int reported = 0;
+
+	if (!wenv) return 0;
+	len = (wenv->environ ? wenv->size : 0);
+	result = malloc((len + 1) * sizeof *result);
+	if (!result) {
+		log_alloc("environment duplicate");
+		return 0;
+	}
+
+	for (i = 0; i < len; i++) {
+		result[i] = strdup(wenv->environ[i]);
+		if (!result[i] && !reported) {
+			log_alloc("environment item duplication");
+			reported = 1;
+		}
+	}
+	result[len] = 0;
+
+	return result;
+}
+
+
+
+/**********************
+ * WATCHTAB INTERFACE *
+ **********************/
+
+/* wtab_release - release children objects but not the struct watchtab */
+void
+wtab_release(struct watchtab *tab) {
+	struct watch_entry *entry = 0;
+
+	if (!tab) return;
+
+	while ((entry = SLIST_FIRST(tab)) != 0) {
+		SLIST_REMOVE_HEAD(tab, next);
+		wentry_free(entry);
+	}
+}
+
+
+/* wtab_readfile - parse the given file to build a new watchtab */
+int
+wtab_readfile(struct watchtab *tab, FILE *input, const char *filename) {
+	char *line = 0;
+	size_t linecap = 0;
+	ssize_t linelen;
+	unsigned line_no = 0;
+	struct watch_entry *entry = 0;
+	int result = 0;
+	size_t i, skip;
+	struct watch_env env;
+
+	if (!tab) {
+		LOG_ASSERT(0);
+		return -1;
+	}
+
+	/* Setup default environment */
+	wenv_init(&env);
+	wenv_set(&env, "SHELL", "/bin/sh", 1);
+	wenv_set(&env, "PATH", "/usr/bin:/bin", 1);
+
+	/* Read the input data */
+	while ((linelen = getdelim(&line, &linecap, '\n', input)) >= 0) {
+		line_no++;
+
+		/* Skip leading blanks */
+		skip = 0;
+		while (line[skip] == ' ' || line[skip] == '\t') skip++;
+
+		/* Trim trailing blanks */
+		while ((size_t)linelen > skip && (line[linelen-1] == '\n'
+		    || line[linelen-1] == '\r' || line[linelen-1] == ' '
+		    || line[linelen-1] == '\t'))
+			linelen--;
+		line[linelen] = 0;
+
+		/* Ignore empty lines and comments */
+		if ((size_t)linelen <= skip || line[skip] == '#')
+			continue;
+
+		/*
+		 * Define environment lines as lines having an '=' before any
+		 * tabulation ('\t') or backslash ('\\').
+		 */
+
+		i = skip;
+		while (line[i] != 0 && line[i] != '='
+		    && line[i] != '\\' && line[i] != '\t')
+			i++;
+
+		/* Record an environment variable */
+		if (line[i] == '=') {
+			/* Compute bounds of variable name */
+			size_t j = i - 1;
+			while (line[j] == ' ' && j > skip) j--;
+			if (j + 1 < i) line[j + 1] = 0;
+
+			/* Compute bounds of variable value */
+			j = i + 1;
+			while (line[j] == ' ') j++;
+
+			/* Set the variable */
+			wenv_set(&env, line + skip, line + j, 1);
+			continue;
+		}
+
+		/* Parse an entry line */
+		entry = malloc(sizeof *entry);
+		if (!entry) {
+			log_alloc("watchtab entry");
+			return -1;
+		}
+		wentry_init(entry);
+		if (wentry_readline(entry, line + skip, &env,
+		    filename, line_no) < 0) {
+			/* propagate an error but keep parsing */
+			result = -1;
+			wentry_free(entry);
+			continue;
+		}
+
+		/* Insert the entry in the list */
+		SLIST_INSERT_HEAD(tab, entry, next);
+	}
+
+	if (ferror(input)) {
+		log_watchtab_read();
+		return -1;
+	}
+
+	return result;
+}
diff --git a/watchtab.h b/watchtab.h
@@ -0,0 +1,113 @@
+/* watchtab.h - configuration tables for file watches */
+
+/*
+ * Copyright (c) 2013, Natacha Porté
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#ifndef FILEWATCHER_WATCHTAB_H
+#define FILEWATCHER_WATCHTAB_H
+
+#include <stdio.h>
+#include <sys/queue.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+
+/********************
+ * TYPE DEFINITIONS *
+ ********************/
+
+/* struct watch_entry - a single watch table entry */
+struct watch_entry {
+	const char	*path;		/* file path to watch */
+	u_int		events;		/* vnode event set to watch */
+	struct timespec	delay;		/* delay before running command */
+	uid_t		uid;		/* uid to set before command */
+	gid_t		gid;		/* gid to set before command */
+	const char	*chroot;	/* path to chroot before command */
+	const char	*command;	/* command to execute */
+	char		**envp;		/* environment variables */
+	int		fd;		/* file descriptor in kernel queue */
+	SLIST_ENTRY(watch_entry) next;
+};
+
+/* struct watchtab - list of watchtab entries */
+SLIST_HEAD(watchtab, watch_entry);
+
+/* struct watch_env - dynamic table of environment variables */
+struct watch_env {
+	const char	**environ;	/* environment strings */
+	size_t		size;		/* index of the last NULL pointer */
+	size_t		capacity;	/* number of string slot available */
+};
+
+
+/********************
+ * PUBLIC INTERFACE *
+ ********************/
+
+/* wentry_init - initialize a watch_entry with null values */
+void
+wentry_init(struct watch_entry *wentry);
+
+/* wentry_release - free internal objects from a watch_entry */
+void
+wentry_release(struct watch_entry *wentry);
+
+/* wentry_free - free a watch_entry and the strinigs it contains */
+void
+wentry_free(struct watch_entry *wentry);
+
+/* wentry_readline - parse a config file line and fill a struct watch_entry */
+int
+wentry_readline(struct watch_entry *dest, char *line,
+    struct watch_env *base_env, const char *filename, unsigned line_no);
+
+
+/* wenv_init - create an empty environment list */
+int
+wenv_init(struct watch_env *wenv);
+
+/* wenv_release - free string memory in a struct watch_env but not the struct*/
+void
+wenv_release(struct watch_env *wenv);
+
+/* wenv_add - append a string to an existing struct watch_env */
+int
+wenv_add(struct watch_env *wenv, const char *env_str);
+
+/* wenv_set - insert or reset an environment variable */
+int
+wenv_set(struct watch_env *wenv, const char *name, const char *value,
+    int overwrite);
+
+/* wenv_get - lookup environment variable */
+const char *
+wenv_get(struct watch_env *wenv, const char *name);
+
+/* wenv_dup - deep copy environment strings */
+char **
+wenv_dup(struct watch_env *wenv);
+
+
+/* wtab_release - release children objects but not the struct watchtab */
+void
+wtab_release(struct watchtab *tab);
+
+/* wtab_readfile - parse the given file to build a new watchtab */
+int
+wtab_readfile(struct watchtab *tab, FILE *input, const char *filename);
+
+#endif /* ndef FILEWATCHER_WATCHTAB_H */