1 /**
2  * @file
3  * Monitor files for changes
4  *
5  * @authors
6  * Copyright (C) 2018 Gero Treuer <gero@70t.de>
7  * Copyright (C) 2020 R Primus <rprimus@gmail.com>
8  *
9  * @copyright
10  * This program is free software: you can redistribute it and/or modify it under
11  * the terms of the GNU General Public License as published by the Free Software
12  * Foundation, either version 2 of the License, or (at your option) any later
13  * version.
14  *
15  * This program is distributed in the hope that it will be useful, but WITHOUT
16  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
18  * details.
19  *
20  * You should have received a copy of the GNU General Public License along with
21  * this program.  If not, see <http://www.gnu.org/licenses/>.
22  */
23 
24 /**
25  * @page neo_monitor Monitor files for changes
26  *
27  * Monitor files for changes
28  */
29 
30 #include "config.h"
31 #include <errno.h>
32 #include <limits.h>
33 #include <poll.h>
34 #include <stdbool.h>
35 #include <stdint.h>
36 #include <stdio.h>
37 #include <string.h>
38 #include <sys/inotify.h>
39 #include <sys/stat.h>
40 #include <unistd.h>
41 #include "mutt/lib.h"
42 #include "core/lib.h"
43 #include "gui/lib.h"
44 #include "monitor.h"
45 #include "context.h"
46 #include "mutt_globals.h"
47 #ifndef HAVE_INOTIFY_INIT1
48 #include <fcntl.h>
49 #endif
50 
51 bool MonitorFilesChanged = false;
52 bool MonitorContextChanged = false;
53 
54 static int INotifyFd = -1;
55 static struct Monitor *Monitor = NULL;
56 static size_t PollFdsCount = 0;
57 static size_t PollFdsLen = 0;
58 static struct pollfd *PollFds = NULL;
59 
60 static int MonitorContextDescriptor = -1;
61 
62 #define INOTIFY_MASK_DIR (IN_MOVED_TO | IN_ATTRIB | IN_CLOSE_WRITE | IN_ISDIR)
63 #define INOTIFY_MASK_FILE IN_CLOSE_WRITE
64 
65 #define EVENT_BUFLEN MAX(4096, sizeof(struct inotify_event) + NAME_MAX + 1)
66 
67 /**
68  * enum ResolveResult - Results for the Monitor functions
69  */
70 enum ResolveResult
71 {
72   RESOLVE_RES_FAIL_NOMAILBOX = -3, ///< No Mailbox to work on
73   RESOLVE_RES_FAIL_NOTYPE = -2,    ///< Can't identify Mailbox type
74   RESOLVE_RES_FAIL_STAT = -1,      ///< Can't stat() the Mailbox file
75   RESOLVE_RES_OK_NOTEXISTING = 0,  ///< File exists, no monitor is attached
76   RESOLVE_RES_OK_EXISTING = 1,     ///< File exists, monitor is already attached
77 };
78 
79 /**
80  * struct Monitor - A watch on a file
81  */
82 struct Monitor
83 {
84   struct Monitor *next; ///< Linked list
85   char *mh_backup_path;
86   dev_t st_dev;
87   ino_t st_ino;
88   enum MailboxType type;
89   int desc;
90 };
91 
92 /**
93  * struct MonitorInfo - Information about a monitored file
94  */
95 struct MonitorInfo
96 {
97   enum MailboxType type;
98   bool is_dir;
99   const char *path;
100   dev_t st_dev;
101   ino_t st_ino;
102   struct Monitor *monitor;
103   struct Buffer path_buf; ///< access via path only (maybe not initialized)
104 };
105 
106 /**
107  * mutt_poll_fd_add - Add a file to the watch list
108  * @param fd     File to watch
109  * @param events Events to listen for, e.g. POLLIN
110  */
mutt_poll_fd_add(int fd,short events)111 static void mutt_poll_fd_add(int fd, short events)
112 {
113   int i = 0;
114   for (; (i < PollFdsCount) && (PollFds[i].fd != fd); i++)
115     ; // do nothing
116 
117   if (i == PollFdsCount)
118   {
119     if (PollFdsCount == PollFdsLen)
120     {
121       PollFdsLen += 2;
122       mutt_mem_realloc(&PollFds, PollFdsLen * sizeof(struct pollfd));
123     }
124     PollFdsCount++;
125     PollFds[i].fd = fd;
126     PollFds[i].events = events;
127   }
128   else
129     PollFds[i].events |= events;
130 }
131 
132 /**
133  * mutt_poll_fd_remove - Remove a file from the watch list
134  * @param fd File to remove
135  * @retval  0 Success
136  * @retval -1 Error
137  */
mutt_poll_fd_remove(int fd)138 static int mutt_poll_fd_remove(int fd)
139 {
140   int i = 0;
141   for (; (i < PollFdsCount) && (PollFds[i].fd != fd); i++)
142     ; // do nothing
143 
144   if (i == PollFdsCount)
145     return -1;
146   int d = PollFdsCount - i - 1;
147   if (d != 0)
148     memmove(&PollFds[i], &PollFds[i + 1], d * sizeof(struct pollfd));
149   PollFdsCount--;
150   return 0;
151 }
152 
153 /**
154  * monitor_init - Set up file monitoring
155  * @retval  0 Success
156  * @retval -1 Error
157  */
monitor_init(void)158 static int monitor_init(void)
159 {
160   if (INotifyFd != -1)
161     return 0;
162 
163 #ifdef HAVE_INOTIFY_INIT1
164   INotifyFd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
165   if (INotifyFd == -1)
166   {
167     mutt_debug(LL_DEBUG2, "inotify_init1 failed, errno=%d %s\n", errno, strerror(errno));
168     return -1;
169   }
170 #else
171   INotifyFd = inotify_init();
172   if (INotifyFd == -1)
173   {
174     mutt_debug(LL_DEBUG2, "monitor: inotify_init failed, errno=%d %s\n", errno,
175                strerror(errno));
176     return -1;
177   }
178   fcntl(INotifyFd, F_SETFL, O_NONBLOCK);
179   fcntl(INotifyFd, F_SETFD, FD_CLOEXEC);
180 #endif
181   mutt_poll_fd_add(0, POLLIN);
182   mutt_poll_fd_add(INotifyFd, POLLIN);
183 
184   return 0;
185 }
186 
187 /**
188  * monitor_check_free - Close down file monitoring
189  */
monitor_check_free(void)190 static void monitor_check_free(void)
191 {
192   if (!Monitor && (INotifyFd != -1))
193   {
194     mutt_poll_fd_remove(INotifyFd);
195     close(INotifyFd);
196     INotifyFd = -1;
197     MonitorFilesChanged = false;
198   }
199 }
200 
201 /**
202  * monitor_new - Create a new file monitor
203  * @param info       Details of file to monitor
204  * @param descriptor Watch descriptor
205  * @retval ptr Newly allocated Monitor
206  */
monitor_new(struct MonitorInfo * info,int descriptor)207 static struct Monitor *monitor_new(struct MonitorInfo *info, int descriptor)
208 {
209   struct Monitor *monitor = mutt_mem_calloc(1, sizeof(struct Monitor));
210   monitor->type = info->type;
211   monitor->st_dev = info->st_dev;
212   monitor->st_ino = info->st_ino;
213   monitor->desc = descriptor;
214   monitor->next = Monitor;
215   if (info->type == MUTT_MH)
216     monitor->mh_backup_path = mutt_str_dup(info->path);
217 
218   Monitor = monitor;
219 
220   return monitor;
221 }
222 
223 /**
224  * monitor_info_init - Set up a file monitor
225  * @param info Monitor to initialise
226  */
monitor_info_init(struct MonitorInfo * info)227 static void monitor_info_init(struct MonitorInfo *info)
228 {
229   memset(info, 0, sizeof(*info));
230 }
231 
232 /**
233  * monitor_info_free - Shutdown a file monitor
234  * @param info Monitor to shut down
235  */
monitor_info_free(struct MonitorInfo * info)236 static void monitor_info_free(struct MonitorInfo *info)
237 {
238   mutt_buffer_dealloc(&info->path_buf);
239 }
240 
241 /**
242  * monitor_delete - Free a file monitor
243  * @param monitor Monitor to free
244  */
monitor_delete(struct Monitor * monitor)245 static void monitor_delete(struct Monitor *monitor)
246 {
247   if (!monitor)
248     return;
249 
250   struct Monitor **ptr = &Monitor;
251 
252   while (true)
253   {
254     if (!*ptr)
255       return;
256     if (*ptr == monitor)
257       break;
258     ptr = &(*ptr)->next;
259   }
260 
261   FREE(&monitor->mh_backup_path);
262   monitor = monitor->next;
263   FREE(ptr);
264   *ptr = monitor;
265 }
266 
267 /**
268  * monitor_handle_ignore - Listen for when a backup file is closed
269  * @param desc Watch descriptor
270  * @retval >=0 New descriptor
271  * @retval  -1 Error
272  */
monitor_handle_ignore(int desc)273 static int monitor_handle_ignore(int desc)
274 {
275   int new_desc = -1;
276   struct Monitor *iter = Monitor;
277   struct stat st = { 0 };
278 
279   while (iter && (iter->desc != desc))
280     iter = iter->next;
281 
282   if (iter)
283   {
284     if ((iter->type == MUTT_MH) && (stat(iter->mh_backup_path, &st) == 0))
285     {
286       new_desc = inotify_add_watch(INotifyFd, iter->mh_backup_path, INOTIFY_MASK_FILE);
287       if (new_desc == -1)
288       {
289         mutt_debug(LL_DEBUG2, "inotify_add_watch failed for '%s', errno=%d %s\n",
290                    iter->mh_backup_path, errno, strerror(errno));
291       }
292       else
293       {
294         mutt_debug(LL_DEBUG3, "inotify_add_watch descriptor=%d for '%s'\n",
295                    desc, iter->mh_backup_path);
296         iter->st_dev = st.st_dev;
297         iter->st_ino = st.st_ino;
298         iter->desc = new_desc;
299       }
300     }
301     else
302     {
303       mutt_debug(LL_DEBUG3, "cleanup watch (implicitly removed) - descriptor=%d\n", desc);
304     }
305 
306     if (MonitorContextDescriptor == desc)
307       MonitorContextDescriptor = new_desc;
308 
309     if (new_desc == -1)
310     {
311       monitor_delete(iter);
312       monitor_check_free();
313     }
314   }
315 
316   return new_desc;
317 }
318 
319 /**
320  * monitor_resolve - Get the monitor for a mailbox
321  * @param[out] info Details of the mailbox's monitor
322  * @param[in]  m    Mailbox
323  * @retval >=0 mailbox is valid and locally accessible:
324  *               0: no monitor / 1: preexisting monitor
325  * @retval  -3 no mailbox (MonitorInfo: no fields set)
326  * @retval  -2 type not set
327  * @retval  -1 stat() failed (see errno; MonitorInfo fields: type, is_dir, path)
328  *
329  * If m is NULL, the current mailbox (Context) is used.
330  */
monitor_resolve(struct MonitorInfo * info,struct Mailbox * m)331 static enum ResolveResult monitor_resolve(struct MonitorInfo *info, struct Mailbox *m)
332 {
333   char *fmt = NULL;
334   struct stat st = { 0 };
335 
336   if (m)
337   {
338     info->type = m->type;
339     info->path = m->realpath;
340   }
341   else if (ctx_mailbox(Context))
342   {
343     info->type = Context->mailbox->type;
344     info->path = Context->mailbox->realpath;
345   }
346   else
347   {
348     return RESOLVE_RES_FAIL_NOMAILBOX;
349   }
350 
351   if (info->type == MUTT_UNKNOWN)
352   {
353     return RESOLVE_RES_FAIL_NOTYPE;
354   }
355   else if (info->type == MUTT_MAILDIR)
356   {
357     info->is_dir = true;
358     fmt = "%s/new";
359   }
360   else
361   {
362     info->is_dir = false;
363     if (info->type == MUTT_MH)
364       fmt = "%s/.mh_sequences";
365   }
366   if (fmt)
367   {
368     mutt_buffer_printf(&info->path_buf, fmt, info->path);
369     info->path = mutt_buffer_string(&info->path_buf);
370   }
371   if (stat(info->path, &st) != 0)
372     return RESOLVE_RES_FAIL_STAT;
373 
374   struct Monitor *iter = Monitor;
375   while (iter && ((iter->st_ino != st.st_ino) || (iter->st_dev != st.st_dev)))
376     iter = iter->next;
377 
378   info->st_dev = st.st_dev;
379   info->st_ino = st.st_ino;
380   info->monitor = iter;
381 
382   return iter ? RESOLVE_RES_OK_EXISTING : RESOLVE_RES_OK_NOTEXISTING;
383 }
384 
385 /**
386  * mutt_monitor_poll - Check for filesystem changes
387  * @retval -3 unknown/unexpected events: poll timeout / fds not handled by us
388  * @retval -2 monitor detected changes, no STDIN input
389  * @retval -1 error (see errno)
390  * @retval  0 (1) input ready from STDIN, or (2) monitoring inactive -> no poll()
391  *
392  * Wait for I/O ready file descriptors or signals.
393  *
394  * MonitorFilesChanged also reflects changes to monitored files.
395  *
396  * Only STDIN and INotify file handles currently expected/supported.
397  * More would ask for common infrastructure (sockets?).
398  */
mutt_monitor_poll(void)399 int mutt_monitor_poll(void)
400 {
401   int rc = 0;
402   char buf[EVENT_BUFLEN] __attribute__((aligned(__alignof__(struct inotify_event))));
403 
404   MonitorFilesChanged = false;
405 
406   if (INotifyFd != -1)
407   {
408     int fds = poll(PollFds, PollFdsLen, MuttGetchTimeout);
409 
410     if (fds == -1)
411     {
412       rc = -1;
413       if (errno != EINTR)
414       {
415         mutt_debug(LL_DEBUG2, "poll() failed, errno=%d %s\n", errno, strerror(errno));
416       }
417     }
418     else
419     {
420       bool input_ready = false;
421       for (int i = 0; fds && (i < PollFdsCount); i++)
422       {
423         if (PollFds[i].revents)
424         {
425           fds--;
426           if (PollFds[i].fd == 0)
427           {
428             input_ready = true;
429           }
430           else if (PollFds[i].fd == INotifyFd)
431           {
432             MonitorFilesChanged = true;
433             mutt_debug(LL_DEBUG3, "file change(s) detected\n");
434             char *ptr = buf;
435             const struct inotify_event *event = NULL;
436 
437             while (true)
438             {
439               int len = read(INotifyFd, buf, sizeof(buf));
440               if (len == -1)
441               {
442                 if (errno != EAGAIN)
443                 {
444                   mutt_debug(LL_DEBUG2, "read inotify events failed, errno=%d %s\n",
445                              errno, strerror(errno));
446                 }
447                 break;
448               }
449 
450               while (ptr < (buf + len))
451               {
452                 event = (const struct inotify_event *) ptr;
453                 mutt_debug(LL_DEBUG3, "+ detail: descriptor=%d mask=0x%x\n",
454                            event->wd, event->mask);
455                 if (event->mask & IN_IGNORED)
456                   monitor_handle_ignore(event->wd);
457                 else if (event->wd == MonitorContextDescriptor)
458                   MonitorContextChanged = true;
459                 ptr += sizeof(struct inotify_event) + event->len;
460               }
461             }
462           }
463         }
464       }
465       if (!input_ready)
466         rc = MonitorFilesChanged ? -2 : -3;
467     }
468   }
469 
470   return rc;
471 }
472 
473 /**
474  * mutt_monitor_add - Add a watch for a mailbox
475  * @param m Mailbox to watch
476  * @retval  0 success: new or already existing monitor
477  * @retval -1 failed:  no mailbox, inaccessible file, create monitor/watcher failed
478  *
479  * If mailbox is NULL, the current mailbox (Context) is used.
480  */
mutt_monitor_add(struct Mailbox * m)481 int mutt_monitor_add(struct Mailbox *m)
482 {
483   struct MonitorInfo info;
484   monitor_info_init(&info);
485 
486   int rc = 0;
487   enum ResolveResult desc = monitor_resolve(&info, m);
488   if (desc != RESOLVE_RES_OK_NOTEXISTING)
489   {
490     if (!m && (desc == RESOLVE_RES_OK_EXISTING))
491       MonitorContextDescriptor = info.monitor->desc;
492     rc = (desc == RESOLVE_RES_OK_EXISTING) ? 0 : -1;
493     goto cleanup;
494   }
495 
496   uint32_t mask = info.is_dir ? INOTIFY_MASK_DIR : INOTIFY_MASK_FILE;
497   if (((INotifyFd == -1) && (monitor_init() == -1)) ||
498       ((desc = inotify_add_watch(INotifyFd, info.path, mask)) == -1))
499   {
500     mutt_debug(LL_DEBUG2, "inotify_add_watch failed for '%s', errno=%d %s\n",
501                info.path, errno, strerror(errno));
502     rc = -1;
503     goto cleanup;
504   }
505 
506   mutt_debug(LL_DEBUG3, "inotify_add_watch descriptor=%d for '%s'\n", desc, info.path);
507   if (!m)
508     MonitorContextDescriptor = desc;
509 
510   monitor_new(&info, desc);
511 
512 cleanup:
513   monitor_info_free(&info);
514   return rc;
515 }
516 
517 /**
518  * mutt_monitor_remove - Remove a watch for a mailbox
519  * @param m Mailbox
520  * @retval 0 monitor removed (not shared)
521  * @retval 1 monitor not removed (shared)
522  * @retval 2 no monitor
523  *
524  * If mailbox is NULL, the current mailbox (Context) is used.
525  */
mutt_monitor_remove(struct Mailbox * m)526 int mutt_monitor_remove(struct Mailbox *m)
527 {
528   struct MonitorInfo info, info2;
529   int rc = 0;
530 
531   monitor_info_init(&info);
532   monitor_info_init(&info2);
533 
534   if (!m)
535   {
536     MonitorContextDescriptor = -1;
537     MonitorContextChanged = false;
538   }
539 
540   if (monitor_resolve(&info, m) != RESOLVE_RES_OK_EXISTING)
541   {
542     rc = 2;
543     goto cleanup;
544   }
545 
546   struct Mailbox *m_ctx = ctx_mailbox(Context);
547   if (m_ctx)
548   {
549     if (m)
550     {
551       if ((monitor_resolve(&info2, NULL) == RESOLVE_RES_OK_EXISTING) &&
552           (info.st_ino == info2.st_ino) && (info.st_dev == info2.st_dev))
553       {
554         rc = 1;
555         goto cleanup;
556       }
557     }
558     else
559     {
560       if (mailbox_find(m_ctx->realpath))
561       {
562         rc = 1;
563         goto cleanup;
564       }
565     }
566   }
567 
568   inotify_rm_watch(info.monitor->desc, INotifyFd);
569   mutt_debug(LL_DEBUG3, "inotify_rm_watch for '%s' descriptor=%d\n", info.path,
570              info.monitor->desc);
571 
572   monitor_delete(info.monitor);
573   monitor_check_free();
574 
575 cleanup:
576   monitor_info_free(&info);
577   monitor_info_free(&info2);
578   return rc;
579 }
580