about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <e@yhbt.net>2011-02-04 10:58:40 +0000
committerEric Wong <normalperson@yhbt.net>2011-02-04 21:54:28 +0000
commit2cc41c38dc270ff764a5cc58fd81fbdacde9ff4b (patch)
tree892b710d55bdf82ed5178e567cd39f726648d93b
parent7812324579b430fa604905410b506e9e88fdda55 (diff)
downloadsleepy_penguin-2cc41c38dc270ff764a5cc58fd81fbdacde9ff4b.tar.gz
It seems to basically work...
-rw-r--r--ext/sleepy_penguin/extconf.rb1
-rw-r--r--ext/sleepy_penguin/init.c7
-rw-r--r--ext/sleepy_penguin/inotify.c282
-rw-r--r--test/test_inotify.rb49
4 files changed, 339 insertions, 0 deletions
diff --git a/ext/sleepy_penguin/extconf.rb b/ext/sleepy_penguin/extconf.rb
index 50eb9a7..0993a3b 100644
--- a/ext/sleepy_penguin/extconf.rb
+++ b/ext/sleepy_penguin/extconf.rb
@@ -4,6 +4,7 @@ have_header("pthread.h") or abort 'pthread.h not found'
 have_header('sys/eventfd.h')
 have_header('sys/signalfd.h')
 have_header('sys/timerfd.h')
+have_header('sys/inotify.h')
 have_header('ruby/io.h') and have_struct_member('rb_io_t', 'fd', 'ruby/io.h')
 have_func('rb_memerror')
 have_func('rb_io_close')
diff --git a/ext/sleepy_penguin/init.c b/ext/sleepy_penguin/init.c
index 7aba64e..a3edd03 100644
--- a/ext/sleepy_penguin/init.c
+++ b/ext/sleepy_penguin/init.c
@@ -12,9 +12,16 @@ void sleepy_penguin_init_eventfd(void);
 #  define sleepy_penguin_init_eventfd() for(;0;)
 #endif
 
+#ifdef HAVE_SYS_INOTIFY_H
+void sleepy_penguin_init_inotify(void);
+#else
+#  define sleepy_penguin_init_inotify() for(;0;)
+#endif
+
 void Init_sleepy_penguin_ext(void)
 {
         sleepy_penguin_init_epoll();
         sleepy_penguin_init_timerfd();
         sleepy_penguin_init_eventfd();
+        sleepy_penguin_init_inotify();
 }
