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 break_if_eof
# 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
# :bottom
which indicates that the file
# should be tailed beginning from the end. :top
# 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 max_interval
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 max_interval
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
# suspicious_interval
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 reopen_suspicious
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 n
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 n
lines of this file, starting
# from the end. The default is to start tailing directly from the
# end of the file.
#
# The additional argument bufsize
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 n
or set the
# return_if_eof
or break_if_eof
# attribute to a true value to stop the method call from blocking.)
#
# If the argument n
is given, only the next n
# 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