filewatch 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|