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