/* Inotify support for Emacs Copyright (C) 2012-2017 Free Software Foundation, Inc. This file is part of GNU Emacs. GNU Emacs 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 3 of the License, or (at your option) any later version. GNU Emacs 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 GNU Emacs. If not, see . */ #include #ifdef HAVE_INOTIFY #include "lisp.h" #include "coding.h" #include "process.h" #include "keyboard.h" #include "termhooks.h" #include #include #include /* Ignore bits that might be undefined on old GNU/Linux systems. */ #ifndef IN_EXCL_UNLINK # define IN_EXCL_UNLINK 0 #endif #ifndef IN_DONT_FOLLOW # define IN_DONT_FOLLOW 0 #endif #ifndef IN_ONLYDIR # define IN_ONLYDIR 0 #endif /* File handle for inotify. */ static int inotifyfd = -1; /* Assoc list of files being watched. Format: (watch-descriptor name callback) */ static Lisp_Object watch_list; static Lisp_Object add_watch_descriptor (int wd, Lisp_Object filename, Lisp_Object callback) { Lisp_Object descriptor = make_number (wd); Lisp_Object elt = Fassoc (descriptor, watch_list); Lisp_Object list = Fcdr (elt); Lisp_Object watch, watch_id; int id = 0; while (! NILP (list)) { id = max (id, 1 + XINT (XCAR (XCAR (list)))); list = XCDR (list); } watch_id = make_number (id); watch = list3 (watch_id, filename, callback); if (NILP (elt)) watch_list = Fcons (Fcons (descriptor, Fcons (watch, Qnil)), watch_list); else XSETCDR (elt, Fcons (watch, XCDR (elt))); return Fcons (descriptor, watch_id); } static void remove_watch_descriptor (Lisp_Object descriptor, Lisp_Object id) { Lisp_Object watches = Fassoc (descriptor, watch_list); if (CONSP (watches)) { Lisp_Object watch = Fassoc (id, XCDR (watches)); if (CONSP (watch)) XSETCDR (watches, Fdelete (watch, XCDR (watches))); if (NILP (XCDR (watches))) watch_list = Fdelete (watches, watch_list); } } static Lisp_Object mask_to_aspects (uint32_t mask) { Lisp_Object aspects = Qnil; if (mask & IN_ACCESS) aspects = Fcons (Qaccess, aspects); if (mask & IN_ATTRIB) aspects = Fcons (Qattrib, aspects); if (mask & IN_CLOSE_WRITE) aspects = Fcons (Qclose_write, aspects); if (mask & IN_CLOSE_NOWRITE) aspects = Fcons (Qclose_nowrite, aspects); if (mask & IN_CREATE) aspects = Fcons (Qcreate, aspects); if (mask & IN_DELETE) aspects = Fcons (Qdelete, aspects); if (mask & IN_DELETE_SELF) aspects = Fcons (Qdelete_self, aspects); if (mask & IN_MODIFY) aspects = Fcons (Qmodify, aspects); if (mask & IN_MOVE_SELF) aspects = Fcons (Qmove_self, aspects); if (mask & IN_MOVED_FROM) aspects = Fcons (Qmoved_from, aspects); if (mask & IN_MOVED_TO) aspects = Fcons (Qmoved_to, aspects); if (mask & IN_OPEN) aspects = Fcons (Qopen, aspects); if (mask & IN_IGNORED) aspects = Fcons (Qignored, aspects); if (mask & IN_ISDIR) aspects = Fcons (Qisdir, aspects); if (mask & IN_Q_OVERFLOW) aspects = Fcons (Qq_overflow, aspects); if (mask & IN_UNMOUNT) aspects = Fcons (Qunmount, aspects); return aspects; } static Lisp_Object inotifyevent_to_event (Lisp_Object watch, struct inotify_event const *ev) { Lisp_Object name = Qnil; if (ev->len > 0) { size_t const len = strlen (ev->name); name = make_unibyte_string (ev->name, min (len, ev->len)); name = DECODE_FILE (name); } else name = XCAR (XCDR (watch)); return list2 (list4 (Fcons (make_number (ev->wd), XCAR (watch)), mask_to_aspects (ev->mask), name, make_number (ev->cookie)), Fnth (make_number (2), watch)); } /* This callback is called when the FD is available for read. The inotify events are read from FD and converted into input_events. */ static void inotify_callback (int fd, void *_) { struct input_event event; int to_read; char *buffer; ssize_t n; size_t i; to_read = 0; if (ioctl (fd, FIONREAD, &to_read) == -1) report_file_notify_error ("Error while retrieving file system events", Qnil); buffer = xmalloc (to_read); n = read (fd, buffer, to_read); if (n < 0) { xfree (buffer); report_file_notify_error ("Error while reading file system events", Qnil); } EVENT_INIT (event); event.kind = FILE_NOTIFY_EVENT; i = 0; while (i < (size_t)n) { struct inotify_event *ev = (struct inotify_event *) &buffer[i]; Lisp_Object descriptor = make_number (ev->wd); Lisp_Object watches = Fassoc (descriptor, watch_list); if (CONSP (watches)) { for (Lisp_Object elt = XCDR (watches); CONSP (elt); elt = XCDR (elt)) { event.arg = inotifyevent_to_event (XCAR (elt), ev); if (!NILP (event.arg)) kbd_buffer_store_event (&event); } /* If event was removed automatically: Drop it from watch list. */ if (ev->mask & IN_IGNORED) for (Lisp_Object elt = XCDR (watches); CONSP (elt); elt = XCDR (elt)) remove_watch_descriptor (descriptor, XCAR (elt)); } i += sizeof (*ev) + ev->len; } xfree (buffer); } static uint32_t symbol_to_inotifymask (Lisp_Object symb) { if (EQ (symb, Qaccess)) return IN_ACCESS; else if (EQ (symb, Qattrib)) return IN_ATTRIB; else if (EQ (symb, Qclose_write)) return IN_CLOSE_WRITE; else if (EQ (symb, Qclose_nowrite)) return IN_CLOSE_NOWRITE; else if (EQ (symb, Qcreate)) return IN_CREATE; else if (EQ (symb, Qdelete)) return IN_DELETE; else if (EQ (symb, Qdelete_self)) return IN_DELETE_SELF; else if (EQ (symb, Qmodify)) return IN_MODIFY; else if (EQ (symb, Qmove_self)) return IN_MOVE_SELF; else if (EQ (symb, Qmoved_from)) return IN_MOVED_FROM; else if (EQ (symb, Qmoved_to)) return IN_MOVED_TO; else if (EQ (symb, Qopen)) return IN_OPEN; else if (EQ (symb, Qmove)) return IN_MOVE; else if (EQ (symb, Qclose)) return IN_CLOSE; else if (EQ (symb, Qdont_follow)) return IN_DONT_FOLLOW; else if (EQ (symb, Qexcl_unlink)) return IN_EXCL_UNLINK; else if (EQ (symb, Qmask_add)) return IN_MASK_ADD; else if (EQ (symb, Qoneshot)) return IN_ONESHOT; else if (EQ (symb, Qonlydir)) return IN_ONLYDIR; else if (EQ (symb, Qt) || EQ (symb, Qall_events)) return IN_ALL_EVENTS; else { errno = EINVAL; report_file_notify_error ("Unknown aspect", symb); } } static uint32_t aspect_to_inotifymask (Lisp_Object aspect) { if (CONSP (aspect)) { Lisp_Object x = aspect; uint32_t mask = 0; while (CONSP (x)) { mask |= symbol_to_inotifymask (XCAR (x)); x = XCDR (x); } return mask; } else return symbol_to_inotifymask (aspect); } DEFUN ("inotify-add-watch", Finotify_add_watch, Sinotify_add_watch, 3, 3, 0, doc: /* Add a watch for FILE-NAME to inotify. Return a watch descriptor. The watch will look for ASPECT events and invoke CALLBACK when an event occurs. ASPECT might be one of the following symbols or a list of those symbols: access attrib close-write close-nowrite create delete delete-self modify move-self moved-from moved-to open all-events or t move close The following symbols can also be added to a list of aspects: dont-follow excl-unlink mask-add oneshot onlydir Watching a directory is not recursive. CALLBACK is passed a single argument EVENT which contains an event structure of the format \(WATCH-DESCRIPTOR ASPECTS NAME COOKIE) WATCH-DESCRIPTOR is the same object that was returned by this function. It can be tested for equality using `equal'. ASPECTS describes the event. It is a list of ASPECT symbols described above and can also contain one of the following symbols ignored isdir q-overflow unmount If a directory is watched then NAME is the name of file that caused the event. COOKIE is an object that can be compared using `equal' to identify two matching renames (moved-from and moved-to). See inotify(7) and inotify_add_watch(2) for further information. The inotify fd is managed internally and there is no corresponding inotify_init. Use `inotify-rm-watch' to remove a watch. */) (Lisp_Object file_name, Lisp_Object aspect, Lisp_Object callback) { uint32_t mask; Lisp_Object encoded_file_name; int wd = -1; CHECK_STRING (file_name); if (inotifyfd < 0) { inotifyfd = inotify_init1 (IN_NONBLOCK|IN_CLOEXEC); if (inotifyfd < 0) report_file_notify_error ("File watching is not available", Qnil); watch_list = Qnil; add_read_fd (inotifyfd, &inotify_callback, NULL); } mask = aspect_to_inotifymask (aspect); encoded_file_name = ENCODE_FILE (file_name); wd = inotify_add_watch (inotifyfd, SSDATA (encoded_file_name), mask); if (wd == -1) report_file_notify_error ("Could not add watch for file", file_name); return add_watch_descriptor (wd, file_name, callback); } DEFUN ("inotify-rm-watch", Finotify_rm_watch, Sinotify_rm_watch, 1, 1, 0, doc: /* Remove an existing WATCH-DESCRIPTOR. WATCH-DESCRIPTOR should be an object returned by `inotify-add-watch'. See inotify_rm_watch(2) for more information. */) (Lisp_Object watch_descriptor) { Lisp_Object descriptor, id; if (! (CONSP (watch_descriptor) && INTEGERP (XCAR (watch_descriptor)) && INTEGERP (XCDR (watch_descriptor)))) report_file_notify_error ("Invalid descriptor ", watch_descriptor); descriptor = XCAR (watch_descriptor); id = XCDR (watch_descriptor); remove_watch_descriptor (descriptor, id); if (NILP (Fassoc (descriptor, watch_list))) { int wd = XINT (descriptor); if (inotify_rm_watch (inotifyfd, wd) == -1) report_file_notify_error ("Could not rm watch", watch_descriptor); } /* Cleanup if no more files are watched. */ if (NILP (watch_list)) { emacs_close (inotifyfd); delete_read_fd (inotifyfd); inotifyfd = -1; } return Qt; } DEFUN ("inotify-valid-p", Finotify_valid_p, Sinotify_valid_p, 1, 1, 0, doc: /* Check a watch specified by its WATCH-DESCRIPTOR. WATCH-DESCRIPTOR should be an object returned by `inotify-add-watch'. A watch can become invalid if the file or directory it watches is deleted, or if the watcher thread exits abnormally for any other reason. Removing the watch by calling `inotify-rm-watch' also makes it invalid. */) (Lisp_Object watch_descriptor) { Lisp_Object watches; if (! (CONSP (watch_descriptor) && INTEGERP (XCAR (watch_descriptor)) && INTEGERP (XCDR (watch_descriptor)))) return Qnil; watches = Fassoc (XCAR (watch_descriptor), watch_list); if (! CONSP (watches)) return Qnil; return CONSP (Fassoc (XCDR (watch_descriptor), XCDR (watches))); } #ifdef DEBUG DEFUN ("inotify-watch-list", Finotify_watch_list, Sinotify_watch_list, 0, 0, 0, doc: /* Return a copy of the internal watch_list. */) { return Fcopy_sequence (watch_list); } DEFUN ("inotify-allocated-p", Finotify_allocated_p, Sinotify_allocated_p, 0, 0, 0, doc: /* Return non-nil, if a inotify instance is allocated. */) { return inotifyfd < 0 ? Qnil : Qt; } #endif void syms_of_inotify (void) { DEFSYM (Qaccess, "access"); /* IN_ACCESS */ DEFSYM (Qattrib, "attrib"); /* IN_ATTRIB */ DEFSYM (Qclose_write, "close-write"); /* IN_CLOSE_WRITE */ DEFSYM (Qclose_nowrite, "close-nowrite"); /* IN_CLOSE_NOWRITE */ DEFSYM (Qcreate, "create"); /* IN_CREATE */ DEFSYM (Qdelete, "delete"); /* IN_DELETE */ DEFSYM (Qdelete_self, "delete-self"); /* IN_DELETE_SELF */ DEFSYM (Qmodify, "modify"); /* IN_MODIFY */ DEFSYM (Qmove_self, "move-self"); /* IN_MOVE_SELF */ DEFSYM (Qmoved_from, "moved-from"); /* IN_MOVED_FROM */ DEFSYM (Qmoved_to, "moved-to"); /* IN_MOVED_TO */ DEFSYM (Qopen, "open"); /* IN_OPEN */ DEFSYM (Qall_events, "all-events"); /* IN_ALL_EVENTS */ DEFSYM (Qmove, "move"); /* IN_MOVE */ DEFSYM (Qclose, "close"); /* IN_CLOSE */ DEFSYM (Qdont_follow, "dont-follow"); /* IN_DONT_FOLLOW */ DEFSYM (Qexcl_unlink, "excl-unlink"); /* IN_EXCL_UNLINK */ DEFSYM (Qmask_add, "mask-add"); /* IN_MASK_ADD */ DEFSYM (Qoneshot, "oneshot"); /* IN_ONESHOT */ DEFSYM (Qonlydir, "onlydir"); /* IN_ONLYDIR */ DEFSYM (Qignored, "ignored"); /* IN_IGNORED */ DEFSYM (Qisdir, "isdir"); /* IN_ISDIR */ DEFSYM (Qq_overflow, "q-overflow"); /* IN_Q_OVERFLOW */ DEFSYM (Qunmount, "unmount"); /* IN_UNMOUNT */ defsubr (&Sinotify_add_watch); defsubr (&Sinotify_rm_watch); defsubr (&Sinotify_valid_p); #ifdef DEBUG defsubr (&Sinotify_watch_list); defsubr (&Sinotify_allocated_p); #endif staticpro (&watch_list); Fprovide (intern_c_string ("inotify"), Qnil); } #endif /* HAVE_INOTIFY */