From: gt_16_gitlab.com@innocircle.com Date: Sun, 3 Jun 2018 20:23:57 +0000 (+0000) Subject: add feature file monitoring with Linux inotify X-Git-Tag: 2019-10-25~668^2~9 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=83c1579c77567d5101b7aaf216ad06331a99ec08;p=neomutt add feature file monitoring with Linux inotify --- diff --git a/Makefile.autosetup b/Makefile.autosetup index 2f3f86442..0eb31b612 100644 --- a/Makefile.autosetup +++ b/Makefile.autosetup @@ -83,6 +83,9 @@ NEOMUTTOBJS+= remailer.o @if USE_LUA NEOMUTTOBJS+= mutt_lua.o @endif +@if USE_INOTIFY +NEOMUTTOBJS+= monitor.o +@endif CLEANFILES+= $(NEOMUTT) $(NEOMUTTOBJS) ALLOBJS+= $(NEOMUTTOBJS) diff --git a/auto.def b/auto.def index a6c7ff2b6..1c5b2bc4c 100644 --- a/auto.def +++ b/auto.def @@ -34,6 +34,7 @@ options { docdir:path => "Documentation root" with-lock:=fcntl => "Select fcntl() or flock() to lock files" fmemopen=0 => "Use fmemopen() for temporary in-memory files" + inotify=1 => "Disable file monitoring support (Linux only)" locales-fix=0 => "Enable locales fix" pgp=1 => "Disable PGP support" smime=1 => "Disable SMIME support" @@ -101,7 +102,7 @@ if {1} { # Keep sorted, please. foreach opt { bdb doc everything fmemopen full-doc gdbm gnutls gpgme gss - homespool idn idn2 kyotocabinet lmdb locales-fix lua mixmaster nls + homespool idn idn2 inotify kyotocabinet lmdb locales-fix lua mixmaster nls notmuch pgp qdbm sasl smime ssl tokyocabinet } { define want-$opt [opt-bool $opt] @@ -368,6 +369,16 @@ if {[get-define want-gpgme]} { define CRYPT_BACKEND_GPGME } +############################################################################### +# INOTIFY +if {[get-define want-inotify]} { + if {[cc-check-functions inotify_init1 inotify_add_watch inotify_rm_watch]} { + if {[cc-check-includes sys/inotify.h]} { + define USE_INOTIFY + } + } +} + ############################################################################### # PGP if {[get-define want-pgp]} { diff --git a/curs_lib.c b/curs_lib.c index 6fe908e64..961928674 100644 --- a/curs_lib.c +++ b/curs_lib.c @@ -61,6 +61,9 @@ #ifdef USE_NOTMUCH #include "notmuch/mutt_notmuch.h" #endif +#ifdef USE_INOTIFY +#include "monitor.h" +#endif /* These Config Variables are only used in curs_lib.c */ bool MetaKey; ///< Config: Interpret 'ALT-x' as 'ESC-x' @@ -149,7 +152,12 @@ struct Event mutt_getch(void) ch = KEY_RESIZE; while (ch == KEY_RESIZE) #endif /* KEY_RESIZE */ - ch = getch(); +#ifdef USE_INOTIFY + if (mutt_monitor_poll() != 0) + ch = ERR; + else +#endif + ch = getch(); mutt_sig_allow_interrupt(0); if (SigInt) diff --git a/curs_main.c b/curs_main.c index 07a6da303..9e691d59a 100644 --- a/curs_main.c +++ b/curs_main.c @@ -85,6 +85,9 @@ #ifdef ENABLE_NLS #include #endif +#ifdef USE_INOTIFY +#include "monitor.h" +#endif /* These Config Variables are only used in curs_main.c */ bool ChangeFolderNext; ///< Config: Suggest the next folder, rather than the first when using '' @@ -565,7 +568,9 @@ static int main_change_folder(struct Menu *menu, int op, char *buf, if (Context) { char *new_last_folder = NULL; - +#ifdef USE_INOTIFY + int monitor_remove_rc = mutt_monitor_remove(NULL); +#endif #ifdef USE_COMPRESSED if (Context->compress_info && Context->realpath) new_last_folder = mutt_str_strdup(Context->realpath); @@ -577,6 +582,10 @@ static int main_change_folder(struct Menu *menu, int op, char *buf, int check = mx_mbox_close(&Context, index_hint); if (check != 0) { +#ifdef USE_INOTIFY + if (monitor_remove_rc == 0) + mutt_monitor_add(NULL); +#endif if (check == MUTT_NEW_MAIL || check == MUTT_REOPENED) update_index(menu, Context, check, *oldcount, *index_hint); @@ -608,6 +617,9 @@ static int main_change_folder(struct Menu *menu, int op, char *buf, if (Context) { menu->current = ci_first_message(); +#ifdef USE_INOTIFY + mutt_monitor_add(NULL); +#endif } else menu->current = 0; @@ -985,6 +997,9 @@ int mutt_index_menu(void) /* force the mailbox check after we enter the folder */ mutt_mailbox_check(MUTT_MAILBOX_CHECK_FORCE); } +#ifdef USE_INOTIFY + mutt_monitor_add(NULL); +#endif if (((Sort & SORT_MASK) == SORT_THREADS) && CollapseAll) { diff --git a/doc/manual.xml.head b/doc/manual.xml.head index 98467836d..7cf69d224 100644 --- a/doc/manual.xml.head +++ b/doc/manual.xml.head @@ -8830,6 +8830,35 @@ subjectrx '\[[^]]*\]:? *' '%L%R' + + Monitoring New Mail + + When the Inotifymechanism for monitoring of + files is supported (Linux only) and not disabled at compilation time, + Mutt immediately notifies about new mail for all folders configured + via the mailboxes + command. Dependent on mailbox format + also added oldmails are tracked (not for Maildir). + + + No configuration variables are available. Trace output is given when + debugging is enabled via command line option + -d3. The lower level 2 only shows errors, the + higher level 5 all including raw Inotify events. + + + + Getting events about new mail is limited to the capabilities of the + underlying mechanism. inotifyonly reports + local changes, i. e. new mail notification works for mails + delivered by an agent on the same machine as mutt, but not when + delivered remotely on a network file system as nfs. also the + monitoring handles might fail in rare conditions, so you better + don't completely rely on this feature. + + + + Calculating Mailbox Message Counts diff --git a/keymap.c b/keymap.c index ccd4fb933..c9eebe28a 100644 --- a/keymap.c +++ b/keymap.c @@ -43,6 +43,9 @@ #ifdef USE_IMAP #include "imap/imap.h" #endif +#ifdef USE_INOTIFY +#include "monitor.h" +#endif /** * Menus - Menu name lookup table @@ -590,7 +593,11 @@ int km_dokey(int menu) * loop now. Otherwise, continue to loop until reaching a total of * $timeout seconds. */ +#ifdef USE_INOTIFY + if (tmp.ch != -2 || SigWinch || MonitorFilesChanged) +#else if (tmp.ch != -2 || SigWinch) +#endif goto gotkey; i -= ImapKeepalive; imap_keepalive(); diff --git a/mailbox.c b/mailbox.c index 9408f5152..16a9e6f1c 100644 --- a/mailbox.c +++ b/mailbox.c @@ -58,6 +58,9 @@ #ifdef USE_POP #include "pop/pop.h" #endif +#ifdef USE_INOTIFY +#include "monitor.h" +#endif /* These Config Variables are only used in mailbox.c */ short MailCheck; ///< Config: Number of seconds before NeoMutt checks for new mail @@ -697,6 +700,10 @@ int mutt_parse_mailboxes(struct Buffer *path, struct Buffer *s, #ifdef USE_SIDEBAR mutt_sb_notify_mailbox(b, true); +#endif +#ifdef USE_INOTIFY + b->magic = mx_path_probe(b->path, NULL); + mutt_monitor_add(b); #endif } return 0; @@ -756,6 +763,9 @@ int mutt_parse_unmailboxes(struct Buffer *path, struct Buffer *s, { #ifdef USE_SIDEBAR mutt_sb_notify_mailbox(np->b, false); +#endif +#ifdef USE_INOTIFY + mutt_monitor_remove(np->b); #endif STAILQ_REMOVE(&AllMailboxes, np, MailboxNode, entries); mailbox_free(&np->b); diff --git a/monitor.c b/monitor.c new file mode 100644 index 000000000..22fd9d630 --- /dev/null +++ b/monitor.c @@ -0,0 +1,438 @@ +/** + * @file + * Monitor files for changes + * + * @authors + * Copyright (C) 2018 Gero Treuer + * + * @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 . + */ + +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "mutt/mutt.h" +#include "monitor.h" +#include "context.h" +#include "globals.h" +#include "mailbox.h" +#include "mutt_curses.h" +#include "mx.h" + +int MonitorFilesChanged; + +struct Monitor +{ + struct Monitor *next; + char *mh_backup_path; + dev_t st_dev; + ino_t st_ino; + short magic; + int desc; +}; + +static int INotifyFd = -1; +static struct Monitor *Monitor = NULL; +static size_t PollFdsCount = 0; +static size_t PollFdsLen = 0; +static struct pollfd *PollFds; + +struct MonitorInfo +{ + short magic; + short isdir; + const char *path; + dev_t st_dev; + ino_t st_ino; + struct Monitor *monitor; + char path_buf[PATH_MAX]; /* access via path only (maybe not initialized) */ +}; + +#define INOTIFY_MASK_DIR (IN_MOVED_TO | IN_ATTRIB | IN_CLOSE_WRITE | IN_ISDIR) +#define INOTIFY_MASK_FILE IN_CLOSE_WRITE + +static void mutt_poll_fd_add(int fd, short events) +{ + int i = 0; + for (; i < PollFdsCount && PollFds[i].fd != fd; ++i) + ; + + 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; +} + +static int mutt_poll_fd_remove(int fd) +{ + int i = 0, d; + for (i = 0; i < PollFdsCount && PollFds[i].fd != fd; ++i) + ; + if (i == PollFdsCount) + return -1; + d = PollFdsCount - i - 1; + if (d) + memmove(&PollFds[i], &PollFds[i + 1], d * sizeof(struct pollfd)); + PollFdsCount--; + return 0; +} + +static int monitor_init(void) +{ + if (INotifyFd == -1) + { + INotifyFd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (INotifyFd == -1) + { + mutt_debug(2, "inotify_init1 failed, errno=%d %s\n", errno, strerror(errno)); + return -1; + } + mutt_poll_fd_add(0, POLLIN); + mutt_poll_fd_add(INotifyFd, POLLIN); + } + return 0; +} + +static void monitor_check_free(void) +{ + if (!Monitor && INotifyFd != -1) + { + mutt_poll_fd_remove(INotifyFd); + close(INotifyFd); + INotifyFd = -1; + MonitorFilesChanged = 0; + } +} + +static struct Monitor *monitor_create(struct MonitorInfo *info, int descriptor) +{ + struct Monitor *monitor = mutt_mem_calloc(1, sizeof(struct Monitor)); + monitor->magic = info->magic; + monitor->st_dev = info->st_dev; + monitor->st_ino = info->st_ino; + monitor->desc = descriptor; + monitor->next = Monitor; + if (info->magic == MUTT_MH) + monitor->mh_backup_path = mutt_str_strdup(info->path); + + Monitor = monitor; + + return monitor; +} + +static void monitor_delete(struct Monitor *monitor) +{ + struct Monitor **ptr = &Monitor; + + if (!monitor) + return; + + while (true) + { + if (!*ptr) + return; + if (*ptr == monitor) + break; + ptr = &(*ptr)->next; + } + + FREE(&monitor->mh_backup_path); /* __FREE_CHECKED__ */ + monitor = monitor->next; + FREE(ptr); /* __FREE_CHECKED__ */ + *ptr = monitor; +} + +static int monitor_handle_ignore(int desc) +{ + int new_descr = -1; + struct Monitor *iter = Monitor; + struct stat sb; + + while (iter && iter->desc != desc) + iter = iter->next; + + if (iter) + { + if (iter->magic == MUTT_MH && stat(iter->mh_backup_path, &sb) == 0) + { + if ((new_descr = inotify_add_watch(INotifyFd, iter->mh_backup_path, + INOTIFY_MASK_FILE)) == -1) + { + mutt_debug(2, "inotify_add_watch failed for '%s', errno=%d %s\n", + iter->mh_backup_path, errno, strerror(errno)); + } + else + { + mutt_debug(3, "inotify_add_watch descriptor=%d for '%s'\n", desc, iter->mh_backup_path); + iter->st_dev = sb.st_dev; + iter->st_ino = sb.st_ino; + iter->desc = new_descr; + } + } + else + { + mutt_debug(3, "cleanup watch (implicitely removed) - descriptor=%d\n", desc); + } + + if (new_descr == -1) + { + monitor_delete(iter); + monitor_check_free(); + } + } + + return new_descr; +} + +#define EVENT_BUFLEN MAX(4096, sizeof(struct inotify_event) + NAME_MAX + 1) + +/* mutt_monitor_poll: Waits for I/O ready file descriptors or signals. + * + * return values: + * -3 unknown/unexpected events: poll timeout / fds not handled by us + * -2 monitor detected changes, no STDIN input + * -1 error (see errno) + * 0 (1) input ready from STDIN, or (2) monitoring inactive -> no poll() + * MonitorFilesChanged also reflects changes to monitored files. + * + * Only STDIN and INotify file handles currently expected/supported. + * More would ask for common infrastructur (sockets?). + */ +int mutt_monitor_poll(void) +{ + int rc = 0, fds, i, inputReady; + char buf[EVENT_BUFLEN] __attribute__((aligned(__alignof__(struct inotify_event)))); + + MonitorFilesChanged = 0; + + if (INotifyFd != -1) + { + fds = poll(PollFds, PollFdsLen, -1); + + if (fds == -1) + { + rc = -1; + if (errno != EINTR) + { + mutt_debug(2, "poll() failed, errno=%d %s\n", errno, strerror(errno)); + } + } + else + { + inputReady = 0; + for (i = 0; fds && i < PollFdsCount; ++i) + { + if (PollFds[i].revents) + { + fds--; + if (PollFds[i].fd == 0) + { + inputReady = 1; + } + else if (PollFds[i].fd == INotifyFd) + { + MonitorFilesChanged = 1; + mutt_debug(3, "file change(s) detected\n"); + int len; + char *ptr = buf; + const struct inotify_event *event; + + while (true) + { + len = read(INotifyFd, buf, sizeof(buf)); + if (len == -1) + { + if (errno != EAGAIN) + mutt_debug(2, "read inotify events failed, errno=%d %s\n", + errno, strerror(errno)); + break; + } + + while (ptr < buf + len) + { + event = (const struct inotify_event *) ptr; + mutt_debug(5, "+ detail: descriptor=%d mask=0x%x\n", event->wd, + event->mask); + if (event->mask & IN_IGNORED) + monitor_handle_ignore(event->wd); + ptr += sizeof(struct inotify_event) + event->len; + } + } + } + } + } + if (!inputReady) + rc = MonitorFilesChanged ? -2 : -3; + } + } + + return rc; +} + +#define RESOLVERES_OK_NOTEXISTING 0 +#define RESOLVERES_OK_EXISTING 1 +#define RESOLVERES_FAIL_NOMAILBOX -3 +#define RESOLVERES_FAIL_NOMAGIC -2 +#define RESOLVERES_FAIL_STAT -1 + +/* monitor_resolve: resolve monitor entry match by Mailbox, or - if NULL - by Context. + * + * return values: + * >=0 mailbox is valid and locally accessible: + * 0: no monitor / 1: preexisting monitor + * -3 no mailbox (MONITORINFO: no fields set) + * -2 magic not set + * -1 stat() failed (see errno; MONITORINFO fields: magic, isdir, path) + */ +static int monitor_resolve(struct MonitorInfo *info, struct Mailbox *mailbox) +{ + struct Monitor *iter; + char *fmt = NULL; + struct stat sb; + + if (mailbox) + { + info->magic = mailbox->magic; + info->path = mailbox->realpath; + } + else if (Context) + { + info->magic = Context->magic; + info->path = Context->realpath; + } + else + { + return RESOLVERES_FAIL_NOMAILBOX; + } + + if (!info->magic) + { + return RESOLVERES_FAIL_NOMAGIC; + } + else if (info->magic == MUTT_MAILDIR) + { + info->isdir = 1; + fmt = "%s/new"; + } + else + { + info->isdir = 0; + if (info->magic == MUTT_MH) + fmt = "%s/.mh_sequences"; + } + + if (fmt) + { + snprintf(info->path_buf, sizeof(info->path_buf), fmt, info->path); + info->path = info->path_buf; + } + if (stat(info->path, &sb) != 0) + return RESOLVERES_FAIL_STAT; + + iter = Monitor; + while (iter && (iter->st_ino != sb.st_ino || iter->st_dev != sb.st_dev)) + iter = iter->next; + + info->st_dev = sb.st_dev; + info->st_ino = sb.st_ino; + info->monitor = iter; + + return iter ? RESOLVERES_OK_EXISTING : RESOLVERES_OK_NOTEXISTING; +} + +/* mutt_monitor_add: add file monitor from Mailbox, or - if NULL - from Context. + * + * return values: + * 0 success: new or already existing monitor + * -1 failed: no mailbox, inaccessible file, create monitor/watcher failed + */ +int mutt_monitor_add(struct Mailbox *mailbox) +{ + struct MonitorInfo info; + uint32_t mask; + int desc; + + desc = monitor_resolve(&info, mailbox); + if (desc != RESOLVERES_OK_NOTEXISTING) + return desc == RESOLVERES_OK_EXISTING ? 0 : -1; + + mask = info.isdir ? INOTIFY_MASK_DIR : INOTIFY_MASK_FILE; + if ((INotifyFd == -1 && monitor_init() == -1) || + (desc = inotify_add_watch(INotifyFd, info.path, mask)) == -1) + { + mutt_debug(2, "inotify_add_watch failed for '%s', errno=%d %s\n", info.path, + errno, strerror(errno)); + return -1; + } + + mutt_debug(3, "inotify_add_watch descriptor=%d for '%s'\n", desc, info.path); + monitor_create(&info, desc); + return 0; +} + +/* mutt_monitor_remove: remove file monitor from Mailbox, or - if NULL - from Context. + * + * return values: + * 0 monitor removed (not shared) + * 1 monitor not removed (shared) + * 2 no monitor + */ +int mutt_monitor_remove(struct Mailbox *mailbox) +{ + struct MonitorInfo info, info2; + + if (monitor_resolve(&info, mailbox) != RESOLVERES_OK_EXISTING) + return 2; + + if (Context) + { + if (mailbox) + { + if (monitor_resolve(&info2, NULL) == RESOLVERES_OK_EXISTING && + info.st_ino == info2.st_ino && info.st_dev == info2.st_dev) + return 1; + } + else + { + if (mutt_find_mailbox(Context->realpath)) + return 1; + } + } + + inotify_rm_watch(info.monitor->desc, INotifyFd); + mutt_debug(3, "inotify_rm_watch for '%s' descriptor=%d\n", info.path, + info.monitor->desc); + + monitor_delete(info.monitor); + monitor_check_free(); + return 0; +} diff --git a/monitor.h b/monitor.h new file mode 100644 index 000000000..1159abe07 --- /dev/null +++ b/monitor.h @@ -0,0 +1,34 @@ +/** + * @file + * Monitor files for changes + * + * @authors + * Copyright (C) 2018 Gero Treuer + * + * @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 . + */ + +#ifndef _MUTT_MONITOR_H +#define _MUTT_MONITOR_H + +extern int MonitorFilesChanged; + +struct Mailbox; + +int mutt_monitor_add(struct Mailbox *b); +int mutt_monitor_remove(struct Mailbox *b); +int mutt_monitor_poll(void); + +#endif /* _MUTT_MONITOR_H */ diff --git a/version.c b/version.c index 0906b372e..4ac06e2f4 100644 --- a/version.c +++ b/version.c @@ -38,6 +38,9 @@ #include "email/email.h" #include "mutt_curses.h" #include "ncrypt/crypt_gpgme.h" +#ifdef USE_INOTIFY +#include "monitor.h" +#endif /* #include "protos.h" */ const char *mutt_make_version(void); @@ -212,6 +215,11 @@ static struct CompileOptions comp_opts[] = { #else { "idn", 0 }, #endif +#ifdef USE_INOTIFY + { "inotify", 1 }, +#else + { "inotify", 0 }, +#endif #ifdef LOCALES_HACK { "locales_hack", 1 }, #else