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 */