filewatch 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.
@@ -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
+