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 +7 -0
- data/bin/globtail +55 -0
- data/lib/JRubyFileExtension.jar +0 -0
- data/lib/filewatch/buftok.rb +139 -0
- data/lib/filewatch/tail.rb +340 -0
- data/lib/filewatch/watch.rb +179 -0
- data/lib/filewatch/winhelper.rb +70 -0
- data/test/filewatch/tail.rb +17 -0
- data/test/globtail/Makefile +7 -0
- data/test/globtail/framework.sh +58 -0
- data/test/globtail/test1.data +5 -0
- data/test/globtail/test1.sh +17 -0
- data/test/globtail/test10.data +4 -0
- data/test/globtail/test10.sh +20 -0
- data/test/globtail/test2.data +2 -0
- data/test/globtail/test2.sh +17 -0
- data/test/globtail/test3.data +3 -0
- data/test/globtail/test3.sh +18 -0
- data/test/globtail/test4.data +4 -0
- data/test/globtail/test4.sh +16 -0
- data/test/globtail/test5.data +6 -0
- data/test/globtail/test5.sh +25 -0
- data/test/globtail/test6.data +6 -0
- data/test/globtail/test6.sh +29 -0
- data/test/globtail/test7.data +5 -0
- data/test/globtail/test7.sh +24 -0
- data/test/globtail/test8.data +5 -0
- data/test/globtail/test8.sh +23 -0
- data/test/globtail/test9.data +3 -0
- data/test/globtail/test9.sh +22 -0
- metadata +77 -0
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,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,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,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,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,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,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,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,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,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,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,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:
|