filewatch 0.1.2 → 0.2.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/gtail +50 -0
- data/lib/filewatch/buftok.rb +139 -0
- data/lib/filewatch/exception.rb +12 -0
- data/lib/filewatch/inotify/event.rb +9 -2
- data/lib/filewatch/inotify/fd.rb +18 -4
- data/lib/filewatch/tail.rb +1 -0
- data/lib/filewatch/tailglob.rb +182 -0
- data/lib/filewatch/watch.rb +2 -1
- data/lib/filewatch/watchglob.rb +79 -0
- data/test/log4j/LogTest.java +21 -0
- data/test/log4j/log4j.properties +20 -0
- metadata +37 -19
data/bin/gtail
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require "rubygems"
|
|
3
|
+
require "filewatch/tailglob"
|
|
4
|
+
require "filewatch/buftok"
|
|
5
|
+
require "optparse"
|
|
6
|
+
|
|
7
|
+
def main(args)
|
|
8
|
+
with_filenames = true
|
|
9
|
+
exclude_patterns = []
|
|
10
|
+
|
|
11
|
+
opts = OptionParser.new do |opts|
|
|
12
|
+
opts.banner = "Usage: #{$0} [options] <path_or_glob> [path_or_glob2] [...]"
|
|
13
|
+
|
|
14
|
+
opts.on("-n", "--no-filename",
|
|
15
|
+
"Supress prefixing of output with file names") do |x|
|
|
16
|
+
with_filenames = false
|
|
17
|
+
end # -n
|
|
18
|
+
|
|
19
|
+
opts.on("-x EXCLUDE", "--exclude EXCLUDE",
|
|
20
|
+
"A pattern to ignore. Wildcard/globs accepted." \
|
|
21
|
+
" Can be specified multiple times") do |pattern|
|
|
22
|
+
exclude_patterns << pattern
|
|
23
|
+
end
|
|
24
|
+
end # OptionParser
|
|
25
|
+
|
|
26
|
+
opts.parse!(args)
|
|
27
|
+
|
|
28
|
+
if args.length == 0
|
|
29
|
+
puts opts.banner
|
|
30
|
+
return 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
tail = FileWatch::TailGlob.new
|
|
34
|
+
ARGV.each do |path|
|
|
35
|
+
tail.tail(path, :exclude => exclude_patterns)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
buffer = BufferedTokenizer.new
|
|
39
|
+
tail.subscribe do |path, data|
|
|
40
|
+
buffer.extract(data).each do |line|
|
|
41
|
+
if with_filenames
|
|
42
|
+
puts "#{path}: #{line}"
|
|
43
|
+
else
|
|
44
|
+
puts line
|
|
45
|
+
end
|
|
46
|
+
end # buffer.extract
|
|
47
|
+
end # tail.subscribe
|
|
48
|
+
end # def main
|
|
49
|
+
|
|
50
|
+
exit(main(ARGV))
|
|
@@ -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
|
+
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
|
|
@@ -12,6 +12,9 @@ class FileWatch::Inotify::Event < FFI::Struct
|
|
|
12
12
|
|
|
13
13
|
attr_accessor :name
|
|
14
14
|
|
|
15
|
+
# Enum of :directory or :file
|
|
16
|
+
attr_accessor :type
|
|
17
|
+
|
|
15
18
|
def initialize(pointer)
|
|
16
19
|
if pointer.is_a?(String)
|
|
17
20
|
pointer = FFI::MemoryPointer.from_string(pointer)
|
|
@@ -54,7 +57,11 @@ class FileWatch::Inotify::Event < FFI::Struct
|
|
|
54
57
|
|
|
55
58
|
def from_stringpipeio(io)
|
|
56
59
|
begin
|
|
57
|
-
|
|
60
|
+
if self[:len] > 0
|
|
61
|
+
@name = io.read(self[:len], true)
|
|
62
|
+
else
|
|
63
|
+
@name = nil
|
|
64
|
+
end
|
|
58
65
|
rescue Errno::EINVAL => e
|
|
59
66
|
$stderr.puts "Read was too small? Confused."
|
|
60
67
|
raise e
|
|
@@ -73,7 +80,7 @@ class FileWatch::Inotify::Event < FFI::Struct
|
|
|
73
80
|
end
|
|
74
81
|
|
|
75
82
|
def to_s
|
|
76
|
-
return "#{@name} (#{self.actions.join(", ")})"
|
|
83
|
+
return "#{@name} (#{self.actions.join(", ")}) [type=#{type}]"
|
|
77
84
|
end
|
|
78
85
|
|
|
79
86
|
def partial?
|
data/lib/filewatch/inotify/fd.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
require "rubygems"
|
|
2
2
|
require "ffi"
|
|
3
|
+
require "fcntl"
|
|
3
4
|
require "filewatch/inotify/event"
|
|
4
5
|
require "filewatch/namespace"
|
|
5
6
|
require "filewatch/stringpipeio"
|
|
7
|
+
require "filewatch/exception"
|
|
6
8
|
|
|
7
9
|
class FileWatch::Inotify::FD
|
|
8
10
|
include Enumerable
|
|
@@ -12,7 +14,7 @@ class FileWatch::Inotify::FD
|
|
|
12
14
|
ffi_lib FFI::Library::LIBC
|
|
13
15
|
|
|
14
16
|
attach_function :inotify_init, [], :int
|
|
15
|
-
attach_function :
|
|
17
|
+
attach_function :fcntl, [:int, :int, :long], :int
|
|
16
18
|
attach_function :inotify_add_watch, [:int, :string, :uint32], :int
|
|
17
19
|
|
|
18
20
|
# So we can read and poll inotify from jruby.
|
|
@@ -24,6 +26,9 @@ class FileWatch::Inotify::FD
|
|
|
24
26
|
|
|
25
27
|
INOTIFY_CLOEXEC = 02000000
|
|
26
28
|
INOTIFY_NONBLOCK = 04000
|
|
29
|
+
|
|
30
|
+
F_SETFL = 4
|
|
31
|
+
O_NONBLOCK = 04000
|
|
27
32
|
|
|
28
33
|
WATCH_BITS = {
|
|
29
34
|
:access => 1 << 0,
|
|
@@ -60,12 +65,19 @@ class FileWatch::Inotify::FD
|
|
|
60
65
|
@watches = {}
|
|
61
66
|
@buffer = FileWatch::StringPipeIO.new
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
#@fd = CInotify.inotify_init1(INOTIFY_NONBLOCK)
|
|
69
|
+
@fd = CInotify.inotify_init()
|
|
64
70
|
|
|
65
71
|
if java?
|
|
66
72
|
@io = nil
|
|
73
|
+
@rc = CInotify.fcntl(@fd, F_SETFL, O_NONBLOCK)
|
|
74
|
+
if @rc == -1
|
|
75
|
+
raise FileWatch::Exception.new(
|
|
76
|
+
"fcntl(#{@fd}, F_SETFL, O_NONBLOCK) failed. #{$?}", @fd, nil)
|
|
77
|
+
end
|
|
67
78
|
else
|
|
68
79
|
@io = IO.for_fd(@fd)
|
|
80
|
+
@io.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
|
|
69
81
|
end
|
|
70
82
|
end
|
|
71
83
|
|
|
@@ -110,10 +122,10 @@ class FileWatch::Inotify::FD
|
|
|
110
122
|
def watch(path, *what_to_watch)
|
|
111
123
|
mask = what_to_watch.inject(0) { |m, val| m |= WATCH_BITS[val] }
|
|
112
124
|
watch_descriptor = CInotify.inotify_add_watch(@fd, path, mask)
|
|
113
|
-
#puts "watch #{path} => #{watch_descriptor}"
|
|
114
125
|
|
|
115
126
|
if watch_descriptor == -1
|
|
116
|
-
raise
|
|
127
|
+
raise FileWatch::Exception.new(
|
|
128
|
+
"inotify_add_watch(#{@fd}, #{path}, #{mask}) failed. #{$?}", @fd, path)
|
|
117
129
|
end
|
|
118
130
|
@watches[watch_descriptor] = {
|
|
119
131
|
:path => path,
|
|
@@ -167,11 +179,13 @@ class FileWatch::Inotify::FD
|
|
|
167
179
|
if event.name == nil
|
|
168
180
|
# Some events don't have the name at all, so add our own.
|
|
169
181
|
event.name = watchpath
|
|
182
|
+
event.type = :file
|
|
170
183
|
else
|
|
171
184
|
# Event paths are relative to the watch, if a directory. Prefix to make
|
|
172
185
|
# the full path.
|
|
173
186
|
if watch[:is_directory]
|
|
174
187
|
event.name = File.join(watchpath, event.name)
|
|
188
|
+
event.type = :directory
|
|
175
189
|
end
|
|
176
190
|
end
|
|
177
191
|
|
data/lib/filewatch/tail.rb
CHANGED
|
@@ -30,6 +30,7 @@ class FileWatch::Tail
|
|
|
30
30
|
if @files.include?(path)
|
|
31
31
|
file = @files[path]
|
|
32
32
|
event.actions.each do |action|
|
|
33
|
+
# call method 'file_action_<action>' like 'file_action_modify'
|
|
33
34
|
method = "file_action_#{action}".to_sym
|
|
34
35
|
if respond_to?(method)
|
|
35
36
|
send(method, file, event, &block)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
require "filewatch/namespace"
|
|
2
|
+
require "filewatch/exception"
|
|
3
|
+
require "filewatch/watchglob"
|
|
4
|
+
|
|
5
|
+
class FileWatch::TailGlob
|
|
6
|
+
|
|
7
|
+
public
|
|
8
|
+
def initialize
|
|
9
|
+
@watch = FileWatch::WatchGlob.new
|
|
10
|
+
|
|
11
|
+
# hash of string path => File
|
|
12
|
+
@files = {}
|
|
13
|
+
@options = {}
|
|
14
|
+
|
|
15
|
+
# hash of string path => action to take on EOF
|
|
16
|
+
#@eof_actions = {}
|
|
17
|
+
end # def initialize
|
|
18
|
+
|
|
19
|
+
# Watch a path glob.
|
|
20
|
+
#
|
|
21
|
+
# Options is a hash of:
|
|
22
|
+
# :exclude => array of globs to ignore.
|
|
23
|
+
public
|
|
24
|
+
def tail(path, options)
|
|
25
|
+
what_to_watch = [ :create, :modify, :delete ]
|
|
26
|
+
|
|
27
|
+
# Save glob options
|
|
28
|
+
@options[path] = options
|
|
29
|
+
|
|
30
|
+
@watch.watch(path, *what_to_watch) do |path|
|
|
31
|
+
# Save per-path options
|
|
32
|
+
@options[path] = options
|
|
33
|
+
|
|
34
|
+
# for each file found by the glob, open it.
|
|
35
|
+
follow_file(path, :end)
|
|
36
|
+
end # @watch.watch
|
|
37
|
+
end # def watch
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
def follow_file(path, seek=:end)
|
|
41
|
+
# Don't follow things that aren't files.
|
|
42
|
+
if !File.file?(path)
|
|
43
|
+
puts "Skipping follow on #{path}, File.file? == false"
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
options = @options[path] || {}
|
|
48
|
+
if options.include?(:exclude)
|
|
49
|
+
options[:exclude].each do |exclusion|
|
|
50
|
+
if File.fnmatch?(exclusion, path)
|
|
51
|
+
puts "Skipping #{path}, matches #{exclusion}"
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
close(path) if @files.include?(path)
|
|
58
|
+
@files[path] = File.new(path, "r")
|
|
59
|
+
|
|
60
|
+
# TODO(sissel): Support 'since'-like support.
|
|
61
|
+
case seek
|
|
62
|
+
when :end; @files[path].sysseek(0, IO::SEEK_END)
|
|
63
|
+
when :beginning; # nothing
|
|
64
|
+
else
|
|
65
|
+
if seek.is_a?(Numeric)
|
|
66
|
+
# 'seek' could be a number that is an offset from
|
|
67
|
+
# the start of the file. We should seek to that point.
|
|
68
|
+
@files[path].sysseek(seek, IO::SEEK_SET)
|
|
69
|
+
end # seek.is_a?(Numeric)
|
|
70
|
+
end # case seek
|
|
71
|
+
end # def follow_file
|
|
72
|
+
|
|
73
|
+
public
|
|
74
|
+
def subscribe(handler=nil, &block)
|
|
75
|
+
# TODO(sissel): Add handler support.
|
|
76
|
+
@watch.subscribe do |event|
|
|
77
|
+
path = event.name
|
|
78
|
+
|
|
79
|
+
event.actions.each do |action|
|
|
80
|
+
method = "file_action_#{action}".to_sym
|
|
81
|
+
if respond_to?(method)
|
|
82
|
+
send(method, path, event, &block)
|
|
83
|
+
else
|
|
84
|
+
$stderr.puts "Unsupported method #{self.class.name}##{method}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end # @watch.subscribe
|
|
88
|
+
end # def subscribe
|
|
89
|
+
|
|
90
|
+
protected
|
|
91
|
+
def file_action_modify(path, event, &block)
|
|
92
|
+
loop do
|
|
93
|
+
file = @files[path]
|
|
94
|
+
begin
|
|
95
|
+
data = file.sysread(4096)
|
|
96
|
+
yield event.name, data
|
|
97
|
+
rescue EOFError
|
|
98
|
+
check_for_truncation_or_deletion(path, event, &block)
|
|
99
|
+
#case @eof_actions[path]
|
|
100
|
+
#when :reopen
|
|
101
|
+
#puts "Reopening #{path} due to eof and new file"
|
|
102
|
+
#reopen(path)
|
|
103
|
+
#end
|
|
104
|
+
|
|
105
|
+
break
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
protected
|
|
111
|
+
def check_for_truncation_or_deletion(path, event, &block)
|
|
112
|
+
file = @files[path]
|
|
113
|
+
pos = file.sysseek(0, IO::SEEK_CUR)
|
|
114
|
+
#puts "EOF(#{path}), pos: #{pos}"
|
|
115
|
+
|
|
116
|
+
# Truncation is determined by comparing the current read position in the
|
|
117
|
+
# file against the size of the file. If the file shrank, than we should
|
|
118
|
+
# assume truncation and seek to the beginning.
|
|
119
|
+
begin
|
|
120
|
+
stat = file.stat
|
|
121
|
+
#p stat.size => pos
|
|
122
|
+
if stat.size < pos
|
|
123
|
+
# Truncated. Seek to beginning and read.
|
|
124
|
+
file.sysseek(0, IO::SEEK_SET)
|
|
125
|
+
file_action_modify(path, event, &block)
|
|
126
|
+
end
|
|
127
|
+
rescue Errno::ENOENT
|
|
128
|
+
# File was deleted or renamed. Stop following it.
|
|
129
|
+
close(path)
|
|
130
|
+
end
|
|
131
|
+
end # def follow_file
|
|
132
|
+
|
|
133
|
+
protected
|
|
134
|
+
def reopen(path)
|
|
135
|
+
close(path)
|
|
136
|
+
follow_file(path, :beginning)
|
|
137
|
+
end # def reopen
|
|
138
|
+
|
|
139
|
+
protected
|
|
140
|
+
def close(path)
|
|
141
|
+
@files[path].close rescue nil
|
|
142
|
+
@files.delete(path)
|
|
143
|
+
return nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
protected
|
|
147
|
+
def file_action_create(path, event, &block)
|
|
148
|
+
if following?(path)
|
|
149
|
+
# TODO(sissel): If we are watching this file already, schedule it to be
|
|
150
|
+
# opened the next time we hit EOF on the current file descriptor for the
|
|
151
|
+
# same file. Maybe? Or is reopening now fine?
|
|
152
|
+
#
|
|
153
|
+
reopen(path)
|
|
154
|
+
|
|
155
|
+
# Schedule a reopen at EOF
|
|
156
|
+
#@eof_actions[path] = :reopen
|
|
157
|
+
else
|
|
158
|
+
# If we are not yet watching this file, watch it.
|
|
159
|
+
follow_file(path, :beginning)
|
|
160
|
+
|
|
161
|
+
# Then read all of the data so far since this is a new file.
|
|
162
|
+
file_action_modify(path, event, &block)
|
|
163
|
+
end
|
|
164
|
+
end # def file_action_create
|
|
165
|
+
|
|
166
|
+
def file_action_delete(path, event, &block)
|
|
167
|
+
close(path)
|
|
168
|
+
# ignore
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def file_action_delete_self(path, event, &block)
|
|
172
|
+
close(path)
|
|
173
|
+
# ignore
|
|
174
|
+
#p :delete_self => path
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Returns true if we are currently following the file at the given path.
|
|
178
|
+
public
|
|
179
|
+
def following?(path)
|
|
180
|
+
return @files.include?(path)
|
|
181
|
+
end # def following
|
|
182
|
+
end # class FileWatch::Tail
|
data/lib/filewatch/watch.rb
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require "filewatch/namespace"
|
|
2
|
+
require "filewatch/exception"
|
|
3
|
+
require "filewatch/watch"
|
|
4
|
+
|
|
5
|
+
class FileWatch::WatchGlob
|
|
6
|
+
# This class exists to wrap inotify, kqueue, periodic polling, etc,
|
|
7
|
+
# to provide you with a way to watch files and directories.
|
|
8
|
+
#
|
|
9
|
+
# For now, it only supports inotify.
|
|
10
|
+
def initialize
|
|
11
|
+
@watch = FileWatch::Watch.new
|
|
12
|
+
@globdirs = []
|
|
13
|
+
@globs = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
public
|
|
17
|
+
def watch(glob, *what_to_watch, &block)
|
|
18
|
+
@globs << [glob, what_to_watch]
|
|
19
|
+
|
|
20
|
+
watching = []
|
|
21
|
+
errors = []
|
|
22
|
+
paths = Dir.glob(glob)
|
|
23
|
+
paths.each do |path|
|
|
24
|
+
begin
|
|
25
|
+
next if watching.include?(path)
|
|
26
|
+
puts "Watching #{path}"
|
|
27
|
+
@watch.watch(path, :create, :delete, :modify)
|
|
28
|
+
watching << path
|
|
29
|
+
|
|
30
|
+
# Yield each file found by glob to the block.
|
|
31
|
+
# This allows initialization on new files found at start.
|
|
32
|
+
yield path if block_given?
|
|
33
|
+
rescue FileWatch::Exception => e
|
|
34
|
+
$stderr.puts "Failed starting watch on #{path} - #{e}"
|
|
35
|
+
errors << e
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Go through the glob and look for paths leading into a '*'
|
|
40
|
+
splitpath = glob.split(File::SEPARATOR)
|
|
41
|
+
splitpath.each_with_index do |part, i|
|
|
42
|
+
current = File.join(splitpath[0 .. i])
|
|
43
|
+
current = "/" if current.empty?
|
|
44
|
+
next if watching.include?(current)
|
|
45
|
+
# TODO(sissel): Do better glob detection
|
|
46
|
+
if part.include?("*")
|
|
47
|
+
globprefix = File.join(splitpath[0 ... i])
|
|
48
|
+
Dir.glob(globprefix).each do |path|
|
|
49
|
+
next if watching.include?(path)
|
|
50
|
+
p "Watching dir #{path}"
|
|
51
|
+
@watch.watch(path, :create)
|
|
52
|
+
@globdirs << path
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end # def watch
|
|
57
|
+
|
|
58
|
+
# TODO(sissel): implement 'unwatch' or cancel?
|
|
59
|
+
|
|
60
|
+
def subscribe(handler=nil, &block)
|
|
61
|
+
@watch.subscribe do |event|
|
|
62
|
+
# If this event is a directory event and the file matches a watched glob,
|
|
63
|
+
# then it should be a new-file creation event. Watch the file.
|
|
64
|
+
if event.type == :directory and @globdirs.include?(File.dirname(event.name))
|
|
65
|
+
glob, what = @globs.find { |glob, what| File.fnmatch?(glob, event.name) }
|
|
66
|
+
if glob
|
|
67
|
+
@watch.watch(event.name, *what)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Push the event to our callback.
|
|
72
|
+
block.call event
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def each(&block)
|
|
77
|
+
@inotify.each(&block)
|
|
78
|
+
end # def each
|
|
79
|
+
end # class FileWatch::Watch
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import org.apache.log4j.Logger;
|
|
2
|
+
import org.apache.log4j.PropertyConfigurator;
|
|
3
|
+
|
|
4
|
+
public class LogTest {
|
|
5
|
+
private static Logger logger = Logger.getLogger(LogTest.class);
|
|
6
|
+
|
|
7
|
+
public static void main(String[] args) {
|
|
8
|
+
//BasicConfigurator.configure();
|
|
9
|
+
PropertyConfigurator.configure("log4j.properties");
|
|
10
|
+
|
|
11
|
+
int i = 0;
|
|
12
|
+
while (true) {
|
|
13
|
+
logger.info("Testing: " + i);
|
|
14
|
+
i++;
|
|
15
|
+
try {
|
|
16
|
+
Thread.sleep(100);
|
|
17
|
+
} catch (Exception e) {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
} /* public static void main(String[]) */
|
|
21
|
+
} /* public class LogTest */
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
log4j.rootLogger=INFO,stdout,rolling
|
|
2
|
+
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
|
|
3
|
+
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
|
|
4
|
+
log4j.appender.stdout.layout.ConversionPattern=%5p %d{HH:mm:ss,SSS} %m%n
|
|
5
|
+
|
|
6
|
+
log4j.appender.rolling=org.apache.log4j.RollingFileAppender
|
|
7
|
+
log4j.appender.rolling.maxFileSize=1000
|
|
8
|
+
log4j.appender.rolling.maxBackupIndex=10
|
|
9
|
+
log4j.appender.rolling.layout=org.apache.log4j.PatternLayout
|
|
10
|
+
log4j.appender.rolling.layout.ConversionPattern=%5p [%t] %d{ISO8601} %F (line %L) %m%n
|
|
11
|
+
log4j.appender.rolling.File=logs/logtest.log
|
|
12
|
+
log4j.appender.rolling.DatePattern=.yyyy-MM-dd.HH-mm-ss
|
|
13
|
+
|
|
14
|
+
#log4j.appender.D=org.apache.log4j.DailyRollingFileAppender
|
|
15
|
+
#log4j.appender.D.layout=org.apache.log4j.PatternLayout
|
|
16
|
+
#log4j.appender.D.layout=org.apache.log4j.JSONLayout
|
|
17
|
+
#log4j.appender.D.layout.ConversionPattern=%5p [%t] %d{ISO8601} %F (line %L) %m%n
|
|
18
|
+
#log4j.appender.D.File=logs/logtest.log
|
|
19
|
+
#log4j.appender.D.DatePattern=.yyyy-MM-dd.HH-mm-ss
|
|
20
|
+
|
metadata
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: filewatch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
+
hash: 23
|
|
4
5
|
prerelease:
|
|
5
|
-
|
|
6
|
+
segments:
|
|
7
|
+
- 0
|
|
8
|
+
- 2
|
|
9
|
+
- 0
|
|
10
|
+
version: 0.2.0
|
|
6
11
|
platform: ruby
|
|
7
12
|
authors:
|
|
8
|
-
|
|
13
|
+
- Jordan Sissel
|
|
9
14
|
autorequire:
|
|
10
15
|
bindir: bin
|
|
11
16
|
cert_chain: []
|
|
12
17
|
|
|
13
|
-
date: 2011-
|
|
18
|
+
date: 2011-04-12 00:00:00 -07:00
|
|
14
19
|
default_executable:
|
|
15
20
|
dependencies: []
|
|
16
21
|
|
|
@@ -23,13 +28,20 @@ extensions: []
|
|
|
23
28
|
extra_rdoc_files: []
|
|
24
29
|
|
|
25
30
|
files:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
- lib/filewatch/buftok.rb
|
|
32
|
+
- lib/filewatch/exception.rb
|
|
33
|
+
- lib/filewatch/tail.rb
|
|
34
|
+
- lib/filewatch/stringpipeio.rb
|
|
35
|
+
- lib/filewatch/inotify/emhandler.rb
|
|
36
|
+
- lib/filewatch/inotify/fd.rb
|
|
37
|
+
- lib/filewatch/inotify/event.rb
|
|
38
|
+
- lib/filewatch/watchglob.rb
|
|
39
|
+
- lib/filewatch/watch.rb
|
|
40
|
+
- lib/filewatch/namespace.rb
|
|
41
|
+
- lib/filewatch/tailglob.rb
|
|
42
|
+
- test/log4j/log4j.properties
|
|
43
|
+
- test/log4j/LogTest.java
|
|
44
|
+
- bin/gtail
|
|
33
45
|
has_rdoc: true
|
|
34
46
|
homepage: https://github.com/jordansissel/ruby-filewatch
|
|
35
47
|
licenses: []
|
|
@@ -38,24 +50,30 @@ post_install_message:
|
|
|
38
50
|
rdoc_options: []
|
|
39
51
|
|
|
40
52
|
require_paths:
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
- lib
|
|
54
|
+
- lib
|
|
43
55
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
56
|
none: false
|
|
45
57
|
requirements:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
hash: 3
|
|
61
|
+
segments:
|
|
62
|
+
- 0
|
|
63
|
+
version: "0"
|
|
49
64
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
65
|
none: false
|
|
51
66
|
requirements:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
hash: 3
|
|
70
|
+
segments:
|
|
71
|
+
- 0
|
|
72
|
+
version: "0"
|
|
55
73
|
requirements: []
|
|
56
74
|
|
|
57
75
|
rubyforge_project:
|
|
58
|
-
rubygems_version: 1.
|
|
76
|
+
rubygems_version: 1.6.2
|
|
59
77
|
signing_key:
|
|
60
78
|
specification_version: 3
|
|
61
79
|
summary: filewatch - file watching for ruby
|