| /* |
| * Copyright 2000-2012 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "fsnotifier.h" |
| |
| #include <dirent.h> |
| #include <errno.h> |
| #include <linux/limits.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/inotify.h> |
| #include <sys/stat.h> |
| #include <syslog.h> |
| #include <unistd.h> |
| |
| #ifdef __amd64__ |
| __asm__(".symver memcpy,memcpy@GLIBC_2.2.5"); |
| #else |
| __asm__(".symver memcpy,memcpy@GLIBC_2.0"); |
| #endif |
| |
| |
| #define WATCH_COUNT_NAME "/proc/sys/fs/inotify/max_user_watches" |
| |
| #define DEFAULT_SUBDIR_COUNT 5 |
| |
| typedef struct __watch_node { |
| int wd; |
| struct __watch_node* parent; |
| array* kids; |
| int path_len; |
| char path[0]; |
| } watch_node; |
| |
| static int inotify_fd = -1; |
| static int watch_count = 0; |
| static table* watches; |
| static bool limit_reached = false; |
| static void (* callback)(const char*, int) = NULL; |
| |
| #define EVENT_SIZE (sizeof(struct inotify_event)) |
| #define EVENT_BUF_LEN (2048 * (EVENT_SIZE + 16)) |
| static char event_buf[EVENT_BUF_LEN]; |
| |
| static char path_buf[2 * PATH_MAX]; |
| |
| static void read_watch_descriptors_count(); |
| static void watch_limit_reached(); |
| |
| |
| bool init_inotify() { |
| inotify_fd = inotify_init(); |
| if (inotify_fd < 0) { |
| int e = errno; |
| userlog(LOG_ERR, "inotify_init: %s", strerror(e)); |
| if (e == EMFILE) { |
| message(MSG_INSTANCE_LIMIT); |
| } |
| return false; |
| } |
| userlog(LOG_DEBUG, "inotify fd: %d", get_inotify_fd()); |
| |
| read_watch_descriptors_count(); |
| if (watch_count <= 0) { |
| close(inotify_fd); |
| inotify_fd = -1; |
| return false; |
| } |
| userlog(LOG_INFO, "inotify watch descriptors: %d", watch_count); |
| |
| watches = table_create(watch_count); |
| if (watches == NULL) { |
| userlog(LOG_ERR, "out of memory"); |
| close(inotify_fd); |
| inotify_fd = -1; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static void read_watch_descriptors_count() { |
| FILE* f = fopen(WATCH_COUNT_NAME, "r"); |
| if (f == NULL) { |
| userlog(LOG_ERR, "can't open %s: %s", WATCH_COUNT_NAME, strerror(errno)); |
| return; |
| } |
| |
| char* str = read_line(f); |
| if (str == NULL) { |
| userlog(LOG_ERR, "can't read from %s", WATCH_COUNT_NAME); |
| } |
| else { |
| watch_count = atoi(str); |
| } |
| |
| fclose(f); |
| } |
| |
| |
| inline void set_inotify_callback(void (* _callback)(const char*, int)) { |
| callback = _callback; |
| } |
| |
| |
| inline int get_inotify_fd() { |
| return inotify_fd; |
| } |
| |
| |
| #define EVENT_MASK IN_MODIFY | IN_ATTRIB | IN_CREATE | IN_DELETE | IN_MOVE | IN_DELETE_SELF | IN_MOVE_SELF |
| |
| static int add_watch(int path_len, watch_node* parent) { |
| int wd = inotify_add_watch(inotify_fd, path_buf, EVENT_MASK); |
| if (wd < 0) { |
| if (errno == EACCES || errno == ENOENT) { |
| userlog(LOG_DEBUG, "inotify_add_watch(%s): %s", path_buf, strerror(errno)); |
| return ERR_IGNORE; |
| } |
| else if (errno == ENOSPC) { |
| userlog(LOG_WARNING, "inotify_add_watch(%s): %s", path_buf, strerror(errno)); |
| watch_limit_reached(); |
| return ERR_CONTINUE; |
| } |
| else { |
| userlog(LOG_ERR, "inotify_add_watch(%s): %s", path_buf, strerror(errno)); |
| return ERR_ABORT; |
| } |
| } |
| else { |
| userlog(LOG_DEBUG, "watching %s: %d", path_buf, wd); |
| } |
| |
| watch_node* node = table_get(watches, wd); |
| if (node != NULL) { |
| if (node->wd != wd) { |
| userlog(LOG_ERR, "table error: corruption at %d:%s / %d:%s)", wd, path_buf, node->wd, node->path); |
| return ERR_ABORT; |
| } |
| else if (strcmp(node->path, path_buf) != 0) { |
| char buf1[PATH_MAX], buf2[PATH_MAX]; |
| const char* normalized1 = realpath(node->path, buf1); |
| const char* normalized2 = realpath(path_buf, buf2); |
| if (normalized1 == NULL || normalized2 == NULL || strcmp(normalized1, normalized2) != 0) { |
| userlog(LOG_ERR, "table error: collision at %d (new %s, existing %s)", wd, path_buf, node->path); |
| return ERR_ABORT; |
| } |
| else { |
| userlog(LOG_INFO, "intersection at %d: (new %s, existing %s, real %s)", wd, path_buf, node->path, normalized1); |
| return ERR_IGNORE; |
| } |
| } |
| |
| return wd; |
| } |
| |
| node = malloc(sizeof(watch_node) + path_len + 1); |
| CHECK_NULL(node, ERR_ABORT); |
| memcpy(node->path, path_buf, path_len + 1); |
| node->path_len = path_len; |
| node->wd = wd; |
| node->parent = parent; |
| node->kids = NULL; |
| |
| if (parent != NULL) { |
| if (parent->kids == NULL) { |
| parent->kids = array_create(DEFAULT_SUBDIR_COUNT); |
| CHECK_NULL(parent->kids, ERR_ABORT); |
| } |
| CHECK_NULL(array_push(parent->kids, node), ERR_ABORT); |
| } |
| |
| if (table_put(watches, wd, node) == NULL) { |
| userlog(LOG_ERR, "table error: unable to put (%d:%s)", wd, path_buf); |
| return ERR_ABORT; |
| } |
| |
| return wd; |
| } |
| |
| static void watch_limit_reached() { |
| if (!limit_reached) { |
| limit_reached = true; |
| message(MSG_WATCH_LIMIT); |
| } |
| } |
| |
| static void rm_watch(int wd, bool update_parent) { |
| watch_node* node = table_get(watches, wd); |
| if (node == NULL) { |
| return; |
| } |
| |
| userlog(LOG_DEBUG, "unwatching %s: %d (%p)", node->path, node->wd, node); |
| |
| if (inotify_rm_watch(inotify_fd, node->wd) < 0) { |
| userlog(LOG_DEBUG, "inotify_rm_watch(%d:%s): %s", node->wd, node->path, strerror(errno)); |
| } |
| |
| for (int i=0; i<array_size(node->kids); i++) { |
| watch_node* kid = array_get(node->kids, i); |
| if (kid != NULL) { |
| rm_watch(kid->wd, false); |
| } |
| } |
| |
| if (update_parent && node->parent != NULL) { |
| for (int i=0; i<array_size(node->parent->kids); i++) { |
| if (array_get(node->parent->kids, i) == node) { |
| array_put(node->parent->kids, i, NULL); |
| break; |
| } |
| } |
| } |
| |
| array_delete(node->kids); |
| free(node); |
| table_put(watches, wd, NULL); |
| } |
| |
| |
| static int walk_tree(int path_len, watch_node* parent, bool recursive, array* mounts) { |
| for (int j=0; j<array_size(mounts); j++) { |
| char* mount = array_get(mounts, j); |
| if (strncmp(path_buf, mount, strlen(mount)) == 0) { |
| userlog(LOG_DEBUG, "watch path '%s' crossed mount point '%s' - skipping", path_buf, mount); |
| return ERR_IGNORE; |
| } |
| } |
| |
| DIR* dir = NULL; |
| if (recursive) { |
| if ((dir = opendir(path_buf)) == NULL) { |
| if (errno == EACCES || errno == ENOENT || errno == ENOTDIR) { |
| userlog(LOG_DEBUG, "opendir(%s): %d", path_buf, errno); |
| return ERR_IGNORE; |
| } |
| else { |
| userlog(LOG_ERR, "opendir(%s): %s", path_buf, strerror(errno)); |
| return ERR_CONTINUE; |
| } |
| } |
| } |
| |
| int id = add_watch(path_len, parent); |
| |
| if (dir == NULL) { |
| return id; |
| } |
| else if (id < 0) { |
| closedir(dir); |
| return id; |
| } |
| |
| path_buf[path_len] = '/'; |
| |
| struct dirent* entry; |
| while ((entry = readdir(dir)) != NULL) { |
| if (entry->d_type != DT_DIR || |
| strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { |
| continue; |
| } |
| |
| int name_len = strlen(entry->d_name); |
| memcpy(path_buf + path_len + 1, entry->d_name, name_len + 1); |
| |
| int subdir_id = walk_tree(path_len + 1 + name_len, table_get(watches, id), recursive, mounts); |
| if (subdir_id < 0 && subdir_id != ERR_IGNORE) { |
| rm_watch(id, true); |
| id = subdir_id; |
| break; |
| } |
| } |
| |
| closedir(dir); |
| return id; |
| } |
| |
| |
| int watch(const char* root, array* mounts) { |
| bool recursive = true; |
| if (root[0] == '|') { |
| root++; |
| recursive = false; |
| } |
| |
| int path_len = strlen(root); |
| if (root[path_len - 1] == '/') { |
| --path_len; |
| } |
| |
| struct stat st; |
| if (stat(root, &st) != 0) { |
| if (errno == ENOENT) { |
| return ERR_MISSING; |
| } |
| else if (errno == EACCES || errno == ELOOP || errno == ENAMETOOLONG || errno == ENOTDIR) { |
| userlog(LOG_INFO, "stat(%s): %s", root, strerror(errno)); |
| return ERR_CONTINUE; |
| } |
| else { |
| userlog(LOG_ERR, "stat(%s): %s", root, strerror(errno)); |
| return ERR_ABORT; |
| } |
| } |
| |
| if (S_ISREG(st.st_mode)) { |
| recursive = false; |
| } |
| else if (!S_ISDIR(st.st_mode)) { |
| userlog(LOG_WARNING, "unexpected node type: %s, %d", root, st.st_mode); |
| return ERR_IGNORE; |
| } |
| |
| memcpy(path_buf, root, path_len); |
| path_buf[path_len] = '\0'; |
| return walk_tree(path_len, NULL, recursive, mounts); |
| } |
| |
| |
| void unwatch(int id) { |
| rm_watch(id, true); |
| } |
| |
| |
| static bool process_inotify_event(struct inotify_event* event) { |
| watch_node* node = table_get(watches, event->wd); |
| if (node == NULL) { |
| return true; |
| } |
| |
| bool is_dir = (event->mask & IN_ISDIR) == IN_ISDIR; |
| userlog(LOG_DEBUG, "inotify: wd=%d mask=%d dir=%d name=%s", event->wd, event->mask & (~IN_ISDIR), is_dir, node->path); |
| |
| int path_len = node->path_len; |
| memcpy(path_buf, node->path, path_len + 1); |
| if (event->len > 0) { |
| path_buf[path_len] = '/'; |
| int name_len = strlen(event->name); |
| memcpy(path_buf + path_len + 1, event->name, name_len + 1); |
| path_len += name_len + 1; |
| } |
| |
| if (callback != NULL) { |
| (*callback)(path_buf, event->mask); |
| } |
| |
| if (is_dir && event->mask & (IN_CREATE | IN_MOVED_TO)) { |
| int result = walk_tree(path_len, node, true, NULL); |
| if (result < 0 && result != ERR_IGNORE && result != ERR_CONTINUE) { |
| return false; |
| } |
| } |
| |
| if (is_dir && event->mask & (IN_DELETE | IN_MOVED_FROM)) { |
| for (int i=0; i<array_size(node->kids); i++) { |
| watch_node* kid = array_get(node->kids, i); |
| if (kid != NULL && strncmp(path_buf, kid->path, kid->path_len) == 0) { |
| rm_watch(kid->wd, false); |
| array_put(node->kids, i, NULL); |
| break; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| |
| bool process_inotify_input() { |
| ssize_t len = read(inotify_fd, event_buf, EVENT_BUF_LEN); |
| if (len < 0) { |
| userlog(LOG_ERR, "read: %s", strerror(errno)); |
| return false; |
| } |
| |
| int i = 0; |
| while (i < len) { |
| struct inotify_event* event = (struct inotify_event*) &event_buf[i]; |
| i += EVENT_SIZE + event->len; |
| |
| if (event->mask & IN_IGNORED) { |
| continue; |
| } |
| if (event->mask & IN_Q_OVERFLOW) { |
| userlog(LOG_INFO, "event queue overflow"); |
| continue; |
| } |
| |
| if (!process_inotify_event(event)) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| |
| void close_inotify() { |
| if (watches != NULL) { |
| table_delete(watches); |
| } |
| |
| if (inotify_fd >= 0) { |
| close(inotify_fd); |
| } |
| } |