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 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(STDOUT)
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
- @fstat = File.stat(@path)
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 modified or
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
- # TODO(sissel): read to EOF, then reopen.
117
- # If the file was moved, treat it like EOF.
118
- eof
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
- # no file found
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
- @pos = 0
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(&block)
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
- @watch = EventMachine::watch_file(@path, EventMachine::FileTail::FileWatcher, block)
144
- end
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
- EventMachine::add_timer(@naptime) do
150
- read
151
- end
152
- end
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
- @pos += data.length
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
- eof
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
- # TODO(sissel): This will be necessary if we can't use inotify or kqueue to
173
- # get notified of file changes
174
- #if @need_scheduling
175
- #@naptime = 0.100 if @naptime == 0
176
- #@naptime *= 2
177
- #@naptime = MAXSLEEP if @naptime > MAXSLEEP
178
- #@logger.info("EOF. Naptime: #{@naptime}")
179
- #end
180
-
181
- # TODO(sissel): should we schedule an fstat instead of doing it now?
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
- fstat = File.stat(@path)
184
- handle_fstat(fstat)
268
+ read_file_metadata do |filestat, linkstat, linktarget|
269
+ handle_fstat(filestat, linkstat, linktarget)
270
+ end
185
271
  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
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
- fstat = File.stat(@path)
192
- handle_fstat(fstat)
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
- # ignore
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(fstat)
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
211
- elsif (fstat.size < @fstat.size)
212
- # Schedule a read if the file size has changed
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
- @fstat = fstat
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(STDOUT)
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
@@ -0,0 +1,4 @@
1
+ /tmp/foo/testfile {
2
+ rotate 5
3
+ size 50
4
+ }
@@ -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
- - 3
8
- - 20100829013522
9
- version: 0.3.20100829013522
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-08-29 00:00:00 -07:00
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