Discussion:
[PATCH] Support for filesystem watching (inotify)
Rüdiger Sonderfeld
2011-06-03 22:34:15 UTC
Permalink
Hello,
I wrote a patch to support watching for file system events. It currently only works with inotify (Linux) but it could be extended to support kqueue
(*BSD, OS X) or Win32. But I wanted to get some feedback first. Watching for filesystem events would be very useful for, e.g., dired or magit's status
view.

Here is a quick example (expects a directory foo containing a file bar):

(file-watch "foo" #'(lambda (path events) (message "FS-Event: %s %s") path events)) :all)
(delete-file "foo/bar")
(file-unwatch "foo")

So please tell me what you think of this.

Regards,
Rüdiger


[PATCH] Added basic file system watching support.

It currently only works with inotify/Linux. It adds file-watch and file-unwatch functions.
---
configure.in | 14 +++
src/Makefile.in | 2 +-
src/emacs.c | 4 +
src/filewatch.c | 242 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lisp.h | 5 +
5 files changed, 266 insertions(+), 1 deletions(-)
create mode 100644 src/filewatch.c

diff --git a/configure.in b/configure.in
index 77deef8..3263876 100644
--- a/configure.in
+++ b/configure.in
@@ -169,6 +169,7 @@ OPTION_DEFAULT_ON([dbus],[don't compile with D-Bus support])
OPTION_DEFAULT_ON([gconf],[don't compile with GConf support])
OPTION_DEFAULT_ON([selinux],[don't compile with SELinux support])
OPTION_DEFAULT_ON([gnutls],[don't use -lgnutls for SSL/TLS support])
+OPTION_DEFAULT_ON([inotify],[don't compile with inotify (file-watch) support])

## For the times when you want to build Emacs but don't have
## a suitable makeinfo, and can live without the manuals.
@@ -1978,6 +1979,19 @@ fi
AC_SUBST(LIBGNUTLS_LIBS)
AC_SUBST(LIBGNUTLS_CFLAGS)

+dnl inotify is only available on GNU/Linux.
+HAVE_INOTIFY=no
+if test "${with_inotify}" = "yes"; then
+ AC_CHECK_HEADERS(sys/inotify.h)
+ if test "$ac_cv_header_sys_inotify_h" = yes ; then
+ AC_CHECK_FUNCS(inotify_init1 inotify_add_watch inotify_rm_watch, HAVE_INOTIFY=yes)
+ fi
+fi
+if test "${HAVE_INOTIFY}" = "yes"; then
+ AC_DEFINE(HAVE_INOTIFY, [1], [Define to 1 to use inotify])
+ AC_DEFINE(HAVE_FILEWATCH, [1], [Define to 1 to support filewatch])
+fi
+
dnl Do not put whitespace before the #include statements below.
dnl Older compilers (eg sunos4 cc) choke on it.
HAVE_XAW3D=no
diff --git a/src/Makefile.in b/src/Makefile.in
index e119596..0cd6d6e 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -354,7 +354,7 @@ base_obj = dispnew.o frame.o scroll.o xdisp.o menu.o $(XMENU_OBJ) window.o \
syntax.o $(UNEXEC_OBJ) bytecode.o \
process.o gnutls.o callproc.o \
region-cache.o sound.o atimer.o \
- doprnt.o intervals.o textprop.o composite.o xml.o \
+ doprnt.o intervals.o textprop.o composite.o xml.o filewatch.o \
$(MSDOS_OBJ) $(MSDOS_X_OBJ) $(NS_OBJ) $(CYGWIN_OBJ) $(FONT_OBJ)
obj = $(base_obj) $(NS_OBJC_OBJ)

diff --git a/src/emacs.c b/src/emacs.c
index 6bdd255..5ca5cda 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -1550,6 +1550,10 @@ main (int argc, char **argv)
syms_of_gnutls ();
#endif

+#ifdef HAVE_FILEWATCH
+ syms_of_filewatch ();
+#endif /* HAVE_FILEWATCH */
+
#ifdef HAVE_DBUS
syms_of_dbusbind ();
#endif /* HAVE_DBUS */
diff --git a/src/filewatch.c b/src/filewatch.c
new file mode 100644
index 0000000..5b7c130
--- /dev/null
+++ b/src/filewatch.c
@@ -0,0 +1,242 @@
+/* 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#ifdef HAVE_FILEWATCH
+
+#include <setjmp.h>
+
+#include "lisp.h"
+#include "process.h"
+
+static Lisp_Object QCmodify, QCmove, QCattrib, QCdelete, QCfrom, QCto, QCall;
+
+#ifdef HAVE_INOTIFY
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+
+enum { uninitialized = -100 };
+static int inotifyfd = uninitialized;
+
+// Assoc list of files being watched.
+static Lisp_Object data;
+
+static void
+inotify_callback(int fd, void *_, int for_read) {
+ eassert(for_read);
+ eassert(data);
+
+ int to_read = 0;
+ if(ioctl(fd, FIONREAD, &to_read) == -1)
+ report_file_error("ioctl(2) on inotify", Qnil);
+ char *const buffer = xmalloc(to_read);
+ ssize_t const n = read(fd, buffer, to_read);
+ if(n < 0)
+ report_file_error("read from inotify", Qnil);
+ else if(n < to_read)
+ {
+ // TODO
+ message1("n < to_read");
+ }
+
+ size_t i = 0;
+ while(i < (size_t)n)
+ {
+ struct inotify_event *ev = (struct inotify_event*)&buffer[i];
+
+ Lisp_Object callback = Fassoc(make_number(ev->wd), data);
+ if(!NILP(callback))
+ {
+ Lisp_Object call[3];
+ call[0] = Fcar(Fcdr(Fcdr(callback))); /* callback */
+ call[1] = Fcar(Fcdr(callback)); /* path */
+
+ /* TODO check how to handle UTF-8 in file names:
+ make_string_from_bytes NCHARS vs. NBYTES */
+ /* ev->len seems to be an arbitrary large number
+ and not the exact length of ev->name */
+ size_t const len = strlen(ev->name);
+ /* if a directory is watched name contains the name
+ of the file that was changed */
+ Lisp_Object name = make_string_from_bytes (ev->name, len, len);
+
+ Lisp_Object 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. */
+ message("File-watch: \"%s\" will be ignored", SSDATA(call[1]));
+ data = Fdelete(callback, data);
+ }
+ if(ev->mask & IN_Q_OVERFLOW)
+ message1("File watch: Inotify Queue Overflow!");
+ }
+
+ i += sizeof(*ev) + ev->len;
+ }
+
+ free(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 PATH. If a change occurs
+CALLBACK is called with PATH 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.
+
+Use `file-unwatch' to stop watching.
+
+usage: (file-watch PATH CALLBACK &rest FLAGS) */)
+ (size_t nargs, Lisp_Object *args)
+{
+ 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("Initializing file watching", Qnil);
+ data = Qnil;
+ add_read_fd(inotifyfd, &inotify_callback, &data);
+ }
+ uint32_t mask = 0;
+ int i;
+ 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)));
+ }
+ int watchdesc = inotify_add_watch(inotifyfd, SSDATA(args[0]), mask);
+ if(watchdesc == -1) {
+ report_file_error("Watching file", Qnil);
+ }
+ data = Fcons(Fcons(make_number(watchdesc), Flist(2, args)), data);
+ return Qt;
+}
+
+DEFUN ("file-unwatch", Ffile_unwatch, Sfile_unwatch, 1, 1, 0,
+ doc: /* Stop watching a file or directory. */)
+ (Lisp_Object path)
+{
+ if(inotifyfd == uninitialized)
+ return Qnil;
+ CHECK_STRING(path);
+
+ Lisp_Object x = data;
+ Lisp_Object cell, path_data;
+ while(!NILP(x))
+ {
+ cell = Fcar(x);
+ x = Fcdr(x);
+ path_data = Fcdr(cell);
+ if(!NILP(path_data) && STRINGP(Fcar(path_data))
+ && Fstring_equal(Fcar(path_data), path)
+ && NUMBERP(Fcar(cell)))
+ {
+ int const magicno = XINT(Fcar(cell));
+ data = Fdelete(cell, data);
+ if(inotify_rm_watch(inotifyfd, magicno) == -1)
+ report_file_error("Unwatch path", Qnil);
+ 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 */
diff --git a/src/lisp.h b/src/lisp.h
index 85838d1..3ed5281 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -3405,6 +3405,11 @@ EXFUN (Fxw_display_color_p, 1);
EXFUN (Fx_focus_frame, 1);
#endif

+/* Defined in filewatch.c */
+#ifdef HAVE_FILEWATCH
+extern void syms_of_filewatch (void);
+#endif
+
/* Defined in xfaces.c */
extern Lisp_Object Qdefault, Qtool_bar, Qregion, Qfringe;
extern Lisp_Object Qheader_line, Qscroll_bar, Qcursor, Qborder, Qmouse, Qmenu;
--
1.7.5.2
j***@verona.se
2011-06-04 08:52:07 UTC
Permalink
Post by Rüdiger Sonderfeld
Hello,
I wrote a patch to support watching for file system events. It currently only works with inotify (Linux) but it could be extended to support kqueue
(*BSD, OS X) or Win32. But I wanted to get some feedback first. Watching for filesystem events would be very useful for, e.g., dired or magit's status
view.
Very interesting! Have you signed copyright papers and so on?
Post by Rüdiger Sonderfeld
(file-watch "foo" #'(lambda (path events) (message "FS-Event: %s %s") path events)) :all)
(delete-file "foo/bar")
(file-unwatch "foo")
So please tell me what you think of this.
Regards,
Rüdiger
[PATCH] Added basic file system watching support.
It currently only works with inotify/Linux. It adds file-watch and file-unwatch functions.
---
configure.in | 14 +++
src/Makefile.in | 2 +-
src/emacs.c | 4 +
src/filewatch.c | 242 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lisp.h | 5 +
5 files changed, 266 insertions(+), 1 deletions(-)
create mode 100644 src/filewatch.c
diff --git a/configure.in b/configure.in
index 77deef8..3263876 100644
--- a/configure.in
+++ b/configure.in
@@ -169,6 +169,7 @@ OPTION_DEFAULT_ON([dbus],[don't compile with D-Bus support])
OPTION_DEFAULT_ON([gconf],[don't compile with GConf support])
OPTION_DEFAULT_ON([selinux],[don't compile with SELinux support])
OPTION_DEFAULT_ON([gnutls],[don't use -lgnutls for SSL/TLS support])
+OPTION_DEFAULT_ON([inotify],[don't compile with inotify (file-watch) support])
## For the times when you want to build Emacs but don't have
## a suitable makeinfo, and can live without the manuals.
@@ -1978,6 +1979,19 @@ fi
AC_SUBST(LIBGNUTLS_LIBS)
AC_SUBST(LIBGNUTLS_CFLAGS)
+dnl inotify is only available on GNU/Linux.
+HAVE_INOTIFY=no
+if test "${with_inotify}" = "yes"; then
+ AC_CHECK_HEADERS(sys/inotify.h)
+ if test "$ac_cv_header_sys_inotify_h" = yes ; then
+ AC_CHECK_FUNCS(inotify_init1 inotify_add_watch inotify_rm_watch, HAVE_INOTIFY=yes)
+ fi
+fi
+if test "${HAVE_INOTIFY}" = "yes"; then
+ AC_DEFINE(HAVE_INOTIFY, [1], [Define to 1 to use inotify])
+ AC_DEFINE(HAVE_FILEWATCH, [1], [Define to 1 to support filewatch])
+fi
+
dnl Do not put whitespace before the #include statements below.
dnl Older compilers (eg sunos4 cc) choke on it.
HAVE_XAW3D=no
diff --git a/src/Makefile.in b/src/Makefile.in
index e119596..0cd6d6e 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -354,7 +354,7 @@ base_obj = dispnew.o frame.o scroll.o xdisp.o menu.o $(XMENU_OBJ) window.o \
syntax.o $(UNEXEC_OBJ) bytecode.o \
process.o gnutls.o callproc.o \
region-cache.o sound.o atimer.o \
- doprnt.o intervals.o textprop.o composite.o xml.o \
+ doprnt.o intervals.o textprop.o composite.o xml.o filewatch.o \
$(MSDOS_OBJ) $(MSDOS_X_OBJ) $(NS_OBJ) $(CYGWIN_OBJ) $(FONT_OBJ)
obj = $(base_obj) $(NS_OBJC_OBJ)
diff --git a/src/emacs.c b/src/emacs.c
index 6bdd255..5ca5cda 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -1550,6 +1550,10 @@ main (int argc, char **argv)
syms_of_gnutls ();
#endif
+#ifdef HAVE_FILEWATCH
+ syms_of_filewatch ();
+#endif /* HAVE_FILEWATCH */
+
#ifdef HAVE_DBUS
syms_of_dbusbind ();
#endif /* HAVE_DBUS */
diff --git a/src/filewatch.c b/src/filewatch.c
new file mode 100644
index 0000000..5b7c130
--- /dev/null
+++ b/src/filewatch.c
@@ -0,0 +1,242 @@
+/* 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#ifdef HAVE_FILEWATCH
+
+#include <setjmp.h>
+
+#include "lisp.h"
+#include "process.h"
+
+static Lisp_Object QCmodify, QCmove, QCattrib, QCdelete, QCfrom, QCto, QCall;
+
+#ifdef HAVE_INOTIFY
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+
+enum { uninitialized = -100 };
+static int inotifyfd = uninitialized;
+
+// Assoc list of files being watched.
+static Lisp_Object data;
+
+static void
+inotify_callback(int fd, void *_, int for_read) {
+ eassert(for_read);
+ eassert(data);
+
+ int to_read = 0;
+ if(ioctl(fd, FIONREAD, &to_read) == -1)
+ report_file_error("ioctl(2) on inotify", Qnil);
+ char *const buffer = xmalloc(to_read);
+ ssize_t const n = read(fd, buffer, to_read);
+ if(n < 0)
+ report_file_error("read from inotify", Qnil);
+ else if(n < to_read)
+ {
+ // TODO
+ message1("n < to_read");
+ }
+
+ size_t i = 0;
+ while(i < (size_t)n)
+ {
+ struct inotify_event *ev = (struct inotify_event*)&buffer[i];
+
+ Lisp_Object callback = Fassoc(make_number(ev->wd), data);
+ if(!NILP(callback))
+ {
+ Lisp_Object call[3];
+ call[0] = Fcar(Fcdr(Fcdr(callback))); /* callback */
+ call[1] = Fcar(Fcdr(callback)); /* path */
+
+ make_string_from_bytes NCHARS vs. NBYTES */
+ /* ev->len seems to be an arbitrary large number
+ and not the exact length of ev->name */
+ size_t const len = strlen(ev->name);
+ /* if a directory is watched name contains the name
+ of the file that was changed */
+ Lisp_Object name = make_string_from_bytes (ev->name, len, len);
+
+ Lisp_Object 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. */
+ message("File-watch: \"%s\" will be ignored", SSDATA(call[1]));
+ data = Fdelete(callback, data);
+ }
+ if(ev->mask & IN_Q_OVERFLOW)
+ message1("File watch: Inotify Queue Overflow!");
+ }
+
+ i += sizeof(*ev) + ev->len;
+ }
+
+ free(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 PATH. If a change occurs
+CALLBACK is called with PATH 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.
+
+Use `file-unwatch' to stop watching.
+
+usage: (file-watch PATH CALLBACK &rest FLAGS) */)
+ (size_t nargs, Lisp_Object *args)
+{
+ 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("Initializing file watching", Qnil);
+ data = Qnil;
+ add_read_fd(inotifyfd, &inotify_callback, &data);
+ }
+ uint32_t mask = 0;
+ int i;
+ 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)));
+ }
+ int watchdesc = inotify_add_watch(inotifyfd, SSDATA(args[0]), mask);
+ if(watchdesc == -1) {
+ report_file_error("Watching file", Qnil);
+ }
+ data = Fcons(Fcons(make_number(watchdesc), Flist(2, args)), data);
+ return Qt;
+}
+
+DEFUN ("file-unwatch", Ffile_unwatch, Sfile_unwatch, 1, 1, 0,
+ doc: /* Stop watching a file or directory. */)
+ (Lisp_Object path)
+{
+ if(inotifyfd == uninitialized)
+ return Qnil;
+ CHECK_STRING(path);
+
+ Lisp_Object x = data;
+ Lisp_Object cell, path_data;
+ while(!NILP(x))
+ {
+ cell = Fcar(x);
+ x = Fcdr(x);
+ path_data = Fcdr(cell);
+ if(!NILP(path_data) && STRINGP(Fcar(path_data))
+ && Fstring_equal(Fcar(path_data), path)
+ && NUMBERP(Fcar(cell)))
+ {
+ int const magicno = XINT(Fcar(cell));
+ data = Fdelete(cell, data);
+ if(inotify_rm_watch(inotifyfd, magicno) == -1)
+ report_file_error("Unwatch path", Qnil);
+ 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 */
diff --git a/src/lisp.h b/src/lisp.h
index 85838d1..3ed5281 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -3405,6 +3405,11 @@ EXFUN (Fxw_display_color_p, 1);
EXFUN (Fx_focus_frame, 1);
#endif
+/* Defined in filewatch.c */
+#ifdef HAVE_FILEWATCH
+extern void syms_of_filewatch (void);
+#endif
+
/* Defined in xfaces.c */
extern Lisp_Object Qdefault, Qtool_bar, Qregion, Qfringe;
extern Lisp_Object Qheader_line, Qscroll_bar, Qcursor, Qborder, Qmouse, Qmenu;
--
Joakim Verona
Rüdiger Sonderfeld
2011-06-04 16:40:43 UTC
Permalink
Post by j***@verona.se
Post by Rüdiger Sonderfeld
Hello,
I wrote a patch to support watching for file system events. It currently
only works with inotify (Linux) but it could be extended to support
kqueue (*BSD, OS X) or Win32. But I wanted to get some feedback first.
Watching for filesystem events would be very useful for, e.g., dired or
magit's status view.
Very interesting! Have you signed copyright papers and so on?
Yes. Since 25 May 2011.
Jan Djärv
2011-06-04 10:43:37 UTC
Permalink
Hi.
In general, you should not call Lisp from the read fd callback. Recursive calls into Lisp may fail. Instead post a Lisp event and handle it in keyboard.c and some Lisp code.

Jan D.
Post by Rüdiger Sonderfeld
Hello,
I wrote a patch to support watching for file system events. It currently only works with inotify (Linux) but it could be extended to support kqueue
(*BSD, OS X) or Win32. But I wanted to get some feedback first. Watching for filesystem events would be very useful for, e.g., dired or magit's status
view.
(file-watch "foo" #'(lambda (path events) (message "FS-Event: %s %s") path events)) :all)
(delete-file "foo/bar")
(file-unwatch "foo")
So please tell me what you think of this.
Regards,
Rüdiger
[PATCH] Added basic file system watching support.
It currently only works with inotify/Linux. It adds file-watch and file-unwatch functions.
---
configure.in | 14 +++
src/Makefile.in | 2 +-
src/emacs.c | 4 +
src/filewatch.c | 242 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lisp.h | 5 +
5 files changed, 266 insertions(+), 1 deletions(-)
create mode 100644 src/filewatch.c
diff --git a/configure.in b/configure.in
index 77deef8..3263876 100644
--- a/configure.in
+++ b/configure.in
@@ -169,6 +169,7 @@ OPTION_DEFAULT_ON([dbus],[don't compile with D-Bus support])
OPTION_DEFAULT_ON([gconf],[don't compile with GConf support])
OPTION_DEFAULT_ON([selinux],[don't compile with SELinux support])
OPTION_DEFAULT_ON([gnutls],[don't use -lgnutls for SSL/TLS support])
+OPTION_DEFAULT_ON([inotify],[don't compile with inotify (file-watch) support])
## For the times when you want to build Emacs but don't have
## a suitable makeinfo, and can live without the manuals.
@@ -1978,6 +1979,19 @@ fi
AC_SUBST(LIBGNUTLS_LIBS)
AC_SUBST(LIBGNUTLS_CFLAGS)
+dnl inotify is only available on GNU/Linux.
+HAVE_INOTIFY=no
+if test "${with_inotify}" = "yes"; then
+ AC_CHECK_HEADERS(sys/inotify.h)
+ if test "$ac_cv_header_sys_inotify_h" = yes ; then
+ AC_CHECK_FUNCS(inotify_init1 inotify_add_watch inotify_rm_watch, HAVE_INOTIFY=yes)
+ fi
+fi
+if test "${HAVE_INOTIFY}" = "yes"; then
+ AC_DEFINE(HAVE_INOTIFY, [1], [Define to 1 to use inotify])
+ AC_DEFINE(HAVE_FILEWATCH, [1], [Define to 1 to support filewatch])
+fi
+
dnl Do not put whitespace before the #include statements below.
dnl Older compilers (eg sunos4 cc) choke on it.
HAVE_XAW3D=no
diff --git a/src/Makefile.in b/src/Makefile.in
index e119596..0cd6d6e 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -354,7 +354,7 @@ base_obj = dispnew.o frame.o scroll.o xdisp.o menu.o $(XMENU_OBJ) window.o \
syntax.o $(UNEXEC_OBJ) bytecode.o \
process.o gnutls.o callproc.o \
region-cache.o sound.o atimer.o \
- doprnt.o intervals.o textprop.o composite.o xml.o \
+ doprnt.o intervals.o textprop.o composite.o xml.o filewatch.o \
$(MSDOS_OBJ) $(MSDOS_X_OBJ) $(NS_OBJ) $(CYGWIN_OBJ) $(FONT_OBJ)
obj = $(base_obj) $(NS_OBJC_OBJ)
diff --git a/src/emacs.c b/src/emacs.c
index 6bdd255..5ca5cda 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -1550,6 +1550,10 @@ main (int argc, char **argv)
syms_of_gnutls ();
#endif
+#ifdef HAVE_FILEWATCH
+ syms_of_filewatch ();
+#endif /* HAVE_FILEWATCH */
+
#ifdef HAVE_DBUS
syms_of_dbusbind ();
#endif /* HAVE_DBUS */
diff --git a/src/filewatch.c b/src/filewatch.c
new file mode 100644
index 0000000..5b7c130
--- /dev/null
+++ b/src/filewatch.c
@@ -0,0 +1,242 @@
+/* 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#ifdef HAVE_FILEWATCH
+
+#include <setjmp.h>
+
+#include "lisp.h"
+#include "process.h"
+
+static Lisp_Object QCmodify, QCmove, QCattrib, QCdelete, QCfrom, QCto, QCall;
+
+#ifdef HAVE_INOTIFY
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+
+enum { uninitialized = -100 };
+static int inotifyfd = uninitialized;
+
+// Assoc list of files being watched.
+static Lisp_Object data;
+
+static void
+inotify_callback(int fd, void *_, int for_read) {
+ eassert(for_read);
+ eassert(data);
+
+ int to_read = 0;
+ if(ioctl(fd, FIONREAD, &to_read) == -1)
+ report_file_error("ioctl(2) on inotify", Qnil);
+ char *const buffer = xmalloc(to_read);
+ ssize_t const n = read(fd, buffer, to_read);
+ if(n < 0)
+ report_file_error("read from inotify", Qnil);
+ else if(n < to_read)
+ {
+ // TODO
+ message1("n < to_read");
+ }
+
+ size_t i = 0;
+ while(i < (size_t)n)
+ {
+ struct inotify_event *ev = (struct inotify_event*)&buffer[i];
+
+ Lisp_Object callback = Fassoc(make_number(ev->wd), data);
+ if(!NILP(callback))
+ {
+ Lisp_Object call[3];
+ call[0] = Fcar(Fcdr(Fcdr(callback))); /* callback */
+ call[1] = Fcar(Fcdr(callback)); /* path */
+
+ make_string_from_bytes NCHARS vs. NBYTES */
+ /* ev->len seems to be an arbitrary large number
+ and not the exact length of ev->name */
+ size_t const len = strlen(ev->name);
+ /* if a directory is watched name contains the name
+ of the file that was changed */
+ Lisp_Object name = make_string_from_bytes (ev->name, len, len);
+
+ Lisp_Object 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. */
+ message("File-watch: \"%s\" will be ignored", SSDATA(call[1]));
+ data = Fdelete(callback, data);
+ }
+ if(ev->mask & IN_Q_OVERFLOW)
+ message1("File watch: Inotify Queue Overflow!");
+ }
+
+ i += sizeof(*ev) + ev->len;
+ }
+
+ free(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 PATH. If a change occurs
+CALLBACK is called with PATH 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.
+
+Use `file-unwatch' to stop watching.
+
+usage: (file-watch PATH CALLBACK &rest FLAGS) */)
+ (size_t nargs, Lisp_Object *args)
+{
+ 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("Initializing file watching", Qnil);
+ data = Qnil;
+ add_read_fd(inotifyfd, &inotify_callback, &data);
+ }
+ uint32_t mask = 0;
+ int i;
+ 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)));
+ }
+ int watchdesc = inotify_add_watch(inotifyfd, SSDATA(args[0]), mask);
+ if(watchdesc == -1) {
+ report_file_error("Watching file", Qnil);
+ }
+ data = Fcons(Fcons(make_number(watchdesc), Flist(2, args)), data);
+ return Qt;
+}
+
+DEFUN ("file-unwatch", Ffile_unwatch, Sfile_unwatch, 1, 1, 0,
+ doc: /* Stop watching a file or directory. */)
+ (Lisp_Object path)
+{
+ if(inotifyfd == uninitialized)
+ return Qnil;
+ CHECK_STRING(path);
+
+ Lisp_Object x = data;
+ Lisp_Object cell, path_data;
+ while(!NILP(x))
+ {
+ cell = Fcar(x);
+ x = Fcdr(x);
+ path_data = Fcdr(cell);
+ if(!NILP(path_data) && STRINGP(Fcar(path_data))
+ && Fstring_equal(Fcar(path_data), path)
+ && NUMBERP(Fcar(cell)))
+ {
+ int const magicno = XINT(Fcar(cell));
+ data = Fdelete(cell, data);
+ if(inotify_rm_watch(inotifyfd, magicno) == -1)
+ report_file_error("Unwatch path", Qnil);
+ 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 */
diff --git a/src/lisp.h b/src/lisp.h
index 85838d1..3ed5281 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -3405,6 +3405,11 @@ EXFUN (Fxw_display_color_p, 1);
EXFUN (Fx_focus_frame, 1);
#endif
+/* Defined in filewatch.c */
+#ifdef HAVE_FILEWATCH
+extern void syms_of_filewatch (void);
+#endif
+
/* Defined in xfaces.c */
extern Lisp_Object Qdefault, Qtool_bar, Qregion, Qfringe;
extern Lisp_Object Qheader_line, Qscroll_bar, Qcursor, Qborder, Qmouse, Qmenu;
--
1.7.5.2
Rüdiger Sonderfeld
2011-06-04 23:36:35 UTC
Permalink
Hi,
Post by Jan Djärv
In general, you should not call Lisp from the read fd callback. Recursive
calls into Lisp may fail. Instead post a Lisp event and handle it in
keyboard.c and some Lisp code.
Thanks for your advise. I changed the code to use events. I imitated the
behaviour of the dbus subsystem. I hope this is what you had in mind.

Subject: [PATCH] Added basic file system watching support.

It currently only works with inotify/Linux. It adds file-watch and file-
unwatch functions.
---
configure.in | 14 +++
src/Makefile.in | 2 +-
src/emacs.c | 4 +
src/filewatch.c | 268
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lisp.h | 5 +
5 files changed, 292 insertions(+), 1 deletions(-)
create mode 100644 src/filewatch.c

diff --git a/configure.in b/configure.in
index 06880ea..5696fa2 100644
--- a/configure.in
+++ b/configure.in
@@ -169,6 +169,7 @@ OPTION_DEFAULT_ON([dbus],[don't compile with D-Bus
support])
OPTION_DEFAULT_ON([gconf],[don't compile with GConf support])
OPTION_DEFAULT_ON([selinux],[don't compile with SELinux support])
OPTION_DEFAULT_ON([gnutls],[don't use -lgnutls for SSL/TLS support])
+OPTION_DEFAULT_ON([inotify],[don't compile with inotify (file-watch)
support])

## For the times when you want to build Emacs but don't have
## a suitable makeinfo, and can live without the manuals.
@@ -1981,6 +1982,19 @@ fi
AC_SUBST(LIBGNUTLS_LIBS)
AC_SUBST(LIBGNUTLS_CFLAGS)

+dnl inotify is only available on GNU/Linux.
+HAVE_INOTIFY=no
+if test "${with_inotify}" = "yes"; then
+ AC_CHECK_HEADERS(sys/inotify.h)
+ if test "$ac_cv_header_sys_inotify_h" = yes ; then
+ AC_CHECK_FUNCS(inotify_init1 inotify_add_watch inotify_rm_watch,
HAVE_INOTIFY=yes)
+ fi
+fi
+if test "${HAVE_INOTIFY}" = "yes"; then
+ AC_DEFINE(HAVE_INOTIFY, [1], [Define to 1 to use inotify])
+ AC_DEFINE(HAVE_FILEWATCH, [1], [Define to 1 to support filewatch])
+fi
+
dnl Do not put whitespace before the #include statements below.
dnl Older compilers (eg sunos4 cc) choke on it.
HAVE_XAW3D=no
diff --git a/src/Makefile.in b/src/Makefile.in
index c4250b9..9135e7d 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -334,7 +334,7 @@ base_obj = dispnew.o frame.o scroll.o xdisp.o menu.o
$(XMENU_OBJ) window.o \
syntax.o $(UNEXEC_OBJ) bytecode.o \
process.o gnutls.o callproc.o \
region-cache.o sound.o atimer.o \
- doprnt.o intervals.o textprop.o composite.o xml.o \
+ doprnt.o intervals.o textprop.o composite.o xml.o filewatch.o \
$(MSDOS_OBJ) $(MSDOS_X_OBJ) $(NS_OBJ) $(CYGWIN_OBJ) $(FONT_OBJ)
obj = $(base_obj) $(NS_OBJC_OBJ)

diff --git a/src/emacs.c b/src/emacs.c
index 3a7c5c0..fb734e5 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -1558,6 +1558,10 @@ main (int argc, char **argv)
syms_of_gnutls ();
#endif

+#ifdef HAVE_FILEWATCH
+ syms_of_filewatch ();
+#endif /* HAVE_FILEWATCH */
+
#ifdef HAVE_DBUS
syms_of_dbusbind ();
#endif /* HAVE_DBUS */
diff --git a/src/filewatch.c b/src/filewatch.c
new file mode 100644
index 0000000..55da25f
--- /dev/null
+++ b/src/filewatch.c
@@ -0,0 +1,268 @@
+/* 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#ifdef HAVE_FILEWATCH
+#include <setjmp.h>
+#include "lisp.h"
+#include "coding.h"
+#include "process.h"
+
+static Lisp_Object QCmodify, QCmove, QCattrib, QCdelete, QCfrom, QCto, QCall;
+
+#ifdef HAVE_INOTIFY
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+
+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-
Post by Jan Djärv
cookie)))),
+ events);
+ if(ev->mask & IN_MOVED_TO)
+ events = Fcons(Fcons(QCmove,
+ Fcons(QCto,
+ Fcons(name, make_number(ev-
Post by Jan Djärv
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 */
diff --git a/src/lisp.h b/src/lisp.h
index 8a504e8..8b21e0e 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -3430,6 +3430,11 @@ EXFUN (Fxw_display_color_p, 1);
EXFUN (Fx_focus_frame, 1);
#endif

+/* Defined in filewatch.c */
+#ifdef HAVE_FILEWATCH
+extern void syms_of_filewatch (void);
+#endif
+
/* Defined in xfaces.c */
extern Lisp_Object Qdefault, Qtool_bar, Qfringe;
extern Lisp_Object Qheader_line, Qscroll_bar, Qcursor;
--
1.7.5.2
Eli Zaretskii
2011-06-05 05:45:58 UTC
Permalink
Date: Sun, 5 Jun 2011 01:36:35 +0200
Post by Jan Djärv
In general, you should not call Lisp from the read fd callback. Recursive
calls into Lisp may fail. Instead post a Lisp event and handle it in
keyboard.c and some Lisp code.
Thanks for your advise. I changed the code to use events.
You did? Did you send the correct patch?
Rüdiger Sonderfeld
2011-06-05 09:48:07 UTC
Permalink
Post by Eli Zaretskii
Date: Sun, 5 Jun 2011 01:36:35 +0200
Post by Jan Djärv
In general, you should not call Lisp from the read fd callback.
Recursive calls into Lisp may fail. Instead post a Lisp event and
handle it in keyboard.c and some Lisp code.
Thanks for your advise. I changed the code to use events.
You did? Did you send the correct patch?
Oh. This should be the correct patch.

Subject: [PATCH] Added basic file system watching support.

It currently only works with inotify/Linux. It adds file-watch and file-unwatch functions.
---
configure.in | 14 +++
lisp/filewatch.el | 54 ++++++++++
src/Makefile.in | 2 +-
src/emacs.c | 4 +
src/filewatch.c | 305 +++++++++++++++++++++++++++++++++++++++++++++++++++++
src/keyboard.c | 28 +++++
src/lisp.h | 5 +
src/termhooks.h | 5 +
8 files changed, 416 insertions(+), 1 deletions(-)
create mode 100644 lisp/filewatch.el
create mode 100644 src/filewatch.c

diff --git a/configure.in b/configure.in
index 06880ea..5696fa2 100644
--- a/configure.in
+++ b/configure.in
@@ -169,6 +169,7 @@ OPTION_DEFAULT_ON([dbus],[don't compile with D-Bus support])
OPTION_DEFAULT_ON([gconf],[don't compile with GConf support])
OPTION_DEFAULT_ON([selinux],[don't compile with SELinux support])
OPTION_DEFAULT_ON([gnutls],[don't use -lgnutls for SSL/TLS support])
+OPTION_DEFAULT_ON([inotify],[don't compile with inotify (file-watch) support])

## For the times when you want to build Emacs but don't have
## a suitable makeinfo, and can live without the manuals.
@@ -1981,6 +1982,19 @@ fi
AC_SUBST(LIBGNUTLS_LIBS)
AC_SUBST(LIBGNUTLS_CFLAGS)

+dnl inotify is only available on GNU/Linux.
+HAVE_INOTIFY=no
+if test "${with_inotify}" = "yes"; then
+ AC_CHECK_HEADERS(sys/inotify.h)
+ if test "$ac_cv_header_sys_inotify_h" = yes ; then
+ AC_CHECK_FUNCS(inotify_init1 inotify_add_watch inotify_rm_watch, HAVE_INOTIFY=yes)
+ fi
+fi
+if test "${HAVE_INOTIFY}" = "yes"; then
+ AC_DEFINE(HAVE_INOTIFY, [1], [Define to 1 to use inotify])
+ AC_DEFINE(HAVE_FILEWATCH, [1], [Define to 1 to support filewatch])
+fi
+
dnl Do not put whitespace before the #include statements below.
dnl Older compilers (eg sunos4 cc) choke on it.
HAVE_XAW3D=no
diff --git a/lisp/filewatch.el b/lisp/filewatch.el
new file mode 100644
index 0000000..2c97589
--- /dev/null
+++ b/lisp/filewatch.el
@@ -0,0 +1,54 @@
+;;; filewatch.el --- Elisp support for watching filesystem events.
+
+;; Copyright (C) 2011 Free Software Foundation, Inc.
+
+;; Author: Rüdiger Sonderfeld <***@c-plusplus.de>
+;; Keywords: files
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file contains the elisp part of the filewatch API.
+
+;;; Code:
+
+(eval-when-compile
+ (require 'cl))
+
+(defun filewatch-event-p (event)
+ "Check if EVENT is a filewatch event."
+ (and (listp event)
+ (eq (car event) 'filewatch-event)))
+
+;;;###autoload
+(defun filewatch-handle-event (event)
+ "Handle file system monitoring event.
+If EVENT is a filewatch event then the callback is called. If EVENT is
+not a filewatch event then a `filewatch-error' is signaled."
+ (interactive "e")
+
+ (unless (filewatch-event-p event)
+ (signal 'filewatch-error (cons "Not a valid filewatch event" event)))
+
+ (loop for ev in (cdr event)
+ unless (and (listp ev) (>= (length ev) 3))
+ do (signal 'filewatch-error (cons "Not a valid filewatch event" event))
+ do (funcall (cadr ev) (car ev) (caddr ev))))
+
+(provide 'filewatch)
+
+;;; filewatch.el ends here
diff --git a/src/Makefile.in b/src/Makefile.in
index c4250b9..9135e7d 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -334,7 +334,7 @@ base_obj = dispnew.o frame.o scroll.o xdisp.o menu.o $(XMENU_OBJ) window.o \
syntax.o $(UNEXEC_OBJ) bytecode.o \
process.o gnutls.o callproc.o \
region-cache.o sound.o atimer.o \
- doprnt.o intervals.o textprop.o composite.o xml.o \
+ doprnt.o intervals.o textprop.o composite.o xml.o filewatch.o \
$(MSDOS_OBJ) $(MSDOS_X_OBJ) $(NS_OBJ) $(CYGWIN_OBJ) $(FONT_OBJ)
obj = $(base_obj) $(NS_OBJC_OBJ)

diff --git a/src/emacs.c b/src/emacs.c
index 3a7c5c0..fb734e5 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -1558,6 +1558,10 @@ main (int argc, char **argv)
syms_of_gnutls ();
#endif

+#ifdef HAVE_FILEWATCH
+ syms_of_filewatch ();
+#endif /* HAVE_FILEWATCH */
+
#ifdef HAVE_DBUS
syms_of_dbusbind ();
#endif /* HAVE_DBUS */
diff --git a/src/filewatch.c b/src/filewatch.c
new file mode 100644
index 0000000..56b79b7
--- /dev/null
+++ b/src/filewatch.c
@@ -0,0 +1,305 @@
+/* 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#ifdef HAVE_FILEWATCH
+#include <setjmp.h> /* 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, Qunkown_aspect;
+
+#ifdef HAVE_INOTIFY
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+
+enum { uninitialized = -100 };
+/* File handle for inotify. */
+static int inotifyfd = uninitialized;
+
+/* Assoc list of files being watched. */
+static Lisp_Object watch_list;
+
+/* 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 callback = Fassoc (make_number (ev->wd), watch_list);
+ if (!NILP (callback))
+ {
+ size_t len;
+ Lisp_Object name, events, event_info;
+
+ /* 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 (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 (!NILP (events))
+ {
+ Lisp_Object args[2], event_info;
+ args[0] = Fcdr (callback);
+ args[1] = Fcons (events, Qnil);
+ event_info = Fcons (Fappend (2, args), Qnil);
+ if (NILP (event.arg))
+ event.arg = event_info;
+ else
+ {
+ args[0] = event.arg;
+ args[1] = event_info;
+ event.arg = Fappend (2, args);
+ }
+ }
+
+ 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 (callback, 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)
+{
+ 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 (EQ (symb, Qt))
+ return IN_MODIFY | IN_CREATE | IN_MOVE_SELF | IN_MOVE | IN_ATTRIB
+ | IN_DELETE_SELF | IN_DELETE;
+ else
+ Fsignal (Qunkown_aspect, Fcons (symb, 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:
+
+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.
+
+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 (file event) (message "%s %s" file event)))
+
+Use `file-unwatch' to stop watching.
+
+usage: (file-watch FILENAME ASPECT CALLBACK) */)
+ (Lisp_Object file_name, Lisp_Object aspect, Lisp_Object callback)
+{
+ uint32_t mask;
+ size_t i;
+ int watchdesc;
+ Lisp_Object decoded_file_name;
+
+ 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);
+ }
+
+ /* Convert ASPECT into the inotify event mask. */
+ if (CONSP (aspect))
+ {
+ Lisp_Object x = aspect;
+ mask = 0;
+ while (!NILP(x))
+ {
+ mask |= symbol_to_inotifymask (Fcar (x));
+ x = Fcdr(x);
+ }
+ }
+ else
+ mask = symbol_to_inotifymask(aspect);
+
+ 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));
+ /* TODO: check if file is already in the watch_list. */
+ watch_list = Fcons (Fcons (make_number (watchdesc),
+ Fcons (file_name, Fcons (callback, Qnil))),
+ 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)
+{
+ DEFSYM (Qmodify, "modify");
+ DEFSYM (Qmove, "move");
+ DEFSYM (Qattrib, "attrib");
+ DEFSYM (Qdelete, "delete");
+ DEFSYM (Qfrom, "from");
+ DEFSYM (Qto, "to");
+
+ DEFSYM (Qunkown_aspect, "unkown-aspect");
+
+ defsubr (&Sfile_watch);
+ defsubr (&Sfile_unwatch);
+}
+
+#endif /* HAVE_FILEWATCH */
diff --git a/src/keyboard.c b/src/keyboard.c
index 7bc406a..ccf25f7 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -329,6 +329,9 @@ static Lisp_Object Qsave_session;
#ifdef HAVE_DBUS
static Lisp_Object Qdbus_event;
#endif
+#ifdef HAVE_FILEWATCH
+static Lisp_Object Qfilewatch_event;
+#endif /* HAVE_FILEWATCH */
static Lisp_Object Qconfig_changed_event;

/* Lisp_Object Qmouse_movement; - also an event header */
@@ -4017,6 +4020,13 @@ kbd_buffer_get_event (KBOARD **kbp,
kbd_fetch_ptr = event + 1;
}
#endif
+#ifdef HAVE_FILEWATCH
+ else if (event->kind == FILEWATCH_EVENT)
+ {
+ obj = make_lispy_event (event);
+ kbd_fetch_ptr = event + 1;
+ }
+#endif
else if (event->kind == CONFIG_CHANGED_EVENT)
{
obj = make_lispy_event (event);
@@ -5913,6 +5923,13 @@ make_lispy_event (struct input_event *event)
}
#endif /* HAVE_DBUS */

+#ifdef HAVE_FILEWATCH
+ case FILEWATCH_EVENT:
+ {
+ return Fcons (Qfilewatch_event, event->arg);
+ }
+#endif /* HAVE_FILEWATCH */
+
case CONFIG_CHANGED_EVENT:
return Fcons (Qconfig_changed_event,
Fcons (event->arg,
@@ -11524,6 +11541,10 @@ syms_of_keyboard (void)
DEFSYM (Qdbus_event, "dbus-event");
#endif

+#ifdef HAVE_FILEWATCH
+ DEFSYM (Qfilewatch_event, "filewatch-event");
+#endif /* HAVE_FILEWATCH */
+
DEFSYM (QCenable, ":enable");
DEFSYM (QCvisible, ":visible");
DEFSYM (QChelp, ":help");
@@ -12274,6 +12295,13 @@ keys_of_keyboard (void)
"dbus-handle-event");
#endif

+#ifdef HAVE_FILEWATCH
+ /* Define a special event which is raised for filewatch callback
+ functions. */
+ initial_define_lispy_key (Vspecial_event_map, "filewatch-event",
+ "filewatch-handle-event");
+#endif /* HAVE_FILEWATCH */
+
initial_define_lispy_key (Vspecial_event_map, "config-changed-event",
"ignore");
}
diff --git a/src/lisp.h b/src/lisp.h
index 8a504e8..8b21e0e 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -3430,6 +3430,11 @@ EXFUN (Fxw_display_color_p, 1);
EXFUN (Fx_focus_frame, 1);
#endif

+/* Defined in filewatch.c */
+#ifdef HAVE_FILEWATCH
+extern void syms_of_filewatch (void);
+#endif
+
/* Defined in xfaces.c */
extern Lisp_Object Qdefault, Qtool_bar, Qfringe;
extern Lisp_Object Qheader_line, Qscroll_bar, Qcursor;
diff --git a/src/termhooks.h b/src/termhooks.h
index 6a58517..9db2019 100644
--- a/src/termhooks.h
+++ b/src/termhooks.h
@@ -206,6 +206,11 @@ enum event_kind
, NS_NONKEY_EVENT
#endif

+#ifdef HAVE_FILEWATCH
+ /* File or directory was changed. */
+ , FILEWATCH_EVENT
+#endif
+
};

/* If a struct input_event has a kind which is SELECTION_REQUEST_EVENT
--
1.7.5.2
Jan Djärv
2011-06-05 07:48:19 UTC
Permalink
Post by Rüdiger Sonderfeld
Hi,
Post by Jan Djärv
In general, you should not call Lisp from the read fd callback. Recursive
calls into Lisp may fail. Instead post a Lisp event and handle it in
keyboard.c and some Lisp code.
Thanks for your advise. I changed the code to use events. I imitated the
behaviour of the dbus subsystem. I hope this is what you had in mind.
As Eli already said, it doesn't appear in the patch you sent.

Jan D.
Andreas Schwab
2011-06-05 07:54:20 UTC
Permalink
What happens if inotify is not available and you call file-watch and
then file-unwatch?

Andreas.
--
Andreas Schwab, ***@linux-m68k.org
GPG Key fingerprint = 58CA 54C7 6D53 942B 1756 01D3 44D5 214B 8276 4ED5
"And now for something completely different."
Rüdiger Sonderfeld
2011-06-05 09:49:38 UTC
Permalink
Post by Andreas Schwab
What happens if inotify is not available and you call file-watch and
then file-unwatch?
Andreas.
Oh you are right. I should reset inotifyfd to uninitialized in case of error.
I changed this in Version 3 of the patch. Thanks!
John Yates
2011-06-05 15:59:03 UTC
Permalink
Is there a good reason not to provide access to the full functionality of
the inotify API?

- Why conflate IN_MODIFY and IN_CREATE?
- Why conflate IN_DELETE and IN_DELETE_SELF?
- Why omit IN_OPEN, IN_ACCESS, IN_CLOSE_WRITE and IN_CLOSE_NOWRITE?

/john
Rüdiger Sonderfeld
2011-06-05 16:14:33 UTC
Permalink
Hi,
Post by John Yates
Is there a good reason not to provide access to the full functionality of
the inotify API?
Yes, the reason is portability. I want the same API to be available on Linux,
*BSD, OS X and Windows. inotify is the most advanced API and therefore I can
not support every inotify feature if I want to stay compatible.

Porting to kqueue (*BSD, OS X) will be especially troublesome because they
don't seem to tell you which file actually changed if you watch a directory.

Regards,
Rüdiger
Eli Zaretskii
2011-06-05 16:58:07 UTC
Permalink
Date: Sun, 5 Jun 2011 18:14:33 +0200
=20
Is there a good reason not to provide access to the full function=
ality of
the inotify API?
=20
Yes, the reason is portability. I want the same API to be available=
on Linux,=20
*BSD, OS X and Windows. inotify is the most advanced API and theref=
ore I can=20
not support every inotify feature if I want to stay compatible.
Being compatible does not mean providing only the least common
denominator. We have already several features that provide different
levels of support depending on the underlying facilities. One exampl=
e
is process-attributes (which is the main primitive on which Proced is
based). All you need is provide a superset of all the supported
attributes, and document which ones are supported on which platform.
Eli Zaretskii
2011-06-06 19:57:50 UTC
Permalink
Date: Mon, 6 Jun 2011 20:56:10 +0200
=20
I could add the IN_OPEN, IN_ACCESS, IN_CLOSE_WRITE and IN_CLOSE_NOW=
RITE=20
feature. But having separate IN_MODIFY, IN_CREATE and IN_DELETE,=
=20
IN_DELETE_SELF would be problematic because they can't be separated=
for=20
kqueue. Maybe I should just export the basic inotify and kqueue API=
and=20
implement a portable layer on top of it in elisp.
I trust you to make the right decision. I just wanted you to know yo=
u
shouldn't confine yourself to a subset for portability's sake, in thi=
s
case.
Rüdiger Sonderfeld
2011-06-06 18:56:10 UTC
Permalink
Post by Eli Zaretskii
Date: Sun, 5 Jun 2011 18:14:33 +0200
Post by John Yates
Is there a good reason not to provide access to the full functionality
of the inotify API?
Yes, the reason is portability. I want the same API to be available on
Linux, *BSD, OS X and Windows. inotify is the most advanced API and
therefore I can not support every inotify feature if I want to stay
compatible.
Being compatible does not mean providing only the least common
denominator. We have already several features that provide different
levels of support depending on the underlying facilities. One example
is process-attributes (which is the main primitive on which Proced is
based). All you need is provide a superset of all the supported
attributes, and document which ones are supported on which platform.
I could add the IN_OPEN, IN_ACCESS, IN_CLOSE_WRITE and IN_CLOSE_NOWRITE
feature. But having separate IN_MODIFY, IN_CREATE and IN_DELETE,
IN_DELETE_SELF would be problematic because they can't be separated for
kqueue. Maybe I should just export the basic inotify and kqueue API and
implement a portable layer on top of it in elisp.

Regards,
Rüdiger
Eli Zaretskii
2011-06-04 11:30:42 UTC
Permalink
Date: Sat, 4 Jun 2011 00:34:15 +0200
=20
I wrote a patch to support watching for file system events. It curr=
ently only works with inotify (Linux) but it could be extended to sup=
port kqueue=20
(*BSD, OS X) or Win32. But I wanted to get some feedback first. Wat=
ching for filesystem events would be very useful for, e.g., dired or =
magit's status=20
view.
Thanks. A few comments below.
+#include <setjmp.h>
Why do you need this header?
+// Assoc list of files being watched.
We don't use C++-style comments, because we don't require C9x compile=
r
to build Emacs.
+static Lisp_Object data;
The name "data" is too generic. Use something more specific and
descriptive, like inotify_watch_alist.
+static void=20
+inotify_callback(int fd, void *_, int for_read) {
What's that _ in the argument list?

Also, the style of braces is not according to GNU coding standards.
+ if(ioctl(fd, FIONREAD, &to_read) =3D=3D -1)
+ report_file_error("ioctl(2) on inotify", Qnil);
Error messages that are shown to the user should not be cryptic.
Please use some text that would make sense to a user who is not an
expert on inotify. I would think something like this would be
appropriate:

File watching feature (inotify) is not available
+ char *const buffer =3D xmalloc(to_read);
+ ssize_t const n =3D read(fd, buffer, to_read);
Can't declare locals in the middle of code: this isn't C++.
+ if(n < 0)
+ report_file_error("read from inotify", Qnil);
The text of this message also needs work, to make it understandable b=
y
mere mortals.
+ size_t i =3D 0;
C++/C9x again.
+ while(i < (size_t)n)
I think this cast, in addition to being ugly, is also unneeded,
because you already verified above that n is positive. So `i' can be
ssize_t as well, and the need for the cast is gone.
+ call[1] =3D Fcar(Fcdr(callback)); /* path */
GNU coding standards frown on using "path" for anything except
PATH-style lists of directories separated by colons. Use "file name"
instead.
+ make_string_from_bytes NCHARS vs. NBYTES */
Not make_string_from_bytes, but DECODE_FILE.
+ size_t const len =3D strlen(ev->name);
C9x again.
+ /* if a directory is watched name contains the name
+ of the file that was changed */
Comments should begin with a capital letter and end with a period and
2 blanks.
+ if(ev->mask & IN_IGNORED)
+ {
+ /* Event was removed automatically: Drop it from dat=
a list. */
+ message("File-watch: \"%s\" will be ignored", SSDATA=
(call[1]));
+ data =3D Fdelete(callback, data);
+ }
Do we really need this message? Consider outputting it only to the
*Messages* buffer.
+ if(ev->mask & IN_Q_OVERFLOW)
+ message1("File watch: Inotify Queue Overflow!");
Same here.
+ free(buffer);
xfree, not free.
+Watching a directory is not recursive. CALLBACK receives the event=
s as a list
+with each list element being a list containing information about a=
n event. The
+first element is a flag symbol. If a directory is watched the seco=
nd element is
+the name of the file that changed. If a file is moved from or to t=
he directory
+the second element is either :from or :to and the third element is=
the file
+name.
Please leave two blanks after a period that ends a sentence. Also, I
think it's best to show the forms of the non-primitive arguments to
CALLBACK, rather than trying to describe them: a picture is worth a
thousand words. Finally, the lines are too long.
A fourth element contains a numeric identifier (cookie) that ca=
n be used
+to identify matching move operations if a file is moved inside the=
directory.

This part is extremely unclear. How about an example?
+ add_read_fd(inotifyfd, &inotify_callback, &data);
I don't see any code that does the corresponding delete_read_fd.
+ uint32_t mask =3D 0;
Why not `unsigned'? uint32_t is not part of C90, so it's less
portable.
+ int watchdesc =3D inotify_add_watch(inotifyfd, SSDATA(args[0]), =
mask);

Declaration in the middle of code.

More importantly, you cannot pass to inotify_add_watch a string in it=
s
internal Emacs representation. You need to run it through ENCODE_FIL=
E
first.
+ if(watchdesc =3D=3D -1) {
+ report_file_error("Watching file", Qnil);
Again, this error message text is not helpful. It should also mentio=
n
the file's name.
+ (Lisp_Object path)
+{
+ if(inotifyfd =3D=3D uninitialized)
+ return Qnil;
+ CHECK_STRING(path);
+
+ Lisp_Object x =3D data;
+ Lisp_Object cell, path_data;
`path' and `path_data' again go against the GNU coding standards wrt
using "path".
+ while(!NILP(x))
+ {
+ cell =3D Fcar(x);
+ x =3D Fcdr(x);
+ path_data =3D Fcdr(cell);
??? Isn't `data' an alist? If so, why not use Fassq etc.?
+ if(!NILP(path_data) && STRINGP(Fcar(path_data))
+ && Fstring_equal(Fcar(path_data), path)
+ && NUMBERP(Fcar(cell)))
+ {
+ int const magicno =3D XINT(Fcar(cell));
+ data =3D Fdelete(cell, data);
+ if(inotify_rm_watch(inotifyfd, magicno) =3D=3D -1)
+ report_file_error("Unwatch path", Qnil);
+ return Qt;
+ }
+ }
I think this should call delete_read_fd when `data' becomes empty.
Rüdiger Sonderfeld
2011-06-04 17:13:08 UTC
Permalink
Post by Eli Zaretskii
Thanks. A few comments below.
Thank you for your comments! I updated the patch and I hope I fixed everything
except the things noted below.
Post by Eli Zaretskii
Post by Rüdiger Sonderfeld
+#include <setjmp.h>
Why do you need this header?
"lisp.h" requires it. (lisp.h:2034:3: error: expected specifier-qualifier-list
before ‘jmp_buf’)
Post by Eli Zaretskii
Post by Rüdiger Sonderfeld
+static void
+inotify_callback(int fd, void *_, int for_read) {
What's that _ in the argument list?
I ignore this argument.
Post by Eli Zaretskii
Post by Rüdiger Sonderfeld
+ while(i < (size_t)n)
I think this cast, in addition to being ugly, is also unneeded,
because you already verified above that n is positive. So `i' can be
ssize_t as well, and the need for the cast is gone.
I think changing i to ssize_t is the wrong way around, because i should never
be negative.
Post by Eli Zaretskii
Post by Rüdiger Sonderfeld
+ if(ev->mask & IN_IGNORED)
+ {
+ /* Event was removed automatically: Drop it from data
list. */ + message("File-watch: \"%s\" will be ignored",
SSDATA(call[1])); + data = Fdelete(callback, data);
+ }
Do we really need this message? Consider outputting it only to the
*Messages* buffer.
Post by Rüdiger Sonderfeld
+ if(ev->mask & IN_Q_OVERFLOW)
+ message1("File watch: Inotify Queue Overflow!");
Same here.
I changes it to use add_to_log. I'm not sure if there is a better way to do
it.
Post by Eli Zaretskii
Post by Rüdiger Sonderfeld
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.
This part is extremely unclear. How about an example?
Post by Rüdiger Sonderfeld
+ uint32_t mask = 0;
Why not `unsigned'? uint32_t is not part of C90, so it's less
portable.
It's the type used in the inotify structure.
Post by Eli Zaretskii
Post by Rüdiger Sonderfeld
+ while(!NILP(x))
+ {
+ cell = Fcar(x);
+ x = Fcdr(x);
+ path_data = Fcdr(cell);
??? Isn't `data' an alist? If so, why not use Fassq etc.?
Because I have to query for the file name and the structure of data is
((magicno . (filename callback)) ...)



Subject: [PATCH] Added basic file system watching support.

It currently only works with inotify/Linux. It adds file-watch and file-unwatch functions.
---
configure.in | 14 +++
src/Makefile.in | 2 +-
src/emacs.c | 4 +
src/filewatch.c | 268 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lisp.h | 5 +
5 files changed, 292 insertions(+), 1 deletions(-)
create mode 100644 src/filewatch.c

diff --git a/configure.in b/configure.in
index 06880ea..5696fa2 100644
--- a/configure.in
+++ b/configure.in
@@ -169,6 +169,7 @@ OPTION_DEFAULT_ON([dbus],[don't compile with D-Bus support])
OPTION_DEFAULT_ON([gconf],[don't compile with GConf support])
OPTION_DEFAULT_ON([selinux],[don't compile with SELinux support])
OPTION_DEFAULT_ON([gnutls],[don't use -lgnutls for SSL/TLS support])
+OPTION_DEFAULT_ON([inotify],[don't compile with inotify (file-watch) support])

## For the times when you want to build Emacs but don't have
## a suitable makeinfo, and can live without the manuals.
@@ -1981,6 +1982,19 @@ fi
AC_SUBST(LIBGNUTLS_LIBS)
AC_SUBST(LIBGNUTLS_CFLAGS)

+dnl inotify is only available on GNU/Linux.
+HAVE_INOTIFY=no
+if test "${with_inotify}" = "yes"; then
+ AC_CHECK_HEADERS(sys/inotify.h)
+ if test "$ac_cv_header_sys_inotify_h" = yes ; then
+ AC_CHECK_FUNCS(inotify_init1 inotify_add_watch inotify_rm_watch, HAVE_INOTIFY=yes)
+ fi
+fi
+if test "${HAVE_INOTIFY}" = "yes"; then
+ AC_DEFINE(HAVE_INOTIFY, [1], [Define to 1 to use inotify])
+ AC_DEFINE(HAVE_FILEWATCH, [1], [Define to 1 to support filewatch])
+fi
+
dnl Do not put whitespace before the #include statements below.
dnl Older compilers (eg sunos4 cc) choke on it.
HAVE_XAW3D=no
diff --git a/src/Makefile.in b/src/Makefile.in
index c4250b9..9135e7d 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -334,7 +334,7 @@ base_obj = dispnew.o frame.o scroll.o xdisp.o menu.o $(XMENU_OBJ) window.o \
syntax.o $(UNEXEC_OBJ) bytecode.o \
process.o gnutls.o callproc.o \
region-cache.o sound.o atimer.o \
- doprnt.o intervals.o textprop.o composite.o xml.o \
+ doprnt.o intervals.o textprop.o composite.o xml.o filewatch.o \
$(MSDOS_OBJ) $(MSDOS_X_OBJ) $(NS_OBJ) $(CYGWIN_OBJ) $(FONT_OBJ)
obj = $(base_obj) $(NS_OBJC_OBJ)

diff --git a/src/emacs.c b/src/emacs.c
index 3a7c5c0..fb734e5 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -1558,6 +1558,10 @@ main (int argc, char **argv)
syms_of_gnutls ();
#endif

+#ifdef HAVE_FILEWATCH
+ syms_of_filewatch ();
+#endif /* HAVE_FILEWATCH */
+
#ifdef HAVE_DBUS
syms_of_dbusbind ();
#endif /* HAVE_DBUS */
diff --git a/src/filewatch.c b/src/filewatch.c
new file mode 100644
index 0000000..55da25f
--- /dev/null
+++ b/src/filewatch.c
@@ -0,0 +1,268 @@
+/* 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#ifdef HAVE_FILEWATCH
+#include <setjmp.h>
+#include "lisp.h"
+#include "coding.h"
+#include "process.h"
+
+static Lisp_Object QCmodify, QCmove, QCattrib, QCdelete, QCfrom, QCto, QCall;
+
+#ifdef HAVE_INOTIFY
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+
+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 */
diff --git a/src/lisp.h b/src/lisp.h
index 8a504e8..8b21e0e 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -3430,6 +3430,11 @@ EXFUN (Fxw_display_color_p, 1);
EXFUN (Fx_focus_frame, 1);
#endif

+/* Defined in filewatch.c */
+#ifdef HAVE_FILEWATCH
+extern void syms_of_filewatch (void);
+#endif
+
/* Defined in xfaces.c */
extern Lisp_Object Qdefault, Qtool_bar, Qfringe;
extern Lisp_Object Qheader_line, Qscroll_bar, Qcursor;
--
1.7.5.2
Eli Zaretskii
2011-06-04 19:15:12 UTC
Permalink
Date: Sat, 4 Jun 2011 19:13:08 +0200
=20
Thank you for your comments! I updated the patch and I hope I fixed=
everything
except the things noted below.
+ if(watchdesc =3D=3D -1) {
+ report_file_error("Could not watch file", Fcons(args[0], Qnil)=
);
+ }
The doc string still uses lines that are too long.

See also Jan's important comment about how to expose these events to
the rest of Emacs.
Thien-Thi Nguyen
2011-06-04 20:10:45 UTC
Permalink
() Rüdiger Sonderfeld <***@c-plusplus.de>
() Sat, 4 Jun 2011 19:13:08 +0200

+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.

I think FLAGS should be symbols, not keywords. Furthermore, "flags"
is too generic; maybe something like "aspect" or even "what" would be
(slightly) better. Instead of ‘:all’ (or ‘all’), consider using ‘t’.

Also, it's cool if the first line names all the args. How about:
"Arrange to call FUNC if ASPECT of FILENAME changes."
?

+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)

For upward-compatability, it is better to not use rest-args for these
aspects. You should specify one arg ASPECT, document that it can be
"either a symbol, one of: ..., or a list of of these symbols".

Style preference: I'd prefer CALLBACK to be last so that long lambda
expressions need not be (visually) scanned to determine the aspects watched.
Consider:

(file-watch "foo" t (lambda (name event)
;; LONG
;; COMPLICATED
;; DRAWN-OUT
;; COMPUTATION
))

vs

(file-watch "foo" (lambda (name event)
;; LONG
;; COMPLICATED
;; DRAWN-OUT
;; COMPUTATION
)
t)

No big deal, just sympathy for the lone t.

+ CHECK_STRING(args[0]);

Please insert a space between a function (or macro) and its arg list.
Generally, (info "(standards) Formatting").

+ else /* TODO: should this be an error? */

Yes.

+ /* TODO: check if file is already in the watch_list. */

Moreover, you need to decide what to do given, for example:
(progn (file-watch "foo" 'modify 'notify-modify)
(file-watch "foo" 'move 'notify-move))
Is this OK, is this an error, is this "close to" an error?
I think a simple "file is already in" check is insufficient.

+ watch_list = Fcons(Fcons(make_number(watchdesc), Flist(2, args)), watch_list);

You can use ‘acons’: (acons K V ALIST) ≡ (cons (cons K V) ALIST).
Thien-Thi Nguyen
2011-06-05 02:15:44 UTC
Permalink
() Rüdiger Sonderfeld <***@c-plusplus.de>
() Sun, 5 Jun 2011 01:43:16 +0200

Yes, this is a bit complicated. The inotify API itself does not create a
separate event number if you try to watch the same file. But this could of
course be done in the implementation by checking which callback corresponds to
which event. But I'm not sure if this is the right way to handle it.

[...]

Sadly acons is not available from the C API.

Perhaps the book-keeping parts should be moved to Lisp.
Štěpán Němec
2011-06-05 09:22:18 UTC
Permalink
Post by Thien-Thi Nguyen
() Sun, 5 Jun 2011 01:43:16 +0200
[...]
Post by Thien-Thi Nguyen
Sadly acons is not available from the C API.
Perhaps the book-keeping parts should be moved to Lisp.
It's not really available from Lisp either. It's a cl.el function, i.e.
still a forbidden territory for GNU Emacs core code.

That alone doesn't necessarily affect validity of your Lisp-moving
suggestion, of course.

Štěpán
Johan Bockgård
2011-06-15 20:53:54 UTC
Permalink
Post by Štěpán Němec
Post by Thien-Thi Nguyen
() Sun, 5 Jun 2011 01:43:16 +0200
[...]
Post by Thien-Thi Nguyen
Sadly acons is not available from the C API.
Perhaps the book-keeping parts should be moved to Lisp.
It's not really available from Lisp either. It's a cl.el function, i.e.
still a forbidden territory for GNU Emacs core code.
FWIW, cl arranges for acons to be inlined at compile time, so it's
enough to

(eval-when-compile (require 'cl))
Štěpán Němec
2011-06-16 08:52:30 UTC
Permalink
Post by Johan Bockgård
FWIW, cl arranges for acons to be inlined at compile time, so it's
enough to
(eval-when-compile (require 'cl))
Interesting, thank you!

Is there a list of such exceptions somewhere? Inspecting the symbol's
plist or trying to byte-compile such functions and see if the warning
pops up or not hardly seems practical even for true GNU-fearing folk.

Štěpán
Rüdiger Sonderfeld
2011-06-04 23:43:16 UTC
Permalink
Hi,
thank your for your comments. I changed the arguments and changed an unkown
aspect into an error. See my reply to Jan Djärv for the version of the patch
containing the changes.
Post by Rüdiger Sonderfeld
+ /* TODO: check if file is already in the watch_list. */
(progn (file-watch "foo" 'modify 'notify-modify)
(file-watch "foo" 'move 'notify-move))
Is this OK, is this an error, is this "close to" an error?
I think a simple "file is already in" check is insufficient.
Yes, this is a bit complicated. The inotify API itself does not create a
separate event number if you try to watch the same file. But this could of
course be done in the implementation by checking which callback corresponds to
which event. But I'm not sure if this is the right way to handle it.
Post by Rüdiger Sonderfeld
+ watch_list = Fcons(Fcons(make_number(watchdesc), Flist(2, args)), watch_list);
You can use ‘acons’: (acons K V ALIST) ≡ (cons (cons K V) ALIST).
Sadly acons is not available from the C API.
Stefan Monnier
2011-06-06 15:21:35 UTC
Permalink
Thanks for your review, I generally agree with your comments.
Post by Thien-Thi Nguyen
(progn (file-watch "foo" 'modify 'notify-modify)
(file-watch "foo" 'move 'notify-move))
Is this OK, is this an error, is this "close to" an error?
I think dired shouldn't need to know if some other package decided to
watch the same directory, so having several watchers for the same file
should be accepted and work correctly, i.e. both callbacks should be run
when needed.
Post by Thien-Thi Nguyen
I think a simple "file is already in" check is insufficient.
+ watch_list = Fcons(Fcons(make_number(watchdesc), Flist(2, args)), watch_list);
You can use ‘acons’: (acons K V ALIST) ≡ (cons (cons K V) ALIST).
No, Fcons is the right thing to use there.
We could #define ACONS(a,b,c) Fcons(Fcons(a,b),c)
but that's orthogonal to this patch.


Stefan
Rüdiger Sonderfeld
2011-06-06 16:25:24 UTC
Permalink
Post by Stefan Monnier
Thanks for your review, I generally agree with your comments.
Post by Thien-Thi Nguyen
(progn (file-watch "foo" 'modify 'notify-modify)
(file-watch "foo" 'move 'notify-move))
Is this OK, is this an error, is this "close to" an error?
I think dired shouldn't need to know if some other package decided to
watch the same directory, so having several watchers for the same file
should be accepted and work correctly, i.e. both callbacks should be run
when needed.
The problem is: How to implement the file-unwatch then? I need a way to
identify each separate file-watch request. What would be the best way to do
that?

Regards,
Rüdiger
Stefan Monnier
2011-06-06 17:11:12 UTC
Permalink
Post by Rüdiger Sonderfeld
The problem is: How to implement the file-unwatch then? I need a way
to identify each separate file-watch request. What would be the best
way to do that?
How about letting file-watch return a "file-watcher" which you then need
to pass to file-unwatch? This "file-watcher" could be any kind of Elisp
data you find convenient for this. You may decide to provide no other
operation than file-unwatch, but you could also decide to provided
additional operations such as changing the callback (I'm not saying
that would necessarily be a good idea, tho, but maybe other operations
would be handy).


Stefan
Ted Zlatanov
2011-06-06 20:16:14 UTC
Permalink
Post by Rüdiger Sonderfeld
The problem is: How to implement the file-unwatch then? I need a way
to identify each separate file-watch request. What would be the best
way to do that?
SM> How about letting file-watch return a "file-watcher" which you then need
SM> to pass to file-unwatch? This "file-watcher" could be any kind of Elisp
SM> data you find convenient for this. You may decide to provide no other
SM> operation than file-unwatch, but you could also decide to provided
SM> additional operations such as changing the callback (I'm not saying
SM> that would necessarily be a good idea, tho, but maybe other operations
SM> would be handy).

(background: url-future.el is really a generic futures library, but the
only expected use for it currently is in url-*.el)

The "file-watcher" could derive from url-future, with the nice
error-catching and accessor functions that come with it, plus it's
protected from double-invocation.

Ted
Stefan Monnier
2011-06-07 14:42:40 UTC
Permalink
Post by Ted Zlatanov
Post by Rüdiger Sonderfeld
The problem is: How to implement the file-unwatch then? I need a way
to identify each separate file-watch request. What would be the best
way to do that?
SM> How about letting file-watch return a "file-watcher" which you then need
SM> to pass to file-unwatch? This "file-watcher" could be any kind of Elisp
SM> data you find convenient for this. You may decide to provide no other
SM> operation than file-unwatch, but you could also decide to provided
SM> additional operations such as changing the callback (I'm not saying
SM> that would necessarily be a good idea, tho, but maybe other operations
SM> would be handy).
Post by Ted Zlatanov
(background: url-future.el is really a generic futures library, but the
only expected use for it currently is in url-*.el)
The "file-watcher" could derive from url-future, with the nice
error-catching and accessor functions that come with it, plus it's
protected from double-invocation.
That doesn't sound right to me. At least I'm having trouble figuring
out how to bend my mind such that this file-watcher matches (even
coarsely) the concept of a "future".


Stefan
Ted Zlatanov
2011-06-07 16:46:26 UTC
Permalink
Post by Ted Zlatanov
Post by Rüdiger Sonderfeld
The problem is: How to implement the file-unwatch then? I need a way
to identify each separate file-watch request. What would be the best
way to do that?
SM> How about letting file-watch return a "file-watcher" which you then need
SM> to pass to file-unwatch? This "file-watcher" could be any kind of Elisp
SM> data you find convenient for this. You may decide to provide no other
SM> operation than file-unwatch, but you could also decide to provided
SM> additional operations such as changing the callback (I'm not saying
SM> that would necessarily be a good idea, tho, but maybe other operations
SM> would be handy).
Post by Ted Zlatanov
(background: url-future.el is really a generic futures library, but the
only expected use for it currently is in url-*.el)
The "file-watcher" could derive from url-future, with the nice
error-catching and accessor functions that come with it, plus it's
protected from double-invocation.
SM> That doesn't sound right to me. At least I'm having trouble figuring
SM> out how to bend my mind such that this file-watcher matches (even
SM> coarsely) the concept of a "future".

In this case it's just a place to stash the callback, with protection
against double invocation and error catching. Each instance holds a
separate `file-watch' request.

`file-unwatch' simply invokes the lambda to get the value to deregister.

So `file-watch' would return

(make-url-future :value (lambda () (unwatch file here))
:errorback (lambda (&rest d) (handle unwatch errors))
:callback (lambda (f) (arbitrary callback)))

Which, in turn, can be used (say it's saved in `f'):

(url-future-call f) ; funcall :value
(url-future-value good) ; whatever (unwatch file here) returned
(url-future-done-p f) ; true
(url-future-completed-p f) ; true if no errors, plus :callback was called
(url-future-errored-p f) ; true if errors, plus :errorback was called

(url-future-call f) ; throws an error

I think that's more convenient than a new special "file-watcher" type,
especially since the "file-watcher" can be derived from "url-future" and
inherit all the functions above under its own namespace, plus of course
any fields it cares to define on top. The built-in support for
callbacks and error callbacks is pretty nice too.

Ted
Stefan Monnier
2011-06-07 18:06:51 UTC
Permalink
SM> That doesn't sound right to me. At least I'm having trouble figuring
SM> out how to bend my mind such that this file-watcher matches (even
SM> coarsely) the concept of a "future".
[...]
Post by Ted Zlatanov
So `file-watch' would return
(make-url-future :value (lambda () (unwatch file here))
:errorback (lambda (&rest d) (handle unwatch errors))
:callback (lambda (f) (arbitrary callback)))
I'm not saying you can't twist the code so that it works, but it makes
no sense at the conceptual level. The file-watcher callback may be
called several times; unwatching a file has nothing to do with "get the
resulting value"; ...
It's just a completely wrong analogy.


Stefan
Ted Zlatanov
2011-06-07 18:26:01 UTC
Permalink
On Tue, 07 Jun 2011 15:06:51 -0300 Stefan Monnier <***@iro.umontreal.ca> wrote:

SM> That doesn't sound right to me. At least I'm having trouble figuring
SM> out how to bend my mind such that this file-watcher matches (even
SM> coarsely) the concept of a "future".
SM> [...]
Post by Ted Zlatanov
So `file-watch' would return
(make-url-future :value (lambda () (unwatch file here))
:errorback (lambda (&rest d) (handle unwatch errors))
:callback (lambda (f) (arbitrary callback)))
SM> I'm not saying you can't twist the code so that it works, but it
SM> makes no sense at the conceptual level [...] It's just a completely
SM> wrong analogy.

So you're cautiously in favor, then? ;)

Ted
Rüdiger Sonderfeld
2011-06-24 00:50:00 UTC
Permalink
Post by Stefan Monnier
Post by Rüdiger Sonderfeld
The problem is: How to implement the file-unwatch then? I need a way
to identify each separate file-watch request. What would be the best
way to do that?
How about letting file-watch return a "file-watcher" which you then need
to pass to file-unwatch? This "file-watcher" could be any kind of Elisp
data you find convenient for this. You may decide to provide no other
operation than file-unwatch, but you could also decide to provided
additional operations such as changing the callback (I'm not saying
that would necessarily be a good idea, tho, but maybe other operations
would be handy).
I updated the patch to return a "file-watcher". file-watch can now be called
several times for the same file with different flags and callbacks. This
however required large changes to the patch. So it would be great if someone
could take a look at it again. I added an automated test. But it doesn't test the
actually file-watching yet. This is a bit tricky to get right in a unit test.
But I'll add it. The code requires some heavy testing.

Regards,
Rüdiger

From: =?UTF-8?q?R=C3=BCdiger=20Sonderfeld?= <***@c-plusplus.de>
Date: Fri, 3 Jun 2011 23:58:12 +0200
Subject: [PATCH] Added basic file system watching support.

It currently only works with inotify/Linux. It adds file-watch and file-unwatch functions.
---
configure.in | 14 +
lisp/filewatch.el | 54 ++++
src/Makefile.in | 2 +-
src/emacs.c | 4 +
src/filewatch.c | 485 +++++++++++++++++++++++++++++++++++++
src/keyboard.c | 28 +++
src/lisp.h | 5 +
src/termhooks.h | 5 +
test/automated/filewatch-tests.el | 50 ++++
9 files changed, 646 insertions(+), 1 deletions(-)
create mode 100644 lisp/filewatch.el
create mode 100644 src/filewatch.c
create mode 100644 test/automated/filewatch-tests.el

diff --git a/configure.in b/configure.in
index 0135b9f..4a816ec 100644
--- a/configure.in
+++ b/configure.in
@@ -174,6 +174,7 @@ OPTION_DEFAULT_ON([dbus],[don't compile with D-Bus support])
OPTION_DEFAULT_ON([gconf],[don't compile with GConf support])
OPTION_DEFAULT_ON([selinux],[don't compile with SELinux support])
OPTION_DEFAULT_ON([gnutls],[don't use -lgnutls for SSL/TLS support])
+OPTION_DEFAULT_ON([inotify],[don't compile with inotify (file-watch) support])

## For the times when you want to build Emacs but don't have
## a suitable makeinfo, and can live without the manuals.
@@ -1999,6 +2000,19 @@ fi
AC_SUBST(LIBGNUTLS_LIBS)
AC_SUBST(LIBGNUTLS_CFLAGS)

+dnl inotify is only available on GNU/Linux.
+HAVE_INOTIFY=no
+if test "${with_inotify}" = "yes"; then
+ AC_CHECK_HEADERS(sys/inotify.h)
+ if test "$ac_cv_header_sys_inotify_h" = yes ; then
+ AC_CHECK_FUNCS(inotify_init1 inotify_add_watch inotify_rm_watch, HAVE_INOTIFY=yes)
+ fi
+fi
+if test "${HAVE_INOTIFY}" = "yes"; then
+ AC_DEFINE(HAVE_INOTIFY, [1], [Define to 1 to use inotify])
+ AC_DEFINE(HAVE_FILEWATCH, [1], [Define to 1 to support filewatch])
+fi
+
dnl Do not put whitespace before the #include statements below.
dnl Older compilers (eg sunos4 cc) choke on it.
HAVE_XAW3D=no
diff --git a/lisp/filewatch.el b/lisp/filewatch.el
new file mode 100644
index 0000000..2c97589
--- /dev/null
+++ b/lisp/filewatch.el
@@ -0,0 +1,54 @@
+;;; filewatch.el --- Elisp support for watching filesystem events.
+
+;; Copyright (C) 2011 Free Software Foundation, Inc.
+
+;; Author: Rüdiger Sonderfeld <***@c-plusplus.de>
+;; Keywords: files
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file contains the elisp part of the filewatch API.
+
+;;; Code:
+
+(eval-when-compile
+ (require 'cl))
+
+(defun filewatch-event-p (event)
+ "Check if EVENT is a filewatch event."
+ (and (listp event)
+ (eq (car event) 'filewatch-event)))
+
+;;;###autoload
+(defun filewatch-handle-event (event)
+ "Handle file system monitoring event.
+If EVENT is a filewatch event then the callback is called. If EVENT is
+not a filewatch event then a `filewatch-error' is signaled."
+ (interactive "e")
+
+ (unless (filewatch-event-p event)
+ (signal 'filewatch-error (cons "Not a valid filewatch event" event)))
+
+ (loop for ev in (cdr event)
+ unless (and (listp ev) (>= (length ev) 3))
+ do (signal 'filewatch-error (cons "Not a valid filewatch event" event))
+ do (funcall (cadr ev) (car ev) (caddr ev))))
+
+(provide 'filewatch)
+
+;;; filewatch.el ends here
diff --git a/src/Makefile.in b/src/Makefile.in
index c4250b9..9135e7d 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -334,7 +334,7 @@ base_obj = dispnew.o frame.o scroll.o xdisp.o menu.o $(XMENU_OBJ) window.o \
syntax.o $(UNEXEC_OBJ) bytecode.o \
process.o gnutls.o callproc.o \
region-cache.o sound.o atimer.o \
- doprnt.o intervals.o textprop.o composite.o xml.o \
+ doprnt.o intervals.o textprop.o composite.o xml.o filewatch.o \
$(MSDOS_OBJ) $(MSDOS_X_OBJ) $(NS_OBJ) $(CYGWIN_OBJ) $(FONT_OBJ)
obj = $(base_obj) $(NS_OBJC_OBJ)

diff --git a/src/emacs.c b/src/emacs.c
index d14acd6..db897d5 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -1563,6 +1563,10 @@ Using an Emacs configured with --with-x-toolkit=lucid does not have this problem
syms_of_gnutls ();
#endif

+#ifdef HAVE_FILEWATCH
+ syms_of_filewatch ();
+#endif /* HAVE_FILEWATCH */
+
#ifdef HAVE_DBUS
syms_of_dbusbind ();
#endif /* HAVE_DBUS */
diff --git a/src/filewatch.c b/src/filewatch.c
new file mode 100644
index 0000000..2833544
--- /dev/null
+++ b/src/filewatch.c
@@ -0,0 +1,485 @@
+/* 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#ifdef HAVE_FILEWATCH
+#include <setjmp.h> /* 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 <sys/inotify.h>
+#include <sys/ioctl.h>
+
+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 */
diff --git a/src/keyboard.c b/src/keyboard.c
index e7a0598..673cb6e 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -329,6 +329,9 @@ static Lisp_Object Qsave_session;
#ifdef HAVE_DBUS
static Lisp_Object Qdbus_event;
#endif
+#ifdef HAVE_FILEWATCH
+static Lisp_Object Qfilewatch_event;
+#endif /* HAVE_FILEWATCH */
static Lisp_Object Qconfig_changed_event;

/* Lisp_Object Qmouse_movement; - also an event header */
@@ -4017,6 +4020,13 @@ kbd_buffer_get_event (KBOARD **kbp,
kbd_fetch_ptr = event + 1;
}
#endif
+#ifdef HAVE_FILEWATCH
+ else if (event->kind == FILEWATCH_EVENT)
+ {
+ obj = make_lispy_event (event);
+ kbd_fetch_ptr = event + 1;
+ }
+#endif
else if (event->kind == CONFIG_CHANGED_EVENT)
{
obj = make_lispy_event (event);
@@ -5913,6 +5923,13 @@ make_lispy_event (struct input_event *event)
}
#endif /* HAVE_DBUS */

+#ifdef HAVE_FILEWATCH
+ case FILEWATCH_EVENT:
+ {
+ return Fcons (Qfilewatch_event, event->arg);
+ }
+#endif /* HAVE_FILEWATCH */
+
case CONFIG_CHANGED_EVENT:
return Fcons (Qconfig_changed_event,
Fcons (event->arg,
@@ -11524,6 +11541,10 @@ syms_of_keyboard (void)
DEFSYM (Qdbus_event, "dbus-event");
#endif

+#ifdef HAVE_FILEWATCH
+ DEFSYM (Qfilewatch_event, "filewatch-event");
+#endif /* HAVE_FILEWATCH */
+
DEFSYM (QCenable, ":enable");
DEFSYM (QCvisible, ":visible");
DEFSYM (QChelp, ":help");
@@ -12274,6 +12295,13 @@ keys_of_keyboard (void)
"dbus-handle-event");
#endif

+#ifdef HAVE_FILEWATCH
+ /* Define a special event which is raised for filewatch callback
+ functions. */
+ initial_define_lispy_key (Vspecial_event_map, "filewatch-event",
+ "filewatch-handle-event");
+#endif /* HAVE_FILEWATCH */
+
initial_define_lispy_key (Vspecial_event_map, "config-changed-event",
"ignore");
}
diff --git a/src/lisp.h b/src/lisp.h
index 1e30363..a8419d1 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -3494,6 +3494,11 @@ EXFUN (Fxw_display_color_p, 1);
EXFUN (Fx_focus_frame, 1);
#endif

+/* Defined in filewatch.c */
+#ifdef HAVE_FILEWATCH
+extern void syms_of_filewatch (void);
+#endif
+
/* Defined in xfaces.c */
extern Lisp_Object Qdefault, Qtool_bar, Qfringe;
extern Lisp_Object Qheader_line, Qscroll_bar, Qcursor;
diff --git a/src/termhooks.h b/src/termhooks.h
index 6a58517..9db2019 100644
--- a/src/termhooks.h
+++ b/src/termhooks.h
@@ -206,6 +206,11 @@ enum event_kind
, NS_NONKEY_EVENT
#endif

+#ifdef HAVE_FILEWATCH
+ /* File or directory was changed. */
+ , FILEWATCH_EVENT
+#endif
+
};

/* If a struct input_event has a kind which is SELECTION_REQUEST_EVENT
diff --git a/test/automated/filewatch-tests.el b/test/automated/filewatch-tests.el
new file mode 100644
index 0000000..494d704
--- /dev/null
+++ b/test/automated/filewatch-tests.el
@@ -0,0 +1,50 @@
+;;; filewatch-tests.el --- Test suite for filewatch.
+
+;; Copyright (C) 2011 Free Software Foundation, Inc.
+
+;; Author: Rüdiger Sonderfeld <***@c-plusplus.de>
+;; Keywords: internal
+;; Human-Keywords: internal
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+
+(when (featurep 'filewatch)
+
+ (ert-deftest filewatch-file-unwatch-type-check ()
+ "Test whether `file-unwatch' does proper type checking."
+ (should-error (file-unwatch "path") :type 'wrong-type-argument)
+ (should-error (file-unwatch '(file 1 2)) :type 'wrong-type-argument))
+
+ (ert-deftest filewatch-file-watch-aspects-check ()
+ "Test whether `file-watch' properly checks the aspects."
+ (let ((temp-file (make-temp-file "filewatch-aspects")))
+ (should (stringp temp-file))
+ (should-error (file-watch temp-file 'wrong nil)
+ :type 'unknown-aspect)
+ (should-error (file-watch temp-file '(modify t) nil)
+ :type 'unknown-aspect)
+ (should-error (file-watch temp-file '(modify Qall-modify) nil)
+ :type 'unknown-aspect)
+ (should-error (file-watch temp-file '(access wrong modify) nil)
+ :type 'unknown-aspect)))
+)
+
+(provide 'filewatch-tests)
+;;; filewatch-tests.el ends here.
--
1.7.5.4
Ted Zlatanov
2011-06-24 10:19:20 UTC
Permalink
On Fri, 24 Jun 2011 02:50:00 +0200 Rüdiger Sonderfeld <***@c-plusplus.de> wrote:

RS> +static Lisp_Object Qmodify, Qmove, Qattrib, Qdelete, Qfrom, Qto, Qall_modify;
...
RS> + if (ev->mask & (IN_MODIFY|IN_CREATE) )
RS> + events = Fcons (Fcons (Qmodify, name), events);
...
RS> + if (EQ (symb, Qmodify))
RS> + return IN_MODIFY | IN_CREATE;

You have several symbols like Qmodify, and I wanted to suggest that you
could use a name prefix like for example gnutls.c:

#+begin_src c
Qgnutls_bootprop_verify_hostname_error = intern_c_string (":verify-error");
staticpro (&Qgnutls_bootprop_verify_error);
#+end_src

and that you assign a numeric value directly to the symbol when it's
initialized instead of statically doing case statements in your code.
That way your code will be shorter and simpler because you'll work with
the integer values directly. This could be a problem if the IN_*
constants overflow the Emacs integer size, but I don't think they will,
after checking inotify.h. Only the special flags are large (2147483648
= IN_ONESHOT = 0x80000000) and you won't use those.

Ted
Ted Zlatanov
2011-06-24 12:18:44 UTC
Permalink
On Fri, 24 Jun 2011 05:19:20 -0500 Ted Zlatanov <***@lifelogs.com> wrote:

TZ> You have several symbols like Qmodify, and I wanted to suggest that you
TZ> could use a name prefix like for example gnutls.c:
TZ> and that you assign a numeric value directly to the symbol when it's
TZ> initialized instead of statically doing case statements in your
TZ> code.

I noticed you were using the DEFSYM macro, I didn't know about it. I
put it into gnutls.c.

#+begin_src c
DEFSYM(Qgnutls_e_interrupted, "gnutls-e-interrupted");
Fput (Qgnutls_e_interrupted, Qgnutls_code,
make_number (GNUTLS_E_INTERRUPTED));
#+end_src

...and I wanted to mention how I put a numeric value in the symbol's
property list instead of setting it as the value. HTH.

Ted
Stefan Monnier
2011-07-06 13:36:26 UTC
Permalink
Do you guys think this patch is good enough to make it into Emacs-24.1?

My preference would be to wait for 24.2 since we're in feature freeze,
but it's the kind of feature which benefits from being available early
so other packages can start using it. WDYT?


Stefan
Post by Rüdiger Sonderfeld
Post by Stefan Monnier
Post by Rüdiger Sonderfeld
The problem is: How to implement the file-unwatch then? I need a way
to identify each separate file-watch request. What would be the best
way to do that?
How about letting file-watch return a "file-watcher" which you then need
to pass to file-unwatch? This "file-watcher" could be any kind of Elisp
data you find convenient for this. You may decide to provide no other
operation than file-unwatch, but you could also decide to provided
additional operations such as changing the callback (I'm not saying
that would necessarily be a good idea, tho, but maybe other operations
would be handy).
I updated the patch to return a "file-watcher". file-watch can now be called
several times for the same file with different flags and callbacks. This
however required large changes to the patch. So it would be great if someone
could take a look at it again. I added an automated test. But it doesn't test the
actually file-watching yet. This is a bit tricky to get right in a unit test.
But I'll add it. The code requires some heavy testing.
Regards,
Rüdiger
Date: Fri, 3 Jun 2011 23:58:12 +0200
Subject: [PATCH] Added basic file system watching support.
It currently only works with inotify/Linux. It adds file-watch and file-unwatch functions.
---
configure.in | 14 +
lisp/filewatch.el | 54 ++++
src/Makefile.in | 2 +-
src/emacs.c | 4 +
src/filewatch.c | 485 +++++++++++++++++++++++++++++++++++++
src/keyboard.c | 28 +++
src/lisp.h | 5 +
src/termhooks.h | 5 +
test/automated/filewatch-tests.el | 50 ++++
9 files changed, 646 insertions(+), 1 deletions(-)
create mode 100644 lisp/filewatch.el
create mode 100644 src/filewatch.c
create mode 100644 test/automated/filewatch-tests.el
diff --git a/configure.in b/configure.in
index 0135b9f..4a816ec 100644
--- a/configure.in
+++ b/configure.in
@@ -174,6 +174,7 @@ OPTION_DEFAULT_ON([dbus],[don't compile with D-Bus support])
OPTION_DEFAULT_ON([gconf],[don't compile with GConf support])
OPTION_DEFAULT_ON([selinux],[don't compile with SELinux support])
OPTION_DEFAULT_ON([gnutls],[don't use -lgnutls for SSL/TLS support])
+OPTION_DEFAULT_ON([inotify],[don't compile with inotify (file-watch) support])
## For the times when you want to build Emacs but don't have
## a suitable makeinfo, and can live without the manuals.
@@ -1999,6 +2000,19 @@ fi
AC_SUBST(LIBGNUTLS_LIBS)
AC_SUBST(LIBGNUTLS_CFLAGS)
+dnl inotify is only available on GNU/Linux.
+HAVE_INOTIFY=no
+if test "${with_inotify}" = "yes"; then
+ AC_CHECK_HEADERS(sys/inotify.h)
+ if test "$ac_cv_header_sys_inotify_h" = yes ; then
+ AC_CHECK_FUNCS(inotify_init1 inotify_add_watch inotify_rm_watch, HAVE_INOTIFY=yes)
+ fi
+fi
+if test "${HAVE_INOTIFY}" = "yes"; then
+ AC_DEFINE(HAVE_INOTIFY, [1], [Define to 1 to use inotify])
+ AC_DEFINE(HAVE_FILEWATCH, [1], [Define to 1 to support filewatch])
+fi
+
dnl Do not put whitespace before the #include statements below.
dnl Older compilers (eg sunos4 cc) choke on it.
HAVE_XAW3D=no
diff --git a/lisp/filewatch.el b/lisp/filewatch.el
new file mode 100644
index 0000000..2c97589
--- /dev/null
+++ b/lisp/filewatch.el
@@ -0,0 +1,54 @@
+;;; filewatch.el --- Elisp support for watching filesystem events.
+
+;; Copyright (C) 2011 Free Software Foundation, Inc.
+
+;; Keywords: files
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+
+;; This file contains the elisp part of the filewatch API.
+
+
+(eval-when-compile
+ (require 'cl))
+
+(defun filewatch-event-p (event)
+ "Check if EVENT is a filewatch event."
+ (and (listp event)
+ (eq (car event) 'filewatch-event)))
+
+;;;###autoload
+(defun filewatch-handle-event (event)
+ "Handle file system monitoring event.
+If EVENT is a filewatch event then the callback is called. If EVENT is
+not a filewatch event then a `filewatch-error' is signaled."
+ (interactive "e")
+
+ (unless (filewatch-event-p event)
+ (signal 'filewatch-error (cons "Not a valid filewatch event" event)))
+
+ (loop for ev in (cdr event)
+ unless (and (listp ev) (>= (length ev) 3))
+ do (signal 'filewatch-error (cons "Not a valid filewatch event" event))
+ do (funcall (cadr ev) (car ev) (caddr ev))))
+
+(provide 'filewatch)
+
+;;; filewatch.el ends here
diff --git a/src/Makefile.in b/src/Makefile.in
index c4250b9..9135e7d 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -334,7 +334,7 @@ base_obj = dispnew.o frame.o scroll.o xdisp.o menu.o $(XMENU_OBJ) window.o \
syntax.o $(UNEXEC_OBJ) bytecode.o \
process.o gnutls.o callproc.o \
region-cache.o sound.o atimer.o \
- doprnt.o intervals.o textprop.o composite.o xml.o \
+ doprnt.o intervals.o textprop.o composite.o xml.o filewatch.o \
$(MSDOS_OBJ) $(MSDOS_X_OBJ) $(NS_OBJ) $(CYGWIN_OBJ) $(FONT_OBJ)
obj = $(base_obj) $(NS_OBJC_OBJ)
diff --git a/src/emacs.c b/src/emacs.c
index d14acd6..db897d5 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -1563,6 +1563,10 @@ Using an Emacs configured with --with-x-toolkit=lucid does not have this problem
syms_of_gnutls ();
#endif
+#ifdef HAVE_FILEWATCH
+ syms_of_filewatch ();
+#endif /* HAVE_FILEWATCH */
+
#ifdef HAVE_DBUS
syms_of_dbusbind ();
#endif /* HAVE_DBUS */
diff --git a/src/filewatch.c b/src/filewatch.c
new file mode 100644
index 0000000..2833544
--- /dev/null
+++ b/src/filewatch.c
@@ -0,0 +1,485 @@
+/* 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#ifdef HAVE_FILEWATCH
+#include <setjmp.h> /* 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 <sys/inotify.h>
+#include <sys/ioctl.h>
+
+enum { uninitialized = -100 };
+/* File handle for inotify. */
+static int inotifyfd = uninitialized;
+
+/* Assoc list of files being watched.
+ (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
+
+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.
+
+(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 */
diff --git a/src/keyboard.c b/src/keyboard.c
index e7a0598..673cb6e 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -329,6 +329,9 @@ static Lisp_Object Qsave_session;
#ifdef HAVE_DBUS
static Lisp_Object Qdbus_event;
#endif
+#ifdef HAVE_FILEWATCH
+static Lisp_Object Qfilewatch_event;
+#endif /* HAVE_FILEWATCH */
static Lisp_Object Qconfig_changed_event;
/* Lisp_Object Qmouse_movement; - also an event header */
@@ -4017,6 +4020,13 @@ kbd_buffer_get_event (KBOARD **kbp,
kbd_fetch_ptr = event + 1;
}
#endif
+#ifdef HAVE_FILEWATCH
+ else if (event->kind == FILEWATCH_EVENT)
+ {
+ obj = make_lispy_event (event);
+ kbd_fetch_ptr = event + 1;
+ }
+#endif
else if (event->kind == CONFIG_CHANGED_EVENT)
{
obj = make_lispy_event (event);
@@ -5913,6 +5923,13 @@ make_lispy_event (struct input_event *event)
}
#endif /* HAVE_DBUS */
+#ifdef HAVE_FILEWATCH
+ {
+ return Fcons (Qfilewatch_event, event->arg);
+ }
+#endif /* HAVE_FILEWATCH */
+
return Fcons (Qconfig_changed_event,
Fcons (event->arg,
@@ -11524,6 +11541,10 @@ syms_of_keyboard (void)
DEFSYM (Qdbus_event, "dbus-event");
#endif
+#ifdef HAVE_FILEWATCH
+ DEFSYM (Qfilewatch_event, "filewatch-event");
+#endif /* HAVE_FILEWATCH */
+
DEFSYM (QCenable, ":enable");
DEFSYM (QCvisible, ":visible");
DEFSYM (QChelp, ":help");
@@ -12274,6 +12295,13 @@ keys_of_keyboard (void)
"dbus-handle-event");
#endif
+#ifdef HAVE_FILEWATCH
+ /* Define a special event which is raised for filewatch callback
+ functions. */
+ initial_define_lispy_key (Vspecial_event_map, "filewatch-event",
+ "filewatch-handle-event");
+#endif /* HAVE_FILEWATCH */
+
initial_define_lispy_key (Vspecial_event_map, "config-changed-event",
"ignore");
}
diff --git a/src/lisp.h b/src/lisp.h
index 1e30363..a8419d1 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -3494,6 +3494,11 @@ EXFUN (Fxw_display_color_p, 1);
EXFUN (Fx_focus_frame, 1);
#endif
+/* Defined in filewatch.c */
+#ifdef HAVE_FILEWATCH
+extern void syms_of_filewatch (void);
+#endif
+
/* Defined in xfaces.c */
extern Lisp_Object Qdefault, Qtool_bar, Qfringe;
extern Lisp_Object Qheader_line, Qscroll_bar, Qcursor;
diff --git a/src/termhooks.h b/src/termhooks.h
index 6a58517..9db2019 100644
--- a/src/termhooks.h
+++ b/src/termhooks.h
@@ -206,6 +206,11 @@ enum event_kind
, NS_NONKEY_EVENT
#endif
+#ifdef HAVE_FILEWATCH
+ /* File or directory was changed. */
+ , FILEWATCH_EVENT
+#endif
+
};
/* If a struct input_event has a kind which is SELECTION_REQUEST_EVENT
diff --git a/test/automated/filewatch-tests.el b/test/automated/filewatch-tests.el
new file mode 100644
index 0000000..494d704
--- /dev/null
+++ b/test/automated/filewatch-tests.el
@@ -0,0 +1,50 @@
+;;; filewatch-tests.el --- Test suite for filewatch.
+
+;; Copyright (C) 2011 Free Software Foundation, Inc.
+
+;; Keywords: internal
+;; Human-Keywords: internal
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+
+(require 'ert)
+
+(when (featurep 'filewatch)
+
+ (ert-deftest filewatch-file-unwatch-type-check ()
+ "Test whether `file-unwatch' does proper type checking."
+ (should-error (file-unwatch "path") :type 'wrong-type-argument)
+ (should-error (file-unwatch '(file 1 2)) :type 'wrong-type-argument))
+
+ (ert-deftest filewatch-file-watch-aspects-check ()
+ "Test whether `file-watch' properly checks the aspects."
+ (let ((temp-file (make-temp-file "filewatch-aspects")))
+ (should (stringp temp-file))
+ (should-error (file-watch temp-file 'wrong nil)
+ :type 'unknown-aspect)
+ (should-error (file-watch temp-file '(modify t) nil)
+ :type 'unknown-aspect)
+ (should-error (file-watch temp-file '(modify Qall-modify) nil)
+ :type 'unknown-aspect)
+ (should-error (file-watch temp-file '(access wrong modify) nil)
+ :type 'unknown-aspect)))
+)
+
+(provide 'filewatch-tests)
+;;; filewatch-tests.el ends here.
--
1.7.5.4
Paul Eggert
2011-07-06 15:54:28 UTC
Permalink
Post by Stefan Monnier
My preference would be to wait for 24.2 since we're in feature freeze,
but it's the kind of feature which benefits from being available early
so other packages can start using it. WDYT?
I briefly looked at it for integer-overflow issues, and found one: it
assumes that inotify cookies fit into an Emacs fixnum. This assumption
isn't true on 32-bit hosts, unless Emacs is configured with
--with-wide-int. As you know I'm a fan of wide integers, and my preferred
solution would be to make --with-wide-int the default, which would solve
the problem. As that is also being considered for 24.2, perhaps the
inotify feature should wait for 24.2 as well.
Stefan Monnier
2011-07-06 18:30:10 UTC
Permalink
Post by Paul Eggert
Post by Stefan Monnier
My preference would be to wait for 24.2 since we're in feature freeze,
but it's the kind of feature which benefits from being available early
so other packages can start using it. WDYT?
I briefly looked at it for integer-overflow issues, and found one: it
assumes that inotify cookies fit into an Emacs fixnum.
Thanks. This needs to be fixed, indeed.
Post by Paul Eggert
This assumption isn't true on 32-bit hosts, unless Emacs is configured
with --with-wide-int. As you know I'm a fan of wide integers, and my
preferred solution would be to make --with-wide-int the default, which
would solve the problem.
No, that would only solve the problem if the --with-wide-int option is
removed on 32bit systems and imposed as the only choice. Even if we may
change the default for 24.2 we're definitely not going to drop support
for "narrow int" operation so soon.


Stefan
Paul Eggert
2011-07-06 20:39:02 UTC
Permalink
Post by Stefan Monnier
that would only solve the problem if the --with-wide-int option is
removed on 32bit systems and imposed as the only choice.
Another possibility would be to enable inotify only if
--with-wide-int is also in effect. Every platform
that has inotify also has wide ints, so in 24.2 this would
work except for small number of people who configure
--without-wide-int.

Come to think of it, we can do that now. I.e., we can put
in the inotify stuff now, but have it available only for
the small number of people who configure --with-wide-int.
This might help early adopters try out inotify without it being
"mainstream" in 24.
Glenn Morris
2011-07-06 19:14:29 UTC
Permalink
As you know I'm a fan of wide integers, and my preferred solution
would be to make --with-wide-int the default, which would solve the
problem.
Could you look at this report sometime?

http://debbugs.gnu.org/cgi/bugreport.cgi?bug=8884
Paul Eggert
2011-07-06 22:31:27 UTC
Permalink
Post by Glenn Morris
http://debbugs.gnu.org/cgi/bugreport.cgi?bug=8884
Thanks for the heads-up. Although the nearby code has a portability bug
regardless of whether --with-wide-int is used, --with-wide-int
is more likely to tickle the bug. The bug could well explain the
symptoms Peter observed.

I committed the following patch into the trunk. Peter,
can you please check whether it solves your problem? If not,
can you please compile with -g and without -O, and use GDB to report
the following values at the point of the crash: buffer_local_flags,
idx, offset, sizeof (struct buffer). Thanks.

=== modified file 'src/ChangeLog'
--- src/ChangeLog 2011-07-05 09:51:56 +0000
+++ src/ChangeLog 2011-07-06 22:22:32 +0000
@@ -1,3 +1,15 @@
+2011-07-06 Paul Eggert <***@cs.ucla.edu>
+
+ Remove unportable assumption about struct layout (Bug#8884).
+ * alloc.c (mark_buffer):
+ * buffer.c (reset_buffer_local_variables, Fbuffer_local_variables)
+ (clone_per_buffer_values): Don't assume that
+ sizeof (struct buffer) is a multiple of sizeof (Lisp_Object).
+ This isn't true in general, and it's particularly not true
+ if Emacs is configured with --with-wide-int.
+ * buffer.h (FIRST_FIELD_PER_BUFFER, LAST_FIELD_PER_BUFFER):
+ New macros, used in the buffer.c change.
+
2011-07-05 Jan Djärv <***@swipnet.se>

* xsettings.c: Use both GConf and GSettings if both are available.

=== modified file 'src/alloc.c'
--- src/alloc.c 2011-06-24 21:25:22 +0000
+++ src/alloc.c 2011-07-06 22:22:32 +0000
@@ -5619,7 +5619,8 @@
/* buffer-local Lisp variables start at `undo_list',
tho only the ones from `name' on are GC'd normally. */
for (ptr = &buffer->BUFFER_INTERNAL_FIELD (name);
- (char *)ptr < (char *)buffer + sizeof (struct buffer);
+ ptr <= &PER_BUFFER_VALUE (buffer,
+ PER_BUFFER_VAR_OFFSET (LAST_FIELD_PER_BUFFER));
ptr++)
mark_object (*ptr);


=== modified file 'src/buffer.c'
--- src/buffer.c 2011-07-04 15:32:22 +0000
+++ src/buffer.c 2011-07-06 22:22:32 +0000
@@ -471,8 +471,8 @@

/* buffer-local Lisp variables start at `undo_list',
tho only the ones from `name' on are GC'd normally. */
- for (offset = PER_BUFFER_VAR_OFFSET (undo_list);
- offset < sizeof *to;
+ for (offset = PER_BUFFER_VAR_OFFSET (FIRST_FIELD_PER_BUFFER);
+ offset <= PER_BUFFER_VAR_OFFSET (LAST_FIELD_PER_BUFFER);
offset += sizeof (Lisp_Object))
{
Lisp_Object obj;
@@ -830,8 +830,8 @@

/* buffer-local Lisp variables start at `undo_list',
tho only the ones from `name' on are GC'd normally. */
- for (offset = PER_BUFFER_VAR_OFFSET (undo_list);
- offset < sizeof *b;
+ for (offset = PER_BUFFER_VAR_OFFSET (FIRST_FIELD_PER_BUFFER);
+ offset <= PER_BUFFER_VAR_OFFSET (LAST_FIELD_PER_BUFFER);
offset += sizeof (Lisp_Object))
{
int idx = PER_BUFFER_IDX (offset);
@@ -1055,8 +1055,8 @@

/* buffer-local Lisp variables start at `undo_list',
tho only the ones from `name' on are GC'd normally. */
- for (offset = PER_BUFFER_VAR_OFFSET (undo_list);
- offset < sizeof (struct buffer);
+ for (offset = PER_BUFFER_VAR_OFFSET (FIRST_FIELD_PER_BUFFER);
+ offset <= PER_BUFFER_VAR_OFFSET (LAST_FIELD_PER_BUFFER);
/* sizeof EMACS_INT == sizeof Lisp_Object */
offset += (sizeof (EMACS_INT)))
{

=== modified file 'src/buffer.h'
--- src/buffer.h 2011-06-21 21:32:10 +0000
+++ src/buffer.h 2011-07-06 21:53:56 +0000
@@ -612,6 +612,7 @@
/* Everything from here down must be a Lisp_Object. */
/* buffer-local Lisp variables start at `undo_list',
tho only the ones from `name' on are GC'd normally. */
+ #define FIRST_FIELD_PER_BUFFER undo_list

/* Changes in the buffer are recorded here for undo.
t means don't record anything.
@@ -846,6 +847,9 @@
t means to use hollow box cursor.
See `cursor-type' for other values. */
Lisp_Object BUFFER_INTERNAL_FIELD (cursor_in_non_selected_windows);
+
+ /* This must be the last field in the above list. */
+ #define LAST_FIELD_PER_BUFFER cursor_in_non_selected_windows
};


Stefan Monnier
2011-07-07 19:43:43 UTC
Permalink
I updated the patch to return a "file-watcher". file-watch can now be
called several times for the same file with different flags and
callbacks. This however required large changes to the patch. So it
would be great if someone could take a look at it again. I added an
automated test. But it doesn't test the actually file-watching
yet. This is a bit tricky to get right in a unit test. But I'll add
it. The code requires some heavy testing.
Here are some comments, only based on a cursory read. I do not know
anything about the inotify API and have not tried your code.
--- /dev/null
+++ b/lisp/filewatch.el
@@ -0,0 +1,54 @@
+;;; filewatch.el --- Elisp support for watching filesystem events.
+
+;; Copyright (C) 2011 Free Software Foundation, Inc.
+
+;; Keywords: files
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+
+;; This file contains the elisp part of the filewatch API.
+
+
+(eval-when-compile
+ (require 'cl))
+
+(defun filewatch-event-p (event)
+ "Check if EVENT is a filewatch event."
+ (and (listp event)
+ (eq (car event) 'filewatch-event)))
+
+;;;###autoload
+(defun filewatch-handle-event (event)
+ "Handle file system monitoring event.
+If EVENT is a filewatch event then the callback is called. If EVENT is
+not a filewatch event then a `filewatch-error' is signaled."
+ (interactive "e")
+
+ (unless (filewatch-event-p event)
+ (signal 'filewatch-error (cons "Not a valid filewatch event" event)))
+
+ (loop for ev in (cdr event)
+ unless (and (listp ev) (>= (length ev) 3))
+ do (signal 'filewatch-error (cons "Not a valid filewatch event" event))
+ do (funcall (cadr ev) (car ev) (caddr ev))))
+
+(provide 'filewatch)
Unless we expect many more functions in this file, we could move those
functions to subr.el (tho after removing the CL dependency since
subr.el can't use CL for bootstrapping reasons).
+/* Assoc list of files being watched.
+ (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))
Most C code should use XCAR/XCDR rather than Fcr/Fcdr.
+ while (!NILP (i))
+ {
+ Lisp_Object e = Fcar (events);
E.g. here, better test CONSP rather than !NILP, which lets you then use
XCAR/XCDR.
+ while (!NILP (e))
+ {
+ if (EQ (Fcar (e), i))
+ ret = Fcons (e, ret);
+ events = Fcdr (events);
+ e = Fcar (events);
+ }
+ aspects = Fcdr (aspects);
+ i = Fcar (aspects);
+ }
I'd have used an outside loop on events so that the inner loop on
aspects can use Fmemq.
+ return ret;
+}
+
+static Lisp_Object
+inotifyevent_to_aspects (Lisp_Object name, struct inotify_event *ev)
+{
It doesn't just return aspects but events (using a Lips_Object
representation), so the name is somewhat misleading.
+ 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,
The structure of those events is a bit odd.

It seems that each event is either of form (SYMBOL . FILENAME) where
FILENAME is the watched object, or of the form (SYMBOL from NAME . COOKIE)
where NAME is the file added/removed from the watched object (a
directory, presumably), but this object's FILENAME is not present.
Any particular reason for this inconsistency?
+ make_number (ev->cookie)))),
+ events);
+ if (ev->mask & IN_MOVED_TO)
+ events = Fcons (Fcons (Qmove,
+ Fcons (Qto,
+ Fcons (name,
+ make_number (ev->cookie)))),
IIUC, these cookies are part of the patch to which Paul objected.
And IIUC their content has no importance, they're only checked for
equality, right?

So they don't have to be represented as numbers, but could be
represented by some other special object, as long as (eq cookie1
cookie2) returns the proper boolean value when comparing those
special objects.

I guess that several `ev' objects can have the same `ev->cookie'
value, right?
+/* 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)
+{
AFAICT, the void* argument will be a pointer to watch_list.
I think that either you should use it here, or you should pass NULL to
add_read_fd (rather than passing &watch_list).
+ 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))
Wouldn't it be a bug for events to be nil here (that would mean you
receive inotify events for files you do not know you're watching, or
something like that)?
+ {
+ 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));
I don't think you need ev->wd in your file-watch objects. You could use
Fcons (Qfile_watch, cell) instead, IIUC. It would be good, because it
would let you use a SAVE_VALUE object for ev->wd (since it wouldn't
escape to Lisp any more), which would solve the problem of losing a few
bits of data in make_number.
+
+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));
Just make it an `error'.
+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)))
Why do you need NUMBERP?
Shouldn't it be an "eassert (NUMBERP (...))" instead?
+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.
We typically try to avoid such "third element" style and use forms like:
"the argument takes the form (OP . FILENAME) or (OP DIRECTION NAME COOKIE)
where OP is blabl, etc...". Also please say how many arguments are
passed to CALLBACK and what means each one of them.
+ mask = aspect_to_inotifymask (aspect);
+ data = get_watch_data_by_file_name (file_name);
+ if (!NILP (data))
+ mask |= IN_MASK_ADD;
Would it hurt to always add IN_MASK_ADD?
+ decoded_file_name = ENCODE_FILE (file_name);
+ watchdesc = inotify_add_watch (inotifyfd, SSDATA (decoded_file_name), mask);
Are we sure that when data is nil, watchdesc is different from all other
watchdesc we currently have, and that when data is non-nil, watchdesc is
equal to the watchdesc stored in data?
If so, let's says so via asserts, and if not, we should check and decide
what to do.

IIUC inotify operates on inodes whereas our watch_list is indexed by
file names, so get_watch_data_by_file_name may think we're changing an
existing watcher but in reality it may operate on a new inode and hence
return a new watchdesc. Inversely, two different filenames can refer to
the same inode, so I suspect that there are also cases where
get_watch_data_by_file_name thinks this is a new watcher but the
watchdesc returned will be one that already exists.

IOW, I think we should always say IN_MASK_ADD (just in case) and we
should not use get_watch_data_by_file_name before calling
inotify_add_watch, but instead use get_watch_data_by_watchdesc afterwards.
+ return list3 (Qfile_watch, make_number (watchdesc), Fcar (info));
See comment earlier about eliding watchdesc and including `info' instead.
+ if (inotifyfd == uninitialized)
+ return Qnil;
Shouldn't this signal an error instead?


Stefan
Rüdiger Sonderfeld
2012-09-28 13:06:24 UTC
Permalink
Hello,
this is an updated version of my "file system watching" patch
(full thread: http://thread.gmane.org/gmane.emacs.devel/140158 )

I don't think the patch will be ready for 24.3. It is still not complete and
needs some heavy testing.

I addressed some of the issues mentioned by Stefan Monnier here
http://article.gmane.org/gmane.emacs.devel/141741
Post by Stefan Monnier
It seems that each event is either of form (SYMBOL . FILENAME) where
FILENAME is the watched object, or of the form (SYMBOL from NAME . COOKIE)
where NAME is the file added/removed from the watched object (a
directory, presumably), but this object's FILENAME is not present.
Any particular reason for this inconsistency?
FILENAME is not always the watched object. If a directory is watched then
it is the file name of the file inside the directory. This should really not
be used as identification for the watched object! That's why CALLBACK
also gets the ID.
Post by Stefan Monnier
IIUC, these cookies are part of the patch to which Paul objected.
And IIUC their content has no importance, they're only checked for
equality, right?
So they don't have to be represented as numbers, but could be
represented by some other special object, as long as (eq cookie1
cookie2) returns the proper boolean value when comparing those
special objects.
I guess that several `ev' objects can have the same `ev->cookie'
value, right?
Yes exactly. The value is unimportant as long as it is unique to an event
and can be compared with eq. What would you propose as an alternative
object here?
Post by Stefan Monnier
Wouldn't it be a bug for events to be nil here (that would mean you
receive inotify events for files you do not know you're watching, or
something like that)?
The problem is that even after removal of a watch descriptor there can
still be issues in the queue. Therefore I think it is best to ignore any
events for files we do not watch. There could also be event types we do not
know about when inotify gets expanded. That's why I think unknown
event types should be ignored instead of reporting an error.
Post by Stefan Monnier
I don't think you need ev->wd in your file-watch objects. You could use
Fcons (Qfile_watch, cell) instead, IIUC. It would be good, because it
would let you use a SAVE_VALUE object for ev->wd (since it wouldn't
escape to Lisp any more), which would solve the problem of losing a few
bits of data in make_number.
I don't need to make the watchdescriptor (wd) available to elisp. But I need
a way to get the descriptor from the watch handle. How could this be
implemented with SAVE_VALUE?
Post by Stefan Monnier
Shouldn't this signal an error instead?
This could be the result of calling file-unwatch too often. Currently
the behaviour is to return nil in that case. But this could also be
seen as an error of course.

Regards,
Rüdiger

-- >8 --

The current patch only works with inotify (Linux). It adds two elisp
functions file-watch and file-unwatch.

Example

(let ((fh (file-watch "foo" t #'(lambda (id event) (message "%s %s" id
event)))))
(delete-file "foo/bar")
(file-unwatch fh))

(Expects a directory foo containing a file bar)

This watches for any kind of changes in a directory foo and then
deletes a file foo/bar and finally removes the watch.

Signed-off-by: Rüdiger Sonderfeld <***@c-plusplus.de>
---
configure.ac | 14 ++
lisp/subr.el | 23 ++
src/Makefile.in | 2 +-
src/emacs.c | 4 +
src/filewatch.c | 457 ++++++++++++++++++++++++++++++++++++++
src/keyboard.c | 28 +++
src/lisp.h | 5 +
src/termhooks.h | 5 +
test/automated/filewatch-tests.el | 50 +++++
9 files changed, 587 insertions(+), 1 deletion(-)
create mode 100644 src/filewatch.c
create mode 100644 test/automated/filewatch-tests.el

diff --git a/configure.ac b/configure.ac
index 5a3aea7..8b8054b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -184,6 +184,7 @@ OPTION_DEFAULT_ON([gconf],[don't compile with GConf support])
OPTION_DEFAULT_ON([gsettings],[don't compile with GSettings support])
OPTION_DEFAULT_ON([selinux],[don't compile with SELinux support])
OPTION_DEFAULT_ON([gnutls],[don't use -lgnutls for SSL/TLS support])
+OPTION_DEFAULT_ON([inotify],[don't compile with inotify (file-watch) support])

## For the times when you want to build Emacs but don't have
## a suitable makeinfo, and can live without the manuals.
@@ -2101,6 +2102,19 @@ fi
AC_SUBST(LIBGNUTLS_LIBS)
AC_SUBST(LIBGNUTLS_CFLAGS)

+dnl inotify is only available on GNU/Linux.
+HAVE_INOTIFY=no
+if test "${with_inotify}" = "yes"; then
+ AC_CHECK_HEADERS(sys/inotify.h)
+ if test "$ac_cv_header_sys_inotify_h" = yes ; then
+ AC_CHECK_FUNCS(inotify_init1 inotify_add_watch inotify_rm_watch, HAVE_INOTIFY=yes)
+ fi
+fi
+if test "${HAVE_INOTIFY}" = "yes"; then
+ AC_DEFINE(HAVE_INOTIFY, [1], [Define to 1 to use inotify])
+ AC_DEFINE(HAVE_FILEWATCH, [1], [Define to 1 to support filewatch])
+fi
+
dnl Do not put whitespace before the #include statements below.
dnl Older compilers (eg sunos4 cc) choke on it.
HAVE_XAW3D=no
diff --git a/lisp/subr.el b/lisp/subr.el
index 8dfe78d..f47d4fb 100644
--- a/lisp/subr.el
+++ b/lisp/subr.el
@@ -4159,6 +4159,29 @@ convenience wrapper around `make-progress-reporter' and friends.
nil ,@(cdr (cdr spec)))))


+;;;; Support for watching filesystem events.
+
+(defun filewatch-event-p (event)
+ "Check if EVENT is a filewatch event."
+ (and (listp event)
+ (eq (car event) 'filewatch-event)))
+
+;;;###autoload
+(defun filewatch-handle-event (event)
+ "Handle file system monitoring event.
+If EVENT is a filewatch event then the callback is called. If EVENT is
+not a filewatch event then a `filewatch-error' is signaled."
+ (interactive "e")
+
+ (unless (filewatch-event-p event)
+ (signal 'filewatch-error (cons "Not a valid filewatch event" event)))
+
+ (dolist (ev (cdr event))
+ (unless (and (listp ev) (>= (length ev) 3))
+ (signal 'filewatch-error (cons "Not a valid filewatch event" event)))
+ (funcall (car (cdr ev)) (car ev) (car (cdr (cdr ev))))))
+
+
;;;; Comparing version strings.

(defconst version-separator "."
diff --git a/src/Makefile.in b/src/Makefile.in
index e43f83e..1f17830 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -338,7 +338,7 @@ base_obj = dispnew.o frame.o scroll.o xdisp.o menu.o $(XMENU_OBJ) window.o \
syntax.o $(UNEXEC_OBJ) bytecode.o \
process.o gnutls.o callproc.o \
region-cache.o sound.o atimer.o \
- doprnt.o intervals.o textprop.o composite.o xml.o \
+ doprnt.o intervals.o textprop.o composite.o xml.o filewatch.o \
profiler.o \
$(MSDOS_OBJ) $(MSDOS_X_OBJ) $(NS_OBJ) $(CYGWIN_OBJ) $(FONT_OBJ) \
$(WINDOW_SYSTEM_OBJ)
diff --git a/src/emacs.c b/src/emacs.c
index 05affee..7c306ee 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -1411,6 +1411,10 @@ Using an Emacs configured with --with-x-toolkit=lucid does not have this problem
syms_of_gnutls ();
#endif

+#ifdef HAVE_FILEWATCH
+ syms_of_filewatch ();
+#endif /* HAVE_FILEWATCH */
+
#ifdef HAVE_DBUS
syms_of_dbusbind ();
#endif /* HAVE_DBUS */
diff --git a/src/filewatch.c b/src/filewatch.c
new file mode 100644
index 0000000..c29e8b1
--- /dev/null
+++ b/src/filewatch.c
@@ -0,0 +1,457 @@
+/* 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#ifdef HAVE_FILEWATCH
+#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 Qfile_watch;
+static Lisp_Object Qaccess, Qclose_write, Qclose_nowrite, Qopen, Qall;
+
+#ifdef HAVE_INOTIFY
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+
+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;
+
+static Lisp_Object
+get_watch_data_by_watchdesc (int watchdesc)
+{
+ return Fassq (make_number (watchdesc), watch_list);
+}
+
+#define CADDR(x) XCAR (XCDR (XCDR (x)))
+#define CADR(x) XCAR (XCDR (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 e;
+ Lisp_Object ret = Qnil;
+ Lisp_Object aspects = CADDR (cell);
+ if (EQ (aspects, Qt) || EQ (aspects, Qall))
+ return events;
+ else if (EQ (aspects, Qall_modify))
+ aspects = list4 (Qmodify, Qmove, Qattrib, Qdelete);
+ else if (! CONSP (aspects))
+ aspects = list1 (aspects);
+
+ while (CONSP (events))
+ {
+ e = XCAR (events);
+ if (! NILP (Fmemq (XCAR (e), aspects)))
+ ret = Fcons (e, ret);
+ events = XCDR (events);
+ }
+ return ret;
+}
+
+static Lisp_Object
+inotifyevent_to_events (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 *_)
+{
+ 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_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 = get_watch_data_by_watchdesc (ev->wd);
+ 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_events (name, ev);
+
+ if (!NILP (events))
+ {
+ Lisp_Object x = CADDR (watch_data);
+ while (CONSP (x))
+ {
+ Lisp_Object cell = XCAR (x);
+ Lisp_Object aspects = match_aspects (cell, events);
+ if (!NILP (aspects))
+ {
+ Lisp_Object id = list3 (Qfile_watch, make_number (ev->wd),
+ XCAR (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 = XCDR (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
+ signal_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), 1);
+ x = XCDR (x);
+ }
+ return mask;
+ }
+ else
+ return symbol_to_inotifymask (aspect, 0);
+}
+
+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 gets passed two
+arguments ID and EVENT. The ID argument is the id returned by
+file-watch. The EVENT argument is a list of events taking the form
+(OP . FILENAME) or (OP DIRECTION NAME COOKIE). OP is the operation
+that caused the event and FILENAME is the name of the file the
+operation applied to. If the even belongs to a move operation inside
+a directory then the (OP DIRECTION NAME COOKIE) form is used and
+DIRECTION is either 'from or 'to indicating the direction of the move
+operation. NAME is the corresponding file name. COOKIE is an
+unspecified object that can be compared using `eq' to identify the
+matching from and to move operation.
+
+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, NULL);
+ }
+
+ mask = aspect_to_inotifymask (aspect) | 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;
+
+ data = get_watch_data_by_watchdesc (watchdesc);
+ if (CONSP (data))
+ XSETCDR (XCDR (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), XCAR (info));
+}
+
+static int
+file_watch_objectp (Lisp_Object obj)
+{
+ return CONSP (obj) && XINT (Flength (obj)) == 3
+ && EQ (XCAR (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;
+
+ XSETCDR (XCDR (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 (XCAR (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 (CONSP (x))
+ {
+ Lisp_Object cell = XCAR (x);
+ mask |= aspect_to_inotifymask (CADDR (cell));
+ x = XCDR (x);
+ }
+ /* Reset watch mask */
+ if (inotify_add_watch (inotifyfd, SSDATA (decoded_file_name), mask) == -1)
+ report_file_error ("Could not unwatch file",
+ Fcons (XCAR (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 (Qfile_watch, "file-watch");
+
+ defsubr (&Sfile_watch);
+ defsubr (&Sfile_unwatch);
+
+ staticpro (&watch_list);
+
+ Fprovide (intern_c_string ("filewatch"), Qnil);
+}
+
+#endif /* HAVE_FILEWATCH */
diff --git a/src/keyboard.c b/src/keyboard.c
index f3d7df5..f694b13 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -321,6 +321,9 @@ static Lisp_Object Qsave_session;
#ifdef HAVE_DBUS
static Lisp_Object Qdbus_event;
#endif
+#ifdef HAVE_FILEWATCH
+static Lisp_Object Qfilewatch_event;
+#endif /* HAVE_FILEWATCH */
static Lisp_Object Qconfig_changed_event;

/* Lisp_Object Qmouse_movement; - also an event header */
@@ -4021,6 +4024,13 @@ kbd_buffer_get_event (KBOARD **kbp,
kbd_fetch_ptr = event + 1;
}
#endif
+#ifdef HAVE_FILEWATCH
+ else if (event->kind == FILEWATCH_EVENT)
+ {
+ obj = make_lispy_event (event);
+ kbd_fetch_ptr = event + 1;
+ }
+#endif
else if (event->kind == CONFIG_CHANGED_EVENT)
{
obj = make_lispy_event (event);
@@ -5938,6 +5948,13 @@ make_lispy_event (struct input_event *event)
}
#endif /* HAVE_DBUS */

+#ifdef HAVE_FILEWATCH
+ case FILEWATCH_EVENT:
+ {
+ return Fcons (Qfilewatch_event, event->arg);
+ }
+#endif /* HAVE_FILEWATCH */
+
case CONFIG_CHANGED_EVENT:
return Fcons (Qconfig_changed_event,
Fcons (event->arg,
@@ -11408,6 +11425,10 @@ syms_of_keyboard (void)
DEFSYM (Qdbus_event, "dbus-event");
#endif

+#ifdef HAVE_FILEWATCH
+ DEFSYM (Qfilewatch_event, "filewatch-event");
+#endif /* HAVE_FILEWATCH */
+
DEFSYM (QCenable, ":enable");
DEFSYM (QCvisible, ":visible");
DEFSYM (QChelp, ":help");
@@ -12168,6 +12189,13 @@ keys_of_keyboard (void)
"dbus-handle-event");
#endif

+#ifdef HAVE_FILEWATCH
+ /* Define a special event which is raised for filewatch callback
+ functions. */
+ initial_define_lispy_key (Vspecial_event_map, "filewatch-event",
+ "filewatch-handle-event");
+#endif /* HAVE_FILEWATCH */
+
initial_define_lispy_key (Vspecial_event_map, "config-changed-event",
"ignore");
#if defined (WINDOWSNT)
diff --git a/src/lisp.h b/src/lisp.h
index 21ac55c..2f3e8b7 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -3497,6 +3497,11 @@ extern void syms_of_fontset (void);
extern Lisp_Object Qfont_param;
#endif

+/* Defined in filewatch.c */
+#ifdef HAVE_FILEWATCH
+extern void syms_of_filewatch (void);
+#endif
+
/* Defined in xfaces.c. */
extern Lisp_Object Qdefault, Qtool_bar, Qfringe;
extern Lisp_Object Qheader_line, Qscroll_bar, Qcursor;
diff --git a/src/termhooks.h b/src/termhooks.h
index f35bd92..ccb5887 100644
--- a/src/termhooks.h
+++ b/src/termhooks.h
@@ -211,6 +211,11 @@ enum event_kind
, NS_NONKEY_EVENT
#endif

+#ifdef HAVE_FILEWATCH
+ /* File or directory was changed. */
+ , FILEWATCH_EVENT
+#endif
+
};

/* If a struct input_event has a kind which is SELECTION_REQUEST_EVENT
diff --git a/test/automated/filewatch-tests.el b/test/automated/filewatch-tests.el
new file mode 100644
index 0000000..494d704
--- /dev/null
+++ b/test/automated/filewatch-tests.el
@@ -0,0 +1,50 @@
+;;; filewatch-tests.el --- Test suite for filewatch.
+
+;; Copyright (C) 2011 Free Software Foundation, Inc.
+
+;; Author: Rüdiger Sonderfeld <***@c-plusplus.de>
+;; Keywords: internal
+;; Human-Keywords: internal
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+
+(when (featurep 'filewatch)
+
+ (ert-deftest filewatch-file-unwatch-type-check ()
+ "Test whether `file-unwatch' does proper type checking."
+ (should-error (file-unwatch "path") :type 'wrong-type-argument)
+ (should-error (file-unwatch '(file 1 2)) :type 'wrong-type-argument))
+
+ (ert-deftest filewatch-file-watch-aspects-check ()
+ "Test whether `file-watch' properly checks the aspects."
+ (let ((temp-file (make-temp-file "filewatch-aspects")))
+ (should (stringp temp-file))
+ (should-error (file-watch temp-file 'wrong nil)
+ :type 'unknown-aspect)
+ (should-error (file-watch temp-file '(modify t) nil)
+ :type 'unknown-aspect)
+ (should-error (file-watch temp-file '(modify Qall-modify) nil)
+ :type 'unknown-aspect)
+ (should-error (file-watch temp-file '(access wrong modify) nil)
+ :type 'unknown-aspect)))
+)
+
+(provide 'filewatch-tests)
+;;; filewatch-tests.el ends here.
--
1.7.11.3
Stefan Monnier
2012-09-28 14:13:52 UTC
Permalink
I don't think the patch will be ready for 24.3. It is still not complete and
needs some heavy testing.
I'm not very familiar with inotify and other file-system watching
facilities, so just as a guideline for others's reviews: I'm OK with
installing in 24.3 a non-complete version of the code, and I'm OK to
install before the freeze a code that needs more testing, *but* only if
those missing functionalities and testing won't require a redesign of
the API.
Post by Stefan Monnier
It seems that each event is either of form (SYMBOL . FILENAME) where
FILENAME is the watched object, or of the form (SYMBOL from NAME . COOKIE)
where NAME is the file added/removed from the watched object (a
directory, presumably), but this object's FILENAME is not present.
Any particular reason for this inconsistency?
FILENAME is not always the watched object. If a directory is watched then
it is the file name of the file inside the directory. This should really not
be used as identification for the watched object! That's why CALLBACK
also gets the ID.
I understand that NAME refers to the added/removed file, but I'm
wondering why we don't also add FILENAME (the name of the directory) to
the event. If inotify doesn't provide it, we can provide it
ourselves, right?
Or is it rarely/never needed?
Or is it because that directory may change name without us knowing?
Post by Stefan Monnier
I guess that several `ev' objects can have the same `ev->cookie'
value, right?
Yes exactly. The value is unimportant as long as it is unique to an event
and can be compared with eq. What would you propose as an alternative
object here?
I can't remember enough of the context, but objects that are "unique"
are plentiful. A cons cell would do, regardless of its value, for
example (now, as to whether it's better than a number, that depends on
what it's use for, e.g. whether you can put the cons's fields to good
use).
The problem is that even after removal of a watch descriptor there can
still be issues in the queue. Therefore I think it is best to ignore any
events for files we do not watch. There could also be event types we do not
know about when inotify gets expanded. That's why I think unknown
event types should be ignored instead of reporting an error.
Sounds fair. Please mention it in the comments.


Stefan
Rüdiger Sonderfeld
2012-09-28 16:27:04 UTC
Permalink
Thank you for your reply!
Post by Stefan Monnier
I'm not very familiar with inotify and other file-system watching
facilities, so just as a guideline for others's reviews: I'm OK with
installing in 24.3 a non-complete version of the code, and I'm OK to
install before the freeze a code that needs more testing, *but* only if
those missing functionalities and testing won't require a redesign of
the API.
It could require a minor redesign of the API. At least I want to gather some
experience using the API first. And maybe porting it to other systems will
require API adjustments. Both BSD's and Windows' filesystem watch APIs are
not as powerful as inotify.
Post by Stefan Monnier
I understand that NAME refers to the added/removed file, but I'm
wondering why we don't also add FILENAME (the name of the directory) to
the event. If inotify doesn't provide it, we can provide it
ourselves, right?
Or is it rarely/never needed?
Or is it because that directory may change name without us knowing?
The name might change. This would require file-watch to register MOVE_SELF
events for every watched file and then update the name. This would make the
code quite complicated and worst of all leave the user wondering why the name
suddenly changed unless we force him to always accept MOVE_SELF events. Which
would then again impose something on the user he might not need. Therefore I
think it is better if the user keeps track of any ID to FILENAME mapping
himself. Maybe there could be some user defined storage in the ID to handle
it.
Post by Stefan Monnier
I can't remember enough of the context, but objects that are "unique"
are plentiful. A cons cell would do, regardless of its value, for
example (now, as to whether it's better than a number, that depends on
what it's use for, e.g. whether you can put the cons's fields to good
use).
Currently the only risk is that cookie is bigger than MOST_POSITIVE_FIXNUM and
then gets mapped to a value that is already taken by another cookie. This is
only a problem on 32bit systems without the use of wide-int because cookie is
uint32_t.

There is a similar problem with the watch descriptor. It is currently also
stored in the ID with make_number. Inotify uses an incremental system for
watch descriptors and should be run out of descriptors by the time
MOST_POSITIVE_FIXNUM is reached. But there is no guarantee for it. Therefore
something like SAVE_VALUE should probably be used.

Regards
Rüdiger
Stefan Monnier
2012-10-01 04:38:09 UTC
Permalink
Post by Rüdiger Sonderfeld
Post by Stefan Monnier
I'm not very familiar with inotify and other file-system watching
facilities, so just as a guideline for others's reviews: I'm OK with
installing in 24.3 a non-complete version of the code, and I'm OK to
install before the freeze a code that needs more testing, *but* only if
those missing functionalities and testing won't require a redesign of
the API.
It could require a minor redesign of the API.
I know of "minor changes" (where backward compatibility is easy to
preserve) and "redesigns" but I don't know what a "minor redesign" would
look like.
Post by Rüdiger Sonderfeld
At least I want to gather some experience using the API first.
And maybe porting it to other systems will require API adjustments.
Both BSD's and Windows' filesystem watch APIs are not as powerful
as inotify.
[ I'm probably rehashing something already discussed at the time. ]
If the API is sufficiently general that it can be reused for other
systems, that's great.

If there's a good chance this won't work without breaking compatibility,
maybe a better option is to provide a low-level API that maps very
closely to inotify and then an Elisp layer on top which abstracts away
differences between different systems. In that case we can install the
inotify support right away while we're still experimenting with the
higher-level abstraction.
Post by Rüdiger Sonderfeld
Post by Stefan Monnier
I understand that NAME refers to the added/removed file, but I'm
wondering why we don't also add FILENAME (the name of the directory) to
the event. If inotify doesn't provide it, we can provide it
ourselves, right?
Or is it rarely/never needed?
Or is it because that directory may change name without us knowing?
The name might change.
OK, makes sense.
Post by Rüdiger Sonderfeld
Post by Stefan Monnier
I can't remember enough of the context, but objects that are "unique"
are plentiful. A cons cell would do, regardless of its value, for
example (now, as to whether it's better than a number, that depends on
what it's use for, e.g. whether you can put the cons's fields to good
use).
Currently the only risk is that cookie is bigger than
MOST_POSITIVE_FIXNUM and then gets mapped to a value that is already
taken by another cookie. This is only a problem on 32bit systems
without the use of wide-int because cookie is uint32_t.
Ah, I remember what this is now. Rather than `cookie', I'd rather name
it `identifier' or `identity'. So IIUC Emacs's code doesn't never needs
to pass this cookie back to the inotify code, right? So the only issue
with the current "make_number (ev->cookie)" is that it might incorrectly
lead to matching two events that are really unrelated, in case they have
the same lower 30bits. I don't know how important those last 2bits are
in practice. But if they're unlikely to be important in practice, then
I guess the current solution might be acceptable.

OTOH we should stipulate that COOKIES should be compared with `equal',
not `eq', so we could later use something like Fcons (make_number
(ev->cookie / 65536), make_number (ev->cookie % 65536)).
Post by Rüdiger Sonderfeld
There is a similar problem with the watch descriptor.
This is slightly different in that here, the descriptor is later passed
back to inotify, so it's indispensable that it be preserved correctly.
Also, descriptors aren't usually compared for equality: it might be OK
to have two watch-descriptors that refer to the same watch object but
which aren't `eq'.

The docstring of file-watch doesn't really make it clear what file-watch
returns (it only says it indirectly with "The ID argument is the id returned by
file-watch") so that should be fixed.
Also this ID should be called WD for "watch descriptor", WH for "watch
handle", or FW for "file-watcher".
Post by Rüdiger Sonderfeld
Inotify uses an incremental system for watch descriptors and should
be run out of descriptors by the time MOST_POSITIVE_FIXNUM is
reached. But there is no guarantee for it. Therefore something like
SAVE_VALUE should probably be used.
If watch descriptors are documented to be "small integers", like file
descriptors, it might be OK to use fixnums, but it's a bit ugly.

I think the cleaner option is to define a new object type for it.
It could be either a new Lisp_Misc type, so you can make them print as
something like "#<file-watcher NNN>" (take a look at "enum
Lisp_Misc_Type" and "union Lisp_Misc" in src/lisp.h for starters; adding
a new type will require adding corresponding branches to the switch
statements in alloc.c and in print.c).


Stefan
Eli Zaretskii
2012-10-01 07:24:39 UTC
Permalink
Date: Mon, 01 Oct 2012 00:38:09 -0400
Post by Rüdiger Sonderfeld
Post by Stefan Monnier
I'm not very familiar with inotify and other file-system watching
facilities, so just as a guideline for others's reviews: I'm OK with
installing in 24.3 a non-complete version of the code, and I'm OK to
install before the freeze a code that needs more testing, *but* only if
those missing functionalities and testing won't require a redesign of
the API.
It could require a minor redesign of the API.
I know of "minor changes" (where backward compatibility is easy to
preserve) and "redesigns" but I don't know what a "minor redesign" would
look like.
Post by Rüdiger Sonderfeld
At least I want to gather some experience using the API first.
And maybe porting it to other systems will require API adjustments.
Both BSD's and Windows' filesystem watch APIs are not as powerful
as inotify.
[ I'm probably rehashing something already discussed at the time. ]
If the API is sufficiently general that it can be reused for other
systems, that's great.
Perhaps the API could be spelled out here, then this question could be
answered for systems without inotify.
Rüdiger Sonderfeld
2012-10-01 14:09:55 UTC
Permalink
Post by Stefan Monnier
If there's a good chance this won't work without breaking compatibility,
maybe a better option is to provide a low-level API that maps very
closely to inotify and then an Elisp layer on top which abstracts away
differences between different systems. In that case we can install the
inotify support right away while we're still experimenting with the
higher-level abstraction.
That's probably the best approach here. I changed the patch to provide a low
level inotify interface. However I did not provide an inotify_init(2) like
function and instead initialization and closing of the inotify handle is done
internally. I don't think that this should be exposed to elisp even in the
low level interface.
Post by Stefan Monnier
But if they're unlikely to be important in practice, then
I guess the current solution might be acceptable.
I think we are safe. I added that `equal' should be used to compare cookies.
So we can easily change it without breaking the API.
Post by Stefan Monnier
I think the cleaner option is to define a new object type for it.
It could be either a new Lisp_Misc type, so you can make them print as
something like "#<file-watcher NNN>" (take a look at "enum
Lisp_Misc_Type" and "union Lisp_Misc" in src/lisp.h for starters; adding
a new type will require adding corresponding branches to the switch
statements in alloc.c and in print.c).
That sounds like the best option. I haven't implemented it yet. Is it
possible to make the Lisp_Misc_Type comparable with `equal'? Because the
watch-descriptor has to be comparable.

Regards,
Rüdiger

-- >8 --

Inotify is a Linux kernel API to monitor file system events. This
patch adds a basic inotify based API to Emacs:

`inotify-add-watch' is based on inotify_add_watch(2). But uses
callbacks and accepts lisp objects instead of a bit mask.

`inotify-rm-watch' is based on inotify_rm_watch(2).

The creation and destruction of an inotify fd with inotify_init1(2) is
handled internally.

Example:

(let ((wd (inotify-add-watch "foo.txt" t
(lambda (ev) (message "FS Event %s" ev)))))
;; ...
(inotify-rm-watch wd))

Signed-off-by: Rüdiger Sonderfeld <***@c-plusplus.de>
---
configure.ac | 13 ++
lisp/subr.el | 21 ++
src/Makefile.in | 2 +-
src/emacs.c | 4 +
src/inotify.c | 431
+++++++++++++++++++++++++++++++++++++++++
src/keyboard.c | 28 +++
src/lisp.h | 5 +
src/termhooks.h | 5 +
test/automated/inotify-test.el | 60 ++++++
9 files changed, 568 insertions(+), 1 deletion(-)
create mode 100644 src/inotify.c
create mode 100644 test/automated/inotify-test.el

diff --git a/configure.ac b/configure.ac
index 5a3aea7..c1ce23f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -184,6 +184,7 @@ OPTION_DEFAULT_ON([gconf],[don't compile with GConf
support])
OPTION_DEFAULT_ON([gsettings],[don't compile with GSettings support])
OPTION_DEFAULT_ON([selinux],[don't compile with SELinux support])
OPTION_DEFAULT_ON([gnutls],[don't use -lgnutls for SSL/TLS support])
+OPTION_DEFAULT_ON([inotify],[don't compile with inotify (file-watch)
support])

## For the times when you want to build Emacs but don't have
## a suitable makeinfo, and can live without the manuals.
@@ -2101,6 +2102,18 @@ fi
AC_SUBST(LIBGNUTLS_LIBS)
AC_SUBST(LIBGNUTLS_CFLAGS)

+dnl inotify is only available on GNU/Linux.
+HAVE_INOTIFY=no
+if test "${with_inotify}" = "yes"; then
+ AC_CHECK_HEADERS(sys/inotify.h)
+ if test "$ac_cv_header_sys_inotify_h" = yes ; then
+ AC_CHECK_FUNCS(inotify_init1 inotify_add_watch inotify_rm_watch,
HAVE_INOTIFY=yes)
+ fi
+fi
+if test "${HAVE_INOTIFY}" = "yes"; then
+ AC_DEFINE(HAVE_INOTIFY, [1], [Define to 1 to use inotify])
+fi
+
dnl Do not put whitespace before the #include statements below.
dnl Older compilers (eg sunos4 cc) choke on it.
HAVE_XAW3D=no
diff --git a/lisp/subr.el b/lisp/subr.el
index 8dfe78d..134a1dc 100644
--- a/lisp/subr.el
+++ b/lisp/subr.el
@@ -4159,6 +4159,27 @@ convenience wrapper around `make-progress-reporter' and
friends.
nil ,@(cdr (cdr spec)))))


+;;;; Support for watching filesystem events.
+
+(defun inotify-event-p (event)
+ "Check if EVENT is an inotify event."
+ (and (listp event)
+ (>= (length event) 3)
+ (eq (car event) 'inotify-event)))
+
+;;;###autoload
+(defun inotify-handle-event (event)
+ "Handle file system monitoring event.
+If EVENT is a filewatch event then the callback is called. If EVENT is
+not a filewatch event then a `filewatch-error' is signaled."
+ (interactive "e")
+
+ (unless (inotify-event-p event)
+ (signal 'inotify-error (cons "Not a valid inotify event" event)))
+
+ (funcall (nth 2 event) (nth 1 event)))
+
+
;;;; Comparing version strings.

(defconst version-separator "."
diff --git a/src/Makefile.in b/src/Makefile.in
index f8da009..1492dc6 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -339,7 +339,7 @@ base_obj = dispnew.o frame.o scroll.o xdisp.o menu.o
$(XMENU_OBJ) window.o \
syntax.o $(UNEXEC_OBJ) bytecode.o \
process.o gnutls.o callproc.o \
region-cache.o sound.o atimer.o \
- doprnt.o intervals.o textprop.o composite.o xml.o \
+ doprnt.o intervals.o textprop.o composite.o xml.o inotify.o \
profiler.o \
$(MSDOS_OBJ) $(MSDOS_X_OBJ) $(NS_OBJ) $(CYGWIN_OBJ) $(FONT_OBJ) \
$(WINDOW_SYSTEM_OBJ)
diff --git a/src/emacs.c b/src/emacs.c
index 05affee..e43fa0e 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -1411,6 +1411,10 @@ Using an Emacs configured with --with-x-toolkit=lucid
does not have this problem
syms_of_gnutls ();
#endif

+#ifdef HAVE_INOTIFY
+ syms_of_inotify ();
+#endif /* HAVE_INOTIFY */
+
#ifdef HAVE_DBUS
syms_of_dbusbind ();
#endif /* HAVE_DBUS */
diff --git a/src/inotify.c b/src/inotify.c
new file mode 100644
index 0000000..49a45cb
--- /dev/null
+++ b/src/inotify.c
@@ -0,0 +1,431 @@
+/* Inotify support for Emacs
+
+Copyright (C) 2012
+ 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 <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#ifdef HAVE_INOTIFY
+
+#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 Qaccess; /* IN_ACCESS */
+static Lisp_Object Qattrib; /* IN_ATTRIB */
+static Lisp_Object Qclose_write; /* IN_CLOSE_WRITE */
+static Lisp_Object Qclose_nowrite; /* IN_CLOSE_NOWRITE */
+static Lisp_Object Qcreate; /* IN_CREATE */
+static Lisp_Object Qdelete; /* IN_DELETE */
+static Lisp_Object Qdelete_self; /* IN_DELETE_SELF */
+static Lisp_Object Qmodify; /* IN_MODIFY */
+static Lisp_Object Qmove_self; /* IN_MOVE_SELF */
+static Lisp_Object Qmoved_from; /* IN_MOVED_FROM */
+static Lisp_Object Qmoved_to; /* IN_MOVED_TO */
+static Lisp_Object Qopen; /* IN_OPEN */
+
+static Lisp_Object Qall_events; /* IN_ALL_EVENTS */
+static Lisp_Object Qmove; /* IN_MOVE */
+static Lisp_Object Qclose; /* IN_CLOSE */
+
+static Lisp_Object Qdont_follow; /* IN_DONT_FOLLOW */
+static Lisp_Object Qexcl_unlink; /* IN_EXCL_UNLINK */
+static Lisp_Object Qmask_add; /* IN_MASK_ADD */
+static Lisp_Object Qoneshot; /* IN_ONESHOT */
+static Lisp_Object Qonlydir; /* IN_ONLYDIR */
+
+static Lisp_Object Qignored; /* IN_IGNORED */
+static Lisp_Object Qisdir; /* IN_ISDIR */
+static Lisp_Object Qq_overflow; /* IN_Q_OVERFLOW */
+static Lisp_Object Qunmount; /* IN_UNMOUNT */
+
+static Lisp_Object Qinotify_event;
+
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+
+enum { uninitialized = -100 };
+/* File handle for inotify. */
+static int inotifyfd = uninitialized;
+
+/* Assoc list of files being watched.
+ Format:
+ (watch-descriptor . callback)
+ */
+static Lisp_Object watch_list;
+
+static Lisp_Object
+make_watch_descriptor (int wd)
+{
+ /* TODO replace this with a Misc Object! */
+ return make_number (wd);
+}
+
+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_object, 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);
+ }
+
+ return list2 (list4 (make_watch_descriptor (ev->wd),
+ mask_to_aspects (ev->mask),
+ make_number (ev->cookie),
+ name),
+ XCDR (watch_object));
+}
+
+/* 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;
+ Lisp_Object watch_object;
+ int to_read;
+ char *buffer;
+ ssize_t n;
+ size_t i;
+
+ 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 = INOTIFY_EVENT;
+ event.arg = Qnil;
+
+ i = 0;
+ while (i < (size_t)n)
+ {
+ struct inotify_event *ev = (struct inotify_event*)&buffer[i];
+
+ watch_object = Fassoc (make_watch_descriptor (ev->wd), watch_list);
+ if (!NILP (watch_object))
+ {
+ event.arg = inotifyevent_to_event (watch_object, ev);
+
+ /* If event was removed automatically: Drop it from watch list. */
+ if (ev->mask & IN_IGNORED)
+ watch_list = Fdelete (watch_object, watch_list);
+ }
+
+ 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)
+{
+ 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
+ signal_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.
+
+A WATCH-DESCRIPTOR is returned on success. 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 gets called in case of an
+event. It gets passed a single argument EVENT which contains an event
structure
+of the format
+
+(WATCH-DESCRIPTOR ASPECTS COOKIE NAME)
+
+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
+
+COOKIE is an object that can be compared using `equal' to identify two
matching
+renames (moved-from and moved-to).
+
+If a directory is watched then NAME is the name of file that caused the
event.
+
+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 watch_object;
+ Lisp_Object decoded_file_name;
+ Lisp_Object watch_descriptor;
+ int watchdesc = -1;
+
+ 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, NULL);
+ }
+
+ mask = aspect_to_inotifymask (aspect);
+ 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 add watch for file", Fcons (file_name,
Qnil));
+
+ watch_descriptor = make_watch_descriptor (watchdesc);
+
+ /* Delete existing watch object. */
+ watch_object = Fassoc (watch_descriptor, watch_list);
+ if (!NILP (watch_object))
+ watch_list = Fdelete (watch_object, watch_list);
+
+ /* Store watch object in watch list. */
+ watch_object = Fcons (watch_descriptor, callback);
+ watch_list = Fcons (watch_object, watch_list);
+
+ return watch_descriptor;
+}
+
+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 watch_object;
+ int wd = XINT (watch_descriptor);
+
+ if (inotify_rm_watch (inotifyfd, wd) == -1)
+ report_file_error ("Could not rm watch", Fcons (watch_descriptor,
+ Qnil));
+
+ /* Remove watch descriptor from watch list. */
+ watch_object = Fassoc (watch_descriptor, watch_list);
+ if (!NILP (watch_object))
+ watch_list = Fdelete (watch_object, watch_list);
+
+ /* Cleanup if no more files are watched. */
+ if (NILP (watch_list))
+ {
+ close (inotifyfd);
+ delete_read_fd (inotifyfd);
+ inotifyfd = uninitialized;
+ }
+
+ return Qt;
+}
+
+void
+syms_of_inotify (void)
+{
+ DEFSYM (Qaccess, "access");
+ DEFSYM (Qattrib, "attrib");
+ DEFSYM (Qclose_write, "close-write");
+ DEFSYM (Qclose_nowrite, "close-nowrite");
+ DEFSYM (Qcreate, "create");
+ DEFSYM (Qdelete, "delete");
+ DEFSYM (Qdelete_self, "delete-self");
+ DEFSYM (Qmodify, "modify");
+ DEFSYM (Qmove_self, "move-self");
+ DEFSYM (Qmoved_from, "moved-from");
+ DEFSYM (Qmoved_to, "moved-to");
+ DEFSYM (Qopen, "open");
+
+ DEFSYM (Qall_events, "all-events");
+ DEFSYM (Qmove, "move");
+ DEFSYM (Qclose, "close");
+
+ DEFSYM (Qdont_follow, "dont-follow");
+ DEFSYM (Qexcl_unlink, "excl-unlink");
+ DEFSYM (Qmask_add, "mask-add");
+ DEFSYM (Qoneshot, "oneshot");
+ DEFSYM (Qonlydir, "onlydir");
+
+ DEFSYM (Qignored, "ignored");
+ DEFSYM (Qisdir, "isdir");
+ DEFSYM (Qq_overflow, "q-overflow");
+ DEFSYM (Qunmount, "unmount");
+
+ DEFSYM (Qinotify_event, "inotify-event");
+
+ defsubr (&Sinotify_add_watch);
+ defsubr (&Sinotify_rm_watch);
+
+ staticpro (&watch_list);
+
+ Fprovide (intern_c_string ("inotify"), Qnil);
+}
+
+#endif /* HAVE_INOTIFY */
diff --git a/src/keyboard.c b/src/keyboard.c
index f3d7df5..02e1473 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -321,6 +321,9 @@ static Lisp_Object Qsave_session;
#ifdef HAVE_DBUS
static Lisp_Object Qdbus_event;
#endif
+#ifdef HAVE_INOTIFY
+static Lisp_Object Qinotify_event;
+#endif /* HAVE_INOTIFY */
static Lisp_Object Qconfig_changed_event;

/* Lisp_Object Qmouse_movement; - also an event header */
@@ -4021,6 +4024,13 @@ kbd_buffer_get_event (KBOARD **kbp,
kbd_fetch_ptr = event + 1;
}
#endif
+#ifdef HAVE_INOTIFY
+ else if (event->kind == INOTIFY_EVENT)
+ {
+ obj = make_lispy_event (event);
+ kbd_fetch_ptr = event + 1;
+ }
+#endif
else if (event->kind == CONFIG_CHANGED_EVENT)
{
obj = make_lispy_event (event);
@@ -5938,6 +5948,13 @@ make_lispy_event (struct input_event *event)
}
#endif /* HAVE_DBUS */

+#ifdef HAVE_INOTIFY
+ case INOTIFY_EVENT:
+ {
+ return Fcons (Qinotify_event, event->arg);
+ }
+#endif /* HAVE_INOTIFY */
+
case CONFIG_CHANGED_EVENT:
return Fcons (Qconfig_changed_event,
Fcons (event->arg,
@@ -11408,6 +11425,10 @@ syms_of_keyboard (void)
DEFSYM (Qdbus_event, "dbus-event");
#endif

+#ifdef HAVE_INOTIFY
+ DEFSYM (Qinotify_event, "inotify-event");
+#endif /* HAVE_INOTIFY */
+
DEFSYM (QCenable, ":enable");
DEFSYM (QCvisible, ":visible");
DEFSYM (QChelp, ":help");
@@ -12168,6 +12189,13 @@ keys_of_keyboard (void)
"dbus-handle-event");
#endif

+#ifdef HAVE_INOTIFY
+ /* Define a special event which is raised for inotify callback
+ functions. */
+ initial_define_lispy_key (Vspecial_event_map, "inotify-event",
+ "inotify-handle-event");
+#endif /* HAVE_INOTIFY */
+
initial_define_lispy_key (Vspecial_event_map, "config-changed-event",
"ignore");
#if defined (WINDOWSNT)
diff --git a/src/lisp.h b/src/lisp.h
index c3cabe0..02bfa08 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -3497,6 +3497,11 @@ extern void syms_of_fontset (void);
extern Lisp_Object Qfont_param;
#endif

+/* Defined in inotify.c */
+#ifdef HAVE_INOTIFY
+extern void syms_of_inotify (void);
+#endif
+
/* Defined in xfaces.c. */
extern Lisp_Object Qdefault, Qtool_bar, Qfringe;
extern Lisp_Object Qheader_line, Qscroll_bar, Qcursor;
diff --git a/src/termhooks.h b/src/termhooks.h
index f35bd92..5890d10 100644
--- a/src/termhooks.h
+++ b/src/termhooks.h
@@ -211,6 +211,11 @@ enum event_kind
, NS_NONKEY_EVENT
#endif

+#ifdef HAVE_INOTIFY
+ /* File or directory was changed. */
+ , INOTIFY_EVENT
+#endif
+
};

/* If a struct input_event has a kind which is SELECTION_REQUEST_EVENT
diff --git a/test/automated/inotify-test.el b/test/automated/inotify-test.el
new file mode 100644
index 0000000..edda7ef
--- /dev/null
+++ b/test/automated/inotify-test.el
@@ -0,0 +1,60 @@
+;;; inotify-tests.el --- Test suite for inotify. -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012 Free Software Foundation, Inc.
+
+;; Author: Rüdiger Sonderfeld <***@c-plusplus.de>
+;; Keywords: internal
+;; Human-Keywords: internal
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+
+(when (featurep 'inotify)
+
+ ;; (ert-deftest filewatch-file-watch-aspects-check ()
+ ;; "Test whether `file-watch' properly checks the aspects."
+ ;; (let ((temp-file (make-temp-file "filewatch-aspects")))
+ ;; (should (stringp temp-file))
+ ;; (should-error (file-watch temp-file 'wrong nil)
+ ;; :type 'error)
+ ;; (should-error (file-watch temp-file '(modify t) nil)
+ ;; :type 'error)
+ ;; (should-error (file-watch temp-file '(modify all-modify) nil)
+ ;; :type 'error)
+ ;; (should-error (file-watch temp-file '(access wrong modify) nil)
+ ;; :type 'error)))
+
+ (ert-deftest inotify-file-watch-simple ()
+ "Test if watching a normal file works."
+ (let ((temp-file (make-temp-file "inotify-simple"))
+ (events 0))
+ (let ((wd
+ (inotify-add-watch temp-file t (lambda (ev)
+ (setq events (1+ events))))))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "Foo\n"))
+ (sit-for 5) ;; Hacky. Wait for 5s until events are processed
+ (should (> events 0)))
+ (inotify-rm-watch wd)))))
+)
+
+(provide 'inotify-tests)
+;;; inotify-tests.el ends here.
--
1.7.11.3
Stefan Monnier
2012-10-01 16:27:36 UTC
Permalink
Post by Rüdiger Sonderfeld
Post by Stefan Monnier
I think the cleaner option is to define a new object type for it.
It could be either a new Lisp_Misc type, so you can make them print as
something like "#<file-watcher NNN>" (take a look at "enum
Lisp_Misc_Type" and "union Lisp_Misc" in src/lisp.h for starters; adding
a new type will require adding corresponding branches to the switch
statements in alloc.c and in print.c).
That sounds like the best option. I haven't implemented it yet. Is it
possible to make the Lisp_Misc_Type comparable with `equal'?
It could be done (fairly easily), but I'd rather not.
Post by Rüdiger Sonderfeld
Because the watch-descriptor has to be comparable.
It's comparable with `eq'. Why would you need two different
#<file-watcher...> objects for the same C-level `wd'?


Stefan
Eli Zaretskii
2012-10-02 21:28:48 UTC
Permalink
Date: Mon, 01 Oct 2012 16:09:55 +0200
=20
If there's a good chance this won't work without breaking compati=
bility,
maybe a better option is to provide a low-level API that maps ver=
y
closely to inotify and then an Elisp layer on top which abstracts=
away
differences between different systems. In that case we can insta=
ll the
inotify support right away while we're still experimenting with t=
he
higher-level abstraction.
=20
That's probably the best approach here. I changed the patch to pro=
vide a low
level inotify interface. However I did not provide an inotify_init=
(2) like=20
function and instead initialization and closing of the inotify hand=
le is done
internally. I don't think that this should be exposed to elisp eve=
n in the=20
low level interface.
Btw, what are Emacs use cases for using this kind of feature?

Watching a directory or even a single file can easily flood Emacs wit=
h
many events, and in some extreme cases (like watching /tmp, especiall=
y
using IN_ACCESS) so many that it will probably make Emacs unusable.
(The equivalent Windows APIs give you an even longer rope, in that yo=
u
can watch a directory and all its subdirectories, recursively, with a
single watch handle.) Also, a typical file operation can be presente=
d
as a series of notifications that are not easy to make sense of,
unless you are a filesystem expert and know exactly what to expect.

Maybe we should think a little more, before we expose all the guts of
inotify to the Lisp level. It's not like anyone will write an OS in
Emacs Lisp any time soon, is it?
Óscar Fuentes
2012-10-02 23:46:33 UTC
Permalink
Post by Eli Zaretskii
Btw, what are Emacs use cases for using this kind of feature?
Apart from those mentioned on the original message (dired or magit's (or
vc-dir) status view) I'll like to mention auto-revert-mode.

Currently auto-revert is a bit of a hack, in the sense that Emacs
implements a poor man's method for being aware of file changes. Checking
the timestamps every N seconds is cumbersome and has annoying
side-effects compared to the Right Thing.
Post by Eli Zaretskii
Watching a directory or even a single file can easily flood Emacs with
many events,
There are users that turn on auto-revert-mode on all file buffers. When
you accumulated quite a few buffers, checking all the associated
timestamps can be a bit lengthy and disruptive, so
auto-revert-stop-on-user-input was introduced (another hack). Plus,
waiting 5 seconds for checking for changes is, sometimes, annoyingly
long, and decreasing that value may end on too much workload for Emacs.
Post by Eli Zaretskii
and in some extreme cases (like watching /tmp, especially
using IN_ACCESS) so many that it will probably make Emacs unusable.
(The equivalent Windows APIs give you an even longer rope, in that you
can watch a directory and all its subdirectories, recursively, with a
single watch handle.) Also, a typical file operation can be presented
as a series of notifications that are not easy to make sense of,
unless you are a filesystem expert and know exactly what to expect.
Maybe we should think a little more, before we expose all the guts of
inotify to the Lisp level. It's not like anyone will write an OS in
Emacs Lisp any time soon, is it?
You are leaving this case to extremes. Emacs already allows you to do
very silly things. Let's not throw out useful features simply because
they might be abused.
Stefan Monnier
2012-10-03 02:10:41 UTC
Permalink
Post by Óscar Fuentes
Post by Eli Zaretskii
Btw, what are Emacs use cases for using this kind of feature?
Apart from those mentioned on the original message (dired or magit's (or
vc-dir) status view) I'll like to mention auto-revert-mode.
Yes, for my own use auto-revert-mode is the main one.


Stefan
Eli Zaretskii
2012-10-03 03:54:28 UTC
Permalink
Date: Tue, 02 Oct 2012 22:10:41 -0400
Post by Óscar Fuentes
Post by Eli Zaretskii
Btw, what are Emacs use cases for using this kind of feature?
Apart from those mentioned on the original message (dired or magit's (or
vc-dir) status view) I'll like to mention auto-revert-mode.
Yes, for my own use auto-revert-mode is the main one.
AFAICS, all these use cases don't need support for every possible
notification inotify lets you define. How about a simpler interface?
Stefan Monnier
2012-10-03 12:46:39 UTC
Permalink
Post by Eli Zaretskii
Post by Stefan Monnier
Post by Óscar Fuentes
Post by Eli Zaretskii
Btw, what are Emacs use cases for using this kind of feature?
Apart from those mentioned on the original message (dired or magit's (or
vc-dir) status view) I'll like to mention auto-revert-mode.
Yes, for my own use auto-revert-mode is the main one.
AFAICS, all these use cases don't need support for every possible
notification inotify lets you define. How about a simpler interface?
The patch he suggests is "as simple as it gets" because it just exposes
the C-level API. And yes, we'll want a simpler interface on top.
Both so as to make it easier to use but also so it can work with other
mechanisms than inotify.


Stefan
Eli Zaretskii
2012-10-03 18:34:01 UTC
Permalink
Date: Wed, 03 Oct 2012 08:46:39 -0400
Post by Eli Zaretskii
Post by Stefan Monnier
Post by Óscar Fuentes
Post by Eli Zaretskii
Btw, what are Emacs use cases for using this kind of feature?
Apart from those mentioned on the original message (dired or magit's (or
vc-dir) status view) I'll like to mention auto-revert-mode.
Yes, for my own use auto-revert-mode is the main one.
AFAICS, all these use cases don't need support for every possible
notification inotify lets you define. How about a simpler interface?
The patch he suggests is "as simple as it gets" because it just exposes
the C-level API. And yes, we'll want a simpler interface on top.
Both so as to make it easier to use but also so it can work with other
mechanisms than inotify.
If providing a higher-level interface is the plan, then I think
inotify.el should be renamed to something like file-notifications.el, or
some other neutral name, because before long it will be home to other
back ends and to those higher-level APIs.

That said, when will this simpler interface be added? before or after
Emacs 24.3 is locked for changes? If before, then you can skip
everything I write below (or save it for a future discussion ;-).
That's assuming the inotify support _will_ be in Emacs 24.3.

But if Emacs 24.3 is supposed to be released without this simpler
interface, then I think we will do a disservice to Lisp programmers.

E.g., consider the Dired use case. This wants to know about any and
all changes to the files in the directory, because it will need to
refresh the listing, right? But as things are, whoever will want to
code this will have to carefully read the inotify(7) man page and
figure out which of the IN_* flags she wants, because there's no
'just-what-dired-needs' flag in the API that is being proposed.

Or consider the autorevert use case. Which flag(s) to use for that?
My first guess was IN_ATTRIB, but the man page says

IN_ATTRIB Metadata changed, e.g., permissions, timestamps,
extended attributes, link count (since Linux 2.6.25),
UID, GID, etc.

Hmmm... does this include file size? I don't know. If it doesn't,
then do I need to catch the series of IN_OPEN, IN_WRITE, IN_CLOSE?

Are you confused yet? Because I am.

Another concern is how to design the Lisp code that gets run by
notifications. Since we are converting notifications into Lisp
events, the most natural way of using them would be to bind a function
to the 'inotify-event' event. (Btw, I'd drop the "event" part from
the symbol name, it's redundant.) But since meaningful file-level
events are likely to produce multiple notifications (see below), such
a function will need to implement a non-trivial state machine to DTRT.

For example, if you type at the shell prompt "echo foo > bar" with
'bar' an existing non-empty file, how many notifications do you see?
On Windows, I get 2 or 3, depending on the details: 2 for size change
(first one when the file is truncated, second one when "foo" is
written into it), and sometimes also an attribute or "security" change
(e.g., if the original file was owned by someone else). Unless
inotify magically merges all these into a single meaningful
notification, the Lisp code that receives this series of notifications
will have hard time doing TRT with them, even if the exact expected
series are known in advance, especially if other notifications are
interspersed with these.

As yet another example, many applications update a file by first
writing a temporary file, then deleting the old file and renaming the
new one. That's another series of notifications someone will have to
figure out in Lisp, when the function bound to 'inotify-event' will
get called several times, because from the user POV, the file got
updated, not overwritten by moving another file over it.

This is why I think we _must_ have a higher-level API in place for
this feature to be useful in practice, even if just inotify back end
is supported. And we should talk _today_ about that API, because some
of the processing needed to produce higher-level abstractions of
events are much easier done in C than in Lisp.
Stefan Monnier
2012-10-03 18:47:44 UTC
Permalink
Post by Eli Zaretskii
That said, when will this simpler interface be added? before or after
Emacs 24.3 is locked for changes?
After: there is no time to design such an API before the freeze.
Post by Eli Zaretskii
That's assuming the inotify support _will_ be in Emacs 24.3.
I think it would be good for it to be in 24.3, mostly to get the
ball rolling.
Post by Eli Zaretskii
Hmmm... does this include file size? I don't know. If it doesn't,
then do I need to catch the series of IN_OPEN, IN_WRITE, IN_CLOSE?
Yes, someone will have to figure that out. Leaving inotify outside of
24.3 won't save this work, tho.
Post by Eli Zaretskii
For example, if you type at the shell prompt "echo foo > bar" with
'bar' an existing non-empty file, how many notifications do you see?
On Windows, I get 2 or 3, depending on the details: 2 for size change
(first one when the file is truncated, second one when "foo" is
written into it), and sometimes also an attribute or "security" change
(e.g., if the original file was owned by someone else). Unless
inotify magically merges all these into a single meaningful
notification, the Lisp code that receives this series of notifications
will have hard time doing TRT with them, even if the exact expected
series are known in advance, especially if other notifications are
interspersed with these.
Yes, but I don't see what alternative approach would save us from doing
this work.
Post by Eli Zaretskii
And we should talk _today_ about that API, because some of the
processing needed to produce higher-level abstractions of events are
much easier done in C than in Lisp.
Ah, so that's what it's about. I don't see why that should be the case,
but if it is so, then indeed we might be better off postponing
the merge. Or to only include it if Emacs is compiled with some
"experimental-inotify" flag.


Stefan
Eli Zaretskii
2012-10-03 19:08:16 UTC
Permalink
Date: Wed, 03 Oct 2012 14:47:44 -0400
Yes, someone will have to figure that out. Leaving inotify outside of
24.3 won't save this work, tho.
I'm not lobbying for leaving it out.
Post by Eli Zaretskii
And we should talk _today_ about that API, because some of the
processing needed to produce higher-level abstractions of events are
much easier done in C than in Lisp.
Ah, so that's what it's about. I don't see why that should be the case
Let me put it this way: we won't be able to see whether or not it is
the case, unless we discuss how to overcome these difficulties. When
we find good solution(s), we can then see where they are implemented
best.
Stefan Monnier
2012-10-03 20:59:43 UTC
Permalink
Post by Eli Zaretskii
Post by Stefan Monnier
Ah, so that's what it's about. I don't see why that should be the case
Let me put it this way: we won't be able to see whether or not it is
the case, unless we discuss how to overcome these difficulties. When
we find good solution(s), we can then see where they are implemented
best.
I think we'll only see it with experience. And (based on our previous
experiences with code on non-trunk branches) as long as there's no
code in trunk for it, we won't see too many people experiment with it.

So I think we should include it in 24.3 but label it as experimental.


Stefan
Eli Zaretskii
2012-10-12 13:54:34 UTC
Permalink
Date: Wed, 03 Oct 2012 16:59:43 -0400
=20
So I think we should include it in 24.3 but label it as experimenta=
l.

Is this still the intention? If so, why isn't inotify.c and stuff in
the repository yet?

With =D3scar's help, I will soon have a w32 implementation for this.
But if the inotify stuff is not going to be committed, it doesn't mak=
e
sense to commit the w32 implementation, either.
Eli Zaretskii
2012-10-14 14:55:46 UTC
Permalink
Date: Fri, 12 Oct 2012 15:54:34 +0200
Date: Wed, 03 Oct 2012 16:59:43 -0400
So I think we should include it in 24.3 but label it as experimental.
Is this still the intention? If so, why isn't inotify.c and stuff in
the repository yet?
Any word on this? Anything at all?
Óscar Fuentes
2012-10-03 19:33:08 UTC
Permalink
Eli Zaretskii <***@gnu.org> writes:

[snip]
Post by Eli Zaretskii
This is why I think we _must_ have a higher-level API in place for
this feature to be useful in practice, even if just inotify back end
is supported.
Agreed. For starters, IMHO we need just two events:
file-contents-changed, which comprises write, truncate, delete, and
rename operations and fulfills the requirements of autorevert-like
features, and file-metadata-changed, which is what dired-like features
need.

BTW, I would take care of the MSWindows implementation, if nobody beats
me to it.

[snip]
Eli Zaretskii
2012-10-05 07:40:51 UTC
Permalink
Date: Wed, 03 Oct 2012 21:33:08 +0200
=20
BTW, I would take care of the MSWindows implementation, if nobody b=
eats
me to it.
Thank you. I started working on that, too, and will probably have
some working code, still outside of Emacs, by tomorrow. I could give
the code to you then, or we could work together. There's a design
issue with how to signal these events to the Emacs input channels; if
you got that figured out, maybe you would like to start from that end=
.
Óscar Fuentes
2012-10-05 13:07:19 UTC
Permalink
Post by Eli Zaretskii
Post by Óscar Fuentes
BTW, I would take care of the MSWindows implementation, if nobody beats
me to it.
Thank you. I started working on that, too, and will probably have
some working code, still outside of Emacs, by tomorrow. I could give
the code to you then, or we could work together. There's a design
issue with how to signal these events to the Emacs input channels; if
you got that figured out, maybe you would like to start from that end.
You can publish your branch somewhere or send a patch to me, explaining
what's missing or broken, and I'll look at it this weekend.

(Note: I'm at home with the MSWindows API, but not even a novice on
Emacs's C<->Elisp interface, so adjust your expectations on accordance
:-)
Eli Zaretskii
2012-10-05 15:19:15 UTC
Permalink
Date: Fri, 05 Oct 2012 15:07:19 +0200
=20
=20
BTW, I would take care of the MSWindows implementation, if nobod=
y beats
me to it.
Thank you. I started working on that, too, and will probably hav=
e
some working code, still outside of Emacs, by tomorrow. I could =
give
the code to you then, or we could work together. There's a desig=
n
issue with how to signal these events to the Emacs input channels=
; if
you got that figured out, maybe you would like to start from that=
end.
=20
You can publish your branch somewhere or send a patch to me, explai=
ning
what's missing or broken, and I'll look at it this weekend.
I cannot send anything directly to you, because your mail server
rejects my mails, has been doing that for months if not years:

Your message cannot be delivered to the following recipients:

Recipient address: ***@wanadoo.es
Reason: Rejection greeting returned by server.
Diagnostic code: smtp;550 Your domain is blacklisted mtaout20.012=
.net.il [80.179.55.166].
Remote system: dns;inw.wanadoo.es (TCP|80.179.55.166|62212|62.36.=
20.20|25) (Your domain is blacklisted mtaout20.012.net.il [80.179.55.=
166].)

If you can whitelist me, fine; otherwise I will post here.
(Note: I'm at home with the MSWindows API, but not even a novice on
Emacs's C<->Elisp interface, so adjust your expectations on accorda=
nce
:-)
In that case, I will stop working on the API part (what I have alread=
y
supports a single watched file), and instead work on incorporating
this into Emacs. You can then extend the API interaction, or even
redesign it, if you think it's not up to speed.

Thanks.
Nix
2012-10-05 16:55:34 UTC
Permalink
Post by Stefan Monnier
Post by Óscar Fuentes
Post by Eli Zaretskii
Btw, what are Emacs use cases for using this kind of feature?
Apart from those mentioned on the original message (dired or magit's (or
vc-dir) status view) I'll like to mention auto-revert-mode.
Yes, for my own use auto-revert-mode is the main one.
Of course you can't rely on inotify rather than polling, because inotify
simply silently omits all changes that come from other hosts when an fs
is mounted or exported over the network. inotify and friends are only
spying on local VFS traffic, which in my experience makes them less than
useful for most applications.
--
NULL && (void)
Eli Zaretskii
2012-10-05 17:15:51 UTC
Permalink
Emacs: Lovecraft was an optimist.
Date: Fri, 05 Oct 2012 17:55:34 +0100
=20
Of course you can't rely on inotify rather than polling, because in=
otify
simply silently omits all changes that come from other hosts when a=
n fs
is mounted or exported over the network. inotify and friends are on=
ly
spying on local VFS traffic
If this is true (I don't see this limitation in the inotify(7) man
page), it is specific to inotify. The MS-Windows equivalents work fo=
r
exported filesystems as well.

Nevertheless, the general observation is correct: these notifications
are not 100% reliable. Even for local filesystems, if the event queu=
e
overflows, events are thrown away without being reported.
Nix
2012-10-05 17:46:23 UTC
Permalink
Post by Eli Zaretskii
Post by Nix
Of course you can't rely on inotify rather than polling, because inotify
simply silently omits all changes that come from other hosts when an fs
is mounted or exported over the network. inotify and friends are only
spying on local VFS traffic
If this is true (I don't see this limitation in the inotify(7) man
page), it is specific to inotify. The MS-Windows equivalents work for
exported filesystems as well.
Yeah, it's specific to inotify/dnotify and anything built atop the
fsnotify Linux kernel subsystem. It is really annoying for those of us
with $HOME on NFS :( I periodically wish I had the time to write
something better, but it's quite hard.
--
NULL && (void)
Stefan Monnier
2012-10-05 18:22:16 UTC
Permalink
Post by Nix
Of course you can't rely on inotify rather than polling, because inotify
simply silently omits all changes that come from other hosts when an fs
is mounted or exported over the network.
Of course, it also doesn't help when Emacs is built without inotify.
The main questions would then be:
- how often does it work? I think this is likely to be "often" since it
should work for most/all the "home desktop" as well as laptop use cases.
- can we reliably determine whether it will work?
IIUC inotify works reliably for local file-systems (even if they're
exported because the other hosts's actions will be locally performed
by the nfsd) but not for file-systems mounted from a remote host.
Can inotify inform Emacs that its notices won't be reliable (so Emacs
can decide to use polling instead)?
Post by Nix
inotify and friends are only spying on local VFS traffic, which in my
experience makes them less than useful for most applications.
Aren't they used by most GUI file managers?


Stefan
Óscar Fuentes
2012-10-05 18:30:50 UTC
Permalink
Post by Stefan Monnier
Post by Nix
inotify and friends are only spying on local VFS traffic, which in my
experience makes them less than useful for most applications.
Aren't they used by most GUI file managers?
For ages, on my Kubuntu machines the usual file managers (Dolphin &
Konqueror) usually fail to notice changes on directories (most
annoyingly, file creation). Dunno if this is a KDE problem or something
about the underlying mechanism.
Nix
2012-10-06 16:39:16 UTC
Permalink
Post by Stefan Monnier
Post by Nix
Of course you can't rely on inotify rather than polling, because inotify
simply silently omits all changes that come from other hosts when an fs
is mounted or exported over the network.
Of course, it also doesn't help when Emacs is built without inotify.
- how often does it work? I think this is likely to be "often" since it
should work for most/all the "home desktop" as well as laptop use cases.
Most, but certainly not all: I have a home desktop, but it so happens
that I have my home directory on a separate low-power home server so
that I can shut the desktop down when not in use. If I run Emacs on the
desktop machine, it's not going to see inotify watches on my home
directory -- or, rather, it'll see those performed by the desktop, but
not those performed by the server (e.g. email delivery).
Post by Stefan Monnier
- can we reliably determine whether it will work?
No :( you get that subset of events that happened on the local machine
only, but even I ran Emacs on the server, it would see a local
filesystem but would *still* miss events -- those happening on the
desktop.
Post by Stefan Monnier
IIUC inotify works reliably for local file-systems (even if they're
exported because the other hosts's actions will be locally performed
by the nfsd)
Those don't always appear :( the nfsd doesn't do all its actions at a
level that inotify can see, alas, and certainly won't necessarily
generate the expected events for them (a touch shows up as an attrib
event locally, but an open if it's something the nfsd has done on behalf
of a remote client (!).)
Post by Stefan Monnier
but not for file-systems mounted from a remote host.
Can inotify inform Emacs that its notices won't be reliable (so Emacs
can decide to use polling instead)?
I don't think so. The answer in any case is 'inotify is never reliable':
even if the FS is not exported across the network and never becomes
exportted across the network, the queue can fill up and lose events and
the like.

I really really wish notify worked over the network :(
Post by Stefan Monnier
Post by Nix
inotify and friends are only spying on local VFS traffic, which in my
experience makes them less than useful for most applications.
Aren't they used by most GUI file managers?
Yes. If you have an NFS-mounted home directory, you get used to hitting
refresh in GUI file managers :(

inotify basically sucks for these reasons and others, but the kernel
people say that they don't care if it doesn't work over the network and
that it can't be made to work anyway, and the desktop people who make
use of inotify say that nobody uses NFS and everyone just has a single
laptop and your use case is out of scope, go away. Meanwhile, Windows
does file notification over the net perfectly well and has for years.

:(
--
NULL && (void)
Stefan Monnier
2012-10-06 17:01:03 UTC
Permalink
Post by Nix
Post by Stefan Monnier
- can we reliably determine whether it will work?
No :( you get that subset of events that happened on the local machine
only, but even I ran Emacs on the server, it would see a local
filesystem but would *still* miss events -- those happening on the
desktop.
That's a misfeature that should be easy to fix (in the kernel): any FS
should be able to indicate whether it will reliably send
inotify notices, so that client code can be told when it requests
inotify events for a particular object whether it will work reliably.
Post by Nix
Post by Stefan Monnier
IIUC inotify works reliably for local file-systems (even if they're
exported because the other hosts's actions will be locally performed
by the nfsd)
Those don't always appear :( the nfsd doesn't do all its actions at a
level that inotify can see, alas,
If nfsd applies a modification and it's not reflected with some inotify
event, I think that's a bug that the kernel developers would want to fix.
Post by Nix
and certainly won't necessarily generate the expected events for them
(a touch shows up as an attrib event locally, but an open if it's
something the nfsd has done on behalf of a remote client (!).)
Slight semantic differences are probably unavoidable, so I think this
would likely be considered as something you need to live with.
Post by Nix
Post by Stefan Monnier
Can inotify inform Emacs that its notices won't be reliable (so Emacs
can decide to use polling instead)?
even if the FS is not exported across the network and never becomes
exportted across the network, the queue can fill up and lose events and
the like.
Can't that be fixed by making it possible to "compress" the queue,
e.g. replace a bunch of events on FILE with a single "something happened
to FILE", or in the worst case replace all the events with a single
"queue filled up, all the files might have been modified in arbitrary
ways".
This would let it be reliable even in the face of lost events.
Post by Nix
Post by Stefan Monnier
Aren't they used by most GUI file managers?
Yes. If you have an NFS-mounted home directory, you get used to hitting
refresh in GUI file managers :(
So there's nothing for Emacs to worry about, because Emacs won't fix
those issues.


Stefan
Nix
2012-10-06 18:51:32 UTC
Permalink
Post by Stefan Monnier
Post by Nix
Post by Stefan Monnier
- can we reliably determine whether it will work?
No :( you get that subset of events that happened on the local machine
only, but even I ran Emacs on the server, it would see a local
filesystem but would *still* miss events -- those happening on the
desktop.
That's a misfeature that should be easy to fix (in the kernel): any FS
should be able to indicate whether it will reliably send
inotify notices, so that client code can be told when it requests
inotify events for a particular object whether it will work reliably.
Unfortunately not, because e.g. a perfectly normal local ext4 filesystem
can be unreliable if it is NFS-exported -- worse, it can be *partially*
unreliable if only a subtree is exported, and it can flip between
reliable and potentially unreliable on the fly as you export or unexport
filesystems.
Post by Stefan Monnier
Post by Nix
Post by Stefan Monnier
IIUC inotify works reliably for local file-systems (even if they're
exported because the other hosts's actions will be locally performed
by the nfsd)
Those don't always appear :( the nfsd doesn't do all its actions at a
level that inotify can see, alas,
If nfsd applies a modification and it's not reflected with some inotify
event, I think that's a bug that the kernel developers would want to fix.
They don't care. If an event doesn't come through the normal VFS layers,
inotify won't see it, and that's that.
Post by Stefan Monnier
Post by Nix
Post by Stefan Monnier
Can inotify inform Emacs that its notices won't be reliable (so Emacs
can decide to use polling instead)?
even if the FS is not exported across the network and never becomes
exportted across the network, the queue can fill up and lose events and
the like.
Can't that be fixed by making it possible to "compress" the queue,
e.g. replace a bunch of events on FILE with a single "something happened
to FILE", or in the worst case replace all the events with a single
"queue filled up, all the files might have been modified in arbitrary
ways".
Theoretically, yes, only that would require you to look at previously
queued events, which soon gets you into locking horrors. (As a first
approximation, *anything* to do with the filesystem soon gets you into
locking horrors.)
Post by Stefan Monnier
This would let it be reliable even in the face of lost events.
Post by Nix
Post by Stefan Monnier
Aren't they used by most GUI file managers?
Yes. If you have an NFS-mounted home directory, you get used to hitting
refresh in GUI file managers :(
So there's nothing for Emacs to worry about, because Emacs won't fix
those issues.
Oh yes, indeed. I'm just arguing against ripping out polling and
replacing it with inotify, really: we need at least a customization knob
for people who know their filesystems are NFS-exported or otherwise
network-affected to tweak to say 'poll, please'.
--
NULL && (void)
Stefan Monnier
2012-10-06 21:26:33 UTC
Permalink
Post by Nix
Post by Stefan Monnier
If nfsd applies a modification and it's not reflected with some inotify
event, I think that's a bug that the kernel developers would want to fix.
They don't care. If an event doesn't come through the normal VFS layers,
inotify won't see it, and that's that.
But that's a bug in nfsd, still.
Post by Nix
Oh yes, indeed. I'm just arguing against ripping out polling and
I have no intention to rip out polling. Rather I hope that the
higher-level API we come up with can be implemented by any of Windows's
low-level API, inotify, MacOSX's equvalent, or polling.


Stefan
Nix
2012-10-06 21:28:49 UTC
Permalink
Post by Stefan Monnier
Post by Nix
Post by Stefan Monnier
If nfsd applies a modification and it's not reflected with some inotify
event, I think that's a bug that the kernel developers would want to fix.
They don't care. If an event doesn't come through the normal VFS layers,
inotify won't see it, and that's that.
But that's a bug in nfsd, still.
Well, yes, I think so, and you think so, but the Linux kernel hackers do
not think so :(
from userspace via the VFS: other activity might
Post by Stefan Monnier
Post by Nix
Oh yes, indeed. I'm just arguing against ripping out polling and
I have no intention to rip out polling. Rather I hope that the
higher-level API we come up with can be implemented by any of Windows's
low-level API, inotify, MacOSX's equvalent, or polling.
... which is what I hoped to hear. I've seen several projects rip out
polling because inotify can do anything, and end up with something that
worked worse for NFS users than what they had before. (NFSv4 could in
theory implement something like inotify, but for earlier versions,
polling is really all you could hope to do. Well, that or have some
parallel daemon inotifying on the clients and informing the server of
inotify activity, and vice versa...)
--
NULL && (void)
Achim Gratz
2012-10-07 07:38:46 UTC
Permalink
Post by Nix
Post by Stefan Monnier
But that's a bug in nfsd, still.
Well, yes, I think so, and you think so, but the Linux kernel hackers do
not think so :(
This behaviour doesn't have anything to do with nfsd (other than the
stateless nature of NFS and the way it implements that) and you can
trigger it quite easily without nfsd involved.


Regards,
Achim.
--
+<[Q+ Matrix-12 WAVE#46+305 Neuron microQkb Andromeda XTk Blofeld]>+

SD adaptation for Waldorf rackAttack V1.04R1:
http://Synth.Stromeko.net/Downloads.html#WaldorfSDada
Stefan Monnier
2012-10-07 13:58:57 UTC
Permalink
Post by Achim Gratz
Post by Nix
Post by Stefan Monnier
But that's a bug in nfsd, still.
Well, yes, I think so, and you think so, but the Linux kernel hackers do
not think so :(
This behaviour doesn't have anything to do with nfsd (other than the
stateless nature of NFS and the way it implements that) and you can
trigger it quite easily without nfsd involved.
Which behavior?


Stefan
Achim Gratz
2012-10-07 14:54:49 UTC
Permalink
Post by Stefan Monnier
Post by Achim Gratz
Post by Nix
Post by Stefan Monnier
But that's a bug in nfsd, still.
Well, yes, I think so, and you think so, but the Linux kernel hackers do
not think so :(
This behaviour doesn't have anything to do with nfsd (other than the
stateless nature of NFS and the way it implements that) and you can
trigger it quite easily without nfsd involved.
Which behavior?
The nfsd is made stateless by using hidden hardlinks to the files it
uses and then uses their inode numbers to manipulate these files (all
IIRC). Inotify watches files based on their names and maps that to
inodes using a cache, which can be out of sync or even drop the
association between inode and filename. Any further operation on that
file will then not be seen by inotify unless you re-create the watch.
It works somewhat more reliably if you monitor individual files rather
than directories, but that isn't foolproof either.


Regards
Achim.
--
+<[Q+ Matrix-12 WAVE#46+305 Neuron microQkb Andromeda XTk Blofeld]>+

Wavetables for the Terratec KOMPLEXER:
http://Synth.Stromeko.net/Downloads.html#KomplexerWaves
Stephen J. Turnbull
2012-10-07 08:24:25 UTC
Permalink
Post by Nix
Post by Stefan Monnier
I have no intention to rip out polling. Rather I hope that the
higher-level API we come up with can be implemented by any of Windows's
low-level API, inotify, MacOSX's equvalent, or polling.
... which is what I hoped to hear.
Plus combinations of the above, as needed. Right?
Stefan Monnier
2012-10-07 14:00:41 UTC
Permalink
Post by Stephen J. Turnbull
Post by Nix
Post by Stefan Monnier
I have no intention to rip out polling. Rather I hope that the
higher-level API we come up with can be implemented by any of Windows's
low-level API, inotify, MacOSX's equvalent, or polling.
... which is what I hoped to hear.
Plus combinations of the above, as needed. Right?
If someone bothers to implement it, why not, tho I'm not sure how
important that would be (unless that patch can automatically figure out
when to use which).


Stefan
Óscar Fuentes
2012-10-07 14:28:17 UTC
Permalink
Post by Stefan Monnier
Post by Stephen J. Turnbull
Post by Nix
Post by Stefan Monnier
I have no intention to rip out polling. Rather I hope that the
higher-level API we come up with can be implemented by any of Windows's
low-level API, inotify, MacOSX's equvalent, or polling.
... which is what I hoped to hear.
Plus combinations of the above, as needed. Right?
If someone bothers to implement it, why not, tho I'm not sure how
important that would be (unless that patch can automatically figure out
when to use which).
In theory, it is possible to automatically figure out when to use
polling and when to use the OS notification features. But if inotify
doesn't work on some filesystems, we need a method for detecting those
filesystems. Does inotify report "I don't work on this" when it is used
on those unsupported filesystems?

Eli and I discussed the high level Lisp API for filesystem
notifications. I think it is compatible with polling, in the sense that
it can transparently fall back to polling, once the issue mentioned on
the previous paragraph is addressed.
Stefan Monnier
2012-10-07 14:38:42 UTC
Permalink
Post by Óscar Fuentes
Eli and I discussed the high level Lisp API for filesystem
notifications. I think it is compatible with polling, in the sense that
it can transparently fall back to polling, once the issue mentioned on
the previous paragraph is addressed.
AFAIK inotify does not provide the needed info, so for now the choice
should be based on a global variable.


Stefan
Stephen J. Turnbull
2012-10-08 07:07:18 UTC
Permalink
Post by Stefan Monnier
Post by Stephen J. Turnbull
Post by Nix
Post by Stefan Monnier
I have no intention to rip out polling. Rather I hope that the
higher-level API we come up with can be implemented by any of Windows's
low-level API, inotify, MacOSX's equvalent, or polling.
... which is what I hoped to hear.
Plus combinations of the above, as needed. Right?
If someone bothers to implement it, why not, tho I'm not sure how
important that would be
According to nix, inotify is unreliable, end of discussion.

If so, it may make sense to back up inotify with polling, although at
a greatly reduced rate. Also, files may get moved across filesystems
which support different mechanisms, etc, etc. So allowing different
files to get notifications in different ways, and perhaps even
changing backends on the fly may be useful/necessary for maximum
reliability.
Eli Zaretskii
2012-10-08 08:06:48 UTC
Permalink
Date: Mon, 08 Oct 2012 16:07:18 +0900
=20
According to nix, inotify is unreliable, end of discussion.
=20
If so, it may make sense to back up inotify with polling, although =
at
a greatly reduced rate. Also, files may get moved across filesyste=
ms
which support different mechanisms, etc, etc. So allowing differen=
t
files to get notifications in different ways, and perhaps even
changing backends on the fly may be useful/necessary for maximum
reliability.
If one wants a 100% reliability, polling cannot be done at slower
rates without degrading response times. Some applications might not
like the long response times. And of course, you won't know when the
notifications are less reliable than you would like to, so selectivel=
y
increasing the polling rate in those problematic cases seems
impossible.

Moreover, both inotify and the equivalent Windows APIs are documented
to lose notifications on a busy filesystem, so even a perfectly local
file/directory cannot be watched with 100% reliability.

And that is even before we think about the effects of the internal
Emacs mechanisms of handling these events. E.g., if there's a lot of
input from the keyboard and the window system, and if some heavy Lisp
is running, it is quite possible to have the file-notification event
be stuck in limbo for some time, before Emacs examines it and takes
action.

IOW, this will work well "most of the time", but that's about it.
Jim Meyering
2012-10-14 15:52:05 UTC
Permalink
Nix wrote:
...
Post by Nix
Oh yes, indeed. I'm just arguing against ripping out polling and
replacing it with inotify, really: we need at least a customization knob
for people who know their filesystems are NFS-exported or otherwise
network-affected to tweak to say 'poll, please'.
GNU tail's -f support has addressed precisely this problem
by distinguishing between file system types for which inotify
is known to work and those for which it does not work reliably.
It uses inotify only for a file system on which it is known to work,
polls for others, and warns about if the file system type is not known.

To see the list, look at coreutils/src/stat.c's S_MAGIC_* case
statements. The ones with "local" in the comment work with inotify,
the ones with admittedly poorly named "remote" in the comment
require that tail -f support use polling.

The only problem is that the list is a moving target.
New file system types arise regularly.
To give you an idea of the pace, there have been two additions
since the latest release, which was less than two months ago.
Here's the NEWS entry for those:

stat and tail know about ZFS, VZFS and VMHGFS. stat -f --format=%T now
reports the file system type, and tail -f now uses inotify for files on
ZFS and VZFS file systems, rather than the default (for unknown file
system types) of issuing a warning and reverting to polling. tail -f
still uses polling for files on VMHGFS file systems.
Stephen J. Turnbull
2012-10-06 07:04:04 UTC
Permalink
[Only capable of] spying on local VFS traffic, which in my
experience makes them less than useful for most applications.
These days, though (as I am very tired of hearing but seems to be
true) most people who use Emacs don't mount remote filesystems.
Andreas Schwab
2012-10-06 07:23:39 UTC
Permalink
Post by Stephen J. Turnbull
These days, though (as I am very tired of hearing but seems to be
true) most people who use Emacs don't mount remote filesystems.
I think NAS boxes are still common.

Andreas.
--
Andreas Schwab, ***@linux-m68k.org
GPG Key fingerprint = 58CA 54C7 6D53 942B 1756 01D3 44D5 214B 8276 4ED5
"And now for something completely different."
Nix
2012-10-06 16:41:16 UTC
Permalink
Post by Andreas Schwab
Post by Stephen J. Turnbull
These days, though (as I am very tired of hearing but seems to be
true) most people who use Emacs don't mount remote filesystems.
I think NAS boxes are still common.
If anything they are more common than they used to be in the large parts
of the world where the cost of power is shooting up, not going down:
NASes often use quite a lot less power than a desktop with the same
number of disks would, and much less than a headless server with the
same number of disks in addition to the desktop. (Though I actually do
have a headless server with a disk farm, but I know that is pretty
unusual for a home user.)
--
NULL && (void)
Stephen J. Turnbull
2012-10-07 08:09:25 UTC
Permalink
Post by Nix
If anything they are more common than they used to be in the large
parts of the world where the cost of power is shooting up, not
Makes sense. Unlike the IT and energy behavior of my host country,
unfortunately. :-P Sorry for spreading disinformation (I got it from
the same people you did, anyway. ;-)
Stefan Monnier
2012-10-07 14:08:52 UTC
Permalink
Post by Stephen J. Turnbull
Post by Nix
If anything they are more common than they used to be in the large
parts of the world where the cost of power is shooting up, not
Makes sense. Unlike the IT and energy behavior of my host country,
unfortunately. :-P Sorry for spreading disinformation (I got it from
the same people you did, anyway. ;-)
BTW, I do have a low-power home-server with a large disk, so that my
desktop can sleep to save energy. But I don't really access it via NFS.

Basically, I stopped sharing my home directory via NFS years ago,
replacing it with a VCS with a remote repository.

So other than collections of large files (typically multimedia), the
increase in disk sizes has made NFS-sharing much less important for me.

OTOH, if you know of a good file-system that can provide a model
half-way between NFS and "VCS + remote repository", so that I get the
best of both worlds (i.e. the transparent synchronization of NFS, along
with the reliability and disconnected operation of VCS), I'd love to
know about it.


Stefan
Eli Zaretskii
2012-10-03 03:57:29 UTC
Permalink
Date: Wed, 03 Oct 2012 01:46:33 +0200
=20
=20
Post by Eli Zaretskii
Btw, what are Emacs use cases for using this kind of feature?
=20
Apart from those mentioned on the original message (dired or magit'=
s (or
vc-dir) status view) I'll like to mention auto-revert-mode.
All those need either a single file to be watched or don't need any
details about what exactly changed (dired). That's a far cry from
what's being proposed.
You are leaving this case to extremes. Emacs already allows you to =
do
very silly things. Let's not throw out useful features simply becau=
se
they might be abused.
I didn't suggest to throw away anything. I suggested to think about
the API we provide, so that it could be simpler and safer. Abuse has
nothing to do with naivette.
Eli Zaretskii
2012-12-02 20:08:44 UTC
Permalink
Date: Mon, 01 Oct 2012 16:09:55 +0200
=20
If there's a good chance this won't work without breaking compati=
bility,
maybe a better option is to provide a low-level API that maps ver=
y
closely to inotify and then an Elisp layer on top which abstracts=
away
differences between different systems. In that case we can insta=
ll the
inotify support right away while we're still experimenting with t=
he
higher-level abstraction.
=20
That's probably the best approach here. I changed the patch to pro=
vide a low
level inotify interface. However I did not provide an inotify_init=
(2) like=20
function and instead initialization and closing of the inotify hand=
le is done
internally. I don't think that this should be exposed to elisp eve=
n in the=20
low level interface.
=20
But if they're unlikely to be important in practice, then
I guess the current solution might be acceptable.
=20
I think we are safe. I added that `equal' should be used to compar=
e cookies. =20
So we can easily change it without breaking the API.
=20
I think the cleaner option is to define a new object type for it.
It could be either a new Lisp_Misc type, so you can make them pri=
nt as
something like "#<file-watcher NNN>" (take a look at "enum
Lisp_Misc_Type" and "union Lisp_Misc" in src/lisp.h for starters;=
adding
a new type will require adding corresponding branches to the swit=
ch
statements in alloc.c and in print.c).
=20
That sounds like the best option. I haven't implemented it yet. I=
s it=20
possible to make the Lisp_Misc_Type comparable with `equal'? Becaus=
e the=20
watch-descriptor has to be comparable.
Any news on this? The corresponding w32 implementation collects dust
in my local branch, waiting for the inotify based implementation to b=
e
committed.
Stefan Monnier
2012-12-03 17:18:50 UTC
Permalink
Post by Eli Zaretskii
Any news on this? The corresponding w32 implementation collects dust
in my local branch, waiting for the inotify based implementation to be
committed.
Please go ahead and install both in trunk,


Stefan
Eli Zaretskii
2012-12-10 14:16:53 UTC
Permalink
Date: Mon, 03 Dec 2012 12:18:50 -0500
=20
Any news on this? The corresponding w32 implementation collects =
dust
in my local branch, waiting for the inotify based implementation =
to be
committed.
=20
Please go ahead and install both in trunk,
Done.

Eli Zaretskii
2012-12-10 11:52:48 UTC
Permalink
Date: Mon, 01 Oct 2012 16:09:55 +0200
=20
If there's a good chance this won't work without breaking compati=
bility,
maybe a better option is to provide a low-level API that maps ver=
y
closely to inotify and then an Elisp layer on top which abstracts=
away
differences between different systems. In that case we can insta=
ll the
inotify support right away while we're still experimenting with t=
he
higher-level abstraction.
=20
That's probably the best approach here. I changed the patch to pro=
vide a low
level inotify interface. However I did not provide an inotify_init=
(2) like=20
function and instead initialization and closing of the inotify hand=
le is done
internally. I don't think that this should be exposed to elisp eve=
n in the=20
low level interface.
=20
But if they're unlikely to be important in practice, then
I guess the current solution might be acceptable.
=20
I think we are safe. I added that `equal' should be used to compar=
e cookies. =20
So we can easily change it without breaking the API.
Committed (with necessary fixes and additions, like ChangeLog entries
and NEWS) as trunk revision 111171.

Thanks.
Rüdiger Sonderfeld
2012-12-10 12:11:04 UTC
Permalink
Hello,
Post by Eli Zaretskii
Committed (with necessary fixes and additions, like ChangeLog entries
and NEWS) as trunk revision 111171.
Thanks.
Great news! Thanks.

I'm currently very busy. But I'll try to find some time between Christmas and
New Year to look into the existing issues.

Regards,
Rüdiger
Stefan Monnier
2011-06-06 15:14:53 UTC
Permalink
Post by Rüdiger Sonderfeld
Thank you for your comments! I updated the patch and I hope I fixed
everything except the things noted below.
Eli's comments took care of most nitpicks, but there's still one I care
about: every open paren should be preceded by a space.

BTW, thanks very much for your contribution.


Stefan
Rüdiger Sonderfeld
2011-06-06 16:21:00 UTC
Permalink
Post by Stefan Monnier
Post by Rüdiger Sonderfeld
Thank you for your comments! I updated the patch and I hope I fixed
everything except the things noted below.
Eli's comments took care of most nitpicks, but there's still one I care
about: every open paren should be preceded by a space.
This should be fixed in Version 3 of the patch

http://lists.gnu.org/archive/html/emacs-devel/2011-06/msg00178.html

Regards,
Rüdiger
Leo
2012-09-18 11:50:49 UTC
Permalink
Post by Rüdiger Sonderfeld
I wrote a patch to support watching for file system events. It currently only works with inotify (Linux) but it could be extended to support kqueue
(*BSD, OS X) or Win32. But I wanted to get some feedback first. Watching for filesystem events would be very useful for, e.g., dired or magit's status
view.
(whole thread: http://thread.gmane.org/gmane.emacs.devel/140158)

Is this patch ready for 24.3?

Leo
Rüdiger Sonderfeld
2012-09-26 12:15:55 UTC
Permalink
Hello,
sorry the Patch is not ready, yet. I haven't added the suggestions made by
Stefan Monnier: <jwv39ii6jmw.fsf-monnier+***@gnu.org>

What is the time window for 24.3?

Regards,
Rüdiger
Post by Leo
Post by Rüdiger Sonderfeld
I wrote a patch to support watching for file system events. It currently
only works with inotify (Linux) but it could be extended to support
kqueue (*BSD, OS X) or Win32. But I wanted to get some feedback first.
Watching for filesystem events would be very useful for, e.g., dired or
magit's status view.
(whole thread: http://thread.gmane.org/gmane.emacs.devel/140158)
Is this patch ready for 24.3?
Leo
Stefan Monnier
2012-09-26 17:52:44 UTC
Permalink
sorry the Patch is not ready, yet. I haven't added the suggestions
What is the time window for 24.3?
The freeze is planned for Oct 1 (i.e. Real Soon Now(tm)).


Stefan
Loading...