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.
- data/lib/inotify/emhandler.rb +16 -0
- data/lib/inotify/event.rb +81 -0
- data/lib/inotify/fd.rb +221 -0
- data/lib/inotify/namespace.rb +2 -0
- data/lib/inotify/stringpipeio.rb +33 -0
- metadata +61 -0
@@ -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,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
|
+
|