eventmachine-tail 0.3.20100829013522 → 0.4.20100903005209
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/em/filetail.rb +182 -65
- data/test/logrotate.conf +4 -0
- data/test/test_filetail.rb +60 -0
- metadata +5 -4
data/lib/em/filetail.rb
CHANGED
@@ -30,11 +30,21 @@ class EventMachine::FileTail
|
|
30
30
|
# Maximum size to read at a time from a single file.
|
31
31
|
CHUNKSIZE = 65536
|
32
32
|
|
33
|
-
#
|
34
33
|
#MAXSLEEP = 2
|
35
34
|
|
36
35
|
# The path of the file being tailed
|
37
36
|
attr_reader :path
|
37
|
+
|
38
|
+
# The current file read position
|
39
|
+
attr_reader :position
|
40
|
+
|
41
|
+
# Check interval when checking symlinks for changes. This is only useful
|
42
|
+
# when you are actually tailing symlinks.
|
43
|
+
attr_accessor :symlink_check_interval
|
44
|
+
|
45
|
+
# Check interval for looking for a file if we are tailing it and it has
|
46
|
+
# gone missing.
|
47
|
+
attr_accessor :missing_file_check_interval
|
38
48
|
|
39
49
|
# Tail a file
|
40
50
|
#
|
@@ -42,36 +52,53 @@ class EventMachine::FileTail
|
|
42
52
|
# * startpos is an offset to start tailing the file at. If -1, start at end of
|
43
53
|
# file.
|
44
54
|
#
|
55
|
+
# If you want debug messages, run ruby with '-d' or set $DEBUG
|
56
|
+
#
|
45
57
|
# See also: EventMachine::file_tail
|
46
58
|
#
|
47
59
|
public
|
48
60
|
def initialize(path, startpos=-1, &block)
|
49
61
|
@path = path
|
50
|
-
@logger = Logger.new(
|
62
|
+
@logger = Logger.new(STDERR)
|
51
63
|
@logger.level = ($DEBUG and Logger::DEBUG or Logger::WARN)
|
52
64
|
@logger.debug("Tailing #{path} starting at position #{startpos}")
|
53
65
|
|
54
66
|
@file = nil
|
55
|
-
@
|
67
|
+
@want_eof = false
|
68
|
+
@want_read = false
|
69
|
+
@want_reopen = false
|
70
|
+
@reopen_on_eof = false
|
71
|
+
@symlink_timer = nil
|
72
|
+
@symlink_target = nil
|
73
|
+
@symlink_stat = nil
|
74
|
+
|
75
|
+
@symlink_check_interval = 1
|
76
|
+
@missing_file_check_interval = 1
|
77
|
+
|
78
|
+
read_file_metadata
|
79
|
+
|
80
|
+
if @filestat.directory?
|
81
|
+
raise Errno::EISDIR.new(@path)
|
82
|
+
end
|
56
83
|
|
57
84
|
if block_given?
|
58
85
|
@handler = block
|
59
86
|
@buffer = BufferedTokenizer.new
|
60
87
|
end
|
61
88
|
|
62
|
-
if @fstat.directory?
|
63
|
-
raise Errno::EISDIR.new(@path)
|
64
|
-
end
|
65
|
-
|
66
89
|
EventMachine::next_tick do
|
67
90
|
open
|
68
|
-
watch { |what| notify(what) }
|
69
91
|
if (startpos == -1)
|
70
92
|
@file.sysseek(0, IO::SEEK_END)
|
93
|
+
# TODO(sissel): if we don't have inotify or kqueue, should we
|
94
|
+
# schedule a next read, here?
|
95
|
+
# Is there a race condition between setting the file position and
|
96
|
+
# watching given the two together are not atomic?
|
71
97
|
else
|
72
98
|
@file.sysseek(startpos, IO::SEEK_SET)
|
73
99
|
schedule_next_read
|
74
100
|
end
|
101
|
+
watch
|
75
102
|
end # EventMachine::next_tick
|
76
103
|
end # def initialize
|
77
104
|
|
@@ -105,19 +132,21 @@ class EventMachine::FileTail
|
|
105
132
|
end
|
106
133
|
end # def receive_data
|
107
134
|
|
108
|
-
# notify is invoked when the file you are tailing has been
|
109
|
-
# otherwise needs to be acted on.
|
135
|
+
# notify is invoked by EM::watch_file when the file you are tailing has been
|
136
|
+
# modified or otherwise needs to be acted on.
|
110
137
|
private
|
111
138
|
def notify(status)
|
112
|
-
@logger.debug("#{status} on #{path}")
|
139
|
+
@logger.debug("notify: #{status} on #{path}")
|
113
140
|
if status == :modified
|
114
141
|
schedule_next_read
|
115
142
|
elsif status == :moved
|
116
|
-
#
|
117
|
-
|
118
|
-
|
143
|
+
# read to EOF, then reopen.
|
144
|
+
@reopen_on_eof = true
|
145
|
+
schedule_next_read
|
146
|
+
elsif status == :unbind
|
147
|
+
# Do what?
|
119
148
|
end
|
120
|
-
end
|
149
|
+
end # def notify
|
121
150
|
|
122
151
|
# Open (or reopen, if necessary) our file and schedule a read.
|
123
152
|
private
|
@@ -126,30 +155,60 @@ class EventMachine::FileTail
|
|
126
155
|
begin
|
127
156
|
@logger.debug "Opening file #{@path}"
|
128
157
|
@file = File.open(@path, "r")
|
129
|
-
rescue Errno::ENOENT
|
130
|
-
|
131
|
-
raise
|
158
|
+
rescue Errno::ENOENT => e
|
159
|
+
@logger.debug("File not found: '#{@path}' (#{e})")
|
160
|
+
raise e
|
132
161
|
end
|
133
162
|
|
134
|
-
@naptime = 0
|
135
|
-
@
|
163
|
+
@naptime = 0
|
164
|
+
@position = 0
|
136
165
|
schedule_next_read
|
137
|
-
end
|
166
|
+
end # def open
|
138
167
|
|
139
168
|
# Watch our file.
|
140
169
|
private
|
141
|
-
def watch
|
170
|
+
def watch
|
171
|
+
@watch.stop_watching if @watch
|
172
|
+
@symlink_timer.cancel if @symlink_timer
|
173
|
+
|
142
174
|
@logger.debug "Starting watch on #{@path}"
|
143
|
-
|
144
|
-
|
175
|
+
callback = proc { |what| notify(what) }
|
176
|
+
@watch = EventMachine::watch_file(@path, EventMachine::FileTail::FileWatcher, callback)
|
177
|
+
watch_symlink if @symlink_target
|
178
|
+
end # def watch
|
179
|
+
|
180
|
+
# Watch a symlink
|
181
|
+
# EM doesn't currently support watching symlinks alone (inotify follows
|
182
|
+
# symlinks by default), so let's periodically stat the symlink.
|
183
|
+
private
|
184
|
+
def watch_symlink(&block)
|
185
|
+
@symlink_timer.cancel if @symlink_timer
|
186
|
+
|
187
|
+
@logger.debug "Launching timer to check for symlink changes since EM can't right now: #{@path}"
|
188
|
+
@symlink_timer = EM::PeriodicTimer.new(@symlink_check_interval) do
|
189
|
+
begin
|
190
|
+
@logger.debug("Checking #{@path}")
|
191
|
+
read_file_metadata do |filestat, linkstat, linktarget|
|
192
|
+
handle_fstat(filestat, linkstat, linktarget)
|
193
|
+
end
|
194
|
+
rescue Errno::ENOENT
|
195
|
+
# The file disappeared. Wait for it to reappear.
|
196
|
+
# This can happen if it was deleted or moved during log rotation.
|
197
|
+
@logger.debug "File not found, waiting for it to reappear. (#{@path})"
|
198
|
+
end # begin/rescue ENOENT
|
199
|
+
end # EM::PeriodicTimer
|
200
|
+
end # def watch_symlink
|
145
201
|
|
146
|
-
# Schedule a read.
|
147
202
|
private
|
148
203
|
def schedule_next_read
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
204
|
+
if !@want_read
|
205
|
+
@want_read = true
|
206
|
+
EventMachine::add_timer(@naptime) do
|
207
|
+
@want_read = false
|
208
|
+
read
|
209
|
+
end
|
210
|
+
end # if !@want_read
|
211
|
+
end # def schedule_next_read
|
153
212
|
|
154
213
|
# Read CHUNKSIZE from our file and pass it to .receive_data()
|
155
214
|
private
|
@@ -157,67 +216,125 @@ class EventMachine::FileTail
|
|
157
216
|
@logger.debug "#{self}: Reading..."
|
158
217
|
begin
|
159
218
|
data = @file.sysread(CHUNKSIZE)
|
219
|
+
|
160
220
|
# Won't get here if sysread throws EOF
|
161
|
-
@
|
221
|
+
@position += data.length
|
162
222
|
@naptime = 0
|
223
|
+
|
224
|
+
# Subclasses should implement receive_data
|
163
225
|
receive_data(data)
|
164
226
|
schedule_next_read
|
165
227
|
rescue EOFError
|
166
|
-
|
228
|
+
schedule_eof
|
167
229
|
end
|
168
230
|
end
|
169
231
|
|
232
|
+
# Do EOF handling on next EM iteration
|
233
|
+
def schedule_eof
|
234
|
+
if !@want_eof
|
235
|
+
@want_eof = true
|
236
|
+
EventMachine::next_tick do
|
237
|
+
@want_eof = false
|
238
|
+
eof
|
239
|
+
end # EventMachine::next_tick
|
240
|
+
end # if !@want_eof
|
241
|
+
end # def schedule_eof
|
242
|
+
|
243
|
+
private
|
244
|
+
def schedule_reopen
|
245
|
+
if !@want_reopen
|
246
|
+
EventMachine::next_tick do
|
247
|
+
@want_reopen = false
|
248
|
+
open
|
249
|
+
watch
|
250
|
+
end
|
251
|
+
end # if !@want_reopen
|
252
|
+
end # def schedule_reopen
|
253
|
+
|
170
254
|
private
|
171
255
|
def eof
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
#
|
180
|
-
|
181
|
-
#
|
256
|
+
@want_eof = false
|
257
|
+
|
258
|
+
if @reopen_on_eof
|
259
|
+
@reopen_on_eof = false
|
260
|
+
schedule_reopen
|
261
|
+
end
|
262
|
+
|
263
|
+
# EOF actions:
|
264
|
+
# - Check if the file inode/device is changed
|
265
|
+
# - If symlink, check if the symlink has changed
|
266
|
+
# - Otherwise, do nothing
|
182
267
|
begin
|
183
|
-
|
184
|
-
|
268
|
+
read_file_metadata do |filestat, linkstat, linktarget|
|
269
|
+
handle_fstat(filestat, linkstat, linktarget)
|
270
|
+
end
|
185
271
|
rescue Errno::ENOENT
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
timer = EM::PeriodicTimer.new(0.250) do
|
272
|
+
# The file disappeared. Wait for it to reappear.
|
273
|
+
# This can happen if it was deleted or moved during log rotation.
|
274
|
+
timer = EM::PeriodicTimer.new(@missing_file_check_interval) do
|
190
275
|
begin
|
191
|
-
|
192
|
-
|
276
|
+
read_file_metadata do |filestat, linkstat, linktarget|
|
277
|
+
handle_fstat(filestat, linkstat, linktarget)
|
278
|
+
end
|
193
279
|
timer.cancel
|
194
280
|
rescue Errno::ENOENT
|
195
|
-
#
|
281
|
+
# The file disappeared. Wait for it to reappear.
|
282
|
+
# This can happen if it was deleted or moved during log rotation.
|
283
|
+
@logger.debug "File not found, waiting for it to reappear. (#{@path})"
|
196
284
|
end # begin/rescue ENOENT
|
197
285
|
end # EM::PeriodicTimer
|
198
286
|
end # begin/rescue ENOENT
|
199
287
|
end # def eof
|
200
288
|
|
289
|
+
private
|
290
|
+
def read_file_metadata(&block)
|
291
|
+
filestat = File.stat(@path)
|
292
|
+
symlink_stat = nil
|
293
|
+
symlink_target = nil
|
294
|
+
|
295
|
+
if File.symlink?(@path)
|
296
|
+
symlink_stat = File.lstat(@path) rescue nil
|
297
|
+
symlink_target = File.readlink(@path) rescue nil
|
298
|
+
end
|
299
|
+
|
300
|
+
if block_given?
|
301
|
+
yield filestat, symlink_stat, symlink_target
|
302
|
+
end
|
303
|
+
|
304
|
+
@filestat = filestat
|
305
|
+
@symlink_stat = symlink_stat
|
306
|
+
@symlink_target = symlink_target
|
307
|
+
end # def read_file_metadata
|
308
|
+
|
201
309
|
# Handle fstat changes appropriately.
|
202
310
|
private
|
203
|
-
def handle_fstat(
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
311
|
+
def handle_fstat(filestat, symlinkstat, symlinktarget)
|
312
|
+
# If the symlink target changes, the filestat.ino is very likely to have
|
313
|
+
# changed since that is the stat on the resolved file (that the link points
|
314
|
+
# to). However, we'll check explicitly for the symlink target changing
|
315
|
+
# for better debuggability.
|
316
|
+
if symlinktarget
|
317
|
+
if symlinkstat.ino != @symlink_stat.ino
|
318
|
+
@logger.debug "Inode or device changed on symlink. Reopening..."
|
319
|
+
@reopen_on_eof = true
|
320
|
+
schedule_next_read
|
321
|
+
elsif symlinktarget != @symlink_target
|
322
|
+
@logger.debug "Symlink target changed. Reopening..."
|
323
|
+
@reopen_on_eof = true
|
324
|
+
schedule_next_read
|
325
|
+
end
|
326
|
+
elsif (filestat.ino != @filestat.ino or filestat.rdev != @filestat.rdev)
|
327
|
+
@logger.debug "Inode or device changed. Reopening..."
|
328
|
+
@logger.debug filestat
|
329
|
+
@reopen_on_eof = true
|
330
|
+
schedule_next_read
|
331
|
+
elsif (filestat.size < @filestat.size)
|
332
|
+
# If the file size shrank, assume truncation and seek to the beginning.
|
213
333
|
@logger.info("File likely truncated... #{path}")
|
214
334
|
@file.sysseek(0, IO::SEEK_SET)
|
215
335
|
schedule_next_read
|
216
|
-
else
|
217
|
-
#@logger.debug "Nothing to run for fstat..."
|
218
336
|
end
|
219
|
-
|
220
|
-
end # def eof
|
337
|
+
end # def handle_fstat
|
221
338
|
|
222
339
|
def to_s
|
223
340
|
return "#{self.class.name}(#{@path}) @ pos:#{@file.sysseek(0, IO::SEEK_CUR)}"
|
@@ -230,7 +347,7 @@ end # class EventMachine::FileTail
|
|
230
347
|
# See also: EventMachine::FileTail#watch
|
231
348
|
class EventMachine::FileTail::FileWatcher < EventMachine::FileWatch
|
232
349
|
def initialize(block)
|
233
|
-
@logger = Logger.new(
|
350
|
+
@logger = Logger.new(STDERR)
|
234
351
|
@logger.level = ($DEBUG and Logger::DEBUG or Logger::WARN)
|
235
352
|
@callback = block
|
236
353
|
end # def initialize
|
data/test/logrotate.conf
ADDED
data/test/test_filetail.rb
CHANGED
@@ -140,5 +140,65 @@ class TestFileTail < Test::Unit::TestCase
|
|
140
140
|
|
141
141
|
File.delete(filename)
|
142
142
|
end # def test_filetail_tracks_renames
|
143
|
+
|
144
|
+
def test_filetail_tracks_symlink_changes
|
145
|
+
to_delete = []
|
146
|
+
link = Tempfile.new("testlink")
|
147
|
+
File.delete(link.path)
|
148
|
+
to_delete << link
|
149
|
+
tmp = Tempfile.new("testfiletail")
|
150
|
+
to_delete << tmp
|
151
|
+
data = DATA.clone
|
152
|
+
File.symlink(tmp.path, link.path)
|
153
|
+
|
154
|
+
data_copy = data.clone
|
155
|
+
|
156
|
+
# Write first so the first read happens immediately
|
157
|
+
tmp.puts data_copy.shift
|
158
|
+
tmp.flush
|
159
|
+
EM.run do
|
160
|
+
abort_after_timeout(DATA.length * SLEEPMAX + 10)
|
161
|
+
|
162
|
+
lineno = 0
|
163
|
+
# Start at file position 0.
|
164
|
+
EM::file_tail(link.path, nil, 0) do |filetail, line|
|
165
|
+
# This needs to be less than the interval at which we are changing symlinks.
|
166
|
+
filetail.symlink_check_interval = 0.1
|
167
|
+
|
168
|
+
lineno += 1
|
169
|
+
expected = data.shift
|
170
|
+
puts "Got #{lineno}: #{line}" if $debug
|
171
|
+
assert_equal(expected, line,
|
172
|
+
"Expected '#{expected}' on line #{lineno}, but got '#{line}'")
|
173
|
+
finish if data.length == 0
|
174
|
+
|
175
|
+
# Start a timer on the first read.
|
176
|
+
# This is to ensure we have the file tailing before
|
177
|
+
# we try to rename.
|
178
|
+
if lineno == 1
|
179
|
+
timer = EM::PeriodicTimer.new(0.2) do
|
180
|
+
value = data_copy.shift
|
181
|
+
tmp.puts value
|
182
|
+
tmp.flush
|
183
|
+
sleep(rand * SLEEPMAX)
|
184
|
+
|
185
|
+
# Make a new file and update the symlink to point to it.
|
186
|
+
# This is to simulate log rotation, etc.
|
187
|
+
tmp.close
|
188
|
+
tmp = Tempfile.new("testfiletail")
|
189
|
+
to_delete << tmp
|
190
|
+
File.delete(link.path)
|
191
|
+
File.symlink(tmp.path, link.path)
|
192
|
+
puts "#{tmp.path} => #{link.path}" if $debug
|
193
|
+
timer.cancel if data_copy.length == 0
|
194
|
+
end # timer
|
195
|
+
end # if lineno == 1
|
196
|
+
end # EM::filetail(...)
|
197
|
+
end # EM.run
|
198
|
+
|
199
|
+
to_delete.each do |f|
|
200
|
+
File.delete(f.path)
|
201
|
+
end
|
202
|
+
end # def test_filetail_tracks_renames
|
143
203
|
end # class TestFileTail
|
144
204
|
|
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
|
+
- 4
|
8
|
+
- 20100903005209
|
9
|
+
version: 0.4.20100903005209
|
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-09-03 00:00:00 -07:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -46,6 +46,7 @@ files:
|
|
46
46
|
- samples/globwatch.rb
|
47
47
|
- samples/tail-with-block.rb
|
48
48
|
- test/test_filetail.rb
|
49
|
+
- test/logrotate.conf
|
49
50
|
- test/test_glob.rb
|
50
51
|
- test/alltests.rb
|
51
52
|
- test/testcase_helpers.rb
|