filewatch 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ require "inotify/fd"
2
+ require "inotify/namespace"
3
+
4
+ class Inotify::EMHandler < EventMachine::Connection
5
+ def initialize(inotify_fd, callback=nil)
6
+ @inotify = inotify_fd
7
+ @callback = callback
8
+ self.notify_readable = true
9
+ end
10
+
11
+ def notify_readable
12
+ @inotify.each do |event|
13
+ @callback.call(event)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,81 @@
1
+ require "inotify/namespace"
2
+ require "inotify/fd"
3
+ require "ffi"
4
+
5
+ class Inotify::Event < FFI::Struct
6
+ layout :wd, :int,
7
+ :mask, :uint32,
8
+ :cookie, :uint32,
9
+ :len, :uint32
10
+ # last piece is the name, but don't hold it in the struct
11
+ #:name, :string,
12
+
13
+ attr_accessor :name
14
+
15
+ def initialize(pointer)
16
+ if pointer.is_a?(String)
17
+ pointer = FFI::MemoryPointer.from_string(pointer)
18
+ end
19
+
20
+ super(pointer)
21
+ end
22
+
23
+ def self.from_stringpipeio(io)
24
+ # This fails in ruby 1.9.2 because it literally calls read(2) with
25
+ # 'self.size' as the byte size to read. This causes EINVAL
26
+ # from inotify, documented thusly in inotify(7):
27
+ #
28
+ # """ The behavior when the buffer given to read(2) is too small to
29
+ # return information about the next event depends on the kernel
30
+ # version: in kernels before 2.6.21, read(2) returns 0; since
31
+ # kernel 2.6.21, read(2) fails with the error EINVAL. """
32
+ #
33
+ # Working around this will require implementing our own read buffering
34
+ # unless comeone clues me in on how to make ruby 1.9.2 read larger
35
+ # blocks and actually do the nice buffered IO we've all come to
36
+ # know and love.
37
+
38
+ begin
39
+ data = io.read(self.size, true)
40
+ rescue Errno::EINVAL => e
41
+ $stderr.puts "Read was too small? Confused."
42
+ raise e
43
+ end
44
+
45
+ return nil if data == nil
46
+
47
+ pointer = FFI::MemoryPointer.from_string(data)
48
+ event = self.new(pointer)
49
+
50
+ event.from_stringpipeio(io)
51
+ return event
52
+ end
53
+
54
+ def from_stringpipeio(io)
55
+ begin
56
+ @name = io.read(self[:len], true)
57
+ rescue Errno::EINVAL => e
58
+ $stderr.puts "Read was too small? Confused."
59
+ raise e
60
+ end
61
+ return self if @name == nil
62
+
63
+ @name = @name.split("\0", 2).first
64
+
65
+ return self
66
+ end
67
+
68
+ def actions
69
+ Inotify::FD::WATCH_BITS.reject do |key, bitmask|
70
+ self[:mask] & bitmask == 0
71
+ end.keys
72
+ end
73
+
74
+ def to_s
75
+ return "#{@name} (#{self.actions.join(", ")})"
76
+ end
77
+
78
+ def partial?
79
+ return self[:len] > 0 && @name == nil
80
+ end # def partial?
81
+ end
data/lib/inotify/fd.rb ADDED
@@ -0,0 +1,221 @@
1
+ require "rubygems"
2
+ require "ffi"
3
+ require "inotify/event"
4
+ require "inotify/namespace"
5
+ require "inotify/stringpipeio"
6
+
7
+ class Inotify::FD
8
+ include Enumerable
9
+
10
+ module CInotify
11
+ extend FFI::Library
12
+ ffi_lib FFI::Library::LIBC
13
+
14
+ attach_function :inotify_init, [], :int
15
+ attach_function :inotify_init1, [:int], :int
16
+ attach_function :inotify_add_watch, [:int, :string, :uint32], :int
17
+
18
+ # So we can read and poll inotify from jruby.
19
+ attach_function :read, [:int, :pointer, :size_t], :int
20
+
21
+ # Poll is pretty crappy, but it's better than nothing.
22
+ attach_function :poll, [:pointer, :int, :int], :int
23
+ end
24
+
25
+ INOTIFY_CLOEXEC = 02000000
26
+ INOTIFY_NONBLOCK = 04000
27
+
28
+ WATCH_BITS = {
29
+ :access => 1 << 0,
30
+ :modify => 1 << 1,
31
+ :attrib => 1 << 2,
32
+ :close_write => 1 << 3,
33
+ :close_nowrite => 1 << 4,
34
+ :open => 1 << 5,
35
+ :moved_from => 1 << 6,
36
+ :moved_to => 1 << 7,
37
+ :create => 1 << 8,
38
+ :delete => 1 << 9,
39
+ :delete_self => 1 << 10,
40
+ :move_self => 1 << 11,
41
+
42
+ # Shortcuts
43
+ :close => (1 << 3) | (1 << 4),
44
+ :move => (1 << 6) | (1 << 7) | (1 << 11),
45
+ :delete => (1 << 9) | (1 << 10),
46
+ }
47
+
48
+ attr_reader :fd
49
+
50
+ public
51
+ def initialize
52
+ @watches = {}
53
+ @buffer = Inotify::StringPipeIO.new
54
+
55
+ @fd = CInotify.inotify_init1(INOTIFY_NONBLOCK)
56
+
57
+ if java?
58
+ @io = nil
59
+ else
60
+ @io = IO.for_fd(@fd)
61
+ end
62
+ end
63
+
64
+ public
65
+ def java?
66
+ return RUBY_PLATFORM == "java"
67
+ end
68
+
69
+ # Add a watch.
70
+ # - path is a string file path
71
+ # - what_to_watch is any of the valid WATCH_BITS keys
72
+ #
73
+ # Example:
74
+ # watch("/tmp", :craete, :delete)
75
+ public
76
+ def watch(path, *what_to_watch)
77
+ mask = what_to_watch.inject(0) { |m, val| m |= WATCH_BITS[val] }
78
+ watch_descriptor = CInotify.inotify_add_watch(@fd, path, mask)
79
+ #puts "watch #{path} => #{watch_descriptor}"
80
+
81
+ if watch_descriptor == -1
82
+ raise "inotify_add_watch(#{@fd}, #{path}, #{mask}) failed. #{$?}"
83
+ end
84
+ @watches[watch_descriptor] = {
85
+ :path => path,
86
+ :partial => nil
87
+ }
88
+ end
89
+
90
+ private
91
+ def normal_read(timeout=nil)
92
+ loop do
93
+ begin
94
+ data = @io.sysread(4096)
95
+ @buffer.write(data)
96
+ rescue Errno::EAGAIN
97
+ # No data left to read, moveon.
98
+ break
99
+ end
100
+ end
101
+ return nil
102
+ end # def normal_read
103
+
104
+ private
105
+ def jruby_read(timeout=nil)
106
+ @jruby_read_buffer = FFI::MemoryPointer.new(:char, 4096)
107
+
108
+ # TODO(sissel): Block with select.
109
+ # Will have to use FFI to call select, too.
110
+
111
+ # We ahve to call libc's read(2) because JRuby/Java can't trivially
112
+ # be told about existing file descriptors.
113
+ loop do
114
+ bytes = CInotify.read(@fd, @jruby_read_buffer, 4096)
115
+
116
+ # read(2) returns -1 on error, which we expect to be EAGAIN, but...
117
+ # TODO(sissel): maybe we should check errno properly...
118
+ # Then again, errno isn't threadsafe, so we'd have to wrap this
119
+ # in a critical block? Fun times
120
+ break if bytes == -1
121
+
122
+ @buffer.write(@jruby_read_buffer.get_bytes(0, bytes))
123
+ end
124
+ return nil
125
+ end # def jruby_read
126
+
127
+ # Make any necessary corrections to the event
128
+ private
129
+ def prepare(event)
130
+ watchpath = @watches[event[:wd]][:path]
131
+ if event.name == nil
132
+ # Some events don't have the name at all, so add our own.
133
+ event.name = watchpath
134
+ else
135
+ # Event paths are relative to the watch. Prefix to make the full path.
136
+ event.name = File.join(watchpath, event.name)
137
+ end
138
+
139
+ return event
140
+ end # def prepare
141
+
142
+ # Get one inotify event.
143
+ #
144
+ # If timeout is not given, this call blocks.
145
+ # If a timeout occurs and no event was read, nil is returned.
146
+ #
147
+ # Returns nil on timeout or an Inotify::Event on success.
148
+ public
149
+ def get(timeout=nil)
150
+ # This big 'loop' is to support pop { |event| ... } shipping each available event.
151
+ # It's not very rubyish (we should probably use Enumerable and such.
152
+ if java?
153
+ jruby_read(timeout)
154
+ else
155
+ normal_read(timeout)
156
+ end
157
+
158
+ # Recover any previous partial event.
159
+ if @partial
160
+ event = @partial.from_stringpipeio(@buffer)
161
+ else
162
+ event = Inotify::Event.from_stringpipeio(@buffer)
163
+ return nil if event == nil
164
+ end
165
+
166
+ if event.partial?
167
+ @partial = event
168
+ return nil
169
+ end
170
+ @partial = nil
171
+
172
+ return prepare(event)
173
+ end # def get
174
+
175
+ # For Enumerable support
176
+ #
177
+ # Yields one Inotify::Event per iteration. If there are no more events
178
+ # at the this time, then this method will end.
179
+ public
180
+ def each(&block)
181
+ loop do
182
+ event = get
183
+ break if event == nil
184
+ yield prepare(event)
185
+ end # loop
186
+ end # def each
187
+
188
+ # Subscribe to inotify events for this instance.
189
+ #
190
+ # If you are running in EventMachine, this will set up a
191
+ # subscription that behaves sanely in EventMachine
192
+ #
193
+ # If you are not running in EventMachine, this method
194
+ # blocks forever, invoking the given block for each event.
195
+ # Further, if you are not using EventMachine, you should
196
+ # not pass a handler, only a block, like this:
197
+ #
198
+ # fd.subscribe do |event|
199
+ # puts event
200
+ # end
201
+ public
202
+ def subscribe(handler=nil, &block)
203
+ if defined?(EventMachine) && EventMachine.reactor_running?
204
+ require "inotify/emhandler"
205
+ handler = Inotify::EMHandler if handler == nil
206
+ EventMachine::watch(@fd, handler, self, block)
207
+ else
208
+ loop do
209
+ if java?
210
+ # No way to select on FFI-derived file descriptors yet,
211
+ # when I grab poll(2) via FFI, this sleep will become
212
+ # a poll or sleep invocation.
213
+ sleep(1)
214
+ else
215
+ IO.select([@io], nil, nil, nil)
216
+ end
217
+ each(&block)
218
+ end
219
+ end
220
+ end
221
+ end # class Inotify::FD
@@ -0,0 +1,2 @@
1
+ module Inotify
2
+ end
@@ -0,0 +1,33 @@
1
+ require "rubygems"
2
+ require "inotify/namespace"
3
+
4
+ class Inotify::StringPipeIO
5
+ def initialize
6
+ @buffer = ""
7
+ end # def initialize
8
+
9
+ def write(string)
10
+ @buffer += string
11
+ end # def write
12
+
13
+ def read(bytes=nil, nil_if_not_enough_data=false)
14
+ return nil if @buffer == nil || @buffer.empty?
15
+
16
+ if nil_if_not_enough_data && bytes > @buffer.size
17
+ return nil
18
+ end
19
+
20
+ bytes = @buffer.size if bytes == nil
21
+ data = @buffer[0 .. bytes]
22
+ if bytes <= @buffer.length
23
+ @buffer = @buffer[bytes .. -1]
24
+ else
25
+ @buffer = ""
26
+ end
27
+ return data
28
+ end # def read
29
+
30
+ def size
31
+ return @buffer.size
32
+ end
33
+ end # class Inotify::StringPipeIO
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: filewatch
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Jordan Sissel
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-03-06 00:00:00 -08:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: Watch files and directories in ruby
18
+ email: jls@semicomplete.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - lib/inotify/namespace.rb
27
+ - lib/inotify/emhandler.rb
28
+ - lib/inotify/stringpipeio.rb
29
+ - lib/inotify/fd.rb
30
+ - lib/inotify/event.rb
31
+ has_rdoc: true
32
+ homepage: https://github.com/jordansissel/ruby-inotify-ffi
33
+ licenses: []
34
+
35
+ post_install_message:
36
+ rdoc_options: []
37
+
38
+ require_paths:
39
+ - lib
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ requirements: []
54
+
55
+ rubyforge_project:
56
+ rubygems_version: 1.6.0
57
+ signing_key:
58
+ specification_version: 3
59
+ summary: filewatch - file watching for ruby
60
+ test_files: []
61
+