/* Watching file system changes.
Copyright (C) 2011
Free Software Foundation, Inc.
This file is part of GNU Emacs.
GNU Emacs is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
GNU Emacs is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with GNU Emacs. If not, see . */
#include
#ifdef HAVE_FILEWATCH
#include
#include "lisp.h"
#include "coding.h"
#include "process.h"
static Lisp_Object QCmodify, QCmove, QCattrib, QCdelete, QCfrom, QCto, QCall;
#ifdef HAVE_INOTIFY
#include
#include
enum { uninitialized = -100 };
static int inotifyfd = uninitialized;
/* Assoc list of files being watched. */
static Lisp_Object watch_list;
static void
inotify_callback(int fd, void *_, int for_read)
{
int to_read;
char *buffer;
ssize_t n;
size_t i;
if(!for_read)
return;
to_read = 0;
if(ioctl(fd, FIONREAD, &to_read) == -1)
report_file_error("Error while trying to retrieve file system events",
Qnil);
buffer = xmalloc(to_read);
n = read(fd, buffer, to_read);
if(n < 0)
report_file_error("Error while trying to read file system events",
Qnil);
i = 0;
while(i < (size_t)n)
{
struct inotify_event *ev = (struct inotify_event*)&buffer[i];
Lisp_Object callback = Fassoc(make_number(ev->wd), watch_list);
if(!NILP(callback))
{
size_t len;
Lisp_Object name, events;
Lisp_Object call[3];
call[0] = Fcar(Fcdr(Fcdr(callback))); /* callback */
call[1] = Fcar(Fcdr(callback)); /* file name */
/* If a directory is watched name contains the name
of the file that was changed. */
len = strlen(ev->name);
name = make_unibyte_string (ev->name, len);
name = DECODE_FILE(name);
events = Qnil;
if(ev->mask & (IN_MODIFY|IN_CREATE) )
events = Fcons(Fcons(QCmodify, name), events);
if(ev->mask & IN_MOVE_SELF)
events = Fcons(Fcons(QCmove, name), events);
if(ev->mask & IN_MOVED_FROM)
events = Fcons(Fcons(QCmove,
Fcons(QCfrom,
Fcons(name, make_number(ev->cookie)))),
events);
if(ev->mask & IN_MOVED_TO)
events = Fcons(Fcons(QCmove,
Fcons(QCto,
Fcons(name, make_number(ev->cookie)))),
events);
if(ev->mask & IN_ATTRIB)
events = Fcons(Fcons(QCattrib, name), events);
if(ev->mask & (IN_DELETE|IN_DELETE_SELF) )
events = Fcons(Fcons(QCdelete, name), events);
if(!NILP(events))
{
call[2] = events;
Ffuncall(3, call);
}
if(ev->mask & IN_IGNORED)
{
/* Event was removed automatically: Drop it from data list. */
add_to_log("File-watch: \"%s\" will be ignored", call[1], Qnil);
watch_list = Fdelete(callback, watch_list);
}
if(ev->mask & IN_Q_OVERFLOW)
add_to_log("File watch: Inotify Queue Overflow!", Qnil, Qnil);
}
i += sizeof(*ev) + ev->len;
}
xfree(buffer);
}
DEFUN ("file-watch", Ffile_watch, Sfile_watch, 3, MANY, 0,
doc: /* Watch a file or directory.
file-watch watches the file or directory given in FILENAME. If a change occurs
CALLBACK is called with FILENAME as first argument and a list of changes as second
argument. FLAGS can be
:modify -- notify when a file is modified or created.
:move -- notify when a file/directory is moved.
:attrib -- notify when attributes change.
:delete -- notify when a file/directory is deleted.
:all -- notify for all of the above.
Watching a directory is not recursive. CALLBACK receives the events as a list
with each list element being a list containing information about an event. The
first element is a flag symbol. If a directory is watched the second element is
the name of the file that changed. If a file is moved from or to the directory
the second element is either :from or :to and the third element is the file
name. A fourth element contains a numeric identifier (cookie) that can be used
to identify matching move operations if a file is moved inside the directory.
Example:
(file-watch "foo" #'(lambda (file event) (message "%s %s" file event)) :all)
Use `file-unwatch' to stop watching.
usage: (file-watch FILENAME CALLBACK &rest FLAGS) */)
(size_t nargs, Lisp_Object *args)
{
uint32_t mask;
size_t i;
int watchdesc;
Lisp_Object decoded_file_name;
if(nargs < 3)
return Qnil;
CHECK_STRING(args[0]);
if(inotifyfd == uninitialized)
{
inotifyfd = inotify_init1(IN_NONBLOCK|IN_CLOEXEC);
if(inotifyfd == -1)
report_file_error("File watching feature (inotify) is not available",
Qnil);
watch_list = Qnil;
add_read_fd(inotifyfd, &inotify_callback, &watch_list);
}
mask = 0;
for(i = 2; i < nargs; ++i)
{
if(EQ(args[i], QCmodify))
mask |= IN_MODIFY | IN_CREATE;
else if(EQ(args[i], QCmove))
mask |= IN_MOVE_SELF | IN_MOVE;
else if(EQ(args[i], QCattrib))
mask |= IN_ATTRIB;
else if(EQ(args[i], QCdelete))
mask |= IN_DELETE_SELF | IN_DELETE;
else if(EQ(args[i], QCall))
mask |= IN_MODIFY | IN_CREATE | IN_MOVE_SELF | IN_MOVE | IN_ATTRIB
| IN_DELETE_SELF | IN_DELETE;
else /* TODO: should this be an error? */
message("Unkown parameter %s (ignored)",
SSDATA(Fprin1_to_string(args[i], Qnil)));
}
decoded_file_name = ENCODE_FILE(args[0]);
watchdesc = inotify_add_watch(inotifyfd, SSDATA(decoded_file_name), mask);
if(watchdesc == -1) {
report_file_error("Could not watch file", Fcons(args[0], Qnil));
}
/* TODO: check if file is already in the watch_list. */
watch_list = Fcons(Fcons(make_number(watchdesc), Flist(2, args)), watch_list);
return Qt;
}
DEFUN ("file-unwatch", Ffile_unwatch, Sfile_unwatch, 1, 1, 0,
doc: /* Stop watching a file or directory. */)
(Lisp_Object file_name)
{
Lisp_Object cell, file_name_data;
Lisp_Object x = watch_list;
if(inotifyfd == uninitialized)
return Qnil;
CHECK_STRING(file_name);
while(!NILP(x))
{
cell = Fcar(x);
x = Fcdr(x);
file_name_data = Fcdr(cell);
if(!NILP(file_name_data) && STRINGP(Fcar(file_name_data))
&& Fstring_equal(Fcar(file_name_data), file_name)
&& NUMBERP(Fcar(cell)))
{
int const magicno = XINT(Fcar(cell));
watch_list = Fdelete(cell, watch_list);
if(inotify_rm_watch(inotifyfd, magicno) == -1)
report_file_error("Could not unwatch file", Fcons(file_name, Qnil));
/* Cleanup if watch_list is empty. */
if(NILP(watch_list))
{
close(inotifyfd);
delete_read_fd(inotifyfd);
inotifyfd = uninitialized;
}
return Qt;
}
}
return Qnil;
}
#else /* HAVE_INOTIFY */
#error "Filewatch defined but no watch mechanism (inotify) available"
#endif /* HAVE_INOTIFY */
void
syms_of_filewatch (void)
{
QCmodify = intern_c_string (":modify");
staticpro (&QCmodify);
QCmove = intern_c_string (":move");
staticpro (&QCmove);
QCattrib = intern_c_string (":attrib");
staticpro (&QCattrib);
QCdelete = intern_c_string (":delete");
staticpro (&QCdelete);
QCfrom = intern_c_string (":from");
staticpro (&QCfrom);
QCto = intern_c_string (":to");
staticpro (&QCto);
QCall = intern_c_string (":all");
staticpro (&QCall);
defsubr (&Sfile_watch);
defsubr (&Sfile_unwatch);
}
#endif /* HAVE_FILEWATCH */