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