eventmachine-tail 0.2.20100525165012 → 0.3.20100829013522
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/em/filetail.rb +46 -16
- data/lib/em/globwatcher.rb +31 -13
- data/test/test_filetail.rb +48 -0
- data/test/test_glob.rb +51 -1
- metadata +4 -4
data/lib/em/filetail.rb
CHANGED
@@ -63,14 +63,16 @@ class EventMachine::FileTail
|
|
63
63
|
raise Errno::EISDIR.new(@path)
|
64
64
|
end
|
65
65
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
66
|
+
EventMachine::next_tick do
|
67
|
+
open
|
68
|
+
watch { |what| notify(what) }
|
69
|
+
if (startpos == -1)
|
70
|
+
@file.sysseek(0, IO::SEEK_END)
|
71
|
+
else
|
72
|
+
@file.sysseek(startpos, IO::SEEK_SET)
|
73
|
+
schedule_next_read
|
74
|
+
end
|
75
|
+
end # EventMachine::next_tick
|
74
76
|
end # def initialize
|
75
77
|
|
76
78
|
# This method is called when a tailed file has data read.
|
@@ -112,7 +114,8 @@ class EventMachine::FileTail
|
|
112
114
|
schedule_next_read
|
113
115
|
elsif status == :moved
|
114
116
|
# TODO(sissel): read to EOF, then reopen.
|
115
|
-
|
117
|
+
# If the file was moved, treat it like EOF.
|
118
|
+
eof
|
116
119
|
end
|
117
120
|
end
|
118
121
|
|
@@ -121,6 +124,7 @@ class EventMachine::FileTail
|
|
121
124
|
def open
|
122
125
|
@file.close if @file
|
123
126
|
begin
|
127
|
+
@logger.debug "Opening file #{@path}"
|
124
128
|
@file = File.open(@path, "r")
|
125
129
|
rescue Errno::ENOENT
|
126
130
|
# no file found
|
@@ -135,7 +139,8 @@ class EventMachine::FileTail
|
|
135
139
|
# Watch our file.
|
136
140
|
private
|
137
141
|
def watch(&block)
|
138
|
-
|
142
|
+
@logger.debug "Starting watch on #{@path}"
|
143
|
+
@watch = EventMachine::watch_file(@path, EventMachine::FileTail::FileWatcher, block)
|
139
144
|
end
|
140
145
|
|
141
146
|
# Schedule a read.
|
@@ -149,6 +154,7 @@ class EventMachine::FileTail
|
|
149
154
|
# Read CHUNKSIZE from our file and pass it to .receive_data()
|
150
155
|
private
|
151
156
|
def read
|
157
|
+
@logger.debug "#{self}: Reading..."
|
152
158
|
begin
|
153
159
|
data = @file.sysread(CHUNKSIZE)
|
154
160
|
# Won't get here if sysread throws EOF
|
@@ -173,25 +179,49 @@ class EventMachine::FileTail
|
|
173
179
|
#end
|
174
180
|
|
175
181
|
# TODO(sissel): should we schedule an fstat instead of doing it now?
|
176
|
-
|
177
|
-
|
182
|
+
begin
|
183
|
+
fstat = File.stat(@path)
|
184
|
+
handle_fstat(fstat)
|
185
|
+
rescue Errno::ENOENT
|
186
|
+
# The file disappeared. Wait for it to reappear.
|
187
|
+
# This can happen if it was deleted or moved during log rotation.
|
188
|
+
@logger.debug "File not found, waiting for it to reappear. (#{@path})"
|
189
|
+
timer = EM::PeriodicTimer.new(0.250) do
|
190
|
+
begin
|
191
|
+
fstat = File.stat(@path)
|
192
|
+
handle_fstat(fstat)
|
193
|
+
timer.cancel
|
194
|
+
rescue Errno::ENOENT
|
195
|
+
# ignore
|
196
|
+
end # begin/rescue ENOENT
|
197
|
+
end # EM::PeriodicTimer
|
198
|
+
end # begin/rescue ENOENT
|
178
199
|
end # def eof
|
179
200
|
|
180
201
|
# Handle fstat changes appropriately.
|
181
202
|
private
|
182
203
|
def handle_fstat(fstat)
|
183
|
-
if (fstat.ino != @fstat.ino)
|
184
|
-
|
185
|
-
|
186
|
-
|
204
|
+
if (fstat.ino != @fstat.ino or fstat.rdev != @fstat.rdev)
|
205
|
+
EventMachine::next_tick do
|
206
|
+
@logger.debug "Inode or device changed. Reopening..."
|
207
|
+
@watch.stop_watching
|
208
|
+
open # Reopen if the inode has changed
|
209
|
+
watch { |what| notify(what) }
|
210
|
+
end
|
187
211
|
elsif (fstat.size < @fstat.size)
|
188
212
|
# Schedule a read if the file size has changed
|
189
213
|
@logger.info("File likely truncated... #{path}")
|
190
214
|
@file.sysseek(0, IO::SEEK_SET)
|
191
215
|
schedule_next_read
|
216
|
+
else
|
217
|
+
#@logger.debug "Nothing to run for fstat..."
|
192
218
|
end
|
193
219
|
@fstat = fstat
|
194
220
|
end # def eof
|
221
|
+
|
222
|
+
def to_s
|
223
|
+
return "#{self.class.name}(#{@path}) @ pos:#{@file.sysseek(0, IO::SEEK_CUR)}"
|
224
|
+
end # def to_s
|
195
225
|
end # class EventMachine::FileTail
|
196
226
|
|
197
227
|
# Internal usage only. This class is used by EventMachine::FileTail
|
data/lib/em/globwatcher.rb
CHANGED
@@ -35,7 +35,7 @@ class EventMachine::FileGlobWatch
|
|
35
35
|
# * interval - number of seconds between scanning the glob for changes
|
36
36
|
def initialize(glob, interval=60)
|
37
37
|
@glob = glob
|
38
|
-
@files =
|
38
|
+
@files = Hash.new
|
39
39
|
@watches = Hash.new
|
40
40
|
@logger = Logger.new(STDOUT)
|
41
41
|
@logger.level = ($DEBUG and Logger::DEBUG or Logger::WARN)
|
@@ -83,29 +83,37 @@ class EventMachine::FileGlobWatch
|
|
83
83
|
private
|
84
84
|
def find_files
|
85
85
|
@logger.info("Searching for files in #{@glob}")
|
86
|
-
list =
|
86
|
+
list = Dir.glob(@glob)
|
87
|
+
|
88
|
+
known_files = @files.clone
|
87
89
|
list.each do |path|
|
88
|
-
|
89
|
-
|
90
|
+
fileinfo = FileInfo.new(path) rescue next
|
91
|
+
# Skip files that have the same inode (renamed or hardlinked)
|
92
|
+
known_files.delete(fileinfo.stat.ino)
|
93
|
+
next if @files.include?(fileinfo.stat.ino)
|
94
|
+
|
95
|
+
track(fileinfo)
|
96
|
+
file_found(path)
|
90
97
|
end
|
91
98
|
|
92
|
-
|
93
|
-
|
99
|
+
# Report missing files.
|
100
|
+
known_files.each do |inode, fileinfo|
|
101
|
+
remove(fileinfo)
|
94
102
|
end
|
95
103
|
end # def find_files
|
96
104
|
|
97
105
|
# Remove a file from being watched and notify file_deleted()
|
98
106
|
private
|
99
|
-
def remove(
|
100
|
-
@files.delete(
|
101
|
-
@watches.delete(path)
|
102
|
-
file_deleted(path)
|
107
|
+
def remove(fileinfo)
|
108
|
+
@files.delete(fileinfo.stat.ino)
|
109
|
+
@watches.delete(fileinfo.path)
|
110
|
+
file_deleted(fileinfo.path)
|
103
111
|
end # def remove
|
104
112
|
|
105
113
|
# Add a file to watch and notify file_found()
|
106
114
|
private
|
107
|
-
def
|
108
|
-
@files.
|
115
|
+
def track(fileinfo)
|
116
|
+
@files[fileinfo.stat.ino] = fileinfo
|
109
117
|
|
110
118
|
# If EventMachine::watch_file fails, that's ok, I guess.
|
111
119
|
# We'll still find the file 'missing' from the next glob attempt.
|
@@ -120,7 +128,6 @@ class EventMachine::FileGlobWatch
|
|
120
128
|
#rescue Errno::EACCES => e
|
121
129
|
#@logger.warn(e)
|
122
130
|
#end
|
123
|
-
file_found(path)
|
124
131
|
end # def watch
|
125
132
|
|
126
133
|
private
|
@@ -139,6 +146,17 @@ class EventMachine::FileGlobWatch
|
|
139
146
|
block.call path
|
140
147
|
end
|
141
148
|
end # class EventMachine::FileGlobWatch::FileWatcher < EventMachine::FileWatch
|
149
|
+
|
150
|
+
private
|
151
|
+
class FileInfo
|
152
|
+
attr_reader :path
|
153
|
+
attr_reader :stat
|
154
|
+
|
155
|
+
def initialize(path)
|
156
|
+
@path = path
|
157
|
+
@stat = File.stat(path)
|
158
|
+
end
|
159
|
+
end # class FileInfo
|
142
160
|
end # class EventMachine::FileGlobWatch
|
143
161
|
|
144
162
|
# A glob tailer for EventMachine
|
data/test/test_filetail.rb
CHANGED
@@ -92,5 +92,53 @@ class TestFileTail < Test::Unit::TestCase
|
|
92
92
|
end
|
93
93
|
end # EM.run
|
94
94
|
end # def test_filetail_with_block
|
95
|
+
|
96
|
+
def test_filetail_tracks_renames
|
97
|
+
tmp = Tempfile.new("testfiletail")
|
98
|
+
data = DATA.clone
|
99
|
+
filename = tmp.path
|
100
|
+
|
101
|
+
data_copy = data.clone
|
102
|
+
|
103
|
+
# Write first so the first read happens immediately
|
104
|
+
tmp.puts data_copy.shift
|
105
|
+
tmp.flush
|
106
|
+
EM.run do
|
107
|
+
abort_after_timeout(DATA.length * SLEEPMAX + 10)
|
108
|
+
|
109
|
+
lineno = 0
|
110
|
+
# Start at file position 0.
|
111
|
+
EM::file_tail(tmp.path, nil, 0) do |filetail, line|
|
112
|
+
lineno += 1
|
113
|
+
expected = data.shift
|
114
|
+
#puts "Got #{lineno}: #{line}"
|
115
|
+
assert_equal(expected, line,
|
116
|
+
"Expected '#{expected}' on line #{lineno}, but got '#{line}'")
|
117
|
+
finish if data.length == 0
|
118
|
+
|
119
|
+
# Start a timer on the first read.
|
120
|
+
# This is to ensure we have the file tailing before
|
121
|
+
# we try to rename.
|
122
|
+
if lineno == 1
|
123
|
+
timer = EM::PeriodicTimer.new(0.2) do
|
124
|
+
value = data_copy.shift
|
125
|
+
tmp.puts value
|
126
|
+
tmp.flush
|
127
|
+
sleep(rand * SLEEPMAX)
|
128
|
+
|
129
|
+
# Rename the file, create a new one in it's place.
|
130
|
+
# This is to simulate log rotation, etc.
|
131
|
+
path_newname = "#{filename}_#{value}"
|
132
|
+
File.rename(filename, path_newname)
|
133
|
+
File.delete(path_newname)
|
134
|
+
tmp = File.open(filename, "w")
|
135
|
+
timer.cancel if data_copy.length == 0
|
136
|
+
end # timer
|
137
|
+
end # if lineno == 1
|
138
|
+
end # EM::filetail(...)
|
139
|
+
end # EM.run
|
140
|
+
|
141
|
+
File.delete(filename)
|
142
|
+
end # def test_filetail_tracks_renames
|
95
143
|
end # class TestFileTail
|
96
144
|
|
data/test/test_glob.rb
CHANGED
@@ -86,11 +86,61 @@ class TestGlobWatcher < Test::Unit::TestCase
|
|
86
86
|
datacopy = @data.clone
|
87
87
|
timer = EM::PeriodicTimer.new(0.2) do
|
88
88
|
#puts "Creating: #{datacopy.first}"
|
89
|
-
File.new(datacopy.shift, "w")
|
89
|
+
File.new(datacopy.shift, "w").close
|
90
90
|
sleep(rand * SLEEPMAX)
|
91
91
|
timer.cancel if datacopy.length == 0
|
92
92
|
end
|
93
93
|
end # EM.run
|
94
94
|
end # def test_glob_finds_newly_created_files_at_runtime
|
95
|
+
|
96
|
+
def test_glob_ignores_file_renames
|
97
|
+
EM.run do
|
98
|
+
abort_after_timeout(SLEEPMAX * @data.length + 10)
|
99
|
+
|
100
|
+
EM::watch_glob("#{@dir}/*", Watcher, @watchinterval, @data.clone, self)
|
101
|
+
|
102
|
+
datacopy = @data.clone
|
103
|
+
timer = EM::PeriodicTimer.new(0.2) do
|
104
|
+
filename = datacopy.shift
|
105
|
+
File.new(filename, "w").close
|
106
|
+
sleep(rand * SLEEPMAX)
|
107
|
+
|
108
|
+
# This file rename should be ignored.
|
109
|
+
EM::Timer.new(2) do
|
110
|
+
newname = "#{filename}.renamed"
|
111
|
+
File.rename(filename, newname)
|
112
|
+
|
113
|
+
# Track the new filename so teardown removes it.
|
114
|
+
@data << newname
|
115
|
+
end
|
116
|
+
timer.cancel if datacopy.length == 0
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end # def test_glob_ignores_file_renames
|
120
|
+
|
121
|
+
def test_glob_ignores_duplicate_hardlinks
|
122
|
+
EM.run do
|
123
|
+
abort_after_timeout(SLEEPMAX * @data.length + 10)
|
124
|
+
|
125
|
+
EM::watch_glob("#{@dir}/*", Watcher, @watchinterval, @data.clone, self)
|
126
|
+
|
127
|
+
datacopy = @data.clone
|
128
|
+
timer = EM::PeriodicTimer.new(0.2) do
|
129
|
+
filename = datacopy.shift
|
130
|
+
File.new(filename, "w").close
|
131
|
+
sleep(rand * SLEEPMAX)
|
132
|
+
|
133
|
+
# This file rename should be ignored.
|
134
|
+
EM::Timer.new(2) do
|
135
|
+
newname = "#{filename}.renamed"
|
136
|
+
File.link(filename, newname)
|
137
|
+
|
138
|
+
# Track the new filename so teardown removes it.
|
139
|
+
@data << newname
|
140
|
+
end
|
141
|
+
timer.cancel if datacopy.length == 0
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end # def test_glob_ignores_file_renames
|
95
145
|
end # class TestGlobWatcher
|
96
146
|
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 3
|
8
|
+
- 20100829013522
|
9
|
+
version: 0.3.20100829013522
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Jordan Sissel
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-08-29 00:00:00 -07:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|