/* 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 #include "lisp.h" #include "coding.h" #include "process.h" static Lisp_Object QCmodify, QCmove, QCattrib, QCdelete, QCfrom, QCto, QCall; #ifdef HAVE_INOTIFY #include #include enum { uninitialized = -100 }; static int inotifyfd = uninitialized; /* Assoc list of files being watched. */ static Lisp_Object watch_list; static void inotify_callback(int fd, void *_, int for_read) { 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) report_file_error("Error while trying to read file system events", Qnil); i = 0; while(i < (size_t)n) { struct inotify_event *ev = (struct inotify_event*)&buffer[i]; Lisp_Object callback = Fassoc(make_number(ev->wd), watch_list); if(!NILP(callback)) { size_t len; Lisp_Object name, events; Lisp_Object call[3]; call[0] = Fcar(Fcdr(Fcdr(callback))); /* callback */ call[1] = Fcar(Fcdr(callback)); /* file name */ /* If a directory is watched name contains the name of the file that was changed. */ len = strlen(ev->name); name = make_unibyte_string (ev->name, len); name = DECODE_FILE(name); events = Qnil; if(ev->mask & (IN_MODIFY|IN_CREATE) ) events = Fcons(Fcons(QCmodify, name), events); if(ev->mask & IN_MOVE_SELF) events = Fcons(Fcons(QCmove, name), events); if(ev->mask & IN_MOVED_FROM) events = Fcons(Fcons(QCmove, Fcons(QCfrom, Fcons(name, make_number(ev->cookie)))), events); if(ev->mask & IN_MOVED_TO) events = Fcons(Fcons(QCmove, Fcons(QCto, Fcons(name, make_number(ev->cookie)))), events); if(ev->mask & IN_ATTRIB) events = Fcons(Fcons(QCattrib, name), events); if(ev->mask & (IN_DELETE|IN_DELETE_SELF) ) events = Fcons(Fcons(QCdelete, name), events); if(!NILP(events)) { call[2] = events; Ffuncall(3, call); } if(ev->mask & IN_IGNORED) { /* Event was removed automatically: Drop it from data list. */ add_to_log("File-watch: \"%s\" will be ignored", call[1], Qnil); watch_list = Fdelete(callback, watch_list); } if(ev->mask & IN_Q_OVERFLOW) add_to_log("File watch: Inotify Queue Overflow!", Qnil, Qnil); } i += sizeof(*ev) + ev->len; } xfree(buffer); } DEFUN ("file-watch", Ffile_watch, Sfile_watch, 3, MANY, 0, doc: /* Watch a file or directory. file-watch watches the file or directory given in FILENAME. If a change occurs CALLBACK is called with FILENAME as first argument and a list of changes as second argument. FLAGS can be :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 -- 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" #'(lambda (file event) (message "%s %s" file event)) :all) Use `file-unwatch' to stop watching. usage: (file-watch FILENAME CALLBACK &rest FLAGS) */) (size_t nargs, Lisp_Object *args) { uint32_t mask; size_t i; int watchdesc; Lisp_Object decoded_file_name; if(nargs < 3) return Qnil; CHECK_STRING(args[0]); if(inotifyfd == uninitialized) { inotifyfd = inotify_init1(IN_NONBLOCK|IN_CLOEXEC); if(inotifyfd == -1) report_file_error("File watching feature (inotify) is not available", Qnil); watch_list = Qnil; add_read_fd(inotifyfd, &inotify_callback, &watch_list); } mask = 0; for(i = 2; i < nargs; ++i) { if(EQ(args[i], QCmodify)) mask |= IN_MODIFY | IN_CREATE; else if(EQ(args[i], QCmove)) mask |= IN_MOVE_SELF | IN_MOVE; else if(EQ(args[i], QCattrib)) mask |= IN_ATTRIB; else if(EQ(args[i], QCdelete)) mask |= IN_DELETE_SELF | IN_DELETE; else if(EQ(args[i], QCall)) mask |= IN_MODIFY | IN_CREATE | IN_MOVE_SELF | IN_MOVE | IN_ATTRIB | IN_DELETE_SELF | IN_DELETE; else /* TODO: should this be an error? */ message("Unkown parameter %s (ignored)", SSDATA(Fprin1_to_string(args[i], Qnil))); } decoded_file_name = ENCODE_FILE(args[0]); watchdesc = inotify_add_watch(inotifyfd, SSDATA(decoded_file_name), mask); if(watchdesc == -1) { report_file_error("Could not watch file", Fcons(args[0], Qnil)); } /* TODO: check if file is already in the watch_list. */ watch_list = Fcons(Fcons(make_number(watchdesc), Flist(2, args)), watch_list); return Qt; } DEFUN ("file-unwatch", Ffile_unwatch, Sfile_unwatch, 1, 1, 0, doc: /* Stop watching a file or directory. */) (Lisp_Object file_name) { Lisp_Object cell, file_name_data; Lisp_Object x = watch_list; if(inotifyfd == uninitialized) return Qnil; 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)) && Fstring_equal(Fcar(file_name_data), file_name) && NUMBERP(Fcar(cell))) { int const magicno = XINT(Fcar(cell)); watch_list = Fdelete(cell, watch_list); if(inotify_rm_watch(inotifyfd, magicno) == -1) report_file_error("Could not unwatch file", Fcons(file_name, Qnil)); /* Cleanup if watch_list is empty. */ if(NILP(watch_list)) { close(inotifyfd); delete_read_fd(inotifyfd); inotifyfd = uninitialized; } return Qt; } } return Qnil; } #else /* HAVE_INOTIFY */ #error "Filewatch defined but no watch mechanism (inotify) available" #endif /* HAVE_INOTIFY */ void syms_of_filewatch (void) { QCmodify = intern_c_string (":modify"); staticpro (&QCmodify); QCmove = intern_c_string (":move"); staticpro (&QCmove); QCattrib = intern_c_string (":attrib"); staticpro (&QCattrib); QCdelete = intern_c_string (":delete"); staticpro (&QCdelete); QCfrom = intern_c_string (":from"); staticpro (&QCfrom); QCto = intern_c_string (":to"); staticpro (&QCto); QCall = intern_c_string (":all"); staticpro (&QCall); defsubr (&Sfile_watch); defsubr (&Sfile_unwatch); } #endif /* HAVE_FILEWATCH */