file-tail-1.2.0/0000755000004100000410000000000013321126504013405 5ustar www-datawww-datafile-tail-1.2.0/COPYING0000644000004100000410000002612413321126504014445 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.2.0/.travis.yml0000644000004100000410000000022713321126504015517 0ustar www-datawww-datarvm: - 2.1 - 2.2 - 2.3.3 - 2.4.1 - ruby-head - jruby-head sudo: false matrix: allow_failures: - rvm: ruby-head - rvm: jruby-head file-tail-1.2.0/README.md0000644000004100000410000000320313321126504014662 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. ## Download The latest version of *File::Tail* (file-tail) can be found at http://flori.github.com/file-tail ## Installation To install file-tail via its gem type: # gem install file-tail ## 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 with File::Tail works like that: 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): 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: log.backward(10).tail { |line| puts line } A command line utility named rtail, that uses File::Tail is provided as well. ## Documentation To create the documentation of this module, type ``` $ rake doc ``` and the API documentation is generated. In the examples direcotry 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. ## Author Florian Frank mailto:flori@ping.de ## License Apache License, Version 2.0 – See the COPYING file in the source archive. file-tail-1.2.0/tests/0000755000004100000410000000000013321126504014547 5ustar www-datawww-datafile-tail-1.2.0/tests/file_tail_group_test.rb0000644000004100000410000000356513321126504021310 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.2.0/tests/file_tail_test.rb0000644000004100000410000002043013321126504020062 0ustar www-datawww-data#!/usr/bin/env ruby require 'test_helper' require 'file/tail' require 'timeout' require 'thread' Thread.abort_on_exception = true class FileTailTest < Test::Unit::TestCase include File::Tail def setup @out = File.new("test.#$$", "wb") 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(2) 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(2) 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(2) 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(2) do @in.tail(110) 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(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(2) do @in.tail(110) do |l| lines << l end end rescue Timeout::Error end end appender = Thread.new do until logger.stop? sleep 0.1 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.2.0/tests/test_helper.rb0000644000004100000410000000025013321126504017407 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.2.0/bin/0000755000004100000410000000000013321126504014155 5ustar www-datawww-datafile-tail-1.2.0/bin/rtail0000755000004100000410000000252313321126504015220 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 < reported a memory leak in long running scripts using file-tail. I think this might be a ruby related problem, which is caused/aggravated by using yield after having &block parameter in a method. I changed file-tail to only use block.call, which seems to improve the memory behaviour. I am still not sure, where the problem actually stems from, though. 2007-04-19 * 1.0.2 * make_doc.rb was missing from the source archive. Thanks to Rick Ohnemus for reporting it. 2007-04-19 * 1.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 * 1.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. I 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 jumped to the wrong end keyword. 2007-02-08 * 0.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 new 0.2.x version of the library. * Added a bit more of documentation. 2005-08-20 * 0.1.3 * Applied LOAD_PATH patch by Daniel Berger, binary mode changes were already in the CVS. Seemed to be like cheating to me, though. ;) * Skipping one windows test for the moment, too. Sigh! 2004-09-30 * 0.1.2 * First Rubyforge release * Added Rakefile * Supports gem build now. 2004-09-01 * 0.1.1 * Josh Endries found a bug that caused File::Tail to malfunction on FreeBSD. Hotfix: Use a side effect of seek to clearerr the tailed file handle after EOFError has been raised. 2004-04-13 * 0.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 * 0.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 liked 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 * 0.0.1 * Initial Release file-tail-1.2.0/file-tail.gemspec0000644000004100000410000000464713321126504016633 0ustar www-datawww-data# -*- encoding: utf-8 -*- # stub: file-tail 1.2.0 ruby lib Gem::Specification.new do |s| s.name = "file-tail".freeze s.version = "1.2.0" 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 = "2017-04-13" 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 = [".gitignore".freeze, ".travis.yml".freeze, "CHANGES".freeze, "COPYING".freeze, "Gemfile".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] 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 = "2.6.11".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] if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_development_dependency(%q.freeze, ["~> 1.9.1"]) s.add_development_dependency(%q.freeze, ["~> 2.4.0"]) s.add_runtime_dependency(%q.freeze, ["~> 1.0"]) else s.add_dependency(%q.freeze, ["~> 1.9.1"]) s.add_dependency(%q.freeze, ["~> 2.4.0"]) s.add_dependency(%q.freeze, ["~> 1.0"]) end else s.add_dependency(%q.freeze, ["~> 1.9.1"]) s.add_dependency(%q.freeze, ["~> 2.4.0"]) s.add_dependency(%q.freeze, ["~> 1.0"]) end end file-tail-1.2.0/.gitignore0000644000004100000410000000010513321126504015371 0ustar www-datawww-data*.rbc .*.sw[pon] .AppleDouble .bundle .rbx Gemfile.lock coverage pkg file-tail-1.2.0/examples/0000755000004100000410000000000013321126504015223 5ustar www-datawww-datafile-tail-1.2.0/examples/tail.rb0000755000004100000410000000034713321126504016510 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.2.0/examples/pager.rb0000755000004100000410000000064313321126504016654 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.2.0/Rakefile0000644000004100000410000000112313321126504015047 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' readme 'README.md' licenses << 'Apache-2.0' dependency 'tins', '~>1.0' development_dependency 'test-unit', '~>2.4.0' end file-tail-1.2.0/lib/0000755000004100000410000000000013321126504014153 5ustar www-datawww-datafile-tail-1.2.0/lib/file-tail.rb0000644000004100000410000000002413321126504016342 0ustar www-datawww-datarequire 'file/tail' file-tail-1.2.0/lib/file/0000755000004100000410000000000013321126504015072 5ustar www-datawww-datafile-tail-1.2.0/lib/file/tail.rb0000644000004100000410000002423513321126504016356 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" 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 # 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) end if stat.size < @stat.size @stat = nil raise ReopenException.new(:top) end end @stat = stat 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 file-tail-1.2.0/lib/file/tail/0000755000004100000410000000000013321126504016023 5ustar www-datawww-datafile-tail-1.2.0/lib/file/tail/version.rb0000644000004100000410000000042313321126504020034 0ustar www-datawww-datamodule File::Tail # File::Tail version VERSION = '1.2.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.2.0/lib/file/tail/group.rb0000644000004100000410000000701113321126504017503 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.2.0/lib/file/tail/logfile.rb0000644000004100000410000000555213321126504020000 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 << opt[: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 nil 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.2.0/lib/file/tail/tailer.rb0000644000004100000410000000202713321126504017631 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.2.0/lib/file/tail/line_extension.rb0000644000004100000410000000055513321126504021400 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.2.0/Gemfile0000644000004100000410000000021213321126504014673 0ustar www-datawww-data# vim: set filetype=ruby et sw=2 ts=2: source 'https://rubygems.org' gemspec group :development do gem 'simplecov' gem 'utils' end file-tail-1.2.0/VERSION0000644000004100000410000000000613321126504014451 0ustar www-datawww-data1.2.0