file-tail-1.4.0/0000755000004100000410000000000015132156035013413 5ustar www-datawww-datafile-tail-1.4.0/bin/0000755000004100000410000000000015132156035014163 5ustar www-datawww-datafile-tail-1.4.0/bin/rtail0000755000004100000410000000251715132156035015231 0ustar www-datawww-data#!/usr/bin/env ruby require 'file/tail' require 'tins/go' include Tins::GO require 'thread' Thread.abort_on_exception = true $opt = go 'n:m:Mh' if $opt['h'] puts <= 2.16.3** - Added changelog generation support to the Rakefile for automated changelog management - Updated Dockerfile dependencies for Ruby version detection and added Ruby **3.4**-alpine image support - Fixed typos in the codebase ## 2024-09-13 v1.3.0 ### Significant Changes * **Improved waiting for log output by counting lines** + Increased timeout from 2 seconds to 10 seconds in multiple places * **Added Ruby version check in Dockerfile** + Update `gem update --system` and installation to be conditional on Ruby version + Replace `bundle` with `bundle install` in script section * **Convert CHANGES file to CHANGES\.md** ### Bug Fixes * **Add exit handler to delete temporary file** - Added at_exit block to delete test file created in setup method. * **Refactor File class for debugging** - Remove hardcoded `$DEBUG` variable usage in reopen_file and output_debug_information methods - Introduce debug? method to check if `FILE_TAIL_DEBUG` environment variable is set to 1. ### Dependency Updates * **Update Ruby dependencies and add new development dependencies** + Added `.all_images.yml` file with Dockerfile configuration + Updated Gemfile to use Ruby **3.5.18** instead of **2.7.8** + Updated Rakefile to ignore additional files + Updated `file-tail.gemspec` to include `.all_images.yml` in the list of files + Updated `tests/file_tail_test.rb` to use absolute path for test file + Added new development dependencies: `all_images`, `simplecov`, and `debug` + Updated dependency versions: `gem_hadar` to **1.17.1**, `test-unit` to **3.0**, and `tins` to **1.0** ## 2016-04-19 v1.2.0 * Make line separator configurable ## 2016-04-19 v1.1.1 * Fix tests on Ruby 2.3.0 ## 2014-09-26 v1.1.0 * Depend on tins ~ 1.0 ## 2012-05-31 v1.0.10 * Use rewind to force IO#lineno to be reset. ## 2012-05-31 v1.0.9 * Reopen file in :top mode at the beginning. ## 2011-12-24 v1.0.8 * Support simplecov. ## 2011-07-15 v1.0.7 * Use gem_hadar to shorten Rakefile. ## 2011-06-25 v1.0.6 * Create a gem spec file again. * Added a File::Tail::Group to tail multiple files more easily. ## 2010-03-25 v1.0.5 * Added rtail executable, a nice app to supervise logfiles and logdirs. * Disabled creation of gem spec file. * Cleaned up documentation a bit. ## 2009-08-21 v1.0.4 * Fixed the threaded tests for Ruby 1.9. * Create a gem spec file. * Some cleanup. ## 2008-04-07 v1.0.3 * Danny Colligan reported a memory leak in long running scripts using file-tail. I changed file-ta il to only use block.call, which seems to improve the memory behaviour. I am still not sure, where the problem actually stems f rom, though. ## 2007-04-19 v1.0.2 * make_doc.rb was missing from the source archive. Thanks to Rick Ohnemus for reporting it. ## 2007-04-19 v1.0.1 * Bugfix: File::Tail::Logfile#open with block, now closes the file like File#open does. Found by Alex Doan , ruby-talk:248383. ## 2007-03-30 v1.0.0 * Bugfix: David.Barzilay@swisscom.com reported, that file tails may skip some log file lines, after rotating it. I think, that I fixed that problem. * Added a after_reopen callback as well, that is called after reopening of the tailed file has occured. * Removed rewind/wind methods even earlier than planned: I placed the deprecation warning for rewind method in File instead of File::Tail, which caused rewind to stop working completely after loading file/tail. Duh! I blame vim's matchit, because it jump ed to the wrong end keyword. ## 2007-02-08 v0.1.4 * Renamed rewind method to backward, and wind method to forward, because someone already had the good idea to name a method IO# rewind, which was overwritten by the mixed in File::Tail methods. The old methods are now deprecated and will be removed in a n ew 0.2.x version of the library. * Added a bit more of documentation. ## 2005-08-20 v0.1.3 * Applied LOAD_PATH patch by Daniel Berger, binary mode changes were already in the CVS. Seemed to be like cheating to me, thou gh. ;) * Skipping one windows test for the moment, too. Sigh! ## 2004-09-30 v0.1.2 * First Rubyforge release * Added Rakefile * Supports gem build now. ## 2004-09-01 v0.1.1 * Josh Endries found a bug that caused File::Tail to malfunction on FreeBSD. Hotfix: Use a side effect of se ek to clearerr the tailed file handle after EOFError has been raised. ## 2004-04-13 v0.1.0 * API documentation with rdoc. * return_if_eof attribute added. * Added array return mode for finite tail call without block given. * install.rb now uses ruby version site_dir. * Some code and directory structure cleanup. ## 2002-08-02 v0.0.2 * Heavy refactoring, more and smaller methods and expception handling * Added check for inode and device equality of files as suggested by James F.Hranicky and Curt Sampson to cover remove rotation * If filesize shrinks suddenly, File::Tail assumes that copy and truncate rotation has happend: The file is reopened and every new line is handled. * NFS-Fix: Errno::ESTALE is caught. * wind added to skip the first n lines, as James F.Hranicky's suggested and changed name of last-method to rewind, because I li ked his method names better than mine ;) * Renamed next to tail either. * The API has changed - but I think very few people care at the moment. * Lots of tests added. ## 2002-07-30 v0.0.1 * Initial Release file-tail-1.4.0/.github/0000755000004100000410000000000015132156035014753 5ustar www-datawww-datafile-tail-1.4.0/.github/workflows/0000755000004100000410000000000015132156035017010 5ustar www-datawww-datafile-tail-1.4.0/.github/workflows/static.yml0000644000004100000410000000265115132156035021026 0ustar www-datawww-data# Simple workflow for deploying static content to GitHub Pages name: Deploy static content to Pages on: # Runs on pushes targeting the default branch push: branches: [ "master" ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: group: "pages" cancel-in-progress: false jobs: # Single deploy job since we're just deploying deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v5 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' - name: Generate Documentation run: | gem install gem_hadar bundle install rake doc - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: 'doc' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 file-tail-1.4.0/lib/0000755000004100000410000000000015132156035014161 5ustar www-datawww-datafile-tail-1.4.0/lib/file-tail.rb0000644000004100000410000000002415132156035016350 0ustar www-datawww-datarequire 'file/tail' file-tail-1.4.0/lib/file/0000755000004100000410000000000015132156035015100 5ustar www-datawww-datafile-tail-1.4.0/lib/file/tail/0000755000004100000410000000000015132156035016031 5ustar www-datawww-datafile-tail-1.4.0/lib/file/tail/group.rb0000644000004100000410000000701115132156035017511 0ustar www-datawww-datarequire 'thread' class File module Tail # This class can be used to coordinate tailing of many files, which have # been added to the group. class Group # Creates a new File::Tail::Group instance. # # The following options can be given as arguments: # :files:: an array of files (or filenames to open) that are placed into # the group. def initialize(opts = {}) @tailers = ThreadGroup.new if files = opts[:files] Array(files).each { |file| add file } end end # Creates a group for +files+ (IO instances or filename strings). def self.[](*files) new(:files => files) end # Add a file (IO instance) or filename (responding to to_str) to this # group. def add(file_or_filename) if file_or_filename.respond_to?(:to_io) add_file file_or_filename.to_io elsif file_or_filename.respond_to?(:to_str) add_filename file_or_filename end end alias << add # Add the IO instance +file+ to this group. def add_file(file) setup_file_tailer file self end # Add a file created by opening +filename+ to this group after stepping # +n+ lines backwards from the end of it. def add_filename(filename, n = 0) file = Logfile.open(filename.to_str, :backward => n) file.backward n setup_file_tailer file self end # Iterate over all files contained in this group yielding to +block+ for # each of them. def each_file(&block) each_tailer { |t| t.file }.map(&block) end # Iterate over all tailers in this group yielding to +block+ for each of # them. def each_tailer(&block) @tailers.list.map(&block) end # Stop all tailers in this group at once. def stop each_tailer { |t| t.stop } each_tailer { |t| t.join } self end # Tail all the lines of all the files in the Tail::Group instance, that # is yield to each of them. # # Every line is extended with the LineExtension module, that adds some # methods to the line string. To get the path of the file this line was # received from call line.file.path. def tail wait_for_activity do |tailer| tailer.pending_lines.each do |line| line.extend LineExtension line.instance_variable_set :@tailer, tailer yield line end end end private def setup_file_tailer(file) file.extend File::Tail setup = ConditionVariable.new mutex = Mutex.new ft = nil mutex.synchronize do ft = Tailer.new do t = Thread.current t[:queue] = Queue.new t[:file] = file mutex.synchronize do setup.signal end file.tail { |line| t[:queue] << line } end setup.wait mutex end @tailers.add ft nil end # Wait until new input is receΡ–ved on any of the tailers in the group. If # so call +block+ with all of these trailers as an argument. def wait_for_activity(&block) loop do pending = each_tailer.select(&:pending_lines?) if pending.empty? interval = each_file.map { |t| t.interval }.compact.min || 0.1 sleep interval else pending.each(&block) end end end end end end file-tail-1.4.0/lib/file/tail/tailer.rb0000644000004100000410000000202715132156035017637 0ustar www-datawww-dataclass File module Tail # This class supervises activity on a tailed fail and collects newly read # lines until the Tail::Group fetches and processes them. class Tailer < ::Thread # True if there are any lines pending on this Tailer, false otherwise. def pending_lines? !queue.empty? end # Fetch all the pending lines from this Tailer and thereby remove them # from the Tailer's queue. def pending_lines Array.new(queue.size) { queue.deq(true) } end alias stop exit # Stop tailing this file and remove it from its File::Tail::Group. # Return true if the thread local variable +id+ is defined or if this # object responds to the method +id+. def respond_to?(id) !self[id].nil? || super end # Return the thread local variable +id+ if it is defined. def method_missing(id, *args, &block) if args.empty? && !(value = self[id]).nil? value else super end end end end end file-tail-1.4.0/lib/file/tail/line_extension.rb0000644000004100000410000000055515132156035021406 0ustar www-datawww-dataclass File module Tail # This module is used to extend all lines received via one of the tailers # of a File::Tail::Group. module LineExtension # The file as a File instance this line was read from. def file tailer.file end # This is the tailer this line was received from. attr_reader :tailer end end end file-tail-1.4.0/lib/file/tail/version.rb0000644000004100000410000000042315132156035020042 0ustar www-datawww-datamodule File::Tail # File::Tail version VERSION = '1.4.0' VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc: VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc: VERSION_MINOR = VERSION_ARRAY[1] # :nodoc: VERSION_BUILD = VERSION_ARRAY[2] # :nodoc: end file-tail-1.4.0/lib/file/tail/logfile.rb0000644000004100000410000000553315132156035020005 0ustar www-datawww-dataclass File module Tail # This is an easy to use Logfile class that includes # the File::Tail module. # # === Usage # The unix command "tail -10f filename" can be emulated like that: # File::Tail::Logfile.open(filename, :backward => 10) do |log| # log.tail { |line| puts line } # end # # Or a bit shorter: # File::Tail::Logfile.tail(filename, :backward => 10) do |line| # puts line # end # # To skip the first 10 lines of the file do that: # File::Tail::Logfile.open(filename, :forward => 10) do |log| # log.tail { |line| puts line } # end # # The unix command "head -10 filename" can be emulated like that: # File::Tail::Logfile.open(filename, :return_if_eof => true) do |log| # log.tail(10) { |line| puts line } # end class Logfile < File include File::Tail # This method creates an File::Tail::Logfile object and # yields to it, and closes it, if a block is given, otherwise it just # returns it. The opts hash takes an option like # * :backward => 10 to go backwards # * :forward => 10 to go forwards # in the logfile for 10 lines at the start. The buffersize # for going backwards can be set with the # * :bufsiz => 8192 option. # To define a callback, that will be called after a reopening occurs, use: # * :after_reopen => lambda { |file| p file } # # Every attribute of File::Tail can be set with a :attributename => # value option. def self.open(filename, opts = {}, &block) # :yields: file file = new filename opts.each do |o, v| writer = o.to_s + "=" file.__send__(writer, v) if file.respond_to? writer end if opts.key?(:wind) or opts.key?(:rewind) warn ":wind and :rewind options are deprecated, "\ "use :forward and :backward instead!" end if backward = opts[:backward] || opts[:rewind] (args = []) << backward args << opts[:bufsiz] if opts[:bufsiz] file.backward(*args) elsif forward = opts[:forward] || opts[:wind] file.forward(forward) end if opts[:after_reopen] file.after_reopen(&opts[:after_reopen]) end if block_given? begin block.call file ensure file.close end else file end end # Like open, but yields to every new line encountered in the logfile in # +block+. def self.tail(filename, opts = {}, &block) if ([ :forward, :backward ] & opts.keys).empty? opts[:backward] = 0 end open(filename, opts) do |log| log.tail { |line| block.call line } end end end end end file-tail-1.4.0/lib/file/tail.rb0000644000004100000410000002471115132156035016363 0ustar www-datawww-dataclass 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" behavior. 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 happened 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 # occurred. 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 interval in seconds before File::Tail # gets suspicious that something has happened 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 # Override the default line separator attr_accessor :line_separator # 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) preset_attributes unless defined? @lines rewind while n > 0 and not eof? readline(@line_separator) 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) preset_attributes unless defined? @lines 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 seek(-bufsize, File::SEEK_CUR) buffer = read(bufsize) n -= buffer.count(@line_separator) seek(-bufsize, File::SEEK_CUR) end else rewind buffer = read(size) n -= buffer.count(@line_separator) rewind end rescue Errno::EINVAL size = tell retry end pos = -1 while n < 0 # forward if we are too far back pos = buffer.index(@line_separator, 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 defined? @lines loop do begin restat read_line(&block) redo rescue ReopenException => e until eof? || @n == 0 block.call readline(@line_separator) @n -= 1 if @n end reopen_file(e.mode) @after_reopen.call self if defined? @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(@line_separator) @lines += 1 @no_read = 0 @n -= 1 output_debug_information end raise ReturnException else block.call readline(@line_separator) @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 unless defined? @reopen_deleted @reopen_suspicious = true unless defined? @reopen_suspicious @break_if_eof = false unless defined? @break_if_eof @return_if_eof = false unless defined? @return_if_eof @max_interval ||= 10 @line_separator ||= $/ @interval ||= @max_interval @suspicious_interval ||= 60 @lines = 0 @no_read = 0 @stat = nil 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) # File ino/dev has changed, start from top end if stat.size < @stat.size @stat = nil raise ReopenException.new(:top) # File shrunk, start from top end end @stat = stat rescue Errno::ENOENT raise ReopenException.new(:top) # File was missing, maybe it has been rotated, start from top rescue Errno::ESTALE raise ReopenException # File is stale let's try opening again with same mo 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 def debug? ENV['FILE_TAIL_DEBUG'].to_i == 1 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 file-tail-1.4.0/tests/0000755000004100000410000000000015132156035014555 5ustar www-datawww-datafile-tail-1.4.0/tests/test_helper.rb0000644000004100000410000000025015132156035017415 0ustar www-datawww-dataif ENV['START_SIMPLECOV'].to_i == 1 require 'simplecov' SimpleCov.start do add_filter "#{File.basename(File.dirname(__FILE__))}/" end end require 'test/unit' file-tail-1.4.0/tests/file_tail_group_test.rb0000644000004100000410000000356515132156035021316 0ustar www-datawww-data#!/usr/bin/env ruby require 'test_helper' require 'file/tail' require 'timeout' require 'thread' require 'tempfile' Thread.abort_on_exception = true class FileTailGroupTest < Test::Unit::TestCase include File::Tail def test_create_group t, = make_file g = Group[t] assert_equal t.path, g.each_tailer.first.file.path assert_equal t.path, g.each_file.first.path end def test_stop_group t, = make_file g = Group[t] assert_equal t.path, g.each_tailer.first.file.path assert_equal t.path, g.each_file.first.path g.stop assert_nil g.each_file.first end def test_add_file_to_group g = Group.new t, = make_file g.add_file t assert_equal t.path, g.each_tailer.first.file.path assert_equal t.path, g.each_file.first.path end def test_add_filename_to_group g = Group.new t, name = make_file t.close g.add_filename name assert_equal name, g.each_tailer.first.file.path assert_equal t.path, g.each_file.first.path end def test_add_generic_to_group g = Group.new t1, n1 = make_file t1.close t2, n1 = make_file g << n1 g << t2 assert g.each_tailer.any? { |t| t.file.path == n1 } assert g.each_tailer.any? { |t| t.file.path == t2.path } assert g.each_file.any? { |t| t.path == n1 } assert g.each_file.any? { |t| t.path == t2.path } end def test_tail_multiple_files t1, = make_file t1.max_interval = 0.1 t2, = make_file t2.max_interval = 0.1 g = Group[t1, t2] q = Queue.new t = Thread.new do g.tail { |l| q << l } end t1.puts "foo" assert_equal "foo\n", q.pop t2.puts "bar" assert_equal "bar\n", q.pop ensure t and t.exit end private def make_file name = File.expand_path(File.join(Dir.tmpdir, "tmp.#$$")) file = File.open(name, 'w+') file.extend File::Tail return file, name end end file-tail-1.4.0/tests/file_tail_test.rb0000644000004100000410000002077715132156035020106 0ustar www-datawww-data#!/usr/bin/env ruby require 'test_helper' require 'file/tail' require 'timeout' require 'thread' require 'fileutils' Thread.abort_on_exception = true class FileTailTest < Test::Unit::TestCase include File::Tail include FileUtils def setup @out = File.new(File.join('tmp', "test.#$$"), "wb") at_exit { path = @out&.path and rm_f File.expand_path(path) } append(@out, 100) @in = File.new(@out.path, "rb") @in.extend(File::Tail) @in.interval = 0.4 @in.max_interval = 0.8 @in.reopen_deleted = true # is default @in.reopen_suspicious = true # is default @in.suspicious_interval = 60 end def test_forward [ 0, 1, 2, 10, 100 ].each do |lines| @in.forward(lines) assert_equal(100 - lines, count(@in)) end @in.forward(101) assert_equal(0, count(@in)) end def test_backward [ 0, 1, 2, 10, 100 ].each do |lines| @in.backward(lines) assert_equal(lines, count(@in)) end @in.backward(101) assert_equal(100, count(@in)) end def test_backward_small_buffer [ 0, 1, 2, 10, 100 ].each do |lines| @in.backward(lines, 100) assert_equal(lines, count(@in)) end @in.backward(101, 100) assert_equal(100, count(@in)) end def test_backward_small_buffer2 @in.default_bufsize = 100 [ 0, 1, 2, 10, 100 ].each do |lines| @in.backward(lines) assert_equal(lines, count(@in)) end @in.backward(101) assert_equal(100, count(@in)) end def test_tail_with_block_without_n Timeout::timeout(10) do lines = [] @in.backward(1) assert_raises(Timeout::Error) do Timeout::timeout(1) { @in.tail { |l| lines << l } } end assert_equal(1, lines.size) # lines = [] @in.backward(10) assert_raises(Timeout::Error) do Timeout::timeout(1) { @in.tail { |l| lines << l } } end assert_equal(10, lines.size) # lines = [] @in.backward(100) assert_raises(Timeout::Error) do Timeout::timeout(1) { @in.tail { |l| lines << l } } end assert_equal(100, lines.size) # lines = [] @in.backward(101) assert_raises(Timeout::Error) do Timeout::timeout(1) { @in.tail { |l| lines << l } } end end end def test_tail_with_block_with_n Timeout::timeout(10) do @in.backward(1) lines = [] Timeout::timeout(1) { @in.tail(0) { |l| lines << l } } assert_equal(0, lines.size) # @in.backward(1) lines = [] Timeout::timeout(1) { @in.tail(1) { |l| lines << l } } assert_equal(1, lines.size) # @in.backward(10) lines = [] Timeout::timeout(1) { @in.tail(10) { |l| lines << l } } assert_equal(10, lines.size) # @in.backward(100) lines = [] @in.backward(1) assert_raises(Timeout::Error) do Timeout::timeout(1) { @in.tail(2) { |l| lines << l } } end assert_equal(1, lines.size) # end end def test_tail_without_block_with_n Timeout::timeout(10) do @in.backward(1) lines = [] Timeout::timeout(1) { lines += @in.tail(0) } assert_equal(0, lines.size) # @in.backward(1) lines = [] Timeout::timeout(1) { lines += @in.tail(1) } assert_equal(1, lines.size) # @in.backward(10) lines = [] Timeout::timeout(1) { lines += @in.tail(10) } assert_equal(10, lines.size) # @in.backward(100) lines = [] @in.backward(1) assert_raises(Timeout::Error) do Timeout::timeout(1) { lines += @in.tail(2) } end assert_equal(0, lines.size) end end def test_tail_withappend @in.backward lines = [] logger = Thread.new do begin Timeout::timeout(1) { @in.tail { |l| lines << l } } rescue Timeout::Error end end appender = Thread.new { append(@out, 10) } appender.join logger.join assert_equal(10, lines.size) end def test_tail_truncated @in.backward lines = [] logger = Thread.new do begin Timeout::timeout(10) do @in.tail do |l| lines << l end end rescue Timeout::Error end end appender = Thread.new do until logger.stop? sleep 0.1 end @out.close File.truncate(@out.path, 0) @out = File.new(@in.path, "ab") append(@out, 10) end appender.join logger.join assert_equal(10, lines.size) end def test_tail_remove return if File::PATH_SEPARATOR == ';' # Grmpf! Windows... @in.backward reopened = false @in.after_reopen { |f| reopened = true } lines = [] logger = Thread.new do begin Timeout::timeout(10) do @in.tail do |l| lines << l end end rescue Timeout::Error end end appender = Thread.new do until logger.stop? sleep 0.1 end @out.close File.unlink(@out.path) @out = File.new(@in.path, "wb") append(@out, 10) end appender.join logger.join assert_equal(10, lines.size) assert reopened end def test_tail_remove2 return if File::PATH_SEPARATOR == ';' # Grmpf! Windows... @in.backward reopened = false @in.after_reopen { |f| reopened = true } lines = [] logger = Thread.new do begin Timeout::timeout(10) do @in.tail do |l| lines << l end end rescue Timeout::Error end end appender = Thread.new do until logger.stop? sleep 0.1 end @out.close File.unlink(@out.path) @out = File.new(@in.path, "wb") append(@out, 10) sleep 1 append(@out, 10) File.unlink(@out.path) @out = File.new(@in.path, "wb") append(@out, 10) end appender.join logger.join assert_equal(30, lines.size) assert reopened end def test_tail_remove3 return if File::PATH_SEPARATOR == ';' # Grmpf! Windows... @in.backward reopened = false @in.after_reopen { |f| reopened = true } lines = [] logger = Thread.new do begin Timeout::timeout(10) do @in.tail(15) do |l| lines << l end end rescue Timeout::Error end end appender = Thread.new do until logger.stop? sleep 0.1 end @out.close File.unlink(@out.path) @out = File.new(@in.path, "wb") append(@out, 10) sleep 1 append(@out, 10) File.unlink(@out.path) @out = File.new(@in.path, "wb") append(@out, 10) end appender.join logger.join assert_equal(15, lines.size) assert reopened end def test_tail_change return if File::PATH_SEPARATOR == ';' # Grmpf! Windows... @in.forward reopened = false assert_equal 0, @in.lineno @in.after_reopen { |f| reopened = true } lines = [] logger = Thread.new do begin Timeout::timeout(10) do @in.tail(110) do |l| lines << l end end rescue Timeout::Error end end appender = Thread.new do Timeout::timeout(10) do until lines.size == 100 sleep 0.1 end end @out.close File.unlink(@out.path) @out = File.new(@in.path, "wb") append(@out, 10) end appender.join logger.join assert_equal(110, lines.size) assert reopened assert_equal 10, @in.lineno end def test_tail_change2 return if File::PATH_SEPARATOR == ';' # Grmpf! Windows... @in.forward reopened = false assert_equal 0, @in.lineno @in.after_reopen { |f| reopened = true } lines = [] logger = Thread.new do begin Timeout::timeout(10) do @in.tail(110) do |l| lines << l end end rescue Timeout::Error end end appender = Thread.new do Timeout::timeout(10) do until lines.size == 100 sleep 0.1 end end @out.truncate 0 @out.close @out = File.new(@in.path, "wb") append(@out, 10) end appender.join logger.join assert_equal(110, lines.size) assert reopened assert_equal 10, @in.lineno end def teardown @in.close @out.close File.unlink(@out.path) end private def count(file) n = 0 until file.eof? file.readline n += 1 end return n end def append(file, n, size = 70) (1..n).each { |x| file << "#{x} #{"A" * size}\n" } file.flush end end file-tail-1.4.0/Rakefile0000644000004100000410000000151415132156035015061 0ustar www-datawww-data# vim: set filetype=ruby et sw=2 ts=2: require 'gem_hadar' GemHadar do name 'file-tail' path_name 'file/tail' author 'Florian Frank' email 'flori@ping.de' homepage "http://github.com/flori/#{name}" summary "#{path_name.camelize} for Ruby" description 'Library to tail files in Ruby' test_dir 'tests' ignore '.*.sw[pon]', 'pkg', 'Gemfile.lock', 'coverage', '*.rbc', '.rbx', '.AppleDouble', '.bundle', 'errors.lst', '.utilsrc', 'tmp', '.yardoc' readme 'README.md' licenses << 'Apache-2.0' changelog do filename 'CHANGES.md' end github_workflows( 'static.yml' => {} ) dependency 'tins', '~>1.0' development_dependency 'test-unit', '~>3.0' development_dependency 'all_images' development_dependency 'simplecov' development_dependency 'debug' end file-tail-1.4.0/.all_images.yml0000644000004100000410000000060615132156035016313 0ustar www-datawww-datadockerfile: |- RUN apk add --no-cache build-base yaml-dev git openssl-dev fail_fast: true script: &script |- echo -e "\e[1m" ruby -v echo -e "\e[0m" bundle update --all bundle install --jobs=$(getconf _NPROCESSORS_ONLN) rake test images: ruby:4.0-alpine: *script ruby:3.4-alpine: *script ruby:3.3-alpine: *script ruby:3.2-alpine: *script ruby:3.1-alpine: *script file-tail-1.4.0/file-tail.gemspec0000644000004100000410000000437215132156035016634 0ustar www-datawww-data# -*- encoding: utf-8 -*- # stub: file-tail 1.4.0 ruby lib Gem::Specification.new do |s| s.name = "file-tail".freeze s.version = "1.4.0".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Florian Frank".freeze] s.date = "1980-01-02" s.description = "Library to tail files in Ruby".freeze s.email = "flori@ping.de".freeze s.extra_rdoc_files = ["README.md".freeze, "lib/file-tail.rb".freeze, "lib/file/tail.rb".freeze, "lib/file/tail/group.rb".freeze, "lib/file/tail/line_extension.rb".freeze, "lib/file/tail/logfile.rb".freeze, "lib/file/tail/tailer.rb".freeze, "lib/file/tail/version.rb".freeze] s.files = [".all_images.yml".freeze, ".github/workflows/static.yml".freeze, ".gitignore".freeze, ".travis.yml".freeze, "CHANGES.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "VERSION".freeze, "bin/rtail".freeze, "examples/pager.rb".freeze, "examples/tail.rb".freeze, "file-tail.gemspec".freeze, "lib/file-tail.rb".freeze, "lib/file/tail.rb".freeze, "lib/file/tail/group.rb".freeze, "lib/file/tail/line_extension.rb".freeze, "lib/file/tail/logfile.rb".freeze, "lib/file/tail/tailer.rb".freeze, "lib/file/tail/version.rb".freeze, "tests/file_tail_group_test.rb".freeze, "tests/file_tail_test.rb".freeze, "tests/test_helper.rb".freeze, "tmp/.gitkeep".freeze] s.homepage = "http://github.com/flori/file-tail".freeze s.licenses = ["Apache-2.0".freeze] s.rdoc_options = ["--title".freeze, "File-tail - File::Tail for Ruby".freeze, "--main".freeze, "README.md".freeze] s.rubygems_version = "4.0.2".freeze s.summary = "File::Tail for Ruby".freeze s.test_files = ["tests/file_tail_group_test.rb".freeze, "tests/file_tail_test.rb".freeze, "tests/test_helper.rb".freeze] s.specification_version = 4 s.add_development_dependency(%q.freeze, [">= 2.16.3".freeze]) s.add_development_dependency(%q.freeze, ["~> 3.0".freeze]) s.add_development_dependency(%q.freeze, [">= 0".freeze]) s.add_development_dependency(%q.freeze, [">= 0".freeze]) s.add_development_dependency(%q.freeze, [">= 0".freeze]) s.add_runtime_dependency(%q.freeze, ["~> 1.0".freeze]) end file-tail-1.4.0/Gemfile0000644000004100000410000000011715132156035014705 0ustar www-datawww-data# vim: set filetype=ruby et sw=2 ts=2: source 'https://rubygems.org' gemspec file-tail-1.4.0/LICENSE0000644000004100000410000002612415132156035014425 0ustar www-datawww-data Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [2017] [Florian Frank] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. file-tail-1.4.0/VERSION0000644000004100000410000000000615132156035014457 0ustar www-datawww-data1.4.0 file-tail-1.4.0/.travis.yml0000644000004100000410000000024315132156035015523 0ustar www-datawww-datarvm: - 2.1 - 2.2 - 2.3 - 2.4 - 2.5 - 2.6 - ruby-head - jruby-head sudo: false matrix: allow_failures: - rvm: ruby-head - rvm: jruby-head file-tail-1.4.0/README.md0000644000004100000410000001720415132156035014676 0ustar www-datawww-data# File::Tail for Ruby ## Description This is a small ruby library that allows it to "tail" files in Ruby, including following a file, that still is growing like the unix command 'tail -f' can. ## Documentation Complete API documentation is available at: [GitHub.io](https://flori.github.io/file-tail/) ## Architecture Overview File::Tail follows a well-defined architectural pattern with clear separation of concerns between several key components: ### Core Components **File::Tail Module** - The central hub extending File objects with tailing capabilities πŸ”„ - Provides core methods for forward/backward traversal and tailing πŸ“Š - Handles file system events like rotation, truncation, and deletion πŸ” **File::Tail::Logfile** - A convenience class that simplifies opening and tailing files with options πŸš€ - Includes File::Tail module for direct file access 🧠 - Supports various initialization patterns for flexible usage πŸ”„ **File::Tail::Group** - Manages multiple files to be tailed together πŸ”„ - Uses ThreadGroup to coordinate multiple tailing threads ⏳ - Provides batch operations for concurrent file monitoring πŸ“Š **File::Tail::Tailer** - A Thread subclass that supervises a single file's tailing 🧠 - Uses queues for line buffering and thread-safe communication πŸ“¦ - Implements method_missing for thread-local variable access ⚑ ### Component Interactions ```mermaid graph LR A[File::Tail] --> B[File::Tail::Logfile] A --> C[File::Tail::Group] C --> D[File::Tail::Tailer] D --> E[Thread] A --> F[File::Tail::LineExtension] F --> G[Lines from tailers] ``` The File::Tail module acts as the foundation, providing core functionality that is extended by Logfile and Group classes. The Group class coordinates multiple Tailer threads, each managing a single file's tailing process. ### Loading Mechanism File::Tail supports two primary access patterns: 1. **Direct File Extension** - Extending File objects directly with File::Tail: ```ruby File.open(filename) do |log| log.extend(File::Tail) log.backward(10) log.tail { |line| puts line } end ``` 2. **Logfile Convenience Class** - Using the Logfile wrapper for simpler usage: ```ruby File::Tail::Logfile.tail('app.log', :backward => 10) do |line| puts line end ``` ## Installation To install file-tail via its gem type: ```bash gem install file-tail ``` You can also put this line into your Gemfile: ```ruby gem 'file-tail' ``` and bundle. This will make the File::Tail module available for extending File objects. ## Usage File::Tail is a module in the File class. A lightweight class interface for logfiles can be seen under File::Tail::Logfile. ### Direct Extension of File Objects ```ruby File.open(filename) do |log| log.extend(File::Tail) log.interval # 10 log.backward(10) log.tail { |line| puts line } end ``` It's also possible to mix File::Tail in your own File classes (see also File::Tail::Logfile): ```ruby class MyFile < File include File::Tail end log = MyFile.new("myfile") log.interval # 10 log.backward(10) log.tail { |line| print line } ``` The forward/backward method returns self, so it's possible to chain methods together like that: ```ruby log.backward(10).tail { |line| puts line } ``` ### Multiple File Tailing with Group ```ruby group = File::Tail::Group.new group.add_filename('app.log') group.add_filename('error.log') group.tail { |line| puts line } ``` ### Advanced Usage with Callbacks ```ruby log = File::Tail::Logfile.open('app.log') log.after_reopen { |file| puts "File reopened: #{file.path}" } log.tail { |line| puts line } ``` ### Backward Traversal ```ruby # Tail last 10 lines of file File::Tail::Logfile.open('app.log', :backward => 10) do |log| log.tail { |line| puts line } end # Skip first 5 lines and tail File::Tail::Logfile.open('app.log', :forward => 5) do |log| log.tail { |line| puts line } end ``` ## Documentation To create the documentation of this module, type ```bash $ rake doc ``` and the API documentation is generated. In the examples directory is a small example of tail and pager program that use this module. You also may want look at the end of examples/tail.rb for a little example. ## Debugging and Troubleshooting File::Tail provides built-in debugging capabilities through environment variables: ### Enabling Debug Output Set the `FILE_TAIL_DEBUG` environment variable to enable detailed debugging information: ```bash export FILE_TAIL_DEBUG=1 ruby your_tail_script.rb ``` This will output information about: - File paths being tailed - Line counts and intervals - Reopening events - Sleep intervals and timing ### Exception Handling File::Tail defines several specific exceptions for different failure scenarios: ```ruby begin log.tail { |line| puts line } rescue File::Tail::DeletedException # Handle file deletion rescue File::Tail::ReopenException # Handle file rotation/reopening rescue File::Tail::BreakException # Handle end-of-file with break_if_eof set rescue File::Tail::ReturnException # Internal exception for controlling tailing behavior end ``` ## Configuration ### Key Attributes File::Tail provides several configurable attributes for fine-tuning behavior: ```ruby log = File::Tail::Logfile.open('app.log') log.interval = 1.0 # Initial sleep interval (default: 10) log.max_interval = 5.0 # Maximum sleep interval (default: 10) log.reopen_deleted = true # Reopen deleted files (default: true) log.reopen_suspicious = true # Reopen on suspicious events (default: true) log.suspicious_interval = 60 # Interval before suspicious event detection (default: 60) log.break_if_eof = false # Break on EOF (default: false) log.return_if_eof = false # Return on EOF (default: false) ``` ### Advanced Configuration ```ruby # Configure for high-traffic logs log.interval = 0.1 log.max_interval = 1.0 log.suspicious_interval = 30 # Configure for low-traffic logs log.interval = 5.0 log.max_interval = 30.0 log.suspicious_interval = 120 ``` ## Performance Considerations ### Thread Safety File::Tail uses thread-safe mechanisms for concurrent file monitoring: - ThreadGroup for managing tailer threads - Mutex and ConditionVariable for synchronization - Queue-based line buffering for thread communication ### Memory Management The library implements efficient memory usage: - Lines are buffered in queues per tailer - Exponential backoff reduces CPU usage when files are quiet - Proper thread lifecycle management prevents resource leaks ### File System Event Handling File::Tail gracefully handles various file system events: - File rotation (log rotation) - File truncation - File deletion - File reopening after deletion ## Error Handling File::Tail provides comprehensive error handling for common file system scenarios: ### File::Tail Exception Hierarchy ```mermaid classDiagram class TailException { <> +message } class DeletedException class ReturnException class BreakException class ReopenException TailException <|-- DeletedException TailException <|-- ReturnException TailException <|-- BreakException TailException <|-- ReopenException ``` ### Safe Usage Pattern ```ruby def safe_tail(filename) File::Tail::Logfile.open(filename) do |log| log.tail { |line| puts line } rescue File::Tail::DeletedException puts "File was deleted, stopping tailing" rescue File::Tail::ReopenException puts "File reopened, continuing tailing" end end ``` ## Download The latest version of *File::Tail* (file-tail) can be found at https://github.com/flori/file-tail ## Author Florian Frank mailto:flori@ping.de ## License This software is licensed under the [Apache 2.0 license](LICENSE). file-tail-1.4.0/examples/0000755000004100000410000000000015132156035015231 5ustar www-datawww-datafile-tail-1.4.0/examples/pager.rb0000755000004100000410000000064315132156035016662 0ustar www-datawww-data#!/usr/bin/env ruby # A poor man's pager... :) require 'file/tail' filename = ARGV.shift or fail "Usage: #$0 filename [height]" height = (ARGV.shift || ENV['LINES'] || 23).to_i - 1 File::Tail::Logfile.open(filename, :break_if_eof => true) do |log| begin log.tail(height) { |line| puts line } print "Press return key to continue!" ; gets print " " redo rescue File::Tail::BreakException end end file-tail-1.4.0/examples/tail.rb0000755000004100000410000000034715132156035016516 0ustar www-datawww-data#!/usr/bin/env ruby require 'file/tail' filename = ARGV.pop or fail "Usage: #$0 number filename" number = (ARGV.pop || 0).to_i.abs File::Tail::Logfile.open(filename) do |log| log.backward(number).tail { |line| puts line } end file-tail-1.4.0/tmp/0000755000004100000410000000000015132156035014213 5ustar www-datawww-datafile-tail-1.4.0/tmp/.gitkeep0000644000004100000410000000000015132156035015632 0ustar www-datawww-data