summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJakub Hrozek <jhrozek@redhat.com>2016-05-27 18:19:59 +0200
committerJakub Hrozek <jhrozek@redhat.com>2017-02-15 14:51:36 +0100
commit8cfb42e1985550e99585d311f68087d414932806 (patch)
tree411ab877f19e5f4535948cab6f0916c4da0af906
parent50c740cbc2bb27cbe488fa8587e2901b8b85cf87 (diff)
downloadsssd-8cfb42e1985550e99585d311f68087d414932806.tar.gz
sssd-8cfb42e1985550e99585d311f68087d414932806.tar.xz
sssd-8cfb42e1985550e99585d311f68087d414932806.zip
UTIL: Add a generic inotify module
Adds a reusable module for watching files using the Linux-specific inotify(7) interface. Adds the possibility to watch the file's parent directory as well to make it possible to watch moves into the directory and allow watching file that doesn't exist at the time the watch is created. This interface is needed to implement the files provider, so this commit is related to: https://fedorahosted.org/sssd/ticket/2228 Reviewed-by: Pavel Březina <pbrezina@redhat.com>
-rw-r--r--Makefile.am20
-rw-r--r--src/external/inotify.m42
-rw-r--r--src/tests/cmocka/test_inotify.c582
-rw-r--r--src/util/inotify.c562
-rw-r--r--src/util/inotify.h61
5 files changed, 1227 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am
index 5cf496002..09c021d22 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -284,6 +284,10 @@ if BUILD_IFP
non_interactive_cmocka_based_tests += ifp_tests
endif # BUILD_IFP
+if HAVE_INOTIFY
+non_interactive_cmocka_based_tests += test_inotify
+endif # HAVE_INOTIFY
+
if BUILD_SAMBA
non_interactive_cmocka_based_tests += \
ad_access_filter_tests \
@@ -642,6 +646,7 @@ dist_noinst_HEADERS = \
src/util/util_safealign.h \
src/util/util_sss_idmap.h \
src/util/util_creds.h \
+ src/util/inotify.h \
src/monitor/monitor.h \
src/monitor/monitor_interfaces.h \
src/monitor/monitor_iface_generated.h \
@@ -3167,6 +3172,21 @@ krb5_common_test_LDADD = \
libdlopen_test_providers.la \
$(NULL)
+test_inotify_SOURCES = \
+ src/util/inotify.c \
+ src/tests/cmocka/test_inotify.c \
+ $(NULL)
+test_inotify_CFLAGS = \
+ $(AM_CFLAGS) \
+ $(NULL)
+test_inotify_LDADD = \
+ $(CMOCKA_LIBS) \
+ $(SSSD_LIBS) \
+ $(SSSD_INTERNAL_LTLIBS) \
+ $(LIBADD_DL) \
+ libsss_test_common.la \
+ $(NULL)
+
endif # HAVE_CMOCKA
noinst_PROGRAMS = pam_test_client
diff --git a/src/external/inotify.m4 b/src/external/inotify.m4
index 25259a817..3ae5ae314 100644
--- a/src/external/inotify.m4
+++ b/src/external/inotify.m4
@@ -29,4 +29,6 @@ int main () {
AS_IF([test x"$inotify_works" = xyes],
[AC_DEFINE_UNQUOTED([HAVE_INOTIFY], [1], [Inotify works])])
AC_SUBST(INOTIFY_LIBS)
+
+ AM_CONDITIONAL([HAVE_INOTIFY], [test x"$inotify_works" = xyes])
])
diff --git a/src/tests/cmocka/test_inotify.c b/src/tests/cmocka/test_inotify.c
new file mode 100644
index 000000000..1f8561df1
--- /dev/null
+++ b/src/tests/cmocka/test_inotify.c
@@ -0,0 +1,582 @@
+/*
+ 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 <errno.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <cmocka.h>
+#include <talloc.h>
+#include <popt.h>
+
+#include "limits.h"
+#include "util/io.h"
+#include "util/inotify.h"
+#include "util/util.h"
+#include "tests/common.h"
+
+struct inotify_test_ctx {
+ char *filename;
+ char *dirname;
+
+ int ncb;
+ int threshold;
+ /* if the cb receives flags not in this set, test fails */
+ uint32_t exp_flags;
+
+ struct sss_test_ctx *tctx;
+ struct tevent_timer *fail_te;
+};
+
+static void test_timeout(struct tevent_context *ev,
+ struct tevent_timer *te,
+ struct timeval t,
+ void *ptr)
+{
+ DEBUG(SSSDBG_FATAL_FAILURE, "The test timed out!\n");
+ talloc_free(te);
+ fail();
+}
+
+static struct inotify_test_ctx *common_setup(TALLOC_CTX *mem_ctx)
+{
+ struct inotify_test_ctx *ctx;
+ struct timeval tv;
+
+ ctx = talloc_zero(mem_ctx, struct inotify_test_ctx);
+ if (ctx == NULL) {
+ return NULL;
+ }
+
+ ctx->tctx = create_ev_test_ctx(ctx);
+ if (ctx->tctx == NULL) {
+ talloc_free(ctx);
+ return NULL;
+ }
+
+ gettimeofday(&tv, NULL);
+ tv.tv_sec += 5;
+ ctx->fail_te = tevent_add_timer(ctx->tctx->ev, ctx,
+ tv, test_timeout, ctx);
+ if (ctx->fail_te == NULL) {
+ DEBUG(SSSDBG_FATAL_FAILURE, "Unable to queue fallback timer!\n");
+ talloc_free(ctx);
+ return NULL;
+ }
+
+ return ctx;
+}
+
+static int inotify_test_setup(void **state)
+{
+ struct inotify_test_ctx *ctx;
+ int fd;
+
+ ctx = common_setup(NULL);
+ if (ctx == NULL) {
+ return 1;
+ }
+
+ ctx->filename = talloc_strdup(ctx, "test_inotify.XXXXXX");
+ if (ctx->filename == NULL) {
+ talloc_free(ctx);
+ return 1;
+ }
+
+ fd = mkstemp(ctx->filename);
+ if (fd == -1) {
+ talloc_free(ctx);
+ return 1;
+ }
+ close(fd);
+
+ *state = ctx;
+ return 0;
+}
+
+static int inotify_test_dir_setup(void **state)
+{
+ struct inotify_test_ctx *ctx;
+
+ ctx = common_setup(NULL);
+ if (ctx == NULL) {
+ return 1;
+ }
+
+ ctx->dirname = talloc_strdup(ctx, "test_inotify_dir.XXXXXX");
+ if (ctx->dirname == NULL) {
+ talloc_free(ctx);
+ return 1;
+ }
+
+ ctx->dirname = mkdtemp(ctx->dirname);
+ if (ctx->dirname == NULL) {
+ talloc_free(ctx);
+ return 1;
+ }
+
+ ctx->filename = talloc_asprintf(ctx, "%s/testfile", ctx->dirname);
+ if (ctx->filename == NULL) {
+ talloc_free(ctx);
+ return 1;
+ }
+
+ *state = ctx;
+ return 0;
+}
+
+static int inotify_test_teardown(void **state)
+{
+ struct inotify_test_ctx *ctx = talloc_get_type_abort(*state,
+ struct inotify_test_ctx);
+ int ret;
+
+ ret = unlink(ctx->filename);
+ if (ret == -1 && errno != ENOENT) {
+ return 1;
+ }
+
+ talloc_free(ctx);
+ return 0;
+}
+
+static int inotify_test_dir_teardown(void **state)
+{
+ struct inotify_test_ctx *ctx = talloc_get_type_abort(*state,
+ struct inotify_test_ctx);
+ int ret;
+
+ ret = unlink(ctx->filename);
+ if (ret == -1 && errno != ENOENT) {
+ return 1;
+ }
+
+ ret = rmdir(ctx->dirname);
+ if (ret == -1 && errno != ENOENT) {
+ return 1;
+ }
+
+ talloc_free(ctx);
+ return 0;
+}
+
+static void file_mod_op(struct tevent_context *ev,
+ struct tevent_timer *te,
+ struct timeval t,
+ void *ptr)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(ptr,
+ struct inotify_test_ctx);
+ FILE *f;
+
+ talloc_free(te);
+
+ f = fopen(test_ctx->filename, "w");
+ if (f == NULL) {
+ test_ctx->tctx->error = errno;
+ test_ctx->tctx->done = true;
+ return;
+ }
+
+ fprintf(f, "%s\n", test_ctx->filename);
+ fflush(f);
+ fclose(f);
+}
+
+static void check_and_set_threshold(struct inotify_test_ctx *test_ctx,
+ uint32_t flags)
+{
+ if (test_ctx->exp_flags != 0 && !(test_ctx->exp_flags & flags)) {
+ fail();
+ }
+
+ test_ctx->ncb++;
+}
+
+static int inotify_set_threshold_cb(const char *filename,
+ uint32_t flags,
+ void *pvt)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(pvt,
+ struct inotify_test_ctx);
+
+ check_and_set_threshold(test_ctx, flags);
+ return EOK;
+}
+
+static int inotify_threshold_cb(const char *filename,
+ uint32_t flags,
+ void *pvt)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(pvt,
+ struct inotify_test_ctx);
+
+ check_and_set_threshold(test_ctx, flags);
+ if (test_ctx->ncb == test_ctx->threshold) {
+ test_ctx->tctx->done = true;
+ return EOK;
+ }
+
+ return EOK;
+}
+
+/* Test that running two modifications fires the callback twice */
+static void test_inotify_mod(void **state)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(*state,
+ struct inotify_test_ctx);
+ struct snotify_ctx *ctx;
+ struct timeval tv;
+ struct tevent_timer *te;
+ errno_t ret;
+
+ ctx = snotify_create(test_ctx, test_ctx->tctx->ev, SNOTIFY_WATCH_DIR,
+ test_ctx->filename, NULL, IN_MODIFY,
+ inotify_threshold_cb, test_ctx);
+ assert_non_null(ctx);
+
+ test_ctx->threshold = 2;
+ test_ctx->exp_flags = IN_MODIFY;
+
+ gettimeofday(&tv, NULL);
+ tv.tv_usec += 500;
+ te = tevent_add_timer(test_ctx->tctx->ev, test_ctx,
+ tv, file_mod_op, test_ctx);
+ if (te == NULL) {
+ DEBUG(SSSDBG_FATAL_FAILURE, "Unable to queue file update!\n");
+ return;
+ }
+
+ gettimeofday(&tv, NULL);
+ tv.tv_sec += 1;
+ te = tevent_add_timer(test_ctx->tctx->ev, test_ctx,
+ tv, file_mod_op, test_ctx);
+ if (te == NULL) {
+ DEBUG(SSSDBG_FATAL_FAILURE, "Unable to queue file update!\n");
+ return;
+ }
+
+ ret = test_ev_loop(test_ctx->tctx);
+ assert_int_equal(ret, EOK);
+
+ talloc_free(ctx);
+}
+
+static void file_mv_op(struct tevent_context *ev,
+ struct tevent_timer *te,
+ struct timeval t,
+ void *ptr)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(ptr,
+ struct inotify_test_ctx);
+ FILE *f;
+ int fd;
+ char src_tmp_file[] = "test_inotify_src.XXXXXX";
+ int ret;
+
+ talloc_free(te);
+
+ fd = mkstemp(src_tmp_file);
+ if (fd == -1) {
+ test_ctx->tctx->error = errno;
+ test_ctx->tctx->done = true;
+ return;
+ }
+
+ f = fdopen(fd, "w");
+ if (f == NULL) {
+ close(fd);
+ unlink(src_tmp_file);
+ test_ctx->tctx->error = errno;
+ test_ctx->tctx->done = true;
+ return;
+ }
+
+ fprintf(f, "%s\n", test_ctx->filename);
+ fflush(f);
+ fclose(f);
+
+ ret = rename(src_tmp_file, test_ctx->filename);
+ if (ret == -1) {
+ unlink(src_tmp_file);
+ test_ctx->tctx->error = errno;
+ test_ctx->tctx->done = true;
+ return;
+ }
+}
+
+static void test_inotify_mv(void **state)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(*state,
+ struct inotify_test_ctx);
+ struct snotify_ctx *ctx;
+ struct timeval tv;
+ struct tevent_timer *te;
+ errno_t ret;
+
+ ctx = snotify_create(test_ctx, test_ctx->tctx->ev, SNOTIFY_WATCH_DIR,
+ test_ctx->filename, NULL, IN_MOVED_TO,
+ inotify_threshold_cb, test_ctx);
+ assert_non_null(ctx);
+
+ test_ctx->threshold = 1;
+ test_ctx->exp_flags = IN_MOVED_TO;
+
+ gettimeofday(&tv, NULL);
+ tv.tv_usec += 200;
+ te = tevent_add_timer(test_ctx->tctx->ev, test_ctx,
+ tv, file_mv_op, test_ctx);
+ if (te == NULL) {
+ DEBUG(SSSDBG_FATAL_FAILURE, "Unable to queue file update!\n");
+ return;
+ }
+
+ ret = test_ev_loop(test_ctx->tctx);
+ assert_int_equal(ret, EOK);
+}
+
+static void file_del_add_op(struct tevent_context *ev,
+ struct tevent_timer *te,
+ struct timeval t,
+ void *ptr)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(ptr,
+ struct inotify_test_ctx);
+ FILE *f;
+ int ret;
+
+ talloc_free(te);
+
+ ret = unlink(test_ctx->filename);
+ if (ret == -1) {
+ test_ctx->tctx->error = errno;
+ test_ctx->tctx->done = true;
+ return;
+ }
+
+ f = fopen(test_ctx->filename, "w");
+ if (f == NULL) {
+ test_ctx->tctx->error = errno;
+ test_ctx->tctx->done = true;
+ return;
+ }
+
+ fprintf(f, "%s\n", test_ctx->filename);
+ fflush(f);
+ fclose(f);
+}
+
+static void test_inotify_del_add(void **state)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(*state,
+ struct inotify_test_ctx);
+ struct snotify_ctx *ctx;
+ struct timeval tv;
+ struct tevent_timer *te;
+ errno_t ret;
+
+ test_ctx->threshold = 1;
+ test_ctx->exp_flags = IN_CREATE;
+
+ ctx = snotify_create(test_ctx, test_ctx->tctx->ev, SNOTIFY_WATCH_DIR,
+ test_ctx->filename, NULL,
+ IN_CREATE,
+ inotify_threshold_cb, test_ctx);
+ assert_non_null(ctx);
+
+ gettimeofday(&tv, NULL);
+ tv.tv_usec += 200;
+ te = tevent_add_timer(test_ctx->tctx->ev, test_ctx,
+ tv, file_del_add_op, test_ctx);
+ if (te == NULL) {
+ DEBUG(SSSDBG_FATAL_FAILURE, "Unable to queue file update!\n");
+ return;
+ }
+
+ ret = test_ev_loop(test_ctx->tctx);
+ assert_int_equal(ret, EOK);
+}
+
+static void test_inotify_file_moved_in(void **state)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(*state,
+ struct inotify_test_ctx);
+ struct snotify_ctx *ctx;
+ struct timeval tv;
+ struct tevent_timer *te;
+ errno_t ret;
+
+ test_ctx->threshold = 1;
+ test_ctx->exp_flags = IN_CREATE;
+
+ ctx = snotify_create(test_ctx, test_ctx->tctx->ev, SNOTIFY_WATCH_DIR,
+ test_ctx->filename, NULL,
+ IN_CREATE | IN_CLOSE_WRITE,
+ inotify_threshold_cb, test_ctx);
+ assert_non_null(ctx);
+
+ gettimeofday(&tv, NULL);
+ tv.tv_usec += 200;
+
+ te = tevent_add_timer(test_ctx->tctx->ev, test_ctx,
+ tv, file_mod_op, test_ctx);
+ if (te == NULL) {
+ DEBUG(SSSDBG_FATAL_FAILURE, "Unable to queue file update!\n");
+ return;
+ }
+
+ ret = test_ev_loop(test_ctx->tctx);
+ assert_int_equal(ret, EOK);
+}
+
+static void file_del_op(struct tevent_context *ev,
+ struct tevent_timer *te,
+ struct timeval t,
+ void *ptr)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(ptr,
+ struct inotify_test_ctx);
+ int ret;
+
+ talloc_free(te);
+
+ ret = unlink(test_ctx->filename);
+ if (ret == -1) {
+ test_ctx->tctx->error = errno;
+ test_ctx->tctx->done = true;
+ return;
+ }
+}
+
+static void check_threshold_cb(struct tevent_context *ev,
+ struct tevent_timer *te,
+ struct timeval t,
+ void *ptr)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(ptr,
+ struct inotify_test_ctx);
+
+ /* tests that no more callbacks were issued and exactly one
+ * was caught for both requests
+ */
+ if (test_ctx->ncb == test_ctx->threshold) {
+ test_ctx->tctx->done = true;
+ return;
+ }
+
+ fail();
+}
+
+static void test_inotify_delay(void **state)
+{
+ struct inotify_test_ctx *test_ctx = talloc_get_type_abort(*state,
+ struct inotify_test_ctx);
+ struct snotify_ctx *ctx;
+ struct timeval tv;
+ struct tevent_timer *te;
+ errno_t ret;
+ struct timeval delay = { .tv_sec = 1, .tv_usec = 0 };
+
+ test_ctx->threshold = 1;
+ test_ctx->exp_flags = IN_CREATE | IN_DELETE;
+
+ ctx = snotify_create(test_ctx, test_ctx->tctx->ev, SNOTIFY_WATCH_DIR,
+ test_ctx->filename, &delay,
+ IN_CREATE | IN_DELETE,
+ inotify_set_threshold_cb, test_ctx);
+ assert_non_null(ctx);
+
+ gettimeofday(&tv, NULL);
+ tv.tv_usec += 100;
+ te = tevent_add_timer(test_ctx->tctx->ev, test_ctx,
+ tv, file_mod_op, test_ctx);
+ if (te == NULL) {
+ DEBUG(SSSDBG_FATAL_FAILURE, "Unable to queue file update!\n");
+ return;
+ }
+
+ gettimeofday(&tv, NULL);
+ tv.tv_usec += 200;
+ te = tevent_add_timer(test_ctx->tctx->ev, test_ctx,
+ tv, file_del_op, test_ctx);
+ if (te == NULL) {
+ DEBUG(SSSDBG_FATAL_FAILURE, "Unable to queue file update!\n");
+ return;
+ }
+
+ gettimeofday(&tv, NULL);
+ tv.tv_sec += 2;
+ te = tevent_add_timer(test_ctx->tctx->ev, test_ctx,
+ tv, check_threshold_cb, test_ctx);
+ if (te == NULL) {
+ DEBUG(SSSDBG_FATAL_FAILURE, "Unable to queue file update!\n");
+ return;
+ }
+
+ ret = test_ev_loop(test_ctx->tctx);
+ assert_int_equal(ret, EOK);
+}
+
+int main(int argc, const char *argv[])
+{
+ poptContext pc;
+ int opt;
+ struct poptOption long_options[] = {
+ POPT_AUTOHELP
+ SSSD_DEBUG_OPTS
+ POPT_TABLEEND
+ };
+
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test_setup_teardown(test_inotify_mv,
+ inotify_test_setup,
+ inotify_test_teardown),
+ cmocka_unit_test_setup_teardown(test_inotify_mod,
+ inotify_test_setup,
+ inotify_test_teardown),
+ cmocka_unit_test_setup_teardown(test_inotify_del_add,
+ inotify_test_setup,
+ inotify_test_teardown),
+ cmocka_unit_test_setup_teardown(test_inotify_file_moved_in,
+ inotify_test_dir_setup,
+ inotify_test_dir_teardown),
+ cmocka_unit_test_setup_teardown(test_inotify_delay,
+ inotify_test_dir_setup,
+ inotify_test_dir_teardown),
+ };
+
+ /* Set debug level to invalid value so we can deside if -d 0 was used. */
+ debug_level = SSSDBG_INVALID;
+
+ pc = poptGetContext(argv[0], argc, argv, long_options, 0);
+ while((opt = poptGetNextOpt(pc)) != -1) {
+ switch(opt) {
+ default:
+ fprintf(stderr, "\nInvalid option %s: %s\n\n",
+ poptBadOption(pc, 0), poptStrerror(opt));
+ poptPrintUsage(pc, stderr, 0);
+ return 1;
+ }
+ }
+ poptFreeContext(pc);
+
+ DEBUG_CLI_INIT(debug_level);
+
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/src/util/inotify.c b/src/util/inotify.c
new file mode 100644
index 000000000..41466520d
--- /dev/null
+++ b/src/util/inotify.c
@@ -0,0 +1,562 @@
+/*
+ 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 <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 recrated 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;
+
+ 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");
+ }
+ return;
+ }
+
+ 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;
+
+ //debug_flags(in_event->mask, in_event->name);
+
+ if (snctx->wctx->dir_wd == in_event->wd) {
+ ret = process_dir_event(snctx, in_event);
+ 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");
+ continue;
+ }
+ } else if (snctx->wctx->file_wd == in_event->wd) {
+ ret = process_file_event(snctx, in_event);
+ if (ret != EOK) {
+ DEBUG(SSSDBG_MINOR_FAILURE,
+ "Failed to process inotify event\n");
+ continue;
+ }
+ } else {
+ DEBUG(SSSDBG_MINOR_FAILURE,
+ "Uknown watch %d\n", in_event->wd);
+ ret = EOK;
+ }
+ }
+ }
+
+ 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 copy_filenames(struct snotify_ctx *snctx,
+ const char *filename)
+{
+ char *p;
+ char fcopy[PATH_MAX];
+
+ strncpy(fcopy, filename, sizeof(fcopy));
+ fcopy[PATH_MAX-1] = '\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, filename, sizeof(fcopy));
+ fcopy[PATH_MAX-1] = '\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, filename);
+ 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;
+}
diff --git a/src/util/inotify.h b/src/util/inotify.h
new file mode 100644
index 000000000..359294452
--- /dev/null
+++ b/src/util/inotify.h
@@ -0,0 +1,61 @@
+/*
+ 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/>.
+*/
+
+#ifndef __INOTIFY_H_
+#define __INOTIFY_H_
+
+#include <talloc.h>
+#include <tevent.h>
+#include <sys/inotify.h>
+
+
+typedef int (*snotify_cb_fn)(const char *filename,
+ uint32_t caught_flags,
+ void *pvt);
+
+#define SNOTIFY_WATCH_DIR 0x0001
+
+/*
+ * Set up an inotify watch for file at filename. When an inotify
+ * event is caught, it must match the "mask" parameter. The watch
+ * would then call snotify_cb_fn() and include the caught flags.
+ *
+ * If snotify_flags includes SNOTIFY_WATCH_DIR, also the parent directory
+ * of this file would be watched to cover cases where the file might not
+ * exist when the watch is created.
+ *
+ * If you wish to batch inotify requests to avoid hammering the caller
+ * with several successive requests, use the delay parameter. The function
+ * would then only send invoke the callback after the delay and the caught
+ * flags would be OR-ed. By default, the callback is invoked immediately.
+ *
+ * Use the pvt parameter to pass a private context to the function
+ */
+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);
+
+#define snotify_create(mem_ctx, ev, snotify_flags, filename, delay, mask, fn, pvt) \
+ _snotify_create(mem_ctx, ev, snotify_flags, filename, delay, mask, fn, #fn, pvt);
+
+#endif /* __INOTIFY_H_ */