File: tail.rb

package info (click to toggle)
ruby-file-tail 1.0.10-1
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 188 kB
  • sloc: ruby: 887; makefile: 5
file content (319 lines) | stat: -rw-r--r-- 10,042 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
class File
  # This module can be included in your own File subclasses or used to extend
  # files you want to tail.
  module Tail
    require 'file/tail/version'
    require 'file/tail/logfile'
    require 'file/tail/group'
    require 'file/tail/tailer'
    require 'file/tail/line_extension'

    # This is the base class of all exceptions that are raised
    # in File::Tail.
    class TailException < Exception; end

    # The DeletedException is raised if a file is
    # deleted while tailing it.
    class DeletedException < TailException; end

    # The ReturnException is raised and caught
    # internally to implement "tail -10" behaviour.
    class ReturnException < TailException; end

    # The BreakException is raised if the <code>break_if_eof</code>
    # attribute is set to a true value and the end of tailed file
    # is reached.
    class BreakException < TailException; end

    # The ReopenException is raised internally if File::Tail
    # gets suspicious something unusual has happend to
    # the tailed file, e. g., it was rotated away. The exception
    # is caught and an attempt to reopen it is made.
    class ReopenException < TailException
      attr_reader :mode

      # Creates an ReopenException object. The mode defaults to
      # <code>:bottom</code> which indicates that the file
      # should be tailed beginning from the end. <code>:top</code>
      # indicates, that it should be tailed from the beginning from the
      # start.
      def initialize(mode = :bottom)
        super(self.class.name)
        @mode = mode
      end
    end

    # The maximum interval File::Tail sleeps, before it tries
    # to take some action like reading the next few lines
    # or reopening the file.
    attr_accessor :max_interval

    # The start value of the sleep interval. This value
    # goes against <code>max_interval</code> if the tailed
    # file is silent for a sufficient time.
    attr_accessor :interval

    # If this attribute is set to a true value, File::Tail persists
    # on reopening a deleted file waiting <code>max_interval</code> seconds
    # between the attempts. This is useful if logfiles are
    # moved away while rotation occurs but are recreated at
    # the same place after a while. It defaults to true.
    attr_accessor :reopen_deleted

    # If this attribute is set to a true value, File::Tail
    # attempts to reopen it's tailed file after
    # <code>suspicious_interval</code> seconds of silence.
    attr_accessor :reopen_suspicious

    # The callback is called with _self_ as an argument after a reopen has
    # occured. This allows a tailing script to find out, if a logfile has been
    # rotated.
    def after_reopen(&block)
      @after_reopen = block
    end

    # This attribute is the invterval in seconds before File::Tail
    # gets suspicious that something has happend to it's tailed file
    # and an attempt to reopen it is made.
    #
    # If the attribute <code>reopen_suspicious</code> is
    # set to a non true value, suspicious_interval is
    # meaningless. It defaults to 60 seconds.
    attr_accessor :suspicious_interval

    # If this attribute is set to a true value, File::Fail's tail method
    # raises a BreakException if the end of the file is reached.
    attr_accessor :break_if_eof

    # If this attribute is set to a true value, File::Fail's tail method
    # just returns if the end of the file is reached.
    attr_accessor :return_if_eof

    # Default buffer size, that is used while going backward from a file's end.
    # This defaults to nil, which means that File::Tail attempts to derive this
    # value from the filesystem block size.
    attr_accessor :default_bufsize

    # Skip the first <code>n</code> lines of this file. The default is to don't
    # skip any lines at all and start at the beginning of this file.
    def forward(n = 0)
      rewind
      while n > 0 and not eof?
        readline
        n -= 1
      end
      self
    end

    # Rewind the last <code>n</code> lines of this file, starting
    # from the end. The default is to start tailing directly from the
    # end of the file.
    #
    # The additional argument <code>bufsize</code> is
    # used to determine the buffer size that is used to step through
    # the file backwards. It defaults to the block size of the
    # filesystem this file belongs to or 8192 bytes if this cannot
    # be determined.
    def backward(n = 0, bufsize = nil)
      if n <= 0
        seek(0, File::SEEK_END)
        return self
      end
      bufsize ||= default_bufsize || stat.blksize || 8192
      size = stat.size
      begin
        if bufsize < size
          seek(0, File::SEEK_END)
          while n > 0 and tell > 0 do
            start = tell
            seek(-bufsize, File::SEEK_CUR)
            buffer = read(bufsize)
            n -= buffer.count("\n")
            seek(-bufsize, File::SEEK_CUR)
          end
        else
          rewind
          buffer = read(size)
          n -= buffer.count("\n")
          rewind
        end
      rescue Errno::EINVAL
        size = tell
        retry
      end
      pos = -1
      while n < 0  # forward if we are too far back
        pos = buffer.index("\n", pos + 1)
        n += 1
      end
      seek(pos + 1, File::SEEK_CUR)
      self
    end

    # This method tails this file and yields to the given block for
    # every new line that is read.
    # If no block is given an array of those lines is
    # returned instead. (In this case it's better to use a
    # reasonable value for <code>n</code> or set the
    # <code>return_if_eof</code> or <code>break_if_eof</code>
    # attribute to a true value to stop the method call from blocking.)
    #
    # If the argument <code>n</code> is given, only the next <code>n</code>
    # lines are read and the method call returns. Otherwise this method
    # call doesn't return, but yields to block for every new line read from
    # this file for ever.
    def tail(n = nil, &block) # :yields: line
      @n = n
      result = []
      array_result = false
      unless block
        block = lambda { |line| result << line }
        array_result = true
      end
      preset_attributes unless @lines
      loop do
        begin
          restat
          read_line(&block)
          redo
        rescue ReopenException => e
          until eof? || @n == 0
            block.call readline
            @n -= 1 if @n
          end
          reopen_file(e.mode)
          @after_reopen.call self if @after_reopen
        rescue ReturnException
          return array_result ? result : nil
        end
      end
    end

    private

    def read_line(&block)
      if @n
        until @n == 0
          block.call readline
          @lines   += 1
          @no_read = 0
          @n       -= 1
          output_debug_information
        end
        raise ReturnException
      else
        block.call readline
        @lines   += 1
        @no_read = 0
        output_debug_information
      end
    rescue EOFError
      seek(0, File::SEEK_CUR)
      raise ReopenException if @reopen_suspicious and
        @no_read > @suspicious_interval
      raise BreakException if @break_if_eof
      raise ReturnException if @return_if_eof
      sleep_interval
    rescue Errno::ENOENT, Errno::ESTALE, Errno::EBADF
      raise ReopenException
    end

    def preset_attributes
      @reopen_deleted       = true if @reopen_deleted.nil?
      @reopen_suspicious    = true if @reopen_suspicious.nil?
      @break_if_eof         = false if @break_if_eof.nil?
      @return_if_eof        = false if @return_if_eof.nil?
      @max_interval         ||= 10
      @interval             ||= @max_interval
      @suspicious_interval  ||= 60
      @lines                = 0
      @no_read              = 0
    end

    def restat
      stat = File.stat(path)
      if @stat
        if stat.ino != @stat.ino or stat.dev != @stat.dev
          @stat = nil
          raise ReopenException.new(:top)
        end
        if stat.size < @stat.size
          @stat = nil
          raise ReopenException.new(:top)
        end
      else
        @stat = stat
      end
    rescue Errno::ENOENT, Errno::ESTALE
      raise ReopenException
    end

    def sleep_interval
      if @lines > 0
        # estimate how much time we will spend on waiting for next line
        @interval = (@interval.to_f / @lines)
        @lines = 0
      else
        # exponential backoff if logfile is quiet
        @interval *= 2
      end
      if @interval > @max_interval
        # max. wait @max_interval
        @interval = @max_interval
      end
      output_debug_information
      sleep @interval
      @no_read += @interval
    end

    def reopen_file(mode)
      $DEBUG and $stdout.print "Reopening '#{path}', mode = #{mode}.\n"
      @no_read = 0
      reopen(path)
      if mode == :bottom
        backward
      elsif mode == :top
        forward
      end
    rescue Errno::ESTALE, Errno::ENOENT
      if @reopen_deleted
        sleep @max_interval
        retry
      else
        raise DeletedException
      end
    end

    def output_debug_information
      $DEBUG or return
      STDERR.puts({
        :path     => path,
        :lines    => @lines,
        :interval => @interval,
        :no_read  => @no_read,
        :n        => @n,
      }.inspect)
      self
    end
  end
end

if $0 == __FILE__
  filename = ARGV.shift or fail "Usage: #$0 filename [number]"
  number = (ARGV.shift || 0).to_i
  File.open(filename) do |log|
    log.extend(File::Tail)
    # Some settings to make watching tail.rb with "ruby -d" fun
    log.interval            = 1
    log.max_interval        = 5
    log.reopen_deleted      = true # is default
    log.reopen_suspicious   = true # is default
    log.suspicious_interval = 20
    number >= 0 ? log.backward(number, 8192) : log.forward(-number)
    #loop do          # grab 5 lines at a time and return
    #  log.tail(5) { |line| puts line }
    #  print "Got 5!\n"
    #end
    log.tail { |line| puts line }
  end
end