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 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