ffilewatch 0.6.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 211f6275bcf6037d2e6379dd206e268ebbf77362
4
+ data.tar.gz: b329672228fc6ddd30802abae8803f550d7e84a1
5
+ SHA512:
6
+ metadata.gz: f9d01a57ce62ab9737847fc61ca14e98f17656ec0d0f1c2db52d3024fd86f56d33f2bf30e13d20de272fff75d38db07c345244463def7bb894fdb2e87ce3f9d1
7
+ data.tar.gz: 61084794c8ffdc1866c4c6b8cb23f94118a8ec92de05c6caffe6f0daa5238382890e71b848f729c3241fdb7e2b31b177f1eef270a521cc5e1994ca26929808ac
data/bin/globtail ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "filewatch/tail"
4
+ require "optparse"
5
+
6
+ progname = File.basename($0)
7
+
8
+ config = {
9
+ :verbose => false,
10
+ }
11
+
12
+ opts = OptionParser.new do |opts|
13
+ opts.banner = "#{progname} [-v] [-s path] [-i interval] [-x glob] path/glob ..."
14
+
15
+ opts.on("-v", "--verbose", "Enable verbose/debug output") do
16
+ config[:verbose] = true
17
+ end
18
+
19
+ opts.on("-x", "--exclude PATH", String, "path to exclude from watching") do |path|
20
+ config[:exclude] ||= []
21
+ config[:exclude] << path
22
+ end
23
+
24
+ opts.on("-s", "--sincedb PATH", String, "Sincedb path") do |path|
25
+ config[:sincedb_path] = path
26
+ end
27
+
28
+ opts.on("-i", "--interval SECONDS", Integer,
29
+ "Sincedb write interval") do |path|
30
+ config[:sincedb_write_interval] = path
31
+ end
32
+ end
33
+
34
+ begin
35
+ opts.order!
36
+ rescue OptionParser::InvalidOption
37
+ $stderr.puts "#{progname}: #{$!}"
38
+ $stderr.puts opts.usage
39
+ end
40
+
41
+ logger = Logger.new(STDERR)
42
+ logger.progname = progname
43
+ logger.level = config[:verbose] ? Logger::DEBUG : Logger::INFO
44
+ config[:logger] = logger
45
+
46
+ tail = FileWatch::Tail.new(config)
47
+
48
+ ARGV.each { |path| tail.tail(path) }
49
+
50
+ Signal.trap("EXIT") { tail.sincedb_write("globtail exiting") }
51
+
52
+ $stdout.sync = true
53
+ tail.subscribe do |path, line|
54
+ puts "#{path}: #{line}"
55
+ end
Binary file
@@ -0,0 +1,139 @@
1
+ # BufferedTokenizer - Statefully split input data by a specifiable token
2
+ #
3
+ # Authors:: Tony Arcieri, Martin Emde
4
+ #
5
+ #----------------------------------------------------------------------------
6
+ #
7
+ # Copyright (C) 2006-07 by Tony Arcieri and Martin Emde
8
+ #
9
+ # Distributed under the Ruby license (http://www.ruby-lang.org/en/LICENSE.txt)
10
+ #
11
+ #---------------------------------------------------------------------------
12
+ #
13
+
14
+ # (C)2006 Tony Arcieri, Martin Emde
15
+ # Distributed under the Ruby license (http://www.ruby-lang.org/en/LICENSE.txt)
16
+
17
+ # BufferedTokenizer takes a delimiter upon instantiation, or acts line-based
18
+ # by default. It allows input to be spoon-fed from some outside source which
19
+ # receives arbitrary length datagrams which may-or-may-not contain the token
20
+ # by which entities are delimited.
21
+ #
22
+ # Commonly used to parse lines out of incoming data:
23
+ #
24
+ # module LineBufferedConnection
25
+ # def receive_data(data)
26
+ # (@buffer ||= BufferedTokenizer.new).extract(data).each do |line|
27
+ # receive_line(line)
28
+ # end
29
+ # end
30
+ # end
31
+
32
+ module FileWatch; class BufferedTokenizer
33
+ # New BufferedTokenizers will operate on lines delimited by "\n" by default
34
+ # or allow you to specify any delimiter token you so choose, which will then
35
+ # be used by String#split to tokenize the input data
36
+ def initialize(delimiter = "\n", size_limit = nil)
37
+ # Store the specified delimiter
38
+ @delimiter = delimiter
39
+
40
+ # Store the specified size limitation
41
+ @size_limit = size_limit
42
+
43
+ # The input buffer is stored as an array. This is by far the most efficient
44
+ # approach given language constraints (in C a linked list would be a more
45
+ # appropriate data structure). Segments of input data are stored in a list
46
+ # which is only joined when a token is reached, substantially reducing the
47
+ # number of objects required for the operation.
48
+ @input = []
49
+
50
+ # Size of the input buffer
51
+ @input_size = 0
52
+ end
53
+
54
+ # Extract takes an arbitrary string of input data and returns an array of
55
+ # tokenized entities, provided there were any available to extract. This
56
+ # makes for easy processing of datagrams using a pattern like:
57
+ #
58
+ # tokenizer.extract(data).map { |entity| Decode(entity) }.each do ...
59
+ def extract(data)
60
+ # Extract token-delimited entities from the input string with the split command.
61
+ # There's a bit of craftiness here with the -1 parameter. Normally split would
62
+ # behave no differently regardless of if the token lies at the very end of the
63
+ # input buffer or not (i.e. a literal edge case) Specifying -1 forces split to
64
+ # return "" in this case, meaning that the last entry in the list represents a
65
+ # new segment of data where the token has not been encountered
66
+ entities = data.split @delimiter, -1
67
+
68
+ # Check to see if the buffer has exceeded capacity, if we're imposing a limit
69
+ if @size_limit
70
+ raise 'input buffer full' if @input_size + entities.first.size > @size_limit
71
+ @input_size += entities.first.size
72
+ end
73
+
74
+ # Move the first entry in the resulting array into the input buffer. It represents
75
+ # the last segment of a token-delimited entity unless it's the only entry in the list.
76
+ @input << entities.shift
77
+
78
+ # If the resulting array from the split is empty, the token was not encountered
79
+ # (not even at the end of the buffer). Since we've encountered no token-delimited
80
+ # entities this go-around, return an empty array.
81
+ return [] if entities.empty?
82
+
83
+ # At this point, we've hit a token, or potentially multiple tokens. Now we can bring
84
+ # together all the data we've buffered from earlier calls without hitting a token,
85
+ # and add it to our list of discovered entities.
86
+ entities.unshift @input.join
87
+
88
+ =begin
89
+ # Note added by FC, 10Jul07. This paragraph contains a regression. It breaks
90
+ # empty tokens. Think of the empty line that delimits an HTTP header. It will have
91
+ # two "\n" delimiters in a row, and this code mishandles the resulting empty token.
92
+ # It someone figures out how to fix the problem, we can re-enable this code branch.
93
+ # Multi-chara
94
+ cter token support.
95
+ # Split any tokens that were incomplete on the last iteration buf complete now.
96
+ entities.map! do |e|
97
+ e.split @delimiter, -1
98
+ end
99
+ # Flatten the resulting array. This has the side effect of removing the empty
100
+ # entry at the end that was produced by passing -1 to split. Add it again if
101
+ # necessary.
102
+ if (entities[-1] == [])
103
+ entities.flatten! << []
104
+ else
105
+ entities.flatten!
106
+ end
107
+ =end
108
+
109
+ # Now that we've hit a token, joined the input buffer and added it to the entities
110
+ # list, we can go ahead and clear the input buffer. All of the segments that were
111
+ # stored before the join can now be garbage collected.
112
+ @input.clear
113
+
114
+ # The last entity in the list is not token delimited, however, thanks to the -1
115
+ # passed to split. It represents the beginning of a new list of as-yet-untokenized
116
+ # data, so we add it to the start of the list.
117
+ @input << entities.pop
118
+
119
+ # Set the new input buffer size, provided we're keeping track
120
+ @input_size = @input.first.size if @size_limit
121
+
122
+ # Now we're left with the list of extracted token-delimited entities we wanted
123
+ # in the first place. Hooray!
124
+ entities
125
+ end
126
+
127
+ # Flush the contents of the input buffer, i.e. return the input buffer even though
128
+ # a token has not yet been encountered
129
+ def flush
130
+ buffer = @input.join
131
+ @input.clear
132
+ buffer
133
+ end
134
+
135
+ # Is the buffer empty?
136
+ def empty?
137
+ @input.empty?
138
+ end
139
+ end; end
@@ -0,0 +1,340 @@
1
+ require "filewatch/buftok"
2
+ require "filewatch/watch"
3
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
4
+ require "filewatch/winhelper"
5
+ end
6
+ require "logger"
7
+ require "rbconfig"
8
+
9
+ include Java if defined? JRUBY_VERSION
10
+ require "JRubyFileExtension.jar" if defined? JRUBY_VERSION
11
+
12
+ module FileWatch
13
+ class Tail
14
+ # how often (in seconds) we @logger.warn a failed file open, per path.
15
+ OPEN_WARN_INTERVAL = ENV["FILEWATCH_OPEN_WARN_INTERVAL"] ?
16
+ ENV["FILEWATCH_OPEN_WARN_INTERVAL"].to_i : 300
17
+
18
+ attr_accessor :logger
19
+
20
+ class NoSinceDBPathGiven < StandardError; end
21
+
22
+ public
23
+ def initialize(opts={})
24
+ @iswindows = ((RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/) != nil)
25
+
26
+ if opts[:logger]
27
+ @logger = opts[:logger]
28
+ else
29
+ @logger = Logger.new(STDERR)
30
+ @logger.level = Logger::INFO
31
+ end
32
+ @evict = nil
33
+ @files = {}
34
+ @locked = Hash.new { |h, k| h[k] = false }
35
+ @changed = Hash.new { |h, k| h[k] = 0 }
36
+ @lastwarn = Hash.new { |h, k| h[k] = 0 }
37
+ @buffers = {}
38
+ @watch = FileWatch::Watch.new
39
+ @watch.logger = @logger
40
+ @sincedb = {}
41
+ @sincedb_last_write = Time.now.to_i
42
+ @sincedb_write_pending = false
43
+ @sincedb_writing = false
44
+ @statcache = {}
45
+ @opts = {
46
+ :eviction_interval => 60,
47
+ :sincedb_write_interval => 10,
48
+ :stat_interval => 1,
49
+ :discover_interval => 5,
50
+ :exclude => [],
51
+ :start_new_files_at => :end,
52
+ :follow_only_path => false
53
+ }.merge(opts)
54
+ if !@opts.include?(:sincedb_path)
55
+ @opts[:sincedb_path] = File.join(ENV["HOME"], ".sincedb") if ENV.include?("HOME")
56
+ @opts[:sincedb_path] = ENV["SINCEDB_PATH"] if ENV.include?("SINCEDB_PATH")
57
+ end
58
+ if !@opts.include?(:sincedb_path)
59
+ raise NoSinceDBPathGiven.new("No HOME or SINCEDB_PATH set in environment. I need one of these set so I can keep track of the files I am following.")
60
+ end
61
+ @watch.exclude(@opts[:exclude])
62
+ _start_eviction
63
+ _sincedb_open
64
+ end # def initialize
65
+
66
+ public
67
+ def logger=(logger)
68
+ @logger = logger
69
+ @watch.logger = logger
70
+ end # def logger=
71
+
72
+ public
73
+ def tail(path)
74
+ @watch.watch(path)
75
+ end # def tail
76
+
77
+ public
78
+ def subscribe(&block)
79
+ # subscribe(stat_interval = 1, discover_interval = 5, &block)
80
+ @watch.subscribe(@opts[:stat_interval],
81
+ @opts[:discover_interval]) do |event, path|
82
+ case event
83
+ when :create, :create_initial
84
+ if @files.member?(path)
85
+ @logger.debug("#{event} for #{path}: already exists in @files")
86
+ next
87
+ end
88
+ if _open_file(path, event)
89
+ _read_file(path, &block)
90
+ end
91
+ when :modify
92
+ if !@files.member?(path)
93
+ @logger.debug(":modify for #{path}, does not exist in @files")
94
+ if _open_file(path, event)
95
+ _read_file(path, &block)
96
+ end
97
+ else
98
+ _read_file(path, &block)
99
+ end
100
+ when :delete
101
+ @logger.debug(":delete for #{path}, deleted from @files")
102
+ _read_file(path, &block)
103
+ @files[path].close
104
+ @files.delete(path)
105
+ @statcache.delete(path)
106
+ when :noupdate
107
+ @logger.debug(":noupdate for #{path}, from @files")
108
+ _sincedb_write_if_pending # will check to see if sincedb_write requests are pending
109
+ else
110
+ @logger.warn("unknown event type #{event} for #{path}")
111
+ end
112
+ end # @watch.subscribe
113
+ end # def each
114
+
115
+ private
116
+ def _open_file(path, event)
117
+ @logger.debug("_open_file: #{path}: opening")
118
+ @locked[path] = true
119
+ begin
120
+ if @iswindows && defined? JRUBY_VERSION
121
+ @files[path] = Java::RubyFileExt::getRubyFile(path)
122
+ else
123
+ @files[path] = File.open(path)
124
+ end
125
+ rescue
126
+ # don't emit this message too often. if a file that we can't
127
+ # read is changing a lot, we'll try to open it more often,
128
+ # and might be spammy.
129
+ now = Time.now.to_i
130
+ if now - @lastwarn[path] > OPEN_WARN_INTERVAL
131
+ @logger.warn("failed to open #{path}: #{$!}")
132
+ @lastwarn[path] = now
133
+ else
134
+ @logger.debug("(warn supressed) failed to open #{path}: #{$!}")
135
+ end
136
+ @files.delete(path)
137
+ @changed[path] = Time.now.to_i
138
+ @locked[path] = false
139
+ return false
140
+ end
141
+
142
+ stat = File::Stat.new(path)
143
+
144
+ if @opts[:follow_only_path]
145
+ # In cases where files are rsynced to the consuming server, inodes will change when
146
+ # updated files overwrite original ones, resulting in inode changes. In order to
147
+ # avoid having the sincedb.member check from failing in this scenario, we'll
148
+ # construct the inode key using the path which will be 'stable'
149
+ inode = [path, stat.dev_major, stat.dev_minor]
150
+ else
151
+ if @iswindows
152
+ fileId = Winhelper.GetWindowsUniqueFileIdentifier(path)
153
+ inode = [fileId, stat.dev_major, stat.dev_minor]
154
+ else
155
+ inode = [stat.ino.to_s, stat.dev_major, stat.dev_minor]
156
+ end
157
+ end
158
+
159
+ @statcache[path] = inode
160
+
161
+ if @sincedb.member?(inode)
162
+ last_size = @sincedb[inode]
163
+ @logger.debug("#{path}: sincedb last value #{@sincedb[inode]}, cur size #{stat.size}")
164
+ if last_size <= stat.size
165
+ @logger.debug("#{path}: sincedb: seeking to #{last_size}")
166
+ @files[path].sysseek(last_size, IO::SEEK_SET)
167
+ else
168
+ @logger.debug("#{path}: last value size is greater than current value, starting over")
169
+ @sincedb[inode] = 0
170
+ end
171
+ elsif event == :create_initial && @files[path]
172
+ # TODO(sissel): Allow starting at beginning of the file.
173
+ if @opts[:start_new_files_at] == :beginning
174
+ @logger.debug("#{path}: initial create, no sincedb, seeking to beginning of file")
175
+ @files[path].sysseek(0, IO::SEEK_SET)
176
+ @sincedb[inode] = 0
177
+ else
178
+ # seek to end
179
+ @logger.debug("#{path}: initial create, no sincedb, seeking to end #{stat.size}")
180
+ @files[path].sysseek(stat.size, IO::SEEK_SET)
181
+ @sincedb[inode] = stat.size
182
+ end
183
+ else
184
+ @logger.debug("#{path}: staying at position 0, no sincedb")
185
+ end
186
+ @changed[path] = Time.now.to_i
187
+ @locked[path] = false
188
+ return true
189
+ end # def _open_file
190
+
191
+ private
192
+ def _read_file(path, &block)
193
+ @locked[path] = true
194
+ @buffers[path] ||= FileWatch::BufferedTokenizer.new
195
+
196
+ changed = false
197
+ loop do
198
+ begin
199
+ data = @files[path].sysread(32768)
200
+ changed = true
201
+ @buffers[path].extract(data).each do |line|
202
+ yield(path, line)
203
+ end
204
+
205
+ @sincedb[@statcache[path]] = @files[path].pos
206
+ rescue Errno::EWOULDBLOCK, Errno::EINTR, EOFError
207
+ break
208
+ end
209
+ end
210
+
211
+ if changed
212
+ _sincedb_write
213
+ end
214
+ @changed[path] = Time.now.to_i
215
+ @locked[path] = false
216
+ end # def _read_file
217
+
218
+ public
219
+ def sincedb_write(reason=nil)
220
+ @logger.debug("caller requested sincedb write (#{reason})")
221
+ _sincedb_write(true) # since this is an external request, force the write
222
+ end
223
+
224
+ private
225
+ def _sincedb_open
226
+ path = @opts[:sincedb_path]
227
+ begin
228
+ db = File.open(path)
229
+ rescue
230
+ @logger.debug("_sincedb_open: #{path}: #{$!}")
231
+ return
232
+ end
233
+
234
+ @logger.debug("_sincedb_open: reading from #{path}")
235
+ db.each do |line|
236
+ ino, dev_major, dev_minor, pos = line.split("*", 4)
237
+ inode = [ino, dev_major.to_i, dev_minor.to_i]
238
+ @logger.debug("_sincedb_open: setting #{inode.inspect} to #{pos.to_i}")
239
+ @sincedb[inode] = pos.to_i
240
+ end
241
+ db.close
242
+ end # def _sincedb_open
243
+
244
+ private
245
+ def _sincedb_write_if_pending
246
+
247
+ # Check to see if sincedb should be written out since there was a file read after the sincedb flush,
248
+ # and during the sincedb_write_interval
249
+
250
+ if @sincedb_write_pending
251
+ _sincedb_write
252
+ end
253
+ end
254
+
255
+ private
256
+ def _sincedb_write(sincedb_force_write=false)
257
+
258
+ # This routine will only write out sincedb if enough time has passed based on @sincedb_write_interval
259
+ # If it hasn't and we were asked to write, then we are pending a write.
260
+
261
+ # if we were called with force == true, then we have to write sincedb and bypass a time check
262
+ # ie. external caller calling the public sincedb_write method
263
+
264
+ if(@sincedb_writing)
265
+ @logger.warn("_sincedb_write already writing")
266
+ return
267
+ end
268
+
269
+ @sincedb_writing = true
270
+
271
+ if (!sincedb_force_write)
272
+ now = Time.now.to_i
273
+ delta = now - @sincedb_last_write
274
+
275
+ # we will have to flush out the sincedb file after the interval expires. So, we will try again later.
276
+ if delta < @opts[:sincedb_write_interval]
277
+ @sincedb_write_pending = true
278
+ @sincedb_writing = false
279
+ return
280
+ end
281
+ end
282
+
283
+ @logger.debug("writing sincedb (delta since last write = #{delta})")
284
+
285
+ path = @opts[:sincedb_path]
286
+ tmp = "#{path}.new"
287
+ begin
288
+ db = File.open(tmp, "w")
289
+ rescue => e
290
+ @logger.warn("_sincedb_write failed: #{tmp}: #{e}")
291
+ @sincedb_writing = false
292
+ return
293
+ end
294
+
295
+ @sincedb.each do |inode, pos|
296
+ db.puts([inode, pos].flatten.join("*"))
297
+ end
298
+ db.close
299
+
300
+ begin
301
+ File.rename(tmp, path)
302
+ rescue => e
303
+ @logger.warn("_sincedb_write rename/sync failed: #{tmp} -> #{path}: #{e}")
304
+ end
305
+
306
+ @sincedb_last_write = now
307
+ @sincedb_write_pending = false
308
+ @sincedb_writing = false
309
+
310
+ # System.gc()
311
+ end # def _sincedb_write
312
+
313
+ public
314
+ def quit
315
+ @evict && @evict.kill
316
+ @watch.quit
317
+ end # def quit
318
+
319
+
320
+ private
321
+ def _start_eviction
322
+ @evict = Thread.new do
323
+ loop do
324
+ now = Time.now.to_i
325
+ @changed.keys.each do |path|
326
+ next if now - @changed[path] < @opts[:eviction_interval]
327
+ next if @locked[path]
328
+ next unless @files.include?(path)
329
+ @logger.debug(":evict for #{path}, deleted from @files")
330
+ @files.delete(path).close
331
+ @statcache.delete(path)
332
+ @changed.delete(path)
333
+ @locked.delete(path)
334
+ end
335
+ sleep 5
336
+ end
337
+ end
338
+ end
339
+ end # class Tail
340
+ end # module FileWatch
@@ -0,0 +1,179 @@
1
+ require "logger"
2
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
3
+ require "filewatch/winhelper"
4
+ end
5
+
6
+ module FileWatch
7
+ class Watch
8
+ attr_accessor :logger
9
+
10
+ public
11
+ def initialize(opts={})
12
+ @iswindows = ((RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/) != nil)
13
+ if opts[:logger]
14
+ @logger = opts[:logger]
15
+ else
16
+ @logger = Logger.new(STDERR)
17
+ @logger.level = Logger::INFO
18
+ end
19
+ @watching = []
20
+ @exclude = []
21
+ @files = Hash.new { |h, k| h[k] = Hash.new }
22
+ end # def initialize
23
+
24
+ public
25
+ def logger=(logger)
26
+ @logger = logger
27
+ end
28
+
29
+ public
30
+ def exclude(path)
31
+ path.to_a.each { |p| @exclude << p }
32
+ end
33
+
34
+ public
35
+ def watch(path)
36
+ if ! @watching.member?(path)
37
+ @watching << path
38
+ _discover_file(path, true)
39
+ end
40
+
41
+ return true
42
+ end # def tail
43
+
44
+ # Calls &block with params [event_type, path]
45
+ # event_type can be one of:
46
+ # :create_initial - initially present file (so start at end for tail)
47
+ # :create - file is created (new file after initial globs, start at 0)
48
+ # :modify - file is modified (size increases)
49
+ # :delete - file is deleted
50
+ public
51
+ def each(&block)
52
+ # Send any creates.
53
+ @files.keys.each do |path|
54
+ if ! @files[path][:create_sent]
55
+ if @files[path][:initial]
56
+ yield(:create_initial, path)
57
+ else
58
+ yield(:create, path)
59
+ end
60
+ @files[path][:create_sent] = true
61
+ end
62
+ end
63
+
64
+ @files.keys.each do |path|
65
+ begin
66
+ stat = File::Stat.new(path)
67
+ rescue Errno::ENOENT
68
+ # file has gone away or we can't read it anymore.
69
+ @files.delete(path)
70
+ @logger.debug("#{path}: stat failed (#{$!}), deleting from @files")
71
+ yield(:delete, path)
72
+ next
73
+ end
74
+
75
+ if @iswindows
76
+ fileId = Winhelper.GetWindowsUniqueFileIdentifier(path)
77
+ inode = [fileId, stat.dev_major, stat.dev_minor]
78
+ else
79
+ inode = [stat.ino.to_s, stat.dev_major, stat.dev_minor]
80
+ end
81
+
82
+ if inode != @files[path][:inode]
83
+ @logger.debug("#{path}: old inode was #{@files[path][:inode].inspect}, new is #{inode.inspect}")
84
+ yield(:delete, path)
85
+ yield(:create, path)
86
+ elsif stat.size < @files[path][:size]
87
+ @logger.debug("#{path}: file rolled, new size is #{stat.size}, old size #{@files[path][:size]}")
88
+ yield(:delete, path)
89
+ yield(:create, path)
90
+ elsif stat.size > @files[path][:size]
91
+ @logger.debug("#{path}: file grew, old size #{@files[path][:size]}, new size #{stat.size}")
92
+ yield(:modify, path)
93
+ else
94
+ # since there is no update, we should pass control back in case the caller needs to do any work
95
+ # otherwise, they can ONLY do other work when a file is created or modified
96
+ @logger.debug("#{path}: nothing to update")
97
+ yield(:noupdate, path)
98
+ end
99
+
100
+ @files[path][:size] = stat.size
101
+ @files[path][:inode] = inode
102
+ end # @files.keys.each
103
+ end # def each
104
+
105
+ public
106
+ def discover
107
+ @watching.each do |path|
108
+ _discover_file(path)
109
+ end
110
+ end
111
+
112
+ public
113
+ def subscribe(stat_interval = 1, discover_interval = 5, &block)
114
+ glob = 0
115
+ @quit = false
116
+ while !@quit
117
+ each(&block)
118
+
119
+ glob += 1
120
+ if glob == discover_interval
121
+ discover
122
+ glob = 0
123
+ end
124
+
125
+ sleep(stat_interval)
126
+ end
127
+ end # def subscribe
128
+
129
+ private
130
+ def _discover_file(path, initial=false)
131
+ globbed_dirs = Dir.glob(path)
132
+ @logger.debug("_discover_file_glob: #{path}: glob is: #{globbed_dirs}")
133
+ if globbed_dirs.empty? && File.file?(path)
134
+ globbed_dirs = [path]
135
+ @logger.debug("_discover_file_glob: #{path}: glob is: #{globbed_dirs} because glob did not work")
136
+ end
137
+ globbed_dirs.each do |file|
138
+ next if @files.member?(file)
139
+ next unless File.file?(file)
140
+
141
+ @logger.debug("_discover_file: #{path}: new: #{file} (exclude is #{@exclude.inspect})")
142
+
143
+ skip = false
144
+ @exclude.each do |pattern|
145
+ if File.fnmatch?(pattern, File.basename(file))
146
+ @logger.debug("_discover_file: #{file}: skipping because it " +
147
+ "matches exclude #{pattern}")
148
+ skip = true
149
+ break
150
+ end
151
+ end
152
+ next if skip
153
+
154
+ stat = File::Stat.new(file)
155
+ @files[file] = {
156
+ :size => 0,
157
+ :inode => [stat.ino, stat.dev_major, stat.dev_minor],
158
+ :create_sent => false,
159
+ }
160
+
161
+ if @iswindows
162
+ fileId = Winhelper.GetWindowsUniqueFileIdentifier(path)
163
+ @files[file][:inode] = [fileId, stat.dev_major, stat.dev_minor]
164
+ else
165
+ @files[file][:inode] = [stat.ino.to_s, stat.dev_major, stat.dev_minor]
166
+ end
167
+
168
+ if initial
169
+ @files[file][:initial] = true
170
+ end
171
+ end
172
+ end # def _discover_file
173
+
174
+ public
175
+ def quit
176
+ @quit = true
177
+ end # def quit
178
+ end # class Watch
179
+ end # module FileWatch
@@ -0,0 +1,70 @@
1
+ require "ffi"
2
+
3
+ module Winhelper
4
+ extend FFI::Library
5
+
6
+ ffi_lib 'kernel32'
7
+ ffi_convention :stdcall
8
+ class FileTime < FFI::Struct
9
+ layout :lowDateTime, :uint,
10
+ :highDateTime, :uint
11
+ end
12
+
13
+ #http://msdn.microsoft.com/en-us/library/windows/desktop/aa363788(v=vs.85).aspx
14
+ class FileInformation < FFI::Struct
15
+ def initialize()
16
+ createTime = FileTime.new
17
+ lastAccessTime = FileTime.new
18
+ lastWriteTime = FileTime.new
19
+ end
20
+
21
+ layout :fileAttributes, :uint, #DWORD dwFileAttributes;
22
+ :createTime, FileTime, #FILETIME ftCreationTime;
23
+ :lastAccessTime, FileTime, #FILETIME ftLastAccessTime;
24
+ :lastWriteTime, FileTime, #FILETIME ftLastWriteTime;
25
+ :volumeSerialNumber, :uint, #DWORD dwVolumeSerialNumber;
26
+ :fileSizeHigh, :uint, #DWORD nFileSizeHigh;
27
+ :fileSizeLow, :uint, #DWORD nFileSizeLow;
28
+ :numberOfLinks, :uint, #DWORD nNumberOfLinks;
29
+ :fileIndexHigh, :uint, #DWORD nFileIndexHigh;
30
+ :fileIndexLow, :uint #DWORD nFileIndexLow;
31
+ end
32
+
33
+
34
+ #http://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx
35
+ #HANDLE WINAPI CreateFile(_In_ LPCTSTR lpFileName,_In_ DWORD dwDesiredAccess,_In_ DWORD dwShareMode,
36
+ # _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,_In_ DWORD dwCreationDisposition,
37
+ # _In_ DWORD dwFlagsAndAttributes,_In_opt_ HANDLE hTemplateFile);
38
+ attach_function :GetOpenFileHandle, :CreateFileA, [:pointer, :uint, :uint, :pointer, :uint, :uint, :pointer], :pointer
39
+
40
+ #http://msdn.microsoft.com/en-us/library/windows/desktop/aa364952(v=vs.85).aspx
41
+ #BOOL WINAPI GetFileInformationByHandle(_In_ HANDLE hFile,_Out_ LPBY_HANDLE_FILE_INFORMATION lpFileInformation);
42
+ attach_function :GetFileInformationByHandle, [:pointer, :pointer], :int
43
+
44
+ attach_function :CloseHandle, [:pointer], :int
45
+
46
+
47
+ def self.GetWindowsUniqueFileIdentifier(path)
48
+ handle = GetOpenFileHandle(path, 0, 7, nil, 3, 128, nil)
49
+ fileInfo = Winhelper::FileInformation.new
50
+ success = GetFileInformationByHandle(handle, fileInfo)
51
+ CloseHandle(handle)
52
+ if success == 1
53
+ #args = [
54
+ # fileInfo[:fileAttributes], fileInfo[:volumeSerialNumber], fileInfo[:fileSizeHigh], fileInfo[:fileSizeLow],
55
+ # fileInfo[:numberOfLinks], fileInfo[:fileIndexHigh], fileInfo[:fileIndexLow]
56
+ # ]
57
+ #p "Information: %u %u %u %u %u %u %u " % args
58
+ #this is only guaranteed on NTFS, for ReFS on windows 2012, GetFileInformationByHandleEx should be used with FILE_ID_INFO, which returns a 128 bit identifier
59
+ return "#{fileInfo[:volumeSerialNumber]}-#{fileInfo[:fileIndexLow]}-#{fileInfo[:fileIndexHigh]}"
60
+ else
61
+ #p "cannot retrieve file information, returning path"
62
+ return path;
63
+ end
64
+ end
65
+ end
66
+
67
+ #fileId = Winhelper.GetWindowsUniqueFileIdentifier('C:\inetpub\logs\LogFiles\W3SVC1\u_ex1fdsadfsadfasdf30612.log')
68
+ #p "FileId: " + fileId
69
+ #p "outside function, sleeping"
70
+ #sleep(10)
@@ -0,0 +1,17 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), "..", "..", "lib"))
2
+ require "minitest/unit"
3
+ require "minitest/autorun"
4
+ require "filewatch/tail"
5
+
6
+ class TailTest < MiniTest::Unit::TestCase
7
+ def test_quit
8
+ require "timeout"
9
+ tail = FileWatch::Tail.new
10
+ #Thread.new(tail) { |t| sleep(1); t.quit }
11
+
12
+ #Timeout.timeout(5) do
13
+ #tail.subscribe { |e| }
14
+ #end
15
+ tail.quit
16
+ end
17
+ end # class TailTest
@@ -0,0 +1,7 @@
1
+ test:
2
+ @i=1; while [ -x "test$${i}.sh" ]; do \
3
+ echo "test$${i}:" ; \
4
+ ./test$${i}.sh || exit 1 ; \
5
+ echo "PASS" ; \
6
+ i=$$(($$i+1)) ; \
7
+ done
@@ -0,0 +1,58 @@
1
+ #!/bin/sh
2
+
3
+ test_init() {
4
+ export TEST_BASE=$(dirname $0)
5
+ export FW_BASE="$TEST_BASE/../.."
6
+ export RUBYLIB=$FW_BASE/lib:$RUBYLIB
7
+ export SINCEDB=$(mktemp)
8
+ export TAIL="$FW_BASE/bin/globtail -v -s $SINCEDB -i 5 -x skip*.log"
9
+ export TEST_DIR=$(mktemp -d)
10
+ export TEST_OUT=$(mktemp)
11
+ touch $TEST_OUT
12
+ mkdir -p $TEST_DIR
13
+ }
14
+
15
+ test_start() {
16
+ $TAIL "$TEST_DIR/*" >>$TEST_OUT 2>&1 &
17
+ export TEST_TAIL_PID=$!
18
+
19
+ # let globtail get started and do it's initial glob
20
+ sleep 3
21
+ }
22
+
23
+ test_stop() {
24
+ kill $TEST_TAIL_PID 2>/dev/null
25
+ count=0
26
+ while kill -0 $TEST_TAIL_PID 2>/dev/null; do
27
+ count=$((count+1))
28
+ sleep 1
29
+ if [ "$count" -eq 5 ]; then
30
+ kill -9 $TEST_TAIL_PID
31
+ count=0
32
+ fi
33
+ done
34
+ export TEST_TAIL_PID=""
35
+ }
36
+
37
+ test_done() {
38
+ [ -n "$TEST_TAIL_PID" ] && test_stop
39
+
40
+ output=$(mktemp)
41
+ output_clean=$(mktemp)
42
+ sed -e "s,^${TEST_DIR}/,," $TEST_OUT | sort > $output
43
+ sed -e '/^D, \[/d' < $output > $output_clean
44
+
45
+ data_file=$(echo $0 | sed -e 's/\.sh$/.data/')
46
+
47
+ diff $TEST_BASE/$data_file $output_clean >/dev/null
48
+ diff_rc=$?
49
+
50
+ if [ $diff_rc -ne 0 ]; then
51
+ diff -u $TEST_BASE/$data_file $output_clean
52
+ echo "$0 TEST FAILURE (output differs)"
53
+ sed -e 's,^,output: ,' $TEST_OUT
54
+ fi
55
+
56
+ rm -rf $TEST_DIR $TEST_OUT $output $output_clean $SINCEDB
57
+ exit $diff_rc
58
+ }
@@ -0,0 +1,5 @@
1
+ a.log: a
2
+ a.log: a
3
+ b.log: b
4
+ c.log: c
5
+ c.log: c
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+ # Test: basic tail support
3
+
4
+ . $(dirname $0)/framework.sh
5
+
6
+ test_init
7
+ test_start
8
+
9
+ echo a > $TEST_DIR/a.log
10
+ echo b > $TEST_DIR/b.log
11
+ echo c > $TEST_DIR/c.log
12
+ echo a >> $TEST_DIR/a.log
13
+ echo c >> $TEST_DIR/c.log
14
+
15
+ sleep 5
16
+
17
+ test_done
@@ -0,0 +1,4 @@
1
+ a.log: 1
2
+ a.log: 2
3
+ a.log: 3
4
+ a.log: 4
@@ -0,0 +1,20 @@
1
+ #!/bin/sh
2
+ # Test: file rename & old file name having new data
3
+
4
+ . $(dirname $0)/framework.sh
5
+
6
+ test_init
7
+ test_start
8
+
9
+ echo 1 > $TEST_DIR/a.log
10
+ sleep 8
11
+
12
+ echo 2 >> $TEST_DIR/a.log
13
+ sleep 3
14
+
15
+ echo 3 >> $TEST_DIR/a.log
16
+ mv $TEST_DIR/a.log $TEST_DIR/b.log
17
+ echo 4 > $TEST_DIR/a.log
18
+ sleep 8
19
+
20
+ test_done
@@ -0,0 +1,2 @@
1
+ a.log: a
2
+ b.log: b
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+ # Test: tests glob discovery of a new file, and in-memory sincedb
3
+ # preserving file position after a rename
4
+
5
+ . $(dirname $0)/framework.sh
6
+
7
+ test_init
8
+ test_start
9
+
10
+ echo a > $TEST_DIR/a.log
11
+ sleep 5
12
+ mv $TEST_DIR/a.log $TEST_DIR/b.log
13
+ sleep 5
14
+ echo b >> $TEST_DIR/b.log
15
+ sleep 3
16
+
17
+ test_done
@@ -0,0 +1,3 @@
1
+ a.log: a
2
+ a.log: b
3
+ b.log: c
@@ -0,0 +1,18 @@
1
+ #!/bin/sh
2
+ # Test: tests glob discovery of a new file, and in-memory sincedb
3
+ # preserving file position after a rename
4
+
5
+ . $(dirname $0)/framework.sh
6
+
7
+ test_init
8
+ test_start
9
+
10
+ echo a > $TEST_DIR/a.log
11
+ echo b >> $TEST_DIR/a.log
12
+ sleep 5
13
+ mv $TEST_DIR/a.log $TEST_DIR/b.log
14
+ sleep 5
15
+ echo c >> $TEST_DIR/b.log
16
+ sleep 3
17
+
18
+ test_done
@@ -0,0 +1,4 @@
1
+ a.log: 1
2
+ a.log: 2
3
+ a.log: 3
4
+ a.log: 4
@@ -0,0 +1,16 @@
1
+ #!/bin/sh
2
+ # Test: tests file truncation
3
+
4
+ . $(dirname $0)/framework.sh
5
+
6
+ test_init
7
+ test_start
8
+
9
+ echo 1 > $TEST_DIR/a.log
10
+ echo 2 >> $TEST_DIR/a.log
11
+ echo 3 >> $TEST_DIR/a.log
12
+ sleep 3
13
+ echo 4 > $TEST_DIR/a.log
14
+ sleep 3
15
+
16
+ test_done
@@ -0,0 +1,6 @@
1
+ a.log: 1
2
+ a.log: 2
3
+ a.log: 3
4
+ a.log: 4
5
+ a.log: 5
6
+ a.log: 6
@@ -0,0 +1,25 @@
1
+ #!/bin/sh
2
+ # Test: make sure we properly write a sincedb on SIGTERM, and pick up
3
+ # new log lines that were written while globtail is being restarted.
4
+
5
+ . $(dirname $0)/framework.sh
6
+
7
+ test_init
8
+ test_start
9
+
10
+ echo 1 > $TEST_DIR/a.log
11
+ echo 2 >> $TEST_DIR/a.log
12
+ echo 3 >> $TEST_DIR/a.log
13
+ sleep 6
14
+
15
+ echo 4 >> $TEST_DIR/a.log
16
+ test_stop
17
+
18
+ echo 5 >> $TEST_DIR/a.log
19
+
20
+ test_start
21
+
22
+ echo 6 >> $TEST_DIR/a.log
23
+ sleep 3
24
+
25
+ test_done
@@ -0,0 +1,6 @@
1
+ a.log: 1
2
+ a.log: 2
3
+ a.log: 3
4
+ a.log: 4
5
+ a.log: 5
6
+ a.log: 6
@@ -0,0 +1,29 @@
1
+ #!/bin/sh
2
+ # Test: ensure sincedb periodic database writing works (make sure we're not
3
+ # relying on SIGTERM handling)
4
+
5
+ . $(dirname $0)/framework.sh
6
+
7
+ test_init
8
+ test_start
9
+
10
+ echo 1 > $TEST_DIR/a.log
11
+ echo 2 >> $TEST_DIR/a.log
12
+ echo 3 >> $TEST_DIR/a.log
13
+ sleep 8
14
+
15
+ echo 4 >> $TEST_DIR/a.log
16
+ sleep 3
17
+
18
+ # send a "kill -9" to test that the sincedb write interval stuff is working
19
+ kill -9 $TEST_TAIL_PID
20
+ test_stop
21
+
22
+ echo 5 >> $TEST_DIR/a.log
23
+
24
+ test_start
25
+
26
+ echo 6 >> $TEST_DIR/a.log
27
+ sleep 3
28
+
29
+ test_done
@@ -0,0 +1,5 @@
1
+ a.log: 1
2
+ a.log: 2
3
+ a.log: 3
4
+ b.log: 4
5
+ b.log: 5
@@ -0,0 +1,24 @@
1
+ #!/bin/sh
2
+ # Test: make sure a renamed file resumes it's since state
3
+
4
+ . $(dirname $0)/framework.sh
5
+
6
+ test_init
7
+ test_start
8
+
9
+ echo 1 > $TEST_DIR/a.log
10
+ echo 2 >> $TEST_DIR/a.log
11
+ echo 3 >> $TEST_DIR/a.log
12
+ sleep 6
13
+
14
+ test_stop
15
+
16
+ echo 4 >> $TEST_DIR/a.log
17
+ mv $TEST_DIR/a.log $TEST_DIR/b.log
18
+
19
+ test_start
20
+
21
+ echo 5 >> $TEST_DIR/b.log
22
+ sleep 3
23
+
24
+ test_done
@@ -0,0 +1,5 @@
1
+ a.log: 1
2
+ a.log: 2
3
+ a.log: 3
4
+ a.log: 4
5
+ a.log: 5
@@ -0,0 +1,23 @@
1
+ #!/bin/sh
2
+ # Test: sincedb, and file truncation between globtail runs
3
+
4
+ . $(dirname $0)/framework.sh
5
+
6
+ test_init
7
+ test_start
8
+
9
+ echo 1 > $TEST_DIR/a.log
10
+ echo 2 >> $TEST_DIR/a.log
11
+ echo 3 >> $TEST_DIR/a.log
12
+ sleep 8
13
+
14
+ test_stop
15
+
16
+ echo 4 > $TEST_DIR/a.log
17
+
18
+ test_start
19
+
20
+ echo 5 >> $TEST_DIR/a.log
21
+ sleep 3
22
+
23
+ test_done
@@ -0,0 +1,3 @@
1
+ a.log: a1
2
+ b.log: b1
3
+ b.log: b2
@@ -0,0 +1,22 @@
1
+ #!/bin/sh
2
+ # Test: exclude support
3
+
4
+ . $(dirname $0)/framework.sh
5
+
6
+ test_init
7
+ test_start
8
+
9
+ echo a1 > $TEST_DIR/a.log
10
+ echo b1 > $TEST_DIR/b.log
11
+ echo nope1 > $TEST_DIR/skip1.log
12
+
13
+ sleep 8
14
+
15
+ mv $TEST_DIR/b.log $TEST_DIR/skip2.log
16
+ echo b2 > $TEST_DIR/b.log
17
+ sleep 8
18
+
19
+ echo nope2 >> $TEST_DIR/skip2.log
20
+ sleep 3
21
+
22
+ test_done
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ffilewatch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: ruby
6
+ authors:
7
+ - Jordan Sissel
8
+ - Pete Fritchman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-08-05 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Watch files and directories in ruby. Also supports tailing and glob file
15
+ patterns.
16
+ email:
17
+ - jls@semicomplete.com
18
+ - petef@databits.net
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - bin/globtail
24
+ - lib/JRubyFileExtension.jar
25
+ - lib/filewatch/buftok.rb
26
+ - lib/filewatch/tail.rb
27
+ - lib/filewatch/watch.rb
28
+ - lib/filewatch/winhelper.rb
29
+ - test/filewatch/tail.rb
30
+ - test/globtail/Makefile
31
+ - test/globtail/framework.sh
32
+ - test/globtail/test1.data
33
+ - test/globtail/test1.sh
34
+ - test/globtail/test10.data
35
+ - test/globtail/test10.sh
36
+ - test/globtail/test2.data
37
+ - test/globtail/test2.sh
38
+ - test/globtail/test3.data
39
+ - test/globtail/test3.sh
40
+ - test/globtail/test4.data
41
+ - test/globtail/test4.sh
42
+ - test/globtail/test5.data
43
+ - test/globtail/test5.sh
44
+ - test/globtail/test6.data
45
+ - test/globtail/test6.sh
46
+ - test/globtail/test7.data
47
+ - test/globtail/test7.sh
48
+ - test/globtail/test8.data
49
+ - test/globtail/test8.sh
50
+ - test/globtail/test9.data
51
+ - test/globtail/test9.sh
52
+ homepage: https://github.com/jordansissel/ruby-filewatch
53
+ licenses: []
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 2.2.2
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: filewatch - file watching for ruby
76
+ test_files: []
77
+ has_rdoc: