diff options
Diffstat (limited to 'src/util/inotify.c')
-rw-r--r-- | src/util/inotify.c | 609 |
1 files changed, 609 insertions, 0 deletions
diff --git a/src/util/inotify.c b/src/util/inotify.c new file mode 100644 index 0000000..a3c33ed --- /dev/null +++ b/src/util/inotify.c @@ -0,0 +1,609 @@ +/* + Copyright (C) 2016 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <talloc.h> +#include <errno.h> +#include <libgen.h> +#include <limits.h> +#include <sys/inotify.h> +#include <sys/time.h> + +#include "util/inotify.h" +#include "util/util.h" + +/* For parent directories, we want to know if a file was moved there or + * created there + */ +#define PARENT_DIR_MASK (IN_CREATE | IN_MOVED_TO) + +/* This structure is recreated if we need to rewatch the file and/or + * directory + */ +struct snotify_watch_ctx { + int inotify_fd; /* The inotify_fd */ + struct tevent_fd *tfd; /* Activity on the fd */ + + struct snotify_ctx *snctx; /* Pointer up to the main snotify struct */ + + /* In case we're also watching the parent directory, otherwise -1. + * We keep the variable here and not in snctx so that we're able + * to catch even changes to the parent directory + */ + int dir_wd; + /* The file watch */ + int file_wd; +}; + +/* This is what we call when an event we're interested in arrives */ +struct snotify_cb_ctx { + snotify_cb_fn fn; + const char *fn_name; + uint32_t mask; + void *pvt; +}; + +/* One instance of a callback. We hoard the inotify notifications + * until timer fires in caught_flags + */ +struct snotify_dispatcher { + struct tevent_timer *te; + uint32_t caught_flags; +}; + +struct snotify_ctx { + struct tevent_context *ev; + + /* The full path of the file we're watching, + * its file and directory components */ + const char *filename; + const char *dir_name; + const char *base_name; + + /* Private pointer passed to the callback */ + struct snotify_cb_ctx cb; + /* A singleton callback dispatcher */ + struct snotify_dispatcher *disp; + + /* Internal snotify flags */ + uint16_t snotify_flags; + /* The caller might decide to batch the updates and receive + * them all together with a delay + */ + struct timeval delay; + /* We keep the structure that actually does the work + * separately to be able to reinitialize it when the + * file is recreated or moved to the directory + */ + struct snotify_watch_ctx *wctx; +}; + +struct flg2str { + uint32_t flg; + const char *str; +} flg_table[] = { + { 0x00000001, "IN_ACCESS" }, + { 0x00000002, "IN_MODIFY" }, + { 0x00000004, "IN_ATTRIB" }, + { 0x00000008, "IN_CLOSE_WRITE" }, + { 0x00000010, "IN_CLOSE_NOWRITE" }, + { 0x00000020, "IN_OPEN" }, + { 0x00000040, "IN_MOVED_FROM" }, + { 0x00000080, "IN_MOVED_TO" }, + { 0x00000100, "IN_CREATE" }, + { 0x00000200, "IN_DELETE" }, + { 0x00000400, "IN_DELETE_SELF" }, + { 0x00000800, "IN_MOVE_SELF" }, + { 0x00002000, "IN_UNMOUNT" }, + { 0x00004000, "IN_Q_OVERFLOW" }, + { 0x00008000, "IN_IGNORED" }, + { 0x01000000, "IN_ONLYDIR" }, + { 0x02000000, "IN_DONT_FOLLOW" }, + { 0x04000000, "IN_EXCL_UNLINK" }, + { 0x20000000, "IN_MASK_ADD" }, + { 0x40000000, "IN_ISDIR" }, + { 0x80000000, "IN_ONESHOT" }, + { 0, NULL }, +}; + +#if 0 +static void debug_flags(uint32_t flags, const char *file) +{ + char msgbuf[1024]; + size_t total = 0; + + if (!DEBUG_IS_SET(SSSDBG_TRACE_LIBS)) { + return; + } + + for (int i = 0; flg_table[i].flg != 0; i++) { + if (flags & flg_table[i].flg) { + total += snprintf(msgbuf+total, + sizeof(msgbuf)-total, + "%s ", flg_table[i].str); + } + } + + if (total == 0) { + snprintf(msgbuf, sizeof(msgbuf), "NONE\n"); + } + DEBUG(SSSDBG_TRACE_LIBS, "Inotify event: %s on %s\n", msgbuf, file); +} +#endif + +static void snotify_process_callbacks(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval t, + void *ptr) +{ + struct snotify_ctx *snctx; + + snctx = talloc_get_type(ptr, struct snotify_ctx); + if (snctx == NULL) { + DEBUG(SSSDBG_FATAL_FAILURE, "Bad pointer\n"); + return; + } + + snctx->cb.fn(snctx->filename, + snctx->disp->caught_flags, + snctx->cb.pvt); + + talloc_zfree(snctx->disp); +} + +static struct snotify_dispatcher *create_dispatcher(struct snotify_ctx *snctx) +{ + struct snotify_dispatcher *disp; + struct timeval tv; + + disp = talloc_zero(snctx, struct snotify_dispatcher); + if (disp == NULL) { + return NULL; + } + + gettimeofday(&tv, NULL); + tv.tv_sec += snctx->delay.tv_sec; + tv.tv_usec += snctx->delay.tv_usec; + + DEBUG(SSSDBG_TRACE_FUNC, + "Running a timer with delay %ld.%ld\n", + (unsigned long) snctx->delay.tv_sec, + (unsigned long) snctx->delay.tv_usec); + + disp->te = tevent_add_timer(snctx->ev, disp, tv, + snotify_process_callbacks, + snctx); + if (disp->te == NULL) { + DEBUG(SSSDBG_FATAL_FAILURE, "Unable to queue file update!\n"); + talloc_free(disp); + return NULL; + } + + return disp; +} + +static struct snotify_dispatcher *get_dispatcher(struct snotify_ctx *snctx) +{ + if (snctx->disp != NULL) { + DEBUG(SSSDBG_TRACE_INTERNAL, "Reusing existing dispatcher\n"); + return snctx->disp; + } + + return create_dispatcher(snctx); +} + +static errno_t dispatch_event(struct snotify_ctx *snctx, + uint32_t ev_flags) +{ + struct snotify_dispatcher *disp; + + if ((snctx->cb.mask & ev_flags) == 0) { + return EOK; + } + + disp = get_dispatcher(snctx); + if (disp == NULL) { + return ENOMEM; + } + + disp->caught_flags |= ev_flags; + DEBUG(SSSDBG_TRACE_FUNC, + "Dispatched an event with combined flags 0x%X\n", + disp->caught_flags); + + snctx->disp = disp; + return EOK; +} + +static errno_t process_dir_event(struct snotify_ctx *snctx, + const struct inotify_event *in_event) +{ + errno_t ret; + + DEBUG(SSSDBG_TRACE_ALL, "inotify name: %s\n", in_event->name); + if (in_event->len == 0 \ + || strcmp(in_event->name, snctx->base_name) != 0) { + DEBUG(SSSDBG_TRACE_FUNC, "Not interested in %s\n", in_event->name); + return EOK; + } + + DEBUG(SSSDBG_TRACE_FUNC, + "received notification for watched file [%s] under %s\n", + in_event->name, snctx->dir_name); + + /* file the event for the file to see if the caller is interested in it */ + ret = dispatch_event(snctx, in_event->mask); + if (ret == EOK) { + /* Tells the outer loop to re-initialize flags once the loop is finished. + * However, finish reading all the events first to make sure we don't + * miss any + */ + return EAGAIN; + } + + return ret; +} + +static errno_t process_file_event(struct snotify_ctx *snctx, + const struct inotify_event *in_event) +{ + if (in_event->mask & IN_IGNORED) { + DEBUG(SSSDBG_TRACE_FUNC, + "Will reopen moved or deleted file %s\n", snctx->filename); + /* Notify caller of the event, don't quit */ + return EAGAIN; + } + + DEBUG(SSSDBG_TRACE_FUNC, + "received notification for watched file %s\n", snctx->filename); + + return dispatch_event(snctx, in_event->mask); +} + +static errno_t snotify_rewatch(struct snotify_ctx *snctx); + +static void snotify_internal_cb(struct tevent_context *ev, + struct tevent_fd *fde, + uint16_t flags, + void *data) +{ + char ev_buf[sizeof(struct inotify_event) + PATH_MAX]; + const char *ptr; + const struct inotify_event *in_event; + struct snotify_ctx *snctx; + ssize_t len; + errno_t ret; + bool rewatch = false; + + snctx = talloc_get_type(data, struct snotify_ctx); + if (snctx == NULL) { + DEBUG(SSSDBG_FATAL_FAILURE, "Bad pointer\n"); + return; + } + + while (1) { + len = read(snctx->wctx->inotify_fd, ev_buf, sizeof(ev_buf)); + if (len == -1) { + ret = errno; + if (ret != EAGAIN) { + DEBUG(SSSDBG_OP_FAILURE, + "Cannot read inotify_event [%d]: %s\n", + ret, strerror(ret)); + } else { + DEBUG(SSSDBG_TRACE_INTERNAL, "All inotify events processed\n"); + } + break; + } + + if ((size_t) len < sizeof(struct inotify_event)) { + /* Did not even read the required amount of data, move on.. */ + continue; + } + + for (ptr = ev_buf; + ptr < ev_buf + len; + ptr += sizeof(struct inotify_event) + in_event->len) { + + in_event = (const struct inotify_event *) ptr; + +#if 0 + debug_flags(in_event->mask, in_event->name); +#endif + + if (snctx->wctx->dir_wd == in_event->wd) { + ret = process_dir_event(snctx, in_event); + } else if (snctx->wctx->file_wd == in_event->wd) { + ret = process_file_event(snctx, in_event); + } else { + DEBUG(SSSDBG_MINOR_FAILURE, + "Unknown watch %d\n", in_event->wd); + ret = EOK; + } + + if (ret == EAGAIN) { + rewatch = true; + /* Continue with the loop and read all the events from + * this descriptor first, then rewatch when done + */ + } else if (ret != EOK) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Failed to process inotify event\n"); + } + } + } + + if (rewatch) { + ret = snotify_rewatch(snctx); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to re-set watch"); + } + } +} + +static int watch_ctx_destructor(void *memptr) +{ + struct snotify_watch_ctx *wctx; + + wctx = talloc_get_type(memptr, struct snotify_watch_ctx); + if (wctx == NULL) { + DEBUG(SSSDBG_FATAL_FAILURE, "Bad pointer\n"); + return 1; + } + + /* We don't need to close the watches explicitly. man 7 inotify says: + * When all file descriptors referring to an inotify instance + * have been closed (using close(2)), the underlying object + * and its resources are freed for reuse by the kernel; all + * associated watches are automatically freed. + */ + if (wctx->inotify_fd != -1) { + DEBUG(SSSDBG_TRACE_INTERNAL, + "Closing inotify fd %d\n", wctx->inotify_fd); + close(wctx->inotify_fd); + } + + return 0; +} + +static errno_t resolve_filename(struct snotify_ctx *snctx, + const char *filename, + char *resolved, + size_t resolved_size) +{ + /* NOTE: The code below relies in the GNU extensions for realpath, + * which will store in 'resolved' the prefix of 'filename' that does + * not exists if realpath call fails and errno is set to ENOENT */ + if (realpath(filename, resolved) == NULL) { + char fcopy[PATH_MAX + 1]; + char *p; + struct stat st; + + if (errno != ENOENT) { + return errno; + } + + /* Check if the unique missing component is the basename. The + * dirname must exist to be notified watching the parent dir. */ + strncpy(fcopy, filename, sizeof(fcopy) - 1); + fcopy[PATH_MAX] = '\0'; + + p = dirname(fcopy); + if (p == NULL) { + return EIO; + } + + if (stat(p, &st) == -1) { + return errno; + } + + /* The basedir exist, check the caller requested to watch it. + * Otherwise return error as never will be notified. */ + + if ((snctx->snotify_flags & SNOTIFY_WATCH_DIR) == 0) { + return ENOENT; + } + } + + return EOK; +} + +static errno_t copy_filenames(struct snotify_ctx *snctx, + const char *filename) +{ + char *p; + char resolved[PATH_MAX + 1]; + char fcopy[PATH_MAX + 1]; + errno_t ret; + + ret = resolve_filename(snctx, filename, resolved, sizeof(resolved)); + if (ret != EOK) { + return ret; + } + + strncpy(fcopy, resolved, sizeof(fcopy) - 1); + fcopy[PATH_MAX] = '\0'; + + p = dirname(fcopy); + if (p == NULL) { + return EIO; + } + + snctx->dir_name = talloc_strdup(snctx, p); + if (snctx->dir_name == NULL) { + return ENOMEM; + } + + strncpy(fcopy, resolved, sizeof(fcopy) - 1); + fcopy[PATH_MAX] = '\0'; + + p = basename(fcopy); + if (p == NULL) { + return EIO; + } + + snctx->base_name = talloc_strdup(snctx, p); + if (snctx->base_name == NULL) { + return ENOMEM; + } + + snctx->filename = talloc_strdup(snctx, resolved); + if (snctx->filename == NULL) { + return ENOMEM; + } + + return EOK; +} + +static struct snotify_watch_ctx *snotify_watch(struct snotify_ctx *snctx, + uint32_t mask) +{ + struct snotify_watch_ctx *wctx; + errno_t ret; + + wctx = talloc_zero(snctx, struct snotify_watch_ctx); + if (wctx == NULL) { + return NULL; + } + wctx->inotify_fd = -1; + wctx->dir_wd = -1; + wctx->file_wd = -1; + wctx->snctx = snctx; + talloc_set_destructor((TALLOC_CTX *)wctx, watch_ctx_destructor); + + wctx->inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (wctx->inotify_fd == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "inotify_init1 failed: %d: %s\n", ret, strerror(ret)); + goto fail; + } + DEBUG(SSSDBG_TRACE_INTERNAL, "Opened inotify fd %d\n", wctx->inotify_fd); + + wctx->tfd = tevent_add_fd(snctx->ev, wctx, wctx->inotify_fd, + TEVENT_FD_READ, snotify_internal_cb, + snctx); + if (wctx->tfd == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Cannot add tevent fd watch for %s\n", + snctx->filename); + goto fail; + } + + wctx->file_wd = inotify_add_watch(wctx->inotify_fd, snctx->filename, mask); + if (wctx->file_wd == -1) { + ret = errno; + if (ret != ENOENT || (!(snctx->snotify_flags & SNOTIFY_WATCH_DIR))) { + DEBUG(SSSDBG_MINOR_FAILURE, + "inotify_add_watch failed [%d]: %s\n", + ret, strerror(ret)); + goto fail; + } + } + DEBUG(SSSDBG_TRACE_INTERNAL, "Opened file watch %d\n", wctx->file_wd); + + if (snctx->snotify_flags & SNOTIFY_WATCH_DIR) { + /* Create a watch for the parent directory. This is useful for cases + * where we start watching a file before it's created, but still want + * a notification when the file is moved in + */ + wctx->dir_wd = inotify_add_watch(wctx->inotify_fd, + snctx->dir_name, PARENT_DIR_MASK); + if (wctx->dir_wd == -1) { + ret = errno; + DEBUG(SSSDBG_MINOR_FAILURE, + "inotify_add_watch failed [%d]: %s\n", + ret, strerror(ret)); + goto fail; + } + DEBUG(SSSDBG_TRACE_INTERNAL, + "Opened directory watch %d\n", wctx->dir_wd); + } + + return wctx; + +fail: + talloc_free(wctx); + return NULL; +} + +static errno_t snotify_rewatch(struct snotify_ctx *snctx) +{ + talloc_free(snctx->wctx); + + snctx->wctx = snotify_watch(snctx, snctx->cb.mask); + if (snctx->wctx == NULL) { + return ENOMEM; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Recreated watch\n"); + return EOK; +} + +struct snotify_ctx *_snotify_create(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + uint16_t snotify_flags, + const char *filename, + struct timeval *delay, + uint32_t mask, + snotify_cb_fn fn, + const char *fn_name, + void *pvt) +{ + errno_t ret; + struct snotify_ctx *snctx; + + snctx = talloc_zero(mem_ctx, struct snotify_ctx); + if (snctx == NULL) { + return NULL; + } + + snctx->ev = ev; + snctx->snotify_flags = snotify_flags; + if (delay) { + snctx->delay.tv_sec = delay->tv_sec; + snctx->delay.tv_usec = delay->tv_usec; + } + + snctx->cb.fn = fn; + snctx->cb.fn_name = fn_name; + snctx->cb.mask = mask; + snctx->cb.pvt = pvt; + + ret = copy_filenames(snctx, filename); + if (ret != EOK) { + talloc_free(snctx); + return NULL; + } + + snctx->wctx = snotify_watch(snctx, mask); + if (snctx->wctx == NULL) { + talloc_free(snctx); + return NULL; + } + + DEBUG(SSSDBG_TRACE_FUNC, + "Added a watch for %s with inotify flags 0x%X " + "internal flags 0x%X " + "using function %s after delay %ld.%ld\n", + snctx->filename, + mask, + snotify_flags, + fn_name, + (unsigned long) snctx->delay.tv_sec, + (unsigned long) snctx->delay.tv_usec); + + return snctx; +} |