/** * @file * Monitor files for changes * * @authors * Copyright (C) 2018 Gero Treuer * Copyright (C) 2020 R Primus * * @copyright * 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 2 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 . */ /** * @page neo_monitor Monitor files for changes * * Monitor files for changes */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "mutt/lib.h" #include "core/lib.h" #include "monitor.h" #include "index/lib.h" #ifndef HAVE_INOTIFY_INIT1 #include #endif /// Set to true when a monitored file has changed bool MonitorFilesChanged = false; /// Set to true when the current mailbox has changed bool MonitorContextChanged = false; /// Inotify file descriptor static int INotifyFd = -1; /// Linked list of monitored Mailboxes static struct Monitor *Monitor = NULL; /// Number of used entries in the #PollFds array static size_t PollFdsCount = 0; /// Size of #PollFds array static size_t PollFdsLen = 0; /// Array of monitored file descriptors static struct pollfd *PollFds = NULL; /// Monitor file descriptor of the current mailbox static int MonitorContextDescriptor = -1; #define INOTIFY_MASK_DIR (IN_MOVED_TO | IN_ATTRIB | IN_CLOSE_WRITE | IN_ISDIR) #define INOTIFY_MASK_FILE IN_CLOSE_WRITE #define EVENT_BUFLEN MAX(4096, sizeof(struct inotify_event) + NAME_MAX + 1) /** * enum ResolveResult - Results for the Monitor functions */ enum ResolveResult { RESOLVE_RES_FAIL_NOMAILBOX = -3, ///< No Mailbox to work on RESOLVE_RES_FAIL_NOTYPE = -2, ///< Can't identify Mailbox type RESOLVE_RES_FAIL_STAT = -1, ///< Can't stat() the Mailbox file RESOLVE_RES_OK_NOTEXISTING = 0, ///< File exists, no monitor is attached RESOLVE_RES_OK_EXISTING = 1, ///< File exists, monitor is already attached }; /** * struct Monitor - A watch on a file */ struct Monitor { struct Monitor *next; ///< Linked list char *mh_backup_path; dev_t st_dev; ino_t st_ino; enum MailboxType type; int desc; }; /** * struct MonitorInfo - Information about a monitored file */ struct MonitorInfo { enum MailboxType type; bool is_dir; const char *path; dev_t st_dev; ino_t st_ino; struct Monitor *monitor; struct Buffer path_buf; ///< access via path only (maybe not initialized) }; /** * mutt_poll_fd_add - Add a file to the watch list * @param fd File to watch * @param events Events to listen for, e.g. POLLIN */ static void mutt_poll_fd_add(int fd, short events) { int i = 0; for (; (i < PollFdsCount) && (PollFds[i].fd != fd); i++) ; // do nothing if (i == PollFdsCount) { if (PollFdsCount == PollFdsLen) { PollFdsLen += 2; mutt_mem_realloc(&PollFds, PollFdsLen * sizeof(struct pollfd)); } PollFdsCount++; PollFds[i].fd = fd; PollFds[i].events = events; } else { PollFds[i].events |= events; } } /** * mutt_poll_fd_remove - Remove a file from the watch list * @param fd File to remove * @retval 0 Success * @retval -1 Error */ static int mutt_poll_fd_remove(int fd) { int i = 0; for (; (i < PollFdsCount) && (PollFds[i].fd != fd); i++) ; // do nothing if (i == PollFdsCount) return -1; int d = PollFdsCount - i - 1; if (d != 0) memmove(&PollFds[i], &PollFds[i + 1], d * sizeof(struct pollfd)); PollFdsCount--; return 0; } /** * monitor_init - Set up file monitoring * @retval 0 Success * @retval -1 Error */ static int monitor_init(void) { if (INotifyFd != -1) return 0; #ifdef HAVE_INOTIFY_INIT1 INotifyFd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); if (INotifyFd == -1) { mutt_debug(LL_DEBUG2, "inotify_init1 failed, errno=%d %s\n", errno, strerror(errno)); return -1; } #else INotifyFd = inotify_init(); if (INotifyFd == -1) { mutt_debug(LL_DEBUG2, "monitor: inotify_init failed, errno=%d %s\n", errno, strerror(errno)); return -1; } fcntl(INotifyFd, F_SETFL, O_NONBLOCK); fcntl(INotifyFd, F_SETFD, FD_CLOEXEC); #endif mutt_poll_fd_add(0, POLLIN); mutt_poll_fd_add(INotifyFd, POLLIN); return 0; } /** * monitor_check_cleanup - Close down file monitoring */ static void monitor_check_cleanup(void) { if (!Monitor && (INotifyFd != -1)) { mutt_poll_fd_remove(INotifyFd); close(INotifyFd); INotifyFd = -1; MonitorFilesChanged = false; } } /** * monitor_new - Create a new file monitor * @param info Details of file to monitor * @param descriptor Watch descriptor * @retval ptr Newly allocated Monitor */ static struct Monitor *monitor_new(struct MonitorInfo *info, int descriptor) { struct Monitor *monitor = mutt_mem_calloc(1, sizeof(struct Monitor)); monitor->type = info->type; monitor->st_dev = info->st_dev; monitor->st_ino = info->st_ino; monitor->desc = descriptor; monitor->next = Monitor; if (info->type == MUTT_MH) monitor->mh_backup_path = mutt_str_dup(info->path); Monitor = monitor; return monitor; } /** * monitor_info_free - Shutdown a file monitor * @param info Monitor to shut down */ static void monitor_info_free(struct MonitorInfo *info) { buf_dealloc(&info->path_buf); } /** * monitor_delete - Free a file monitor * @param monitor Monitor to free */ static void monitor_delete(struct Monitor *monitor) { if (!monitor) return; struct Monitor **ptr = &Monitor; while (true) { if (!*ptr) return; if (*ptr == monitor) break; ptr = &(*ptr)->next; } FREE(&monitor->mh_backup_path); monitor = monitor->next; FREE(ptr); *ptr = monitor; } /** * monitor_handle_ignore - Listen for when a backup file is closed * @param desc Watch descriptor * @retval >=0 New descriptor * @retval -1 Error */ static int monitor_handle_ignore(int desc) { int new_desc = -1; struct Monitor *iter = Monitor; struct stat st = { 0 }; while (iter && (iter->desc != desc)) iter = iter->next; if (iter) { if ((iter->type == MUTT_MH) && (stat(iter->mh_backup_path, &st) == 0)) { new_desc = inotify_add_watch(INotifyFd, iter->mh_backup_path, INOTIFY_MASK_FILE); if (new_desc == -1) { mutt_debug(LL_DEBUG2, "inotify_add_watch failed for '%s', errno=%d %s\n", iter->mh_backup_path, errno, strerror(errno)); } else { mutt_debug(LL_DEBUG3, "inotify_add_watch descriptor=%d for '%s'\n", desc, iter->mh_backup_path); iter->st_dev = st.st_dev; iter->st_ino = st.st_ino; iter->desc = new_desc; } } else { mutt_debug(LL_DEBUG3, "cleanup watch (implicitly removed) - descriptor=%d\n", desc); } if (MonitorContextDescriptor == desc) MonitorContextDescriptor = new_desc; if (new_desc == -1) { monitor_delete(iter); monitor_check_cleanup(); } } return new_desc; } /** * monitor_resolve - Get the monitor for a mailbox * @param[out] info Details of the mailbox's monitor * @param[in] m Mailbox * @retval >=0 mailbox is valid and locally accessible: * 0: no monitor / 1: preexisting monitor * @retval -3 no mailbox (MonitorInfo: no fields set) * @retval -2 type not set * @retval -1 stat() failed (see errno; MonitorInfo fields: type, is_dir, path) * * If m is NULL, try to get the current mailbox from the Index. */ static enum ResolveResult monitor_resolve(struct MonitorInfo *info, struct Mailbox *m) { char *fmt = NULL; struct stat st = { 0 }; struct Mailbox *m_cur = get_current_mailbox(); if (m) { info->type = m->type; info->path = m->realpath; } else if (m_cur) { info->type = m_cur->type; info->path = m_cur->realpath; } else { return RESOLVE_RES_FAIL_NOMAILBOX; } if (info->type == MUTT_UNKNOWN) { return RESOLVE_RES_FAIL_NOTYPE; } else if (info->type == MUTT_MAILDIR) { info->is_dir = true; fmt = "%s/new"; } else { info->is_dir = false; if (info->type == MUTT_MH) fmt = "%s/.mh_sequences"; } if (fmt) { buf_printf(&info->path_buf, fmt, info->path); info->path = buf_string(&info->path_buf); } if (stat(info->path, &st) != 0) return RESOLVE_RES_FAIL_STAT; struct Monitor *iter = Monitor; while (iter && ((iter->st_ino != st.st_ino) || (iter->st_dev != st.st_dev))) iter = iter->next; info->st_dev = st.st_dev; info->st_ino = st.st_ino; info->monitor = iter; return iter ? RESOLVE_RES_OK_EXISTING : RESOLVE_RES_OK_NOTEXISTING; } /** * mutt_monitor_poll - Check for filesystem changes * @retval -3 unknown/unexpected events: poll timeout / fds not handled by us * @retval -2 monitor detected changes, no STDIN input * @retval -1 error (see errno) * @retval 0 (1) input ready from STDIN, or (2) monitoring inactive -> no poll() * * Wait for I/O ready file descriptors or signals. * * MonitorFilesChanged also reflects changes to monitored files. * * Only STDIN and INotify file handles currently expected/supported. * More would ask for common infrastructure (sockets?). */ int mutt_monitor_poll(void) { int rc = 0; char buf[EVENT_BUFLEN] __attribute__((aligned(__alignof__(struct inotify_event)))) = { 0 }; MonitorFilesChanged = false; if (INotifyFd != -1) { int fds = poll(PollFds, PollFdsCount, 1000); // 1 Second if (fds == -1) { rc = -1; if (errno != EINTR) { mutt_debug(LL_DEBUG2, "poll() failed, errno=%d %s\n", errno, strerror(errno)); } } else { bool input_ready = false; for (int i = 0; fds && (i < PollFdsCount); i++) { if (PollFds[i].revents) { fds--; if (PollFds[i].fd == 0) { input_ready = true; } else if (PollFds[i].fd == INotifyFd) { MonitorFilesChanged = true; mutt_debug(LL_DEBUG3, "file change(s) detected\n"); char *ptr = buf; const struct inotify_event *event = NULL; while (true) { int len = read(INotifyFd, buf, sizeof(buf)); if (len == -1) { if (errno != EAGAIN) { mutt_debug(LL_DEBUG2, "read inotify events failed, errno=%d %s\n", errno, strerror(errno)); } break; } while (ptr < (buf + len)) { event = (const struct inotify_event *) ptr; mutt_debug(LL_DEBUG3, "+ detail: descriptor=%d mask=0x%x\n", event->wd, event->mask); if (event->mask & IN_IGNORED) monitor_handle_ignore(event->wd); else if (event->wd == MonitorContextDescriptor) MonitorContextChanged = true; ptr += sizeof(struct inotify_event) + event->len; } } } } } if (!input_ready) rc = MonitorFilesChanged ? -2 : -3; } } return rc; } /** * mutt_monitor_add - Add a watch for a mailbox * @param m Mailbox to watch * @retval 0 success: new or already existing monitor * @retval -1 failed: no mailbox, inaccessible file, create monitor/watcher failed * * If m is NULL, try to get the current mailbox from the Index. */ int mutt_monitor_add(struct Mailbox *m) { struct MonitorInfo info = { 0 }; int rc = 0; enum ResolveResult desc = monitor_resolve(&info, m); if (desc != RESOLVE_RES_OK_NOTEXISTING) { if (!m && (desc == RESOLVE_RES_OK_EXISTING)) MonitorContextDescriptor = info.monitor->desc; rc = (desc == RESOLVE_RES_OK_EXISTING) ? 0 : -1; goto cleanup; } uint32_t mask = info.is_dir ? INOTIFY_MASK_DIR : INOTIFY_MASK_FILE; if (((INotifyFd == -1) && (monitor_init() == -1)) || ((desc = inotify_add_watch(INotifyFd, info.path, mask)) == -1)) { mutt_debug(LL_DEBUG2, "inotify_add_watch failed for '%s', errno=%d %s\n", info.path, errno, strerror(errno)); rc = -1; goto cleanup; } mutt_debug(LL_DEBUG3, "inotify_add_watch descriptor=%d for '%s'\n", desc, info.path); if (!m) MonitorContextDescriptor = desc; monitor_new(&info, desc); cleanup: monitor_info_free(&info); return rc; } /** * mutt_monitor_remove - Remove a watch for a mailbox * @param m Mailbox * @retval 0 monitor removed (not shared) * @retval 1 monitor not removed (shared) * @retval 2 no monitor * * If m is NULL, try to get the current mailbox from the Index. */ int mutt_monitor_remove(struct Mailbox *m) { struct MonitorInfo info = { 0 }; struct MonitorInfo info2 = { 0 }; int rc = 0; if (!m) { MonitorContextDescriptor = -1; MonitorContextChanged = false; } if (monitor_resolve(&info, m) != RESOLVE_RES_OK_EXISTING) { rc = 2; goto cleanup; } struct Mailbox *m_cur = get_current_mailbox(); if (m_cur) { if (m) { if ((monitor_resolve(&info2, NULL) == RESOLVE_RES_OK_EXISTING) && (info.st_ino == info2.st_ino) && (info.st_dev == info2.st_dev)) { rc = 1; goto cleanup; } } else { if (mailbox_find(m_cur->realpath)) { rc = 1; goto cleanup; } } } inotify_rm_watch(info.monitor->desc, INotifyFd); mutt_debug(LL_DEBUG3, "inotify_rm_watch for '%s' descriptor=%d\n", info.path, info.monitor->desc); monitor_delete(info.monitor); monitor_check_cleanup(); cleanup: monitor_info_free(&info); monitor_info_free(&info2); return rc; }