Support filesystem notification through inotify on GNU/Linux.
[bpt/emacs.git] / src / inotify.c
diff --git a/src/inotify.c b/src/inotify.c
new file mode 100644 (file)
index 0000000..4db95a0
--- /dev/null
@@ -0,0 +1,438 @@
+/* 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 */
+
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+
+/* Ignore bits that might be undefined on old GNU/Linux systems.  */
+#ifndef IN_EXCL_UNLINK
+# define IN_EXCL_UNLINK 0
+#endif
+#ifndef IN_DONT_FOLLOW
+# define IN_DONT_FOLLOW 0
+#endif
+#ifndef IN_ONLYDIR
+# define IN_ONLYDIR 0
+#endif
+
+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 = FILE_NOTIFY_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");
+
+  defsubr (&Sinotify_add_watch);
+  defsubr (&Sinotify_rm_watch);
+
+  staticpro (&watch_list);
+
+  Fprovide (intern_c_string ("inotify"), Qnil);
+}
+
+#endif /* HAVE_INOTIFY */