swerling-sinotify 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/History.txt +3 -0
- data/README.rdoc +139 -0
- data/README.txt +139 -0
- data/Rakefile +50 -0
- data/examples/watcher.rb +30 -0
- data/ext/extconf.rb +12 -0
- data/ext/src/inotify-syscalls.h +24 -0
- data/ext/src/inotify.h +113 -0
- data/ext/src/sinotify.c +205 -0
- data/lib/sinotify.rb +18 -0
- data/lib/sinotify/event.rb +185 -0
- data/lib/sinotify/notifier.rb +308 -0
- data/lib/sinotify/prim_event.rb +118 -0
- data/lib/sinotify/watch.rb +21 -0
- data/lib/sinotify_info.rb +47 -0
- data/sinotify.gemspec +79 -0
- data/spec/prim_notify_spec.rb +98 -0
- data/spec/sinotify_spec.rb +247 -0
- data/spec/spec_helper.rb +14 -0
- metadata +95 -0
data/ext/src/sinotify.c
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
|
3
|
+
// includes that are different for ruby 1.8 vs. ruby 1.9
|
4
|
+
#ifdef RUBY_19
|
5
|
+
#warning "INCLUDING RUBY19 HEADERS (not really a warning, just for info)"
|
6
|
+
#include <ruby/io.h>
|
7
|
+
#else
|
8
|
+
#warning "INCLUDING RUBY18 HEADERS (not really a warning, just for info) "
|
9
|
+
#include <rubyio.h>
|
10
|
+
|
11
|
+
#endif
|
12
|
+
|
13
|
+
#ifdef HAVE_VERSION_H
|
14
|
+
#include <version.h>
|
15
|
+
#endif
|
16
|
+
|
17
|
+
#ifdef HAVE_LINUX_INOTIFY_H
|
18
|
+
#include <asm/unistd.h>
|
19
|
+
#include <linux/inotify.h>
|
20
|
+
#else
|
21
|
+
#include "inotify.h"
|
22
|
+
#include "inotify-syscalls.h"
|
23
|
+
#endif
|
24
|
+
|
25
|
+
#include <sys/syscall.h>
|
26
|
+
#include <unistd.h>
|
27
|
+
|
28
|
+
static inline int inotify_init (void)
|
29
|
+
{
|
30
|
+
return syscall (__NR_inotify_init);
|
31
|
+
}
|
32
|
+
|
33
|
+
static inline int inotify_add_watch (int fd, const char *name, __u32 mask)
|
34
|
+
{
|
35
|
+
return syscall (__NR_inotify_add_watch, fd, name, mask);
|
36
|
+
}
|
37
|
+
|
38
|
+
static inline int inotify_rm_watch (int fd, __u32 wd)
|
39
|
+
{
|
40
|
+
return syscall (__NR_inotify_rm_watch, fd, wd);
|
41
|
+
}
|
42
|
+
|
43
|
+
VALUE rb_cSinotify;
|
44
|
+
VALUE rb_cNotifier;
|
45
|
+
VALUE rb_cSinotifyEvent;
|
46
|
+
|
47
|
+
int event_check (int fd) {
|
48
|
+
struct timeval;
|
49
|
+
int r;
|
50
|
+
|
51
|
+
fd_set rfds;
|
52
|
+
FD_ZERO(&rfds);
|
53
|
+
FD_SET(fd, &rfds);
|
54
|
+
|
55
|
+
// Not using timout anymore
|
56
|
+
r = rb_thread_select (fd+1, &rfds, NULL, NULL, NULL);
|
57
|
+
return r;
|
58
|
+
}
|
59
|
+
|
60
|
+
static VALUE rb_inotify_event_new(struct inotify_event *event) {
|
61
|
+
VALUE retval;
|
62
|
+
retval = Data_Wrap_Struct(rb_cSinotifyEvent, NULL, free, event);
|
63
|
+
rb_obj_call_init(retval, 0, NULL);
|
64
|
+
return retval;
|
65
|
+
}
|
66
|
+
|
67
|
+
static VALUE rb_inotify_new(VALUE klass) {
|
68
|
+
int *fd;
|
69
|
+
VALUE retval;
|
70
|
+
fd = malloc(sizeof(int));
|
71
|
+
*fd = inotify_init();
|
72
|
+
if(*fd < 0) rb_sys_fail("inotify_init()");
|
73
|
+
retval = Data_Wrap_Struct(klass, NULL, free, fd);
|
74
|
+
rb_obj_call_init(retval, 0, NULL);
|
75
|
+
return retval;
|
76
|
+
}
|
77
|
+
|
78
|
+
static VALUE rb_inotify_add_watch(VALUE self, VALUE filename, VALUE mask) {
|
79
|
+
int *fd, wd;
|
80
|
+
Data_Get_Struct(self, int, fd);
|
81
|
+
wd = inotify_add_watch(*fd, RSTRING_PTR(filename), NUM2INT(mask));
|
82
|
+
if(wd < 0) {
|
83
|
+
rb_sys_fail(RSTRING_PTR(filename));
|
84
|
+
}
|
85
|
+
return INT2NUM(wd);
|
86
|
+
}
|
87
|
+
|
88
|
+
static VALUE rb_inotify_rm_watch(VALUE self, VALUE wdnum) {
|
89
|
+
int *fd;
|
90
|
+
Data_Get_Struct(self, int, fd);
|
91
|
+
if(inotify_rm_watch(*fd, NUM2INT(wdnum)) < 0) {
|
92
|
+
rb_sys_fail("removing watch");
|
93
|
+
}
|
94
|
+
return Qtrue;
|
95
|
+
}
|
96
|
+
|
97
|
+
static VALUE rb_inotify_each_event(VALUE self) {
|
98
|
+
int *fd, r;
|
99
|
+
struct inotify_event *event, *pevent;
|
100
|
+
char buffer[16384];
|
101
|
+
size_t buffer_n, event_size;
|
102
|
+
|
103
|
+
Data_Get_Struct(self, int, fd);
|
104
|
+
while(1) {
|
105
|
+
r = event_check(*fd);
|
106
|
+
if(r == 0) {
|
107
|
+
continue;
|
108
|
+
}
|
109
|
+
if((r = read(*fd, buffer, 16384)) < 0) {
|
110
|
+
rb_sys_fail("reading event");
|
111
|
+
}
|
112
|
+
buffer_n = 0;
|
113
|
+
while (buffer_n < r) {
|
114
|
+
pevent = (struct inotify_event *)&buffer[buffer_n];
|
115
|
+
event_size = sizeof(struct inotify_event) + pevent->len;
|
116
|
+
event = malloc(event_size);
|
117
|
+
memmove(event, pevent, event_size);
|
118
|
+
buffer_n += event_size;
|
119
|
+
rb_yield(rb_inotify_event_new(event));
|
120
|
+
}
|
121
|
+
}
|
122
|
+
return Qnil;
|
123
|
+
}
|
124
|
+
|
125
|
+
static VALUE rb_inotify_close(VALUE self) {
|
126
|
+
int *fd;
|
127
|
+
Data_Get_Struct(self, int, fd);
|
128
|
+
if(close(*fd) != 0) {
|
129
|
+
rb_sys_fail("closing inotify");
|
130
|
+
}
|
131
|
+
return Qnil;
|
132
|
+
}
|
133
|
+
|
134
|
+
static VALUE rb_inotify_event_name(VALUE self) {
|
135
|
+
struct inotify_event *event;
|
136
|
+
Data_Get_Struct(self, struct inotify_event, event);
|
137
|
+
if(event->len) {
|
138
|
+
return rb_str_new2(event->name);
|
139
|
+
} else {
|
140
|
+
return Qnil;
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
static VALUE rb_inotify_event_wd(VALUE self) {
|
145
|
+
struct inotify_event *event;
|
146
|
+
Data_Get_Struct(self, struct inotify_event, event);
|
147
|
+
return INT2NUM(event->wd);
|
148
|
+
}
|
149
|
+
|
150
|
+
|
151
|
+
static VALUE rb_inotify_event_mask(VALUE self) {
|
152
|
+
struct inotify_event *event;
|
153
|
+
Data_Get_Struct(self, struct inotify_event, event);
|
154
|
+
return LONG2NUM(event->mask);
|
155
|
+
}
|
156
|
+
|
157
|
+
void Init_sinotify () {
|
158
|
+
rb_cSinotify = rb_define_module("Sinotify");
|
159
|
+
|
160
|
+
//
|
161
|
+
// The Sinotify::PrimNotifier class
|
162
|
+
//
|
163
|
+
rb_cNotifier = rb_define_class_under(rb_cSinotify, "PrimNotifier", rb_cObject);
|
164
|
+
|
165
|
+
rb_define_singleton_method(rb_cNotifier, "new", rb_inotify_new, 0);
|
166
|
+
rb_define_method(rb_cNotifier, "add_watch", rb_inotify_add_watch, 2);
|
167
|
+
rb_define_method(rb_cNotifier, "rm_watch", rb_inotify_rm_watch, 1);
|
168
|
+
rb_define_method(rb_cNotifier, "each_event", rb_inotify_each_event, 0);
|
169
|
+
rb_define_method(rb_cNotifier, "close", rb_inotify_close, 0);
|
170
|
+
|
171
|
+
//
|
172
|
+
// The Sinotify::PrimEvent class
|
173
|
+
//
|
174
|
+
rb_cSinotifyEvent = rb_define_class_under(rb_cSinotify, "PrimEvent", rb_cObject);
|
175
|
+
rb_define_method(rb_cSinotifyEvent, "prim_name", rb_inotify_event_name, 0);
|
176
|
+
rb_define_method(rb_cSinotifyEvent, "prim_wd", rb_inotify_event_wd, 0);
|
177
|
+
rb_define_method(rb_cSinotifyEvent, "prim_mask", rb_inotify_event_mask, 0);
|
178
|
+
|
179
|
+
// following inotify masks taken from inotify lib, see 'man inotify', section
|
180
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("ACCESS"), INT2NUM(IN_ACCESS));
|
181
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("MODIFY"), INT2NUM(IN_MODIFY));
|
182
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("ATTRIB"), INT2NUM(IN_ATTRIB));
|
183
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("CLOSE_WRITE"), INT2NUM(IN_CLOSE_WRITE));
|
184
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("CLOSE_NOWRITE"), INT2NUM(IN_CLOSE_NOWRITE));
|
185
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("OPEN"), INT2NUM(IN_OPEN));
|
186
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("MOVED_FROM"), INT2NUM(IN_MOVED_FROM));
|
187
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("MOVED_TO"), INT2NUM(IN_MOVED_TO));
|
188
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("CREATE"), INT2NUM(IN_CREATE));
|
189
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("DELETE"), INT2NUM(IN_DELETE));
|
190
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("DELETE_SELF"), INT2NUM(IN_DELETE_SELF));
|
191
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("MOVE_SELF"), INT2NUM(IN_MOVE_SELF));
|
192
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("UNMOUNT"), INT2NUM(IN_UNMOUNT));
|
193
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("Q_OVERFLOW"), INT2NUM(IN_Q_OVERFLOW));
|
194
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("IGNORED"), INT2NUM(IN_IGNORED));
|
195
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("CLOSE"), INT2NUM(IN_CLOSE));
|
196
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("MOVE"), INT2NUM(IN_MOVE));
|
197
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("ONLYDIR"), INT2NUM(IN_ONLYDIR));
|
198
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("DONT_FOLLOW"), INT2NUM(IN_DONT_FOLLOW));
|
199
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("MASK_ADD"), INT2NUM(IN_MASK_ADD));
|
200
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("ISDIR"), INT2NUM(IN_ISDIR));
|
201
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("ONESHOT"), INT2NUM(IN_ONESHOT));
|
202
|
+
rb_const_set(rb_cSinotifyEvent, rb_intern("ALL_EVENTS"), INT2NUM(IN_ALL_EVENTS));
|
203
|
+
|
204
|
+
|
205
|
+
}
|
data/lib/sinotify.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
ext_lib = File.join(File.dirname(__FILE__), '../ext/sinotify.so')
|
2
|
+
unless File.exist?(ext_lib)
|
3
|
+
raise "Could not find ext/sinotify.so. \n" \
|
4
|
+
+ "Please build the sinotify.so extention first (cd [sinotify gem]/ext && ruby extconf.rb && make)"
|
5
|
+
end
|
6
|
+
|
7
|
+
# load base info and c lib
|
8
|
+
require ext_lib
|
9
|
+
require File.join(File.dirname(__FILE__), 'sinotify_info')
|
10
|
+
|
11
|
+
# load external dependencies
|
12
|
+
require 'rubygems'
|
13
|
+
require 'cosell'
|
14
|
+
require 'logger'
|
15
|
+
|
16
|
+
# load application
|
17
|
+
Sinotify.require_all_libs_relative_to(__FILE__)
|
18
|
+
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module Sinotify
|
2
|
+
|
3
|
+
#
|
4
|
+
# Sinotify events are triggered as they come in to the Notifier ('announced'
|
5
|
+
# in the parlance of the Cosell announcement framework that sinotify uses).
|
6
|
+
# Each event has the 'path' of the file or dir that was effected, the
|
7
|
+
# timestamp of the event (generated in ruby, not at the primitive level), and
|
8
|
+
# whether the event was on a file or a directory. Also available is the event
|
9
|
+
# type, called the 'etype,' which can be :modify, :create, :delete, etc. The
|
10
|
+
# list of event types is below.
|
11
|
+
#
|
12
|
+
# A Sinotify::Event does not perfectly model a linux inotify event. See
|
13
|
+
# Sinotify::PrimEvent for that.
|
14
|
+
#
|
15
|
+
# This event class deviates from Sinotify::PrimEvent in one significant regard. Sinotify does not
|
16
|
+
# pass events about children of a given directory, it only passes events about the directory
|
17
|
+
# (or file) itself. That is _not_ to say you can't setup a recursive watch in the Notifier class,
|
18
|
+
# just that _the event itself_ only pertains the the inode/file/directory being altered, not to
|
19
|
+
# its children.
|
20
|
+
#
|
21
|
+
# This is perhaps best illustrated by an example. Let's say you
|
22
|
+
#
|
23
|
+
# 1. Create a directory called '/tmp/test'
|
24
|
+
# 2. Create a file in '/tmp/test' called '/tmp/test/blah'
|
25
|
+
# 3. You put a watch on the directory '/tmp/test'
|
26
|
+
# 4. You then do a 'rm -rf /tmp/test'
|
27
|
+
# (thus deleting both the file /tmp/test/blah AND the directory /tmp/test)
|
28
|
+
#
|
29
|
+
# In linux inotify, you would get two events in this scenario, _both_ on the
|
30
|
+
# watch for the /tmp/test directory. One of the events would be a ':delete' event
|
31
|
+
# (that is, the mask of the event would be equal to
|
32
|
+
# Sinotify::PrimEvent::DELETE, or the 'etype' of the PrimEvent would equal
|
33
|
+
# ':delete'), and the 'name' slot in the event would be 'blah.' This is your
|
34
|
+
# cue that the event _really_ happened on a child of the thing being watched
|
35
|
+
# ('/tmp/test'), not to the directory itself. Since you deleted both the file
|
36
|
+
# and the directory with your 'rm -rf' command, another event would come in
|
37
|
+
# of the etype :delete_self for the directory, and 'is_dir' would be in the
|
38
|
+
# mask (ie. the mask would be Sinotify::PrimEvent::DELETE & Sinotify::PrimEvent::IS_DIR).
|
39
|
+
#
|
40
|
+
# Sinotify events would be a bit different in the example above.
|
41
|
+
# You would still get 2 events, but both would be :delete events,
|
42
|
+
# one where the 'path' is '/tmp/test', and the other where the 'path'
|
43
|
+
# is '/tmp/test/blah'. In the case of the event for '/tmp/test', the call
|
44
|
+
# to 'directory?' would return true.
|
45
|
+
#
|
46
|
+
# If you want to work with an event notifier that works more like the low level linux inotify
|
47
|
+
# (receiving both :delete with name slot filled in and another event w/ :delete_self),
|
48
|
+
# you will have to work directly with PrimNotifier and PrimEvent (along with their irritating
|
49
|
+
# linux inotify-style blocking synchronous event loop)
|
50
|
+
#
|
51
|
+
# Here is the list of possible events adapted from the definitions
|
52
|
+
# in [linux_src]/include/linux/inotify.h:
|
53
|
+
#
|
54
|
+
# File related:
|
55
|
+
# :access # File was accessed
|
56
|
+
# :modify # file modified
|
57
|
+
# :attrib # meta data changed
|
58
|
+
# :close_write # writable file was closed
|
59
|
+
# :close_nowrite # unwritable file was closed
|
60
|
+
# :open # file was opened
|
61
|
+
# :moved_from # file moved from X
|
62
|
+
# :moved_to # file moved to Y
|
63
|
+
# :create # file created
|
64
|
+
# :delete # file deleted
|
65
|
+
# :delete_self # self was deleted
|
66
|
+
# :move_self # self was moved
|
67
|
+
#
|
68
|
+
# File related helpers:
|
69
|
+
#
|
70
|
+
# :close # (close_write | close_nowrite)
|
71
|
+
# :move # (moved_from | moved_to)
|
72
|
+
#
|
73
|
+
# Misc events
|
74
|
+
#
|
75
|
+
# :unmount # backing fs was unmounted
|
76
|
+
# :q_overflow # event queue overflowed
|
77
|
+
# :ignored # file was ignored
|
78
|
+
# :mask_add # added to mask of already existing event
|
79
|
+
# :isdir # event occurred against dir
|
80
|
+
# :oneshot # only send event once
|
81
|
+
#
|
82
|
+
class Event
|
83
|
+
|
84
|
+
attr_accessor :prim_event, :path, :timestamp, :is_dir
|
85
|
+
|
86
|
+
# a few attr declarations just so they show up in rdoc
|
87
|
+
|
88
|
+
# Given a prim_event, and the Watch associated with the event's watch descriptor,
|
89
|
+
# return a Sinotify::Event.
|
90
|
+
def self.from_prim_event_and_watch(prim_event, watch)
|
91
|
+
path = watch.path # path for the watch associated w/ this even
|
92
|
+
is_dir = watch.directory? # original watch was on a dir or a file?
|
93
|
+
|
94
|
+
# This gets a little odd. The prim_event's 'name' field
|
95
|
+
# will be nil if the change was to a directory itself, or if
|
96
|
+
# the watch was on a file to begin with. However,
|
97
|
+
# when a watch is on a dir, but the event occurs on a file in that dir
|
98
|
+
# inotify sets the 'name' field to the file. :isdir will be in the etypes
|
99
|
+
# if that file happens to be a subdir. Sinotify events do not
|
100
|
+
# play this game, only sending events for the thing that was altered
|
101
|
+
# in the first place. So right here is where we deduce if the
|
102
|
+
# event was _really_ on a file or a dir.
|
103
|
+
unless prim_event.name.nil?
|
104
|
+
path = File.join(path, prim_event.name)
|
105
|
+
is_dir = prim_event.etypes.include?(:isdir)
|
106
|
+
end
|
107
|
+
|
108
|
+
# is_dir must be passed along, since it may no longer exist (and thus cant be deduced later)
|
109
|
+
# inotify prim_events to not retain enough information to make it possible to deduce the
|
110
|
+
# original fullpath and whether it was a file or directory, so this info must be passed around.
|
111
|
+
return Sinotify::Event.new(:prim_event => prim_event,
|
112
|
+
:path => path,
|
113
|
+
:timestamp => Time.now, # any way to get this from prim event?
|
114
|
+
:is_dir => is_dir)
|
115
|
+
end
|
116
|
+
|
117
|
+
def initialize(args={})
|
118
|
+
args.each{|k,v| self.send("#{k}=",v)}
|
119
|
+
@timestamp ||= Time.now
|
120
|
+
|
121
|
+
# initialize a few variables just to shut up the ruby warnings
|
122
|
+
@etypes = nil
|
123
|
+
end
|
124
|
+
|
125
|
+
# The Sinotify::PrimEvent associated with this event (a straight
|
126
|
+
# wrapper around the linux inotify event)
|
127
|
+
def prim_event; @prim_event; end
|
128
|
+
|
129
|
+
# The full path of the file or directory on which the event happened
|
130
|
+
def path; @path; end
|
131
|
+
|
132
|
+
# when the event happened
|
133
|
+
def timestamp; @timestamp; end
|
134
|
+
|
135
|
+
|
136
|
+
def to_s; self.inspect_or_to_s(false); end
|
137
|
+
def inspect; self.inspect_or_to_s(true); end
|
138
|
+
|
139
|
+
# The etypes associated with this event (eg. :create, :modify, :delete, etc)
|
140
|
+
def etypes
|
141
|
+
# The etype/mask functions delegated to prim_event, EXCEPT: when :delete_self is in
|
142
|
+
# the list, and path is a directory, change it to 'delete'. If you want
|
143
|
+
# the etypes in the original prim_event, ask for event.prim_event.etypes
|
144
|
+
if @etypes.nil?
|
145
|
+
@etypes = self.prim_event.etypes
|
146
|
+
|
147
|
+
# change :delete_self into :delete
|
148
|
+
if self.directory? and @etypes.include?(:delete_self)
|
149
|
+
@etypes.delete(:delete_self)
|
150
|
+
@etypes << :delete
|
151
|
+
end
|
152
|
+
|
153
|
+
# add :close if :close_write or :close_nowrite are there, but :close is not
|
154
|
+
if @etypes.include?(:close_write) || @etypes.include?(:close_nowrite)
|
155
|
+
(@etypes << :close) unless @etypes.include?(:close)
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
@etypes
|
160
|
+
end
|
161
|
+
|
162
|
+
def directory?
|
163
|
+
self.is_dir.eql?(true)
|
164
|
+
end
|
165
|
+
|
166
|
+
def has_etype? etype
|
167
|
+
self.etypes.include?(etype)
|
168
|
+
end
|
169
|
+
|
170
|
+
def watch_descriptor
|
171
|
+
self.prim_event.watch_descriptor
|
172
|
+
end
|
173
|
+
|
174
|
+
protected
|
175
|
+
|
176
|
+
# :stopdoc:
|
177
|
+
def inspect_or_to_s(show_prim_event = false)
|
178
|
+
prim_event = (show_prim_event)? ", :prim_event => #{self.prim_event.inspect}" : ''
|
179
|
+
"<#{self.class} :path => '#{self.path}', dir? => #{self.directory?}, :etypes => #{self.etypes.inspect rescue 'could not determine'}#{prim_event}>"
|
180
|
+
end
|
181
|
+
|
182
|
+
# :startdoc:
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
@@ -0,0 +1,308 @@
|
|
1
|
+
module Sinotify
|
2
|
+
|
3
|
+
#
|
4
|
+
# Watch a directory or file for events like create, modify, delete, etc.
|
5
|
+
# (See Sinotify::Event for full list).
|
6
|
+
#
|
7
|
+
# See the synopsis section in the README.txt for example usage.
|
8
|
+
#
|
9
|
+
#
|
10
|
+
class Notifier
|
11
|
+
include Cosell
|
12
|
+
|
13
|
+
attr_accessor :file_or_dir_name, :etypes, :recurse, :recurse_throttle, :logger
|
14
|
+
|
15
|
+
# Required Args
|
16
|
+
#
|
17
|
+
# file_or_dir_name: the file/directory to watch
|
18
|
+
#
|
19
|
+
# Options:
|
20
|
+
# :recurse => (true|false)
|
21
|
+
# whether to automatically create watches on sub directories
|
22
|
+
# default: true if file_or_dir_name is a directory, else false
|
23
|
+
# raises if true and file_or_dir_name is not a directory
|
24
|
+
#
|
25
|
+
# :recurse_throttle =>
|
26
|
+
# When recursing, a background thread drills down into all the child directories
|
27
|
+
# creating notifiers on them. The recurse_throttle tells the notifier how far
|
28
|
+
# to recurse before sleeping for 0.1 seconds, so that drilling down does not hog
|
29
|
+
# the system on large directorie hierarchies.
|
30
|
+
# default is 10
|
31
|
+
#
|
32
|
+
# :etypes =>
|
33
|
+
# which inotify file system event types to listen for (eg :create, :delete, etc)
|
34
|
+
# See docs for Sinotify::Event for list of event types.
|
35
|
+
# default is :all_types
|
36
|
+
#
|
37
|
+
# :logger =>
|
38
|
+
# Where to log errors to. Default is Logger.new(STDOUT).
|
39
|
+
#
|
40
|
+
# :announcement_throttle =>
|
41
|
+
# How many events can be announced at a time before the queue goes back to sleep for a cycle.
|
42
|
+
# (ie. Cosell's 'announcements_per_cycle')
|
43
|
+
#
|
44
|
+
# :announcements_sleep_time =>
|
45
|
+
# How long the queue should sleep for before announcing the next batch of queued up
|
46
|
+
# Sinotify::Events (ie. Cosell's 'sleep_time')
|
47
|
+
#
|
48
|
+
def initialize(file_or_dir_name, opts = {})
|
49
|
+
|
50
|
+
initialize_cosell! # init the announcements framework
|
51
|
+
|
52
|
+
raise "Could not find #{file_or_dir_name}" unless File.exist?(file_or_dir_name)
|
53
|
+
self.file_or_dir_name = file_or_dir_name
|
54
|
+
|
55
|
+
# by default, recurse if directory?. If opts[:recurse] was true and passed in,
|
56
|
+
# make sure the watch is on a directory
|
57
|
+
self.recurse = opts[:recurse].nil?? self.on_directory? : opts[:recurse]
|
58
|
+
raise "Cannot recurse, #{file_or_dir_name} is not a directory" if self.recurse? && !self.on_directory?
|
59
|
+
|
60
|
+
# how many directories at a time to register.
|
61
|
+
self.recurse_throttle = opts[:recurse_throttle] || 10
|
62
|
+
|
63
|
+
self.etypes = Array(opts[:etypes] || :all_events)
|
64
|
+
validate_etypes!
|
65
|
+
|
66
|
+
self.prim_notifier = Sinotify::PrimNotifier.new
|
67
|
+
|
68
|
+
# setup async announcements queue (part of the Cosell mixin)
|
69
|
+
@logger = opts[:logger] || Logger.new(STDOUT)
|
70
|
+
sleep_time = opts[:announcements_sleep_time] || 0.05
|
71
|
+
announcement_throttle = opts[:announcement_throttle] || 50
|
72
|
+
self.queue_announcements!(:sleep_time => sleep_time,
|
73
|
+
:logger => opts[:logger],
|
74
|
+
:announcements_per_cycle => announcement_throttle)
|
75
|
+
|
76
|
+
self.closed = false
|
77
|
+
|
78
|
+
# initialize a few variables just to shut up the ruby warnings
|
79
|
+
# Apparently the lazy init idiom using ||= is no longer approved of. Shame that.
|
80
|
+
@spy_logger = nil
|
81
|
+
@spy_logger_level = nil
|
82
|
+
@watch_thread = nil
|
83
|
+
end
|
84
|
+
|
85
|
+
# whether this watch is on a directory
|
86
|
+
def on_directory?
|
87
|
+
File.directory?(self.file_or_dir_name)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Start watching for inotify file system events.
|
91
|
+
def watch!
|
92
|
+
raise "Cannot reopen an inotifier. Create a new one instead" if self.closed?
|
93
|
+
self.add_all_directories_in_background
|
94
|
+
self.start_prim_event_loop_thread
|
95
|
+
return self
|
96
|
+
end
|
97
|
+
|
98
|
+
# Close this notifier. Notifiers cannot be reopened after close!.
|
99
|
+
def close!
|
100
|
+
@closed = true
|
101
|
+
self.remove_all_watches
|
102
|
+
end
|
103
|
+
|
104
|
+
# Log a message every time a prim_event comes in (will be logged even if it is considered 'noise'),
|
105
|
+
# and log a message whenever an event is announced. Overrides Cosell's spy! method (and uses cosell's
|
106
|
+
# spy! to log announced events).
|
107
|
+
#
|
108
|
+
# Options:
|
109
|
+
# :logger => The log to log to. Default is a logger on STDOUT
|
110
|
+
# :level => The log level to log with. Default is :info
|
111
|
+
#
|
112
|
+
def spy!(opts = {})
|
113
|
+
@spy_logger = opts[:logger] || Logger.new(STDOUT)
|
114
|
+
@spy_logger_level = opts[:level] || :info
|
115
|
+
opts[:on] = Sinotify::Event
|
116
|
+
opts[:preface_with] = "Sinotify::Notifier Event Spy"
|
117
|
+
super(opts)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Return a list of files/directories currently being watched. Will only contain one entry unless
|
121
|
+
# this notifier was setup on a directory with the option :recurse => true.
|
122
|
+
def all_directories_being_watched
|
123
|
+
self.watches.values.collect{|w| w.path }.sort
|
124
|
+
end
|
125
|
+
|
126
|
+
def watches
|
127
|
+
@watches ||= {}
|
128
|
+
end
|
129
|
+
|
130
|
+
# Whether this notifier watches all the files in all of the subdirectories
|
131
|
+
# of the directory being watched.
|
132
|
+
def recurse?
|
133
|
+
self.recurse
|
134
|
+
end
|
135
|
+
|
136
|
+
def to_s
|
137
|
+
"Sinotify::Notifier[#{self.file_or_dir_name}, :watches => #{self.watches.size}]"
|
138
|
+
end
|
139
|
+
|
140
|
+
protected
|
141
|
+
|
142
|
+
#:stopdoc:
|
143
|
+
|
144
|
+
def validate_etypes!
|
145
|
+
bad = self.etypes.detect{|etype| PrimEvent.mask_from_etype(etype).nil? }
|
146
|
+
raise "Unrecognized etype '#{bad}'. Please see valid list in docs for Sinotify::Event" if bad
|
147
|
+
end
|
148
|
+
|
149
|
+
# some events we don't want to report (certain events are generated just from creating watches)
|
150
|
+
def event_is_noise? prim_event, watch
|
151
|
+
|
152
|
+
etypes_strings = prim_event.etypes.map{|e|e.to_s}.sort
|
153
|
+
|
154
|
+
# the simple act of creating a watch causes these to fire"
|
155
|
+
return true if ["close_nowrite", "isdir"].eql?(etypes_strings)
|
156
|
+
return true if ["isdir", "open"].eql?(etypes_strings)
|
157
|
+
return true if ["ignored"].eql?(etypes_strings)
|
158
|
+
|
159
|
+
# If the event is on a subdir of the directory specified in watch, don't send it because
|
160
|
+
# there should be another even (on the subdir itself) that comes through, and this one
|
161
|
+
# will be redundant.
|
162
|
+
return true if ["delete", "isdir"].eql?(etypes_strings)
|
163
|
+
|
164
|
+
return false
|
165
|
+
end
|
166
|
+
|
167
|
+
# Open up a background thread that adds all the watches on @file_or_dir_name and,
|
168
|
+
# if @recurse is true, all of its subdirs.
|
169
|
+
def add_all_directories_in_background
|
170
|
+
@child_dir_thread = Thread.new do
|
171
|
+
begin
|
172
|
+
self.add_watches!
|
173
|
+
rescue Exception => x
|
174
|
+
log "Exception: #{x}, trace: \n\t#{x.backtrace.join("\n\t")}", :error
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def add_watches!(fn = self.file_or_dir_name, throttle = 0)
|
180
|
+
|
181
|
+
return if closed?
|
182
|
+
if throttle.eql?(self.recurse_throttle)
|
183
|
+
sleep 0.1
|
184
|
+
throttle = 0
|
185
|
+
end
|
186
|
+
throttle += 1
|
187
|
+
|
188
|
+
self.add_watch(fn)
|
189
|
+
|
190
|
+
if recurse?
|
191
|
+
Dir[File.join(fn, '/**')].each do |child_fn|
|
192
|
+
next if child_fn.eql?(fn)
|
193
|
+
self.add_watches!(child_fn, throttle) if File.directory?(child_fn)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
|
199
|
+
def add_watch(fn)
|
200
|
+
watch_descriptor = self.prim_notifier.add_watch(fn, self.raw_mask)
|
201
|
+
# puts "ADDED WATCH: #{watch_descriptor} for #{fn}"
|
202
|
+
remove_watch(watch_descriptor) # remove the existing, if it exists
|
203
|
+
watch = Watch.new(:path => fn, :watch_descriptor => watch_descriptor)
|
204
|
+
self.watches[watch_descriptor.to_s] = watch
|
205
|
+
end
|
206
|
+
|
207
|
+
# Remove the watch associated with the watch_descriptor passed in
|
208
|
+
def remove_watch(watch_descriptor, prim_remove = false)
|
209
|
+
if watches[watch_descriptor.to_s]
|
210
|
+
# puts "REMOVING: #{watch_descriptor}"
|
211
|
+
self.watches.delete(watch_descriptor.to_s)
|
212
|
+
|
213
|
+
# the prim_notifier will complain if we remove a watch on a deleted file,
|
214
|
+
# since the watch will have automatically been removed already. Be default we
|
215
|
+
# don't care, but if caller is sure there are some prim watches to clean
|
216
|
+
# up, then they can pass 'true' for prim_remove. Another way to handle
|
217
|
+
# this would be to just default to true and fail silently, but trying this
|
218
|
+
# more conservative approach for now.
|
219
|
+
self.prim_notifier.rm_watch(watch_descriptor.to_i) if prim_remove
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def remove_all_watches
|
224
|
+
self.watches.keys.each{|watch_descriptor| self.remove_watch(watch_descriptor, true) }
|
225
|
+
end
|
226
|
+
|
227
|
+
def log(msg, level = :debug)
|
228
|
+
puts(msg) unless [:debug, :info].include?(level)
|
229
|
+
self.logger.send(level, msg) if self.logger
|
230
|
+
end
|
231
|
+
|
232
|
+
def spy_on_event(prim_event)
|
233
|
+
if @spy_logger
|
234
|
+
msg = "Sinotify::Notifier Prim Event Spy: #{prim_event.inspect}"
|
235
|
+
@spy_logger.send(@spy_logger_level, msg)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Listen for linux inotify events, and as they come in
|
240
|
+
# 1. adapt them into Sinotify::Event objects
|
241
|
+
# 2. 'announce' them using Cosell.
|
242
|
+
# By default, Cosell is setup to Queue the announcements in a bg thread.
|
243
|
+
#
|
244
|
+
# The references to two different logs in this method may be a bit confusing. The @spy_logger
|
245
|
+
# exclusively logs (spys on) events and announcements. The "log" method instead uses the @logger
|
246
|
+
# and logs errors and exceptions. The @logger is defined when creating this object (using the :logger
|
247
|
+
# option), and the @spy_logger is defined in the :spy! method.
|
248
|
+
#
|
249
|
+
def start_prim_event_loop_thread
|
250
|
+
|
251
|
+
raise "Already watching!" unless @watch_thread.nil?
|
252
|
+
|
253
|
+
@watch_thread = Thread.new do
|
254
|
+
begin
|
255
|
+
self.prim_notifier.each_event do |prim_event|
|
256
|
+
watch = self.watches[prim_event.watch_descriptor.to_s]
|
257
|
+
if event_is_noise?(prim_event, watch)
|
258
|
+
@spy_logger.debug("Sinotify::Notifier Spy: Skipping noise[#{prim_event.inspect}]") if @spy_logger
|
259
|
+
else
|
260
|
+
spy_on_event(prim_event)
|
261
|
+
if watch.nil?
|
262
|
+
self.log "Could not determine watch from descriptor #{prim_event.watch_descriptor}, something is wrong. Event: #{prim_event.inspect}", :warn
|
263
|
+
else
|
264
|
+
event = Sinotify::Event.from_prim_event_and_watch(prim_event, watch)
|
265
|
+
self.announce event
|
266
|
+
if event.has_etype?(:create) && event.directory?
|
267
|
+
Thread.new do
|
268
|
+
# have to thread this because the :create event comes in _before_ the directory exists,
|
269
|
+
# and inotify will not permit a watch on a file unless it exists
|
270
|
+
sleep 0.1
|
271
|
+
self.add_watch(event.path)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
# puts "REMOVING: #{event.inspect}, WATCH: #{self.watches[event.watch_descriptor.to_s]}" if event.has_etype?(:delete) && event.directory?
|
275
|
+
self.remove_watch(event.watch_descriptor) if event.has_etype?(:delete) && event.directory?
|
276
|
+
break if closed?
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
rescue Exception => x
|
281
|
+
log "Exception: #{x}, trace: \n\t#{x.backtrace.join("\n\t")}", :error
|
282
|
+
end
|
283
|
+
|
284
|
+
log "Exiting prim event loop thread for #{self}"
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
def raw_mask
|
290
|
+
if @raw_mask.nil?
|
291
|
+
(self.etypes << :delete_self) if self.etypes.include?(:delete)
|
292
|
+
@raw_mask = self.etypes.inject(0){|raw, etype| raw | PrimEvent.mask_from_etype(etype) }
|
293
|
+
end
|
294
|
+
@raw_mask
|
295
|
+
end
|
296
|
+
|
297
|
+
# ruby gives warnings in verbose mode if you use attr_accessor to set these next few:
|
298
|
+
def prim_notifier; @prim_notifier; end
|
299
|
+
def prim_notifier= x; @prim_notifier = x; end
|
300
|
+
def watch_descriptor; @watch_descriptor; end
|
301
|
+
def watch_descriptor= x; @watch_descriptor = x; end
|
302
|
+
def closed?; @closed.eql?(true); end
|
303
|
+
def closed= x; @closed = x; end
|
304
|
+
|
305
|
+
#:startdoc:
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|