eventmachine-tail 0.3.20100829013522 → 0.4.20100903005209
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/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
|