/* Watching file system changes. Copyright (C) 2011 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_FILEWATCH #include /* Required for lisp.h. */ #include "lisp.h" #include "coding.h" #include "process.h" #include "keyboard.h" #include "character.h" #include "frame.h" /* Required for termhooks.h. */ #include "termhooks.h" static Lisp_Object Qmodify, Qmove, Qattrib, Qdelete, Qfrom, Qto, Qall_modify; static Lisp_Object Qunknown_aspect, Qfile_watch; static Lisp_Object Qaccess, Qclose_write, Qclose_nowrite, Qopen, Qall; #ifdef HAVE_INOTIFY #include #include enum { uninitialized = -100 }; /* File handle for inotify. */ static int inotifyfd = uninitialized; /* Assoc list of files being watched. Format: (id . (filename ((subid callback flags) (subid callbacks flags) ...))) */ static Lisp_Object watch_list; #define CADDR(x) Fcar (Fcdr (Fcdr (x))) #define CADR(x) Fcar (Fcdr (x)) static Lisp_Object append2 (Lisp_Object s1, Lisp_Object s2) { Lisp_Object args[2]; args[0] = s1; args[1] = s2; return Fappend (2, args); } /* Returns a list with all the elements from EVENTS the `watch_list' CELL is interested in. */ static Lisp_Object match_aspects (Lisp_Object cell, Lisp_Object events) { Lisp_Object ret = Qnil; Lisp_Object aspects = CADDR (cell); Lisp_Object i; if (EQ (aspects, Qt) || EQ (aspects, Qall)) return events; else if (EQ (aspects, Qall_modify)) aspects = list4 (Qmodify, Qmove, Qattrib, Qdelete); i = Fcar (aspects); while (!NILP (i)) { Lisp_Object e = Fcar (events); while (!NILP (e)) { if (EQ (Fcar (e), i)) ret = Fcons (e, ret); events = Fcdr (events); e = Fcar (events); } aspects = Fcdr (aspects); i = Fcar (aspects); } return ret; } static Lisp_Object inotifyevent_to_aspects (Lisp_Object name, struct inotify_event *ev) { Lisp_Object events = Qnil; if (ev->mask & (IN_MODIFY|IN_CREATE) ) events = Fcons (Fcons (Qmodify, name), events); if (ev->mask & IN_MOVE_SELF) events = Fcons (Fcons (Qmove, name), events); if (ev->mask & IN_MOVED_FROM) events = Fcons (Fcons (Qmove, Fcons (Qfrom, Fcons (name, make_number (ev->cookie)))), events); if (ev->mask & IN_MOVED_TO) events = Fcons (Fcons (Qmove, Fcons (Qto, Fcons (name, make_number (ev->cookie)))), events); if (ev->mask & IN_ATTRIB) events = Fcons (Fcons (Qattrib, name), events); if (ev->mask & (IN_DELETE|IN_DELETE_SELF) ) events = Fcons (Fcons (Qdelete, name), events); if (ev->mask & IN_ACCESS) events = Fcons (Fcons (Qaccess, name), events); if (ev->mask & IN_CLOSE_WRITE) events = Fcons (Fcons (Qclose_write, name), events); if (ev->mask & IN_CLOSE_NOWRITE) events = Fcons (Fcons (Qclose_nowrite, name), events); if (ev->mask & IN_OPEN) events = Fcons (Fcons (Qopen, name), events); return events; } /* 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 *_, int for_read) { struct input_event event; int to_read; char *buffer; ssize_t n; size_t i; if (!for_read) return; to_read = 0; if (ioctl (fd, FIONREAD, &to_read) == -1) report_file_error ("Error while trying to retrieve file system events", Qnil); buffer = xmalloc (to_read); n = read (fd, buffer, to_read); if (n < 0) { xfree (buffer); report_file_error ("Error while trying to read file system events", Qnil); } EVENT_INIT (event); event.kind = FILEWATCH_EVENT; event.arg = Qnil; i = 0; while (i < (size_t)n) { struct inotify_event *ev = (struct inotify_event*)&buffer[i]; Lisp_Object watch_data = Fassoc (make_number (ev->wd), watch_list); if (!NILP (watch_data)) { Lisp_Object name, events; /* If a directory is watched name contains the name of the file that was changed. */ 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 = CADR (watch_data); } events = inotifyevent_to_aspects (name, ev); if (!NILP (events)) { Lisp_Object x = CADDR (watch_data); while (!NILP (x)) { Lisp_Object cell = Fcar (x); Lisp_Object aspects = match_aspects (cell, events); if (!NILP (aspects)) { Lisp_Object id = list3 (Qfile_watch, make_number (ev->wd), Fcar (cell)); Lisp_Object event_info = list1 (list3 (id, CADR (cell), aspects)); if (NILP (event.arg)) event.arg = event_info; else event.arg = append2 (event.arg, event_info); } x = Fcdr (x); } } if (ev->mask & IN_IGNORED) { /* Event was removed automatically: Drop it from data list. */ add_to_log ("File-watch: \"%s\" will be ignored", name, Qnil); watch_list = Fdelete (watch_data, watch_list); } if (ev->mask & IN_Q_OVERFLOW) add_to_log ("File watch: Inotify Queue Overflow!", Qnil, Qnil); } i += sizeof (*ev) + ev->len; } if (!NILP (event.arg)) kbd_buffer_store_event (&event); xfree (buffer); } static uint32_t symbol_to_inotifymask (Lisp_Object symb, int in_list) { if (EQ (symb, Qmodify)) return IN_MODIFY | IN_CREATE; else if (EQ (symb, Qmove)) return IN_MOVE_SELF | IN_MOVE; else if (EQ (symb, Qattrib)) return IN_ATTRIB; else if (EQ (symb, Qdelete)) return IN_DELETE_SELF | IN_DELETE; else if (!in_list && EQ (symb, Qall_modify)) return IN_MODIFY | IN_CREATE | IN_MOVE_SELF | IN_MOVE | IN_ATTRIB | IN_DELETE_SELF | IN_DELETE; else if (EQ (symb, Qaccess)) return IN_ACCESS; else if (EQ (symb, Qclose_write)) return IN_CLOSE_WRITE; else if (EQ (symb, Qclose_nowrite)) return IN_CLOSE_NOWRITE; else if (EQ (symb, Qopen)) return IN_OPEN; else if (!in_list && (EQ (symb, Qt) || EQ (symb, Qall))) return IN_MODIFY | IN_CREATE | IN_MOVE_SELF | IN_MOVE | IN_ATTRIB | IN_DELETE_SELF | IN_DELETE | IN_ACCESS | IN_CLOSE_WRITE | IN_CLOSE_NOWRITE | IN_OPEN; else Fsignal (Qunknown_aspect, Fcons (symb, Qnil)); } static uint32_t aspect_to_inotifymask (Lisp_Object aspect) { if (CONSP (aspect)) { Lisp_Object x = aspect; uint32_t mask = 0; while (!NILP (x)) { mask |= symbol_to_inotifymask (Fcar (x), 1); x = Fcdr (x); } return mask; } else return symbol_to_inotifymask (aspect, 0); } static Lisp_Object get_watch_data_by_file_name (Lisp_Object file_name) { Lisp_Object cell, file_name_data; Lisp_Object x = watch_list; CHECK_STRING (file_name); while (!NILP (x)) { cell = Fcar (x); x = Fcdr (x); file_name_data = Fcdr (cell); if (!NILP (file_name_data) && STRINGP (Fcar (file_name_data)) && !NILP (Fstring_equal (Fcar (file_name_data), file_name)) && NUMBERP (Fcar (cell))) return cell; } return Qnil; } DEFUN ("file-watch", Ffile_watch, Sfile_watch, 3, 3, 0, doc: /* Arrange to call FUNC if ASPECT of FILENAME changes. ASPECT might be one of the following symbols or a list of those symbols (except for all-modify and t): modify -- notify when a file is modified or created. move -- notify when a file/directory is moved. attrib -- notify when attributes change. delete -- notify when a file/directory is deleted. all-modify -- notify for all of the above (all modifying changes). access -- notify when file was accessed/read. close-write -- notify when a file opened for writing was closed. close-nowrite -- notify when a file not opened for writing was closed. open -- notify when a file was opened. t -- notify for all of the above. Watching a directory is not recursive. CALLBACK receives the events as a list with each list element being a list containing information about an event. The first element is a flag symbol. If a directory is watched the second element is the name of the file that changed. If a file is moved from or to the directory the second element is either 'from or 'to and the third element is the file name. A fourth element contains a numeric identifier (cookie) that can be used to identify matching move operations if a file is moved inside the directory. Example: (file-watch "foo" t #'(lambda (id event) (message "%s %s" id event))) Use `file-unwatch' to stop watching. usage: (file-watch FILENAME ASPECT CALLBACK) */) (Lisp_Object file_name, Lisp_Object aspect, Lisp_Object callback) { static int intern_counter = 0; /* assign local ids */ uint32_t mask; int watchdesc; Lisp_Object decoded_file_name; Lisp_Object data; Lisp_Object info; CHECK_STRING (file_name); if (inotifyfd == uninitialized) { inotifyfd = inotify_init1 (IN_NONBLOCK|IN_CLOEXEC); if (inotifyfd == -1) { inotifyfd = uninitialized; report_file_error ("File watching feature (inotify) is not available", Qnil); } watch_list = Qnil; add_read_fd (inotifyfd, &inotify_callback, &watch_list); } mask = aspect_to_inotifymask (aspect); data = get_watch_data_by_file_name (file_name); if (!NILP (data)) mask |= IN_MASK_ADD; decoded_file_name = ENCODE_FILE (file_name); watchdesc = inotify_add_watch (inotifyfd, SSDATA (decoded_file_name), mask); if (watchdesc == -1) report_file_error ("Could not watch file", Fcons (file_name, Qnil)); info = list3 (make_number (intern_counter), callback, aspect); ++intern_counter; if (!NILP (data)) Fsetcdr (Fcdr (data), Fcons (append2 (CADDR (data), Fcons (info, Qnil)), Qnil)); else watch_list = Fcons (list3 (make_number (watchdesc), file_name, Fcons (info, Qnil)), watch_list); return list3 (Qfile_watch, make_number (watchdesc), Fcar (info)); } static int file_watch_objectp (Lisp_Object obj) { return CONSP (obj) && XINT (Flength (obj)) == 3 && EQ (Fcar (obj), Qfile_watch); } DEFUN ("file-unwatch", Ffile_unwatch, Sfile_unwatch, 1, 1, 0, doc: /* Stop watching a file or directory. */) (Lisp_Object watch_object) { Lisp_Object watch_data; Lisp_Object info; if (!file_watch_objectp (watch_object)) wrong_type_argument (Qfile_watch, watch_object); if (inotifyfd == uninitialized) return Qnil; watch_data = Fassoc ( CADR (watch_object), watch_list ); if (NILP (watch_data)) return Qnil; info = Fassoc (CADDR (watch_object), CADDR (watch_data)); if (NILP (info)) return Qnil; Fsetcdr (Fcdr (watch_data), Fcons (Fdelete (info, CADDR (watch_data)), Qnil)); if (NILP (CADDR (watch_data))) { int const magicno = XINT (CADR (watch_object)); watch_list = Fdelete (watch_data, watch_list); if (inotify_rm_watch (inotifyfd, magicno) == -1) report_file_error ("Could not unwatch file", Fcons (Fcar (watch_data), Qnil)); /* Cleanup if watch_list is empty. */ if (NILP (watch_list)) { close (inotifyfd); delete_read_fd (inotifyfd); inotifyfd = uninitialized; } } else { Lisp_Object decoded_file_name = ENCODE_FILE (CADR (watch_data)); Lisp_Object x = CADDR (watch_data); uint32_t mask = 0; while (!NILP (x)) { Lisp_Object cell = Fcar (x); mask |= aspect_to_inotifymask (CADDR (cell)); x = Fcdr (x); } /* Reset watch mask */ if (inotify_add_watch (inotifyfd, SSDATA (decoded_file_name), mask) == -1) report_file_error ("Could not unwatch file", Fcons (Fcar (watch_data), Qnil)); } return Qt; } #else /* HAVE_INOTIFY */ #error "Filewatch defined but no watch mechanism (inotify) available" #endif /* HAVE_INOTIFY */ void syms_of_filewatch (void) { DEFSYM (Qmodify, "modify"); DEFSYM (Qmove, "move"); DEFSYM (Qattrib, "attrib"); DEFSYM (Qdelete, "delete"); DEFSYM (Qfrom, "from"); DEFSYM (Qto, "to"); DEFSYM (Qall_modify, "all-modify"); DEFSYM (Qaccess, "access"); DEFSYM (Qclose_write, "close-write"); DEFSYM (Qclose_nowrite, "close-nowrite"); DEFSYM (Qopen, "open"); DEFSYM (Qall, "all"); DEFSYM (Qunknown_aspect, "unknown-aspect"); DEFSYM (Qfile_watch, "file-watch"); Fput (Qunknown_aspect, Qerror_conditions, list2 (Qunknown_aspect, Qerror)); Fput (Qunknown_aspect, Qerror_message, make_pure_c_string ("Unknown Aspect")); defsubr (&Sfile_watch); defsubr (&Sfile_unwatch); staticpro (&watch_list); Fprovide (intern_c_string ("filewatch"), Qnil); } #endif /* HAVE_FILEWATCH */