swerling-sinotify 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|