diff --git a/ext/sleepy_penguin/inotify.c b/ext/sleepy_penguin/inotify.c
new file mode 100644
index 0000000..76082f7
--- /dev/null
+++ b/ext/sleepy_penguin/inotify.c
@@ -0,0 +1,282 @@
+#ifdef HAVE_SYS_INOTIFY_H
+#include "sleepy_penguin.h"
+#include "nonblock.h"
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+static ID id_for_fd, id_inotify_buf, id_inotify_tmp, id_mask;
+static VALUE cEvent, checks;
+
+#ifndef IN_CLOEXEC
+#  define IN_CLOEXEC 02000000
+#endif
+#ifndef IN_NONBLOCK
+#  define IN_NONBLOCK O_NONBLOCK
+#endif
+#ifndef IN_ATTRIB
+#  define IN_ATTRIB 0x00000004
+#endif
+#ifndef IN_ONLYDIR
+#  define IN_ONLYDIR 0x01000000
+#endif
+#ifndef IN_DONT_FOLLOW
+#  define IN_DONT_FOLLOW 0x02000000
+#endif
+#ifndef IN_EXCL_UNLINK
+#  define IN_EXCL_UNLINK 0x04000000
+#endif
+#ifndef IN_MASK_ADD
+#  define IN_MASK_ADD 0x20000000
+#endif
+#ifndef IN_ONESHOT
+#  define IN_ONESHOT 0x80000000
+#endif
+
+#ifndef HAVE_INOTIFY_INIT1
+/*
+ * fake inotify_init1() since some systems don't have it
+ * Don't worry about thread-safety since current Ruby 1.9 won't
+ * call this without GVL.
+ */
+static int my_inotify_init1(int flags)
+{
+        int fd = inotify_init();
+        int tmp;
+
+        if (fd < 0)
+                return fd;
+        if ((flags & IN_CLOEXEC) && (fcntl(fd, F_SETFD, FD_CLOEXEC) == -1))
+                goto fcntl_err;
+        if (flags & IN_NONBLOCK) {
+                tmp = fcntl(fd, F_GETFL);
+                if (tmp == -1)
+                        goto fcntl_err;
+                if ((fcntl(fd, F_SETFL, tmp | O_NONBLOCK) != 0))
+                        goto fcntl_err;
+        }
+
+        return fd;
+fcntl_err:
+        tmp = errno;
+        close(fd);
+        errno = tmp;
+        rb_sys_fail("fcntl");
+}
+# define inotify_init1 my_inotify_init1
+#endif /* HAVE_INOTIFY_INIT1 */
+
+static VALUE s_init(int argc, VALUE *argv, VALUE klass)
+{
+        VALUE _flags, rv;
+        int flags;
+        int fd;
+
+        rb_scan_args(argc, argv, "01", &_flags);
+        flags = NIL_P(_flags) ? 0 : NUM2INT(_flags);
+
+        fd = inotify_init1(flags);
+        if (fd == -1) {
+                if (errno == EMFILE || errno == ENFILE || errno == ENOMEM) {
+                        rb_gc();
+                        fd = inotify_init1(flags);
+                }
+                if (fd == -1)
+                        rb_sys_fail("inotify_init1");
+        }
+
+        rv = rb_funcall(klass, id_for_fd, 1, INT2NUM(fd));
+        rb_ivar_set(rv, id_inotify_buf, rb_str_new(0, 128));
+        rb_ivar_set(rv, id_inotify_tmp, rb_ary_new());
+
+        return rv;
+}
+
+static VALUE add_watch(VALUE self, VALUE path, VALUE vmask)
+{
+        int fd = my_fileno(self);
+        const char *pathname = StringValuePtr(path);
+        uint32_t mask = NUM2UINT(vmask);
+        int rc = inotify_add_watch(fd, pathname, mask);
+
+        if (rc == -1) {
+                if (errno == ENOMEM) {
+                        rb_gc();
+                        rc = inotify_add_watch(fd, pathname, mask);
+                }
+                if (rc == -1)
+                        rb_sys_fail("inotify_add_watch");
+        }
+        return UINT2NUM((uint32_t)rc);
+}
+
+static VALUE rm_watch(VALUE self, VALUE vwd)
+{
+        uint32_t wd = NUM2UINT(vwd);
+        int fd = my_fileno(self);
+        int rc = inotify_rm_watch(fd, wd);
+
+        if (rc == -1)
+                rb_sys_fail("inotify_rm_watch");
+        return INT2NUM(rc);
+}
+
+static size_t event_len(struct inotify_event *e)
+{
+        return sizeof(struct inotify_event) + e->len;
+}
+
+static VALUE event_new(struct inotify_event *e)
+{
+        VALUE wd = INT2NUM(e->wd);
+        VALUE mask = UINT2NUM(e->mask);
+        VALUE cookie = UINT2NUM(e->cookie);
+        VALUE name;
+
+        /* name may be zero-padded, so we do strlen() */
+        name = e->len ? rb_str_new(e->name, strlen(e->name)) : Qnil;
+
+        return rb_struct_new(cEvent, wd, mask, cookie, name);
+}
+
+static VALUE take(int argc, VALUE *argv, VALUE self)
+{
+        int fd = my_fileno(self);
+        VALUE buf = rb_ivar_get(self, id_inotify_buf);
+        VALUE tmp = rb_ivar_get(self, id_inotify_tmp);
+        struct inotify_event *ptr;
+        struct inotify_event *e, *end;
+        long len;
+        ssize_t r;
+        VALUE rv = Qnil;
+        VALUE nonblock;
+
+        if (RARRAY_LEN(tmp) > 0)
+                return rb_ary_shift(tmp);
+
+        rb_scan_args(argc, argv, "01", &nonblock);
+
+        len = RSTRING_LEN(buf);
+        ptr = (struct inotify_event *)RSTRING_PTR(buf);
+        do {
+                set_nonblock(fd);
+                r = read(fd, ptr, len);
+                if (r == 0 || (r < 0 && errno == EINVAL)) {
+                        int newlen;
+                        if (len > 0x10000)
+                                rb_raise(rb_eRuntimeError, "path too long");
+                        if (ioctl(fd, FIONREAD, &newlen) != 0)
+                                rb_sys_fail("ioctl(inotify,FIONREAD)");
+                        rb_str_resize(buf, newlen);
+                        ptr = (struct inotify_event *)RSTRING_PTR(buf);
+                        len = newlen;
+                } else if (r < 0) {
+                        if (errno == EAGAIN) {
+                                if (RTEST(nonblock))
+                                        return Qnil;
+                                rb_io_wait_readable(fd);
+                        } else {
+                                rb_sys_fail("read(inotify)");
+                        }
+                } else {
+                        end = (struct inotify_event *)((char *)ptr + r);
+                        for (e = ptr; e < end; ) {
+                                VALUE event = event_new(e);
+                                if (NIL_P(rv))
+                                        rv = event;
+                                else
+                                        rb_ary_push(tmp, event);
+                                e = (struct inotify_event *)
+                                    ((char *)e + event_len(e));
+                        }
+                }
+        } while (NIL_P(rv));
+
+        return rv;
+}
+
+static VALUE events(VALUE self)
+{
+        long len = RARRAY_LEN(checks);
+        VALUE *ptr = RARRAY_PTR(checks);
+        VALUE pair;
+        VALUE sym;
+        VALUE rv = rb_ary_new();
+        uint32_t mask;
+        uint32_t event_mask = NUM2UINT(rb_funcall(self, id_mask, 0));
+
+        for (; (len -= 2) >= 0;) {
+                sym = *ptr++;
+                mask = NUM2UINT(*ptr++);
+                if ((event_mask & mask) == mask)
+                        rb_ary_push(rv, sym);
+        }
+
+        return rv;
+}
+
+void sleepy_penguin_init_inotify(void)
+{
+        VALUE mSleepyPenguin, cInotify;
+
+        mSleepyPenguin = rb_define_module("SleepyPenguin");
+        cInotify = rb_define_class_under(mSleepyPenguin, "Inotify", rb_cIO);
+        rb_define_method(cInotify, "add_watch", add_watch, 2);
+        rb_define_method(cInotify, "rm_watch", rm_watch, 1);
+        rb_define_method(cInotify, "take", take, -1);
+        cEvent = rb_struct_define(NULL, "wd", "mask", "cookie", "name", NULL);
+        rb_define_const(cInotify, "Event", cEvent);
+        rb_define_method(cEvent, "events", events, 0);
+        rb_define_singleton_method(cInotify, "new", s_init, -1);
+        id_for_fd = rb_intern("for_fd");
+        id_inotify_buf = rb_intern("@inotify_buf");
+        id_inotify_tmp = rb_intern("@inotify_tmp");
+        id_mask = rb_intern("mask");
+        checks = rb_ary_new();
+        rb_global_variable(&checks);
+#define IN(x) rb_define_const(cInotify,#x,UINT2NUM(IN_##x))
+#define IN2(x) do { \
+        VALUE val = UINT2NUM(IN_##x); \
+        rb_define_const(cInotify,#x,val); \
+        rb_ary_push(checks, ID2SYM(rb_intern(#x))); \
+        rb_ary_push(checks, val); \
+} while (0)
+
+        rb_define_const(cInotify, "FIONREAD", INT2NUM(FIONREAD));
+
+        IN(ALL_EVENTS);
+
+/* events a user can watch for */
+        IN2(ACCESS);
+        IN2(MODIFY);
+        IN2(ATTRIB);
+        IN2(CLOSE_WRITE);
+        IN2(CLOSE_NOWRITE);
+        IN2(OPEN);
+        IN2(MOVED_FROM);
+        IN2(MOVED_TO);
+        IN2(CREATE);
+        IN2(DELETE);
+        IN2(DELETE_SELF);
+        IN2(MOVE_SELF);
+
+/* sent as needed to any watch */
+        IN2(UNMOUNT);
+        IN2(Q_OVERFLOW);
+        IN2(IGNORED);
+        IN2(ISDIR);
+
+/* helpers */
+        IN(CLOSE);
+        IN(MOVE);
+
+/* special flags */
+        IN(ONLYDIR);
+        IN(DONT_FOLLOW);
+        IN(EXCL_UNLINK);
+        IN(MASK_ADD);
+        IN(ONESHOT);
+
+/* for inotify_init1() */
+        IN(NONBLOCK);
+        IN(CLOEXEC);
+}
+#endif /* HAVE_SYS_INOTIFY_H */
diff --git a/test/test_inotify.rb b/test/test_inotify.rb
new file mode 100644
index 0000000..f0c135b
--- /dev/null
+++ b/test/test_inotify.rb
@@ -0,0 +1,49 @@
+require 'test/unit'
+require 'fcntl'
+require 'tempfile'
+$-w = true
+require 'sleepy_penguin'
+
+class TestInotify < Test::Unit::TestCase
+  include SleepyPenguin
+
+  def test_new
+    ino = Inotify.new
+    assert_kind_of(IO, ino)
+  end
+
+  def test_new_nonblock
+    ino = Inotify.new Inotify::NONBLOCK
+    flags = ino.fcntl(Fcntl::F_GETFL) & Fcntl::O_NONBLOCK
+    assert_equal(Fcntl::O_NONBLOCK, flags)
+  end
+
+  def test_new_cloeexec
+    ino = Inotify.new Inotify::CLOEXEC
+    flags = ino.fcntl(Fcntl::F_GETFD) & Fcntl::FD_CLOEXEC
+    assert_equal(Fcntl::FD_CLOEXEC, flags)
+  end
+
+  def test_add_take
+    ino = Inotify.new Inotify::CLOEXEC
+    tmp1 = Tempfile.new 'take'
+    tmp2 = Tempfile.new 'take'
+    wd = ino.add_watch File.dirname(tmp1.path), Inotify::MOVE
+    assert_kind_of Integer, wd
+    File.rename tmp1.path, tmp2.path
+    event = ino.take
+    assert_equal wd, event.wd
+    assert_kind_of Inotify::Event, event
+    assert_equal File.basename(tmp1.path), event.name
+    others = ino.instance_variable_get(:@inotify_tmp)
+    assert_kind_of Array, others
+    assert_equal 1, others.size
+    assert_equal File.basename(tmp2.path), others[0].name
+    assert_equal [ :MOVED_FROM ], event.events
+    assert_equal [ :MOVED_TO ], others[0].events
+    assert_equal wd, others[0].wd
+    second_id = others[0].object_id
+    assert_equal second_id, ino.take.object_id
+    assert_nil ino.take(true)
+  end
+end