/* * Copyright (C) 2022, Red Hat Inc. * * 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 . */ #include #include #include #include #include #include #include #include #include #include #include "util/inotify.h" #include "util/util.h" #include "util/file_watch.h" #define MISSING_FILE_POLL_TIME 10 /* seconds */ #define FILE_WATCH_POLL_INTERVAL 5 /* seconds */ struct file_watch_ctx { struct tevent_context *ev; const char *filename; bool use_inotify; struct config_file_inotify_check { struct snotify_ctx *snctx; } inotify_check; struct config_file_poll_check { struct tevent_timer *timer; time_t modified; } poll_check; fw_callback cb; void *cb_arg; }; static void poll_watched_file(struct tevent_context *ev, struct tevent_timer *te, struct timeval t, void *ptr); static errno_t create_poll_timer(struct file_watch_ctx *fw_ctx) { struct timeval tv; tv = tevent_timeval_current_ofs(FILE_WATCH_POLL_INTERVAL, 0); fw_ctx->poll_check.timer = tevent_add_timer(fw_ctx->ev, fw_ctx, tv, poll_watched_file, fw_ctx); if (!fw_ctx->poll_check.timer) { return EIO; } return EOK; } static void poll_watched_file(struct tevent_context *ev, struct tevent_timer *te, struct timeval t, void *ptr) { int ret, err; struct stat file_stat; struct file_watch_ctx *fw_ctx; fw_ctx = talloc_get_type(ptr, struct file_watch_ctx); ret = stat(fw_ctx->filename, &file_stat); if (ret < 0) { err = errno; DEBUG(SSSDBG_IMPORTANT_INFO, "Could not stat file [%s]. Error [%d:%s]\n", fw_ctx->filename, err, strerror(err)); return; } if (file_stat.st_mtime != fw_ctx->poll_check.modified) { /* Parse the file and invoke the callback */ /* Note: this will fire if the modification time changes into the past * as well as the future. */ DEBUG(SSSDBG_TRACE_INTERNAL, "File [%s] changed\n", fw_ctx->filename); fw_ctx->poll_check.modified = file_stat.st_mtime; /* Tell the caller the file changed */ fw_ctx->cb(fw_ctx->filename, fw_ctx->cb_arg); } ret = create_poll_timer(fw_ctx); if (ret != EOK) { DEBUG(SSSDBG_IMPORTANT_INFO, "Error: File [%s] no longer monitored for changes!\n", fw_ctx->filename); } } static int watched_file_inotify_cb(const char *filename, uint32_t flags, void *pvt) { static char received[PATH_MAX + 1]; static char expected[PATH_MAX + 1]; struct file_watch_ctx *fw_ctx; char *res; DEBUG(SSSDBG_TRACE_LIBS, "Received inotify notification for %s\n", filename); fw_ctx = talloc_get_type(pvt, struct file_watch_ctx); if (fw_ctx == NULL) { return EINVAL; } res = realpath(fw_ctx->filename, expected); if (res == NULL) { DEBUG(SSSDBG_TRACE_LIBS, "Normalization failed for expected %s. Skipping the callback.\n", fw_ctx->filename); goto done; } res = realpath(filename, received); if (res == NULL) { DEBUG(SSSDBG_TRACE_LIBS, "Normalization failed for received %s. Skipping the callback.\n", filename); goto done; } if (strcmp(expected, received) == 0) { if (access(received, F_OK) == 0) { fw_ctx->cb(received, fw_ctx->cb_arg); } else { DEBUG(SSSDBG_TRACE_LIBS, "File %s is missing. Skipping the callback.\n", filename); } } done: return EOK; } static int try_inotify(struct file_watch_ctx *fw_ctx) { #ifdef HAVE_INOTIFY struct snotify_ctx *snctx; /* We will queue the file for update in one second. * This way, if there is a script writing to the file * repeatedly, we won't be attempting to update multiple * times. */ struct timeval delay = { .tv_sec = 1, .tv_usec = 0 }; snctx = snotify_create(fw_ctx, fw_ctx->ev, SNOTIFY_WATCH_DIR, fw_ctx->filename, &delay, IN_DELETE_SELF | IN_CLOSE_WRITE | IN_MOVE_SELF | \ IN_CREATE | IN_MOVED_TO | IN_IGNORED, watched_file_inotify_cb, fw_ctx); if (snctx == NULL) { return EIO; } return EOK; #else return EINVAL; #endif /* HAVE_INOTIFY */ } static errno_t fw_watch_file_poll(struct file_watch_ctx *fw_ctx) { struct stat file_stat; int ret, err; ret = stat(fw_ctx->filename, &file_stat); if (ret < 0) { err = errno; if (err == ENOENT) { DEBUG(SSSDBG_IMPORTANT_INFO, "File [%s] is missing. Will try again later.\n", fw_ctx->filename); } else { DEBUG(SSSDBG_IMPORTANT_INFO, "Could not stat file [%s]. Error [%d:%s]\n", fw_ctx->filename, err, strerror(err)); } return err; } fw_ctx->poll_check.modified = file_stat.st_mtime; if(!fw_ctx->poll_check.timer) { ret = create_poll_timer(fw_ctx); if (ret != EOK) { DEBUG(SSSDBG_TRACE_LIBS, "Cannot create poll timer\n"); return ret; } } return EOK; } static int watch_file(struct file_watch_ctx *fw_ctx) { int ret = EOK; bool use_inotify; use_inotify = fw_ctx->use_inotify; if (use_inotify) { ret = try_inotify(fw_ctx); if (ret != EOK) { DEBUG(SSSDBG_TRACE_LIBS, "Falling back to polling\n"); use_inotify = false; } } if (!use_inotify) { ret = fw_watch_file_poll(fw_ctx); } return ret; } static void set_file_watching(struct tevent_context *ev, struct tevent_timer *te, struct timeval tv, void *data) { int ret; struct file_watch_ctx *fw_ctx = talloc_get_type(data, struct file_watch_ctx); ret = watch_file(fw_ctx); if (ret == EOK) { if (access(fw_ctx->filename, F_OK) == 0) { fw_ctx->cb(fw_ctx->filename, fw_ctx->cb_arg); } } else if (ret == ENOENT) { DEBUG(SSSDBG_TRACE_LIBS, "%s missing. Waiting for it to appear.\n", fw_ctx->filename); tv = tevent_timeval_current_ofs(MISSING_FILE_POLL_TIME, 0); te = tevent_add_timer(fw_ctx->ev, fw_ctx, tv, set_file_watching, fw_ctx); if (te == NULL) { DEBUG(SSSDBG_IMPORTANT_INFO, "tevent_add_timer failed. %s will be ignored.\n", fw_ctx->filename); } } else { DEBUG(SSSDBG_IMPORTANT_INFO, "watch_file failed. %s will be ignored: [%i] %s\n", fw_ctx->filename, ret, sss_strerror(ret)); } } struct file_watch_ctx *fw_watch_file(TALLOC_CTX *mem_ctx, struct tevent_context *ev, const char *filename, bool use_inotify, fw_callback cb, void *cb_arg) { int ret; struct timeval tv; struct file_watch_ctx *fw_ctx; if (ev == NULL || filename == NULL || cb == NULL) { DEBUG(SSSDBG_TRACE_LIBS, "Invalid parameter\n"); return NULL; } fw_ctx = talloc_zero(mem_ctx, struct file_watch_ctx); if (fw_ctx == NULL) { DEBUG(SSSDBG_IMPORTANT_INFO, "Failed to allocate the context\n"); return NULL; } fw_ctx->ev = ev; fw_ctx->use_inotify = use_inotify; fw_ctx->cb = cb; fw_ctx->cb_arg = cb_arg; fw_ctx->filename = talloc_strdup(fw_ctx, filename); if (fw_ctx->filename == NULL) { DEBUG(SSSDBG_IMPORTANT_INFO, "talloc_strdup() failed\n"); ret = ENOMEM; goto done; } /* Watch for changes to the requested file, and retry periodically * if the file does not exist */ tv = tevent_timeval_current_ofs(0, 0); // Not actually used set_file_watching(fw_ctx->ev, NULL, tv, fw_ctx); ret = EOK; done: if (ret != EOK) { talloc_free(fw_ctx); fw_ctx = NULL; } return fw_ctx; }