filewatch 0.2.5 → 0.3.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.
- data/bin/globtail +55 -0
- data/lib/filewatch/buftok.rb +2 -2
- data/lib/filewatch/tail.rb +171 -48
- data/lib/filewatch/watch.rb +145 -26
- 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 +38 -38
- data/bin/gtail +0 -50
- data/lib/filewatch/exception.rb +0 -12
- data/lib/filewatch/inotify/emhandler.rb +0 -16
- data/lib/filewatch/inotify/event.rb +0 -101
- data/lib/filewatch/inotify/fd.rb +0 -319
- data/lib/filewatch/namespace.rb +0 -3
- data/lib/filewatch/rubyfixes.rb +0 -8
- data/lib/filewatch/stringpipeio.rb +0 -33
- data/lib/filewatch/tailglob.rb +0 -244
- data/lib/filewatch/watchglob.rb +0 -83
- data/test/log4j/LogTest.java +0 -21
- data/test/log4j/log4j.properties +0 -20
- data/test/logrotate/logrotate.conf +0 -5
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
|
data/lib/filewatch/buftok.rb
CHANGED
@@ -29,7 +29,7 @@
|
|
29
29
|
# end
|
30
30
|
# end
|
31
31
|
|
32
|
-
class BufferedTokenizer
|
32
|
+
module FileWatch; class BufferedTokenizer
|
33
33
|
# New BufferedTokenizers will operate on lines delimited by "\n" by default
|
34
34
|
# or allow you to specify any delimiter token you so choose, which will then
|
35
35
|
# be used by String#split to tokenize the input data
|
@@ -136,4 +136,4 @@ cter token support.
|
|
136
136
|
def empty?
|
137
137
|
@input.empty?
|
138
138
|
end
|
139
|
-
end
|
139
|
+
end; end
|
data/lib/filewatch/tail.rb
CHANGED
@@ -1,58 +1,181 @@
|
|
1
|
+
require "filewatch/buftok"
|
1
2
|
require "filewatch/watch"
|
2
|
-
require "
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
@
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module FileWatch
|
6
|
+
class Tail
|
7
|
+
attr_accessor :logger
|
8
|
+
|
9
|
+
public
|
10
|
+
def initialize(opts={})
|
11
|
+
if opts[:logger]
|
12
|
+
@logger = opts[:logger]
|
13
|
+
else
|
14
|
+
@logger = Logger.new(STDERR)
|
15
|
+
@logger.level = Logger::INFO
|
16
|
+
end
|
17
|
+
@files = {}
|
18
|
+
@buffers = {}
|
19
|
+
@watch = FileWatch::Watch.new
|
20
|
+
@watch.logger = @logger
|
21
|
+
@sincedb = {}
|
22
|
+
@sincedb_last_write = 0
|
23
|
+
@statcache = {}
|
24
|
+
@opts = {
|
25
|
+
:sincedb_write_interval => 10,
|
26
|
+
:sincedb_path => "#{ENV["HOME"]}/.sincedb",
|
27
|
+
:exclude => [],
|
28
|
+
}.merge(opts)
|
29
|
+
@watch.exclude(@opts[:exclude])
|
30
|
+
|
31
|
+
_sincedb_open
|
32
|
+
end # def initialize
|
33
|
+
|
34
|
+
public
|
35
|
+
def logger=(logger)
|
36
|
+
@logger = logger
|
37
|
+
@watch.logger = logger
|
38
|
+
end # def logger=
|
39
|
+
|
40
|
+
public
|
41
|
+
def tail(path)
|
42
|
+
@watch.watch(path)
|
43
|
+
end # def tail
|
44
|
+
|
45
|
+
public
|
46
|
+
def subscribe(&block)
|
47
|
+
# subscribe(stat_interval = 1, discover_interval = 5, &block)
|
48
|
+
@watch.subscribe do |event, path|
|
49
|
+
case event
|
50
|
+
when :create, :create_initial
|
51
|
+
if @files.member?(path)
|
52
|
+
@logger.debug("#{event} for #{path}: already exists in @files")
|
53
|
+
next
|
39
54
|
end
|
55
|
+
_open_file(path, event)
|
56
|
+
_read_file(path, &block)
|
57
|
+
when :modify
|
58
|
+
if !@files.member?(path)
|
59
|
+
@logger.debug(":modify for #{path}, does not exist in @files")
|
60
|
+
_open_file(path)
|
61
|
+
end
|
62
|
+
_read_file(path, &block)
|
63
|
+
when :delete
|
64
|
+
@logger.debug(":delete for #{path}, deleted from @files")
|
65
|
+
_read_file(path, &block)
|
66
|
+
@files[path].close
|
67
|
+
@files.delete(path)
|
68
|
+
@statcache.delete(path)
|
69
|
+
else
|
70
|
+
@logger.warn("unknown event type #{event} for #{path}")
|
71
|
+
end
|
72
|
+
end # @watch.subscribe
|
73
|
+
end # def each
|
74
|
+
|
75
|
+
private
|
76
|
+
def _open_file(path, event)
|
77
|
+
@logger.debug("_open_file: #{path}: opening")
|
78
|
+
# TODO(petef): handle File.open failing
|
79
|
+
begin
|
80
|
+
@files[path] = File.open(path)
|
81
|
+
rescue Errno::ENOENT
|
82
|
+
@logger.warn("#{path}: open: #{$!}")
|
83
|
+
@files.delete(path)
|
84
|
+
return
|
85
|
+
end
|
86
|
+
|
87
|
+
stat = File::Stat.new(path)
|
88
|
+
inode = [stat.ino, stat.dev_major, stat.dev_minor]
|
89
|
+
@statcache[path] = inode
|
90
|
+
|
91
|
+
if @sincedb.member?(inode)
|
92
|
+
last_size = @sincedb[inode]
|
93
|
+
@logger.debug("#{path}: sincedb last value #{@sincedb[inode]}, cur size #{stat.size}")
|
94
|
+
if last_size <= stat.size
|
95
|
+
@logger.debug("#{path}: sincedb: seeking to #{last_size}")
|
96
|
+
@files[path].sysseek(last_size, IO::SEEK_SET)
|
97
|
+
else
|
98
|
+
@logger.debug("#{path}: last value size is greater than current value, starting over")
|
99
|
+
@sincedb[inode] = 0
|
40
100
|
end
|
101
|
+
elsif event == :create_initial && @files[path]
|
102
|
+
@logger.debug("#{path}: initial create, no sincedb, seeking to end #{stat.size}")
|
103
|
+
@files[path].sysseek(stat.size, IO::SEEK_SET)
|
104
|
+
@sincedb[inode] = stat.size
|
41
105
|
else
|
42
|
-
|
106
|
+
@logger.debug("#{path}: staying at position 0, no sincedb")
|
43
107
|
end
|
108
|
+
end # def _open_file
|
109
|
+
|
110
|
+
private
|
111
|
+
def _read_file(path, &block)
|
112
|
+
@buffers[path] ||= FileWatch::BufferedTokenizer.new
|
113
|
+
|
114
|
+
changed = false
|
115
|
+
loop do
|
116
|
+
begin
|
117
|
+
data = @files[path].read_nonblock(4096)
|
118
|
+
changed = true
|
119
|
+
@buffers[path].extract(data).each do |line|
|
120
|
+
yield(path, line)
|
121
|
+
end
|
122
|
+
|
123
|
+
@sincedb[@statcache[path]] = @files[path].pos
|
124
|
+
rescue Errno::EWOULDBLOCK, Errno::EINTR, EOFError
|
125
|
+
break
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
if changed
|
130
|
+
now = Time.now.to_i
|
131
|
+
delta = now - @sincedb_last_write
|
132
|
+
if delta >= @opts[:sincedb_write_interval]
|
133
|
+
@logger.debug("writing sincedb (delta since last write = #{delta})")
|
134
|
+
_sincedb_write
|
135
|
+
@sincedb_last_write = now
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end # def _read_file
|
139
|
+
|
140
|
+
public
|
141
|
+
def sincedb_write(reason=nil)
|
142
|
+
@logger.debug("caller requested sincedb write (#{reason})")
|
143
|
+
_sincedb_write
|
44
144
|
end
|
45
|
-
end # def subscribe
|
46
145
|
|
47
|
-
|
48
|
-
|
146
|
+
private
|
147
|
+
def _sincedb_open
|
148
|
+
path = @opts[:sincedb_path]
|
49
149
|
begin
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
150
|
+
db = File.open(path)
|
151
|
+
rescue
|
152
|
+
@logger.debug("_sincedb_open: #{path}: #{$!}")
|
153
|
+
return
|
54
154
|
end
|
55
|
-
end
|
56
|
-
end
|
57
155
|
|
58
|
-
|
156
|
+
@logger.debug("_sincedb_open: reading from #{path}")
|
157
|
+
db.each do |line|
|
158
|
+
ino, dev_major, dev_minor, pos = line.split(" ", 4)
|
159
|
+
inode = [ino.to_i, dev_major.to_i, dev_minor.to_i]
|
160
|
+
@logger.debug("_sincedb_open: setting #{inode.inspect} to #{pos.to_i}")
|
161
|
+
@sincedb[inode] = pos.to_i
|
162
|
+
end
|
163
|
+
end # def _sincedb_open
|
164
|
+
|
165
|
+
private
|
166
|
+
def _sincedb_write
|
167
|
+
path = @opts[:sincedb_path]
|
168
|
+
begin
|
169
|
+
db = File.open(path, "w")
|
170
|
+
rescue
|
171
|
+
@logger.debug("_sincedb_write: #{path}: #{$!}")
|
172
|
+
return
|
173
|
+
end
|
174
|
+
|
175
|
+
@sincedb.each do |inode, pos|
|
176
|
+
db.puts([inode, pos].flatten.join(" "))
|
177
|
+
end
|
178
|
+
db.close
|
179
|
+
end # def _sincedb_write
|
180
|
+
end # class Watch
|
181
|
+
end # module FileWatch
|
data/lib/filewatch/watch.rb
CHANGED
@@ -1,26 +1,145 @@
|
|
1
|
-
require "
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module FileWatch
|
4
|
+
class Watch
|
5
|
+
attr_accessor :logger
|
6
|
+
|
7
|
+
public
|
8
|
+
def initialize(opts={})
|
9
|
+
if opts[:logger]
|
10
|
+
@logger = opts[:logger]
|
11
|
+
else
|
12
|
+
@logger = Logger.new(STDERR)
|
13
|
+
@logger.level = Logger::INFO
|
14
|
+
end
|
15
|
+
@watching = []
|
16
|
+
@exclude = []
|
17
|
+
@files = Hash.new { |h, k| h[k] = Hash.new }
|
18
|
+
end # def initialize
|
19
|
+
|
20
|
+
public
|
21
|
+
def logger=(logger)
|
22
|
+
@logger = logger
|
23
|
+
end
|
24
|
+
|
25
|
+
public
|
26
|
+
def exclude(path)
|
27
|
+
path.to_a.each { |p| @exclude << p }
|
28
|
+
end
|
29
|
+
|
30
|
+
public
|
31
|
+
def watch(path)
|
32
|
+
if ! @watching.member?(path)
|
33
|
+
@watching << path
|
34
|
+
_discover_file(path, true)
|
35
|
+
end
|
36
|
+
|
37
|
+
return true
|
38
|
+
end # def tail
|
39
|
+
|
40
|
+
# Calls &block with params [event_type, path]
|
41
|
+
# event_type can be one of:
|
42
|
+
# :create_initial - initially present file (so start at end for tail)
|
43
|
+
# :create - file is created (new file after initial globs, start at 0)
|
44
|
+
# :modify - file is modified (size increases)
|
45
|
+
# :delete - file is deleted
|
46
|
+
public
|
47
|
+
def each(&block)
|
48
|
+
# Send any creates.
|
49
|
+
@files.keys.each do |path|
|
50
|
+
if ! @files[path][:create_sent]
|
51
|
+
if @files[path][:initial]
|
52
|
+
yield(:create_initial, path)
|
53
|
+
else
|
54
|
+
yield(:create, path)
|
55
|
+
end
|
56
|
+
@files[path][:create_sent] = true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@files.keys.each do |path|
|
61
|
+
begin
|
62
|
+
stat = File::Stat.new(path)
|
63
|
+
rescue Errno::ENOENT
|
64
|
+
# file has gone away or we can't read it anymore.
|
65
|
+
@files.delete(path)
|
66
|
+
@logger.debug("#{path}: stat failed (#{$!}), deleting from @files")
|
67
|
+
yield(:delete, path)
|
68
|
+
next
|
69
|
+
end
|
70
|
+
|
71
|
+
inode = [stat.ino, stat.dev_major, stat.dev_minor]
|
72
|
+
if inode != @files[path][:inode]
|
73
|
+
@logger.debug("#{path}: old inode was #{@files[path][:inode].inspect}, new is #{inode.inspect}")
|
74
|
+
yield(:delete, path)
|
75
|
+
yield(:create, path)
|
76
|
+
elsif stat.size < @files[path][:size]
|
77
|
+
@logger.debug("#{path}: file rolled, new size is #{stat.size}, old size #{@files[path][:size]}")
|
78
|
+
yield(:delete, path)
|
79
|
+
yield(:create, path)
|
80
|
+
elsif stat.size > @files[path][:size]
|
81
|
+
@logger.debug("#{path}: file grew, old size #{@files[path][:size]}, new size #{stat.size}")
|
82
|
+
yield(:modify, path)
|
83
|
+
end
|
84
|
+
|
85
|
+
@files[path][:size] = stat.size
|
86
|
+
@files[path][:inode] = inode
|
87
|
+
end # @files.keys.each
|
88
|
+
end # def each
|
89
|
+
|
90
|
+
public
|
91
|
+
def discover
|
92
|
+
@watching.each do |path|
|
93
|
+
_discover_file(path)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
public
|
98
|
+
def subscribe(stat_interval = 1, discover_interval = 5, &block)
|
99
|
+
glob = 0
|
100
|
+
loop do
|
101
|
+
each(&block)
|
102
|
+
|
103
|
+
glob += 1
|
104
|
+
if glob == discover_interval
|
105
|
+
discover
|
106
|
+
glob = 0
|
107
|
+
end
|
108
|
+
|
109
|
+
sleep(stat_interval)
|
110
|
+
end
|
111
|
+
end # def subscribe
|
112
|
+
|
113
|
+
private
|
114
|
+
def _discover_file(path, initial=false)
|
115
|
+
Dir.glob(path).each do |file|
|
116
|
+
next if @files.member?(file)
|
117
|
+
next unless File.file?(file)
|
118
|
+
|
119
|
+
@logger.debug("_discover_file: #{path}: new: #{file} (exclude is #{@exclude.inspect})")
|
120
|
+
|
121
|
+
skip = false
|
122
|
+
@exclude.each do |pattern|
|
123
|
+
if File.fnmatch?(pattern, File.basename(file))
|
124
|
+
@logger.debug("_discover_file: #{file}: skipping because it " +
|
125
|
+
"matches exclude #{pattern}")
|
126
|
+
skip = true
|
127
|
+
break
|
128
|
+
end
|
129
|
+
end
|
130
|
+
next if skip
|
131
|
+
|
132
|
+
stat = File::Stat.new(file)
|
133
|
+
@files[file] = {
|
134
|
+
:size => 0,
|
135
|
+
:inode => [stat.ino, stat.dev_major, stat.dev_minor],
|
136
|
+
:create_sent => false,
|
137
|
+
}
|
138
|
+
if initial
|
139
|
+
@files[file][:initial] = true
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end # def _discover_file
|
143
|
+
|
144
|
+
end # class Watch
|
145
|
+
end # module FileWatch
|