sinotify 0.0.2

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.
@@ -0,0 +1,47 @@
1
+ module Sinotify
2
+
3
+ VERSION = '0.0.2'
4
+
5
+ # :stopdoc:
6
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
7
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
8
+
9
+ # Returns the version string for the library.
10
+ #
11
+ def self.version
12
+ VERSION
13
+ end
14
+
15
+ # Returns the library path for the module. If any arguments are given,
16
+ # they will be joined to the end of the libray path using
17
+ # <tt>File.join</tt>.
18
+ #
19
+ def self.libpath( *args )
20
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
21
+ end
22
+
23
+ # Returns the lpath for the module. If any arguments are given,
24
+ # they will be joined to the end of the path using
25
+ # <tt>File.join</tt>.
26
+ #
27
+ def self.path( *args )
28
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
29
+ end
30
+
31
+ # Utility method used to require all files ending in .rb that lie in the
32
+ # directory below this file that has the same name as the filename passed
33
+ # in. Optionally, a specific _directory_ name can be passed in such that
34
+ # the _filename_ does not have to be equivalent to the directory.
35
+ #
36
+ def self.require_all_libs_relative_to( fname, dir = nil )
37
+ dir ||= ::File.basename(fname, '.*')
38
+ search_me = ::File.expand_path(
39
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
40
+
41
+ Dir.glob(search_me).sort.each {|rb| require rb}
42
+ end
43
+
44
+ # :startdoc:
45
+
46
+ end # module Sinotify
47
+
@@ -0,0 +1,80 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{sinotify}
5
+ s.version = "0.0.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Steven Swerling"]
9
+ s.date = %q{2009-08-09}
10
+ s.description = %q{ALPHA Alert -- just uploaded initial release.
11
+
12
+ Linux inotify is a means to receive events describing file system activity (create, modify, delete, close, etc).
13
+
14
+ Sinotify was derived from aredridel's package (http://raa.ruby-lang.org/project/ruby-inotify/), with the addition of
15
+ Paul Boon's tweak for making the event_check thread more polite (see
16
+ http://www.mindbucket.com/2009/02/24/ruby-daemons-verifying-good-behavior/)
17
+
18
+ In sinotify, the classes Sinotify::PrimNotifier and Sinotify::PrimEvent provide a low level wrapper to inotify, with
19
+ the ability to establish 'watches' and then listen for inotify events using one of inotify's synchronous event loops,
20
+ and providing access to the events' masks (see 'man inotify' for details). Sinotify::PrimEvent class adds a little semantic sugar
21
+ to the event in to the form of 'etypes', which are just ruby symbols that describe the event mask. If the event has a
22
+ raw mask of (DELETE_SELF & IS_DIR), then the etypes array would be [:delete_self, :is_dir].
23
+
24
+ In addition to the 'straight' wrapper in inotify, sinotify provides an asynchronous implementation of the 'observer
25
+ pattern' for notification. In other words, Sinotify::Notifier listens in the background for inotify events, adapting
26
+ them into instances of Sinotify::Event as they come in and immediately placing them in a concurrent queue, from which
27
+ they are 'announced' to 'subscribers' of the event. [Sinotify uses the 'cosell' implementation of the Announcements
28
+ event notification framework, hence the terminology 'subscribe' and 'announce' rather then 'listen' and 'trigger' used
29
+ in the standard event observer pattern. See the 'cosell' package on github for details.]
30
+
31
+ A variety of 'knobs' are provided for controlling the behavior of the notifier: whether a watch should apply to a
32
+ single directory or should recurse into subdirectores, how fast it should broadcast queued events, etc (see
33
+ Sinotify::Notifier, and the example in the synopsis section below). An event 'spy' can also be setup to log all
34
+ Sinotify::PrimEvents and Sinotify::Events.
35
+
36
+ Sinotify::Event simplifies inotify's muddled event model, sending events only for those files/directories that have
37
+ changed. That's not to say you can't setup a notifier that recurses into subdirectories, just that any individual
38
+ event will apply to a single file, and not to its children. Also, event types are identified using words (in the form
39
+ of ruby :symbols) instead of inotify's event masks. See Sinotify::Event for more explanation.
40
+
41
+ The README for inotify:
42
+
43
+ http://www.kernel.org/pub/linux/kernel/people/rml/inotify/README
44
+
45
+ Selected quotes from the README for inotify:
46
+
47
+ * "Rumor is that the 'd' in 'dnotify' does not stand for 'directory' but for 'suck.'"
48
+
49
+ * "The 'i' in inotify does not stand for 'suck' but for 'inode' -- the logical
50
+ choice since inotify is inode-based."
51
+
52
+ (The 's' in 'sinotify' does in fact stand for 'suck.')}
53
+ s.email = %q{sswerling@yahoo.com}
54
+ s.extensions = ["ext/extconf.rb"]
55
+ s.extra_rdoc_files = ["History.txt", "README.txt"]
56
+ s.files = [".gitignore", "History.txt", "README.rdoc", "README.txt", "Rakefile", "TODO", "examples/watcher.rb", "ext/extconf.rb", "ext/src/inotify-syscalls.h", "ext/src/inotify.h", "ext/src/sinotify.c", "lib/sinotify.rb", "lib/sinotify/event.rb", "lib/sinotify/notifier.rb", "lib/sinotify/prim_event.rb", "lib/sinotify/watch.rb", "lib/sinotify_info.rb", "sinotify.gemspec", "spec/prim_notify_spec.rb", "spec/sinotify_spec.rb", "spec/spec_helper.rb"]
57
+ s.has_rdoc = true
58
+ s.homepage = %q{http://tab-a.slot-z.net}
59
+ s.rdoc_options = ["--inline-source", "--main", "README.txt"]
60
+ s.require_paths = ["lib", "ext"]
61
+ s.rubyforge_project = %q{sinotify}
62
+ s.rubygems_version = %q{1.3.2}
63
+ s.summary = %q{ALPHA Alert -- just uploaded initial release}
64
+
65
+ if s.respond_to? :specification_version then
66
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
67
+ s.specification_version = 3
68
+
69
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
70
+ s.add_runtime_dependency(%q<cosell>, [">= 0"])
71
+ s.add_development_dependency(%q<bones>, [">= 2.5.1"])
72
+ else
73
+ s.add_dependency(%q<cosell>, [">= 0"])
74
+ s.add_dependency(%q<bones>, [">= 2.5.1"])
75
+ end
76
+ else
77
+ s.add_dependency(%q<cosell>, [">= 0"])
78
+ s.add_dependency(%q<bones>, [">= 2.5.1"])
79
+ end
80
+ end
@@ -0,0 +1,98 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+ require 'fileutils'
3
+
4
+ #
5
+ # The tests for the inotify wrapper.
6
+ # Mostly taken straight from ruby-inotify's tests, w/ some tweaks.
7
+ #
8
+ describe Sinotify::PrimNotifier do
9
+
10
+ before(:each) do
11
+ @inotify = Sinotify::PrimNotifier.new
12
+ end
13
+
14
+ it "should be able to create and remove a watch descriptor" do
15
+ wd = @inotify.add_watch("/tmp", Sinotify::PrimEvent::CREATE)
16
+ wd.class.should be_eql(Fixnum)
17
+ wd.should_not be_eql(0)
18
+ @inotify.rm_watch(wd).should be_true
19
+ end
20
+
21
+ it "should get events on watched directory and get name of altered file in watched directory" do
22
+ test_fn = "/tmp/sinotify-test"
23
+ FileUtils.rm_f test_fn
24
+ wd = @inotify.add_watch("/tmp", Sinotify::PrimEvent::DELETE | Sinotify::PrimEvent::CREATE)
25
+ begin
26
+ FileUtils.touch test_fn
27
+ @inotify.each_event do |ev|
28
+ #puts "-----------#{ev.etypes.inspect}"
29
+ ev.class.should be_eql(Sinotify::PrimEvent)
30
+ ev.name.should be_eql('sinotify-test')
31
+ ev.mask.should be_eql(Sinotify::PrimEvent::CREATE)
32
+ ev.has_etype?(:create).should be_true
33
+ ev.etypes.size.should be_eql(1)
34
+ ev.inspect.should be_eql "<Sinotify::PrimEvent :name => 'sinotify-test', :etypes => [:create], :mask => 100, :watch_descriptor => 1>"
35
+ break
36
+ end
37
+ FileUtils.rm_f test_fn
38
+ @inotify.each_event do |ev|
39
+ #puts "-----------#{ev.etypes.inspect}"
40
+ ev.has_etype?(:delete).should be_true
41
+ ev.etypes.size.should be_eql(1)
42
+ break
43
+ end
44
+ ensure
45
+ @inotify.rm_watch(wd)
46
+ FileUtils.rm_f test_fn
47
+ end
48
+ end
49
+
50
+ it "should get events on watched file" do
51
+ test_fn = "/tmp/sinotify-test"
52
+ FileUtils.touch test_fn
53
+ wd = @inotify.add_watch(test_fn, Sinotify::PrimEvent::ATTRIB | Sinotify::PrimEvent::DELETE | Sinotify::PrimEvent::MODIFY)
54
+ begin
55
+ FileUtils.touch test_fn
56
+ @inotify.each_event do |ev|
57
+ ev.name.should be_nil # name is only set when watching a directory
58
+ ev.has_etype?(:attrib).should be_true
59
+ break
60
+ end
61
+
62
+ File.open(test_fn, 'a'){|f| f << 'hi'}
63
+ @inotify.each_event do |ev|
64
+ ev.name.should be_nil # name is only set when watching a directory
65
+ ev.has_etype?(:modify).should be_true
66
+ break
67
+ end
68
+
69
+ FileUtils.rm_f test_fn
70
+ @inotify.each_event do |ev|
71
+ #puts "-----------#{ev.inspect}"
72
+ # TODO: Look into this -- when deleting a file, it gets an event of type :attrib instead of :delete.
73
+ # Is this a bug or something I am doing?
74
+ # ev.has_etype?(:delete).should be_true
75
+ break
76
+ end
77
+
78
+ # since the event is deleted, it should not be possible to remove the watch
79
+ lambda{@inotify.rm_watch(wd)}.should raise_error
80
+
81
+ ensure
82
+ @inotify.rm_watch(wd) rescue nil
83
+ FileUtils.rm_f test_fn
84
+ end
85
+ end
86
+
87
+ protected
88
+
89
+ def little_bench(msg, &block)
90
+ start = Time.now
91
+ result = block.call
92
+ puts "#{msg}: #{Time.now - start} sec"
93
+ return result
94
+ end
95
+ end
96
+
97
+ # EOF
98
+
@@ -0,0 +1,265 @@
1
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
2
+ require 'fileutils'
3
+
4
+ class MockPrimEvent
5
+ attr_accessor :etypes, :mask, :wd, :name
6
+ end
7
+
8
+ #
9
+ # WARNING: These tests are a bit brittle. They depend on events taking place in threads as a result
10
+ # of filesytem events (inotify events). Sometimes the file system events dont come in as fast as
11
+ # desirable for the test, or sometimes ruby threads themselves may not get scheduled fast enough.
12
+ # If a test is failing on your system, it may start to succeed if you increase the values
13
+ # in tiny_pause!, pause!, or big_pause! methods below.
14
+ #
15
+ describe Sinotify do
16
+
17
+ # A lot of Sinotify work occurs in background threads (eg. adding watches, adding subdirectories),
18
+ # so the tests may insert a tiny pauses to allow the bg threads to do their thing before making
19
+ # any assertions.
20
+ def tiny_pause!
21
+ 25.times{sleep 0.0001}
22
+ end
23
+ def pause!
24
+ 25.times{sleep 0.005}
25
+ end
26
+ def big_pause!
27
+ 10.times{sleep 0.1}
28
+ end
29
+
30
+ def reset_test_dir!
31
+ raise 'not gonna happen' unless @test_root_dir =~ /\/tmp\//
32
+ FileUtils.rm_rf(@test_root_dir)
33
+ FileUtils.mkdir(@test_root_dir)
34
+ ('a'..'z').each{|ch| FileUtils.mkdir(File.join(@test_root_dir, ch))}
35
+ pause!
36
+ end
37
+
38
+ before(:each) do
39
+ @test_root_dir = '/tmp/sinotifytestdir'
40
+ end
41
+
42
+ it "should properly create event mask from etypes" do
43
+ notifier = Sinotify::Notifier.new('/tmp', :recurse => false, :etypes => [:create, :modify])
44
+
45
+ #decided to make raw_mask private: notifier.raw_mask.should be_eql(Sinotify::CREATE | Sinotify::MODIFY)
46
+
47
+ # should not be able to create a notifier using a bogus event type (eg. 'blah')
48
+ lambda{Sinotify::Notifier.new('/tmp', :recurse => false, :etypes => [:blah])}.should raise_error
49
+ lambda{Sinotify::Notifier.new('/tmp', :recurse => false, :etypes => [:create, :blah])}.should raise_error
50
+ end
51
+
52
+ it "should properly create Event from PrimEvent" do
53
+ # mimic delete of a directory -- change :delete_self into :delete
54
+ prim_event = MockPrimEvent.new
55
+ prim_event.etypes = [:delete_self]
56
+ watch = Sinotify::Watch.new(:is_dir => true, :path => '/tmp')
57
+ event = Sinotify::Event.from_prim_event_and_watch(prim_event, watch)
58
+ event.etypes.should be_include(:delete)
59
+ event.etypes.should_not be_include(:delete_self)
60
+
61
+ # :close should get added if event is :close_nowrite or :close_write
62
+ prim_event = MockPrimEvent.new
63
+ prim_event.etypes = [:close_nowrite]
64
+ event = Sinotify::Event.from_prim_event_and_watch(prim_event, watch)
65
+ event.etypes.should be_include(:close)
66
+ prim_event = MockPrimEvent.new
67
+ prim_event.etypes = [:close_write]
68
+ event = Sinotify::Event.from_prim_event_and_watch(prim_event, watch)
69
+ event.etypes.should be_include(:close)
70
+ end
71
+
72
+ it "should add watches for all child directories if recursive" do
73
+
74
+ reset_test_dir!
75
+
76
+ # make a watch, recurse false. There should only be one watch
77
+ notifier = Sinotify::Notifier.new(@test_root_dir, :recurse => false).watch!
78
+
79
+ tiny_pause!
80
+ notifier.all_directories_being_watched.should be_eql([@test_root_dir])
81
+
82
+ # make a watch, recurse TRUE. There should only be 27 watches (a-z, and @test_root_dir)
83
+ notifier = Sinotify::Notifier.new(@test_root_dir,
84
+ :etypes => [:all_events],
85
+ :recurse => true).watch!
86
+ # notifier.spy!(:logger => Logger.new('/tmp/spy.log'))
87
+
88
+ pause!
89
+ notifier.all_directories_being_watched.size.should be_eql(27)
90
+
91
+ # check a single announcement on a file in a subdir
92
+ events = []
93
+ test_fn = File.join(@test_root_dir, 'a', 'hi')
94
+ notifier.on_event { |event| events << event }
95
+ FileUtils.touch test_fn
96
+ pause!
97
+ #puts events.map{|e|e.to_s}.join("\n")
98
+ events.detect{|e| e.path.eql?(test_fn) && e.etypes.include?(:create) }.should_not be_nil
99
+ events.detect{|e| e.path.eql?(test_fn) && e.etypes.include?(:open) }.should_not be_nil
100
+ events.detect{|e| e.path.eql?(test_fn) && e.etypes.include?(:close_write) }.should_not be_nil
101
+ events.detect{|e| e.path.eql?(test_fn) && e.etypes.include?(:close) }.should_not be_nil
102
+
103
+ events = []
104
+ File.open(test_fn, 'a'){|f| f << 'ho'}
105
+ pause!
106
+ events.detect{|e| e.path.eql?(test_fn) && e.etypes.include?(:open) }.should_not be_nil
107
+ events.detect{|e| e.path.eql?(test_fn) && e.etypes.include?(:modify) }.should_not be_nil
108
+ events.detect{|e| e.path.eql?(test_fn) && e.etypes.include?(:close_write) }.should_not be_nil
109
+ events.detect{|e| e.path.eql?(test_fn) && e.etypes.include?(:close) }.should_not be_nil
110
+
111
+ # quickly create and delete the file
112
+ events = []
113
+ FileUtils.rm test_fn
114
+ tiny_pause!
115
+ FileUtils.touch test_fn
116
+ pause!
117
+ events.detect{|e| e.path.eql?(test_fn) && e.etypes.include?(:delete) }.should_not be_nil
118
+ events.detect{|e| e.path.eql?(test_fn) && e.etypes.include?(:create) }.should_not be_nil
119
+ end
120
+
121
+ it "should add a watch when a new subdirectory is created" do
122
+ # setup
123
+ reset_test_dir! # creates 27 directories, the root dir and 'a'...'z'
124
+ subdir_a = File.join(@test_root_dir, 'a')
125
+ events = []
126
+ notifier = Sinotify::Notifier.new(@test_root_dir, :recurse => true).watch!
127
+ #notifier.spy!(:logger => spylog = Logger.new(STDOUT))
128
+ notifier.on_event { |event| events << event }
129
+
130
+ # one watch for the root and the 26 subdirs 'a'..'z'
131
+ pause!
132
+ notifier.all_directories_being_watched.size.should be_eql(27)
133
+
134
+ # create a new subdir
135
+ subdir_abc = File.join(@test_root_dir, 'a', 'abc')
136
+ FileUtils.mkdir subdir_abc
137
+ pause! # takes a moment to sink in because the watch is added in a bg thread
138
+ notifier.all_directories_being_watched.size.should be_eql(28)
139
+ pause!
140
+
141
+ # create a file in the new subdir, it should send and event
142
+ file_in_subdir_abc = File.join(subdir_abc, 'new_file')
143
+ events_before = events.size
144
+ FileUtils.touch file_in_subdir_abc
145
+ pause!
146
+ events.size.should be_eql(events_before + 1)
147
+ end
148
+
149
+ it "should delete watches for on subdirectories when a parent directory is deleted" do
150
+
151
+ # Setup (create the usual test dir and 26 subdirs, and an additional sub-subdir, and a file
152
+ reset_test_dir! # creates the root dir and 'a'...'z'
153
+ subdir_a = File.join(@test_root_dir, 'a')
154
+ FileUtils.mkdir File.join(@test_root_dir, 'a', 'def')
155
+ test_fn = File.join(subdir_a, 'hi')
156
+ FileUtils.touch test_fn
157
+
158
+ # Setup: create the notifier
159
+ events = []
160
+ notifier = Sinotify::Notifier.new(@test_root_dir, :recurse => true).watch!
161
+ #notifier.spy!(:logger => Logger.new('/tmp/spy.log'))
162
+ notifier.on_event { |event| events << event }
163
+
164
+ # first assert: all directories should have a watch
165
+ pause!
166
+ notifier.all_directories_being_watched.size.should be_eql(28) # all the directories should have watches
167
+
168
+
169
+ # Should get delete events for the subdir_a and its file 'hi' when removing subdir_a.
170
+ # There should be 26 watches left (after removing watches for subdir_a and its sub-subdir)
171
+ FileUtils.rm_rf subdir_a
172
+ pause!
173
+ events.detect{|e| e.path.eql?(subdir_a) && e.directory? && e.etypes.include?(:delete) }.should_not be_nil
174
+ events.detect{|e| e.path.eql?(test_fn) && !e.directory? && e.etypes.include?(:delete) }.should_not be_nil
175
+ notifier.all_directories_being_watched.size.should be_eql(26)
176
+ end
177
+
178
+ it "should exit and nil out watch_thread when closed" do
179
+ # really need this?
180
+ end
181
+
182
+ it "should close children when closed if recursive" do
183
+ # Setup (create the usual test dir and 26 subdirs, and an additional sub-subdir, and a file
184
+ reset_test_dir! # creates the root dir and 'a'...'z'
185
+ FileUtils.mkdir File.join(@test_root_dir, 'a', 'def')
186
+
187
+ # Setup: create the notifier
188
+ events = []
189
+ notifier = Sinotify::Notifier.new(@test_root_dir, :recurse => true).watch!
190
+ #notifier.spy!(:logger => Logger.new('/tmp/spy.log'))
191
+ notifier.on_event { |event| events << event }
192
+
193
+ # first assert: all directories should have a watch
194
+ pause!
195
+ notifier.all_directories_being_watched.size.should be_eql(28) # all the directories should have watches
196
+
197
+ notifier.close!
198
+ notifier.all_directories_being_watched.size.should be_eql(0) # all watches should have been deleted
199
+ end
200
+
201
+ it "pound it" do
202
+ # Setup (create the usual test dir and 26 subdirs, and an additional sub-subdir, and a file
203
+ reset_test_dir! # creates the root dir and 'a'...'z'
204
+
205
+ a_z = ('a'..'z').collect{|x|x}
206
+
207
+ # Setup: create the notifier
208
+ notifier = Sinotify::Notifier.new(@test_root_dir,
209
+ :announcements_sleep_time => 0.01,
210
+ :announcement_throttle => 10000,
211
+ :etypes => [:create, :modify, :delete, :close],
212
+ :recurse => true).watch!
213
+ #notifier.spy!(:logger => Logger.new('/tmp/spy.log'))
214
+ creates = deletes = modifies = closes = 0
215
+ notifier.on_event do |event|
216
+ creates += 1 if event.etypes.include?(:create)
217
+ deletes += 1 if event.etypes.include?(:delete)
218
+ modifies += 1 if event.etypes.include?(:modify)
219
+ closes += 1 if event.etypes.include?(:close)
220
+ end
221
+
222
+ # Create, append to, and then delete a bunch of random files
223
+ tiny_pause!
224
+ total_iterations = 1000
225
+ total_iterations.times do
226
+ sub_dir = File.join(@test_root_dir, a_z[rand(a_z.size)])
227
+ test_fn = File.join(sub_dir, "zzz#{rand(10000)}")
228
+ FileUtils.touch test_fn
229
+ File.open(test_fn, 'a'){|f| f << rand(1000).to_s }
230
+ FileUtils.rm test_fn
231
+ end
232
+ puts "created and modified and deleted #{total_iterations} files in sub directories of #{@test_root_dir}"
233
+
234
+ start_wait = Time.now
235
+
236
+ # wait up to 15 seconds for all the create events to come through
237
+ waits = 0
238
+ puts "Waiting for events, will wait for up to 30 sec"
239
+ while(creates < total_iterations) do
240
+ sleep 1
241
+ waits += 1
242
+ break if waits > 30
243
+ #raise "Tired of waiting for create events to reach #{total_iterations}, it is only at #{creates}" if waits > 30
244
+ end
245
+
246
+ puts "It took #{Time.now - start_wait} seconds for all the create/modify/delete/close events to come through"
247
+
248
+ # give it a tiny bit longer to let any remaining modify/delete/close stragglers to come through
249
+ 5.times{tiny_pause!}
250
+
251
+ puts "Ceates detected: #{creates}"
252
+ puts "Deletes: #{deletes}"
253
+ puts "Modifies: #{modifies}"
254
+ puts "Closes: #{closes}"
255
+
256
+ creates.should be_eql(total_iterations)
257
+ deletes.should be_eql(total_iterations)
258
+ modifies.should be_eql(total_iterations)
259
+ closes.should be_eql(2 * total_iterations) # should get a close both after the create and the modify
260
+
261
+ end
262
+
263
+ end
264
+
265
+ # EOF