io-tail 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/.travis.yml +8 -0
- data/CHANGES +78 -0
- data/COPYING +340 -0
- data/Gemfile +8 -0
- data/README.rdoc +79 -0
- data/Rakefile +44 -0
- data/bin/rtail +66 -0
- data/examples/pager.rb +17 -0
- data/examples/tail.rb +10 -0
- data/lib/io-tail.rb +7 -0
- data/lib/io/tail.rb +363 -0
- data/lib/io/tail/group.rb +124 -0
- data/lib/io/tail/line_extension.rb +15 -0
- data/lib/io/tail/logfile.rb +85 -0
- data/lib/io/tail/process.rb +58 -0
- data/lib/io/tail/tailer.rb +36 -0
- data/lib/io/tail/version.rb +8 -0
- data/tests/file_tail_group_test.rb +86 -0
- data/tests/file_tail_test.rb +315 -0
- data/tests/process_tail_test.rb +106 -0
- data/tests/test_helper.rb +7 -0
- metadata +117 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
class IO
|
4
|
+
module Tail
|
5
|
+
# This class can be used to coordinate tailing of many files, which have
|
6
|
+
# been added to the group.
|
7
|
+
class Group
|
8
|
+
# Creates a new IO::Tail::Group instance.
|
9
|
+
#
|
10
|
+
# The following options can be given as arguments:
|
11
|
+
# :files:: an array of files (or filenames to open) that are placed into
|
12
|
+
# the group.
|
13
|
+
def initialize(opts = {})
|
14
|
+
@tailers = ThreadGroup.new
|
15
|
+
if files = opts[:files]
|
16
|
+
Array(files).each { |file| add file }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Creates a group for +files+ (IO instances or filename strings).
|
21
|
+
def self.[](*files)
|
22
|
+
new(:files => files)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Add a file (IO instance) or filename (responding to to_str) to this
|
26
|
+
# group.
|
27
|
+
def add(file_or_filename)
|
28
|
+
if file_or_filename.is_a?(IO::Tail::Tailable)
|
29
|
+
add_tailable file_or_filename
|
30
|
+
elsif file_or_filename.respond_to?(:to_str)
|
31
|
+
add_filename file_or_filename
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
alias << add
|
36
|
+
|
37
|
+
# Add the IO instance +file+ to this group.
|
38
|
+
def add_tailable(file)
|
39
|
+
setup_file_tailer file
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add a file created by opening +filename+ to this group after stepping
|
44
|
+
# +n+ lines backwards from the end of it.
|
45
|
+
def add_filename(filename, n = 0)
|
46
|
+
file = Logfile.open(filename.to_str, :backward => n)
|
47
|
+
file.backward n
|
48
|
+
setup_file_tailer file
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
# Iterate over all files contained in this group yielding to +block+ for
|
53
|
+
# each of them.
|
54
|
+
def each_file(&block)
|
55
|
+
each_tailer { |t| t.file }.map(&block)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Iterate over all tailers in this group yielding to +block+ for each of
|
59
|
+
# them.
|
60
|
+
def each_tailer(&block)
|
61
|
+
@tailers.list.map(&block)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Stop all tailers in this group at once.
|
65
|
+
def stop
|
66
|
+
each_tailer { |t| t.stop }
|
67
|
+
each_tailer { |t| t.join }
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
# Tail all the lines of all the files in the Tail::Group instance, that
|
72
|
+
# is yield to each of them.
|
73
|
+
#
|
74
|
+
# Every line is extended with the LineExtension module, that adds some
|
75
|
+
# methods to the line string. To get the path of the file this line was
|
76
|
+
# received from call line.file.path.
|
77
|
+
def tail
|
78
|
+
wait_for_activity do |tailer|
|
79
|
+
tailer.pending_lines.each do |line|
|
80
|
+
line.extend LineExtension
|
81
|
+
line.instance_variable_set :@tailer, tailer
|
82
|
+
yield line
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def setup_file_tailer(file)
|
90
|
+
setup = ConditionVariable.new
|
91
|
+
mutex = Mutex.new
|
92
|
+
ft = nil
|
93
|
+
mutex.synchronize do
|
94
|
+
ft = Tailer.new do
|
95
|
+
t = Thread.current
|
96
|
+
t[:queue] = Queue.new
|
97
|
+
t[:file] = file
|
98
|
+
mutex.synchronize do
|
99
|
+
setup.signal
|
100
|
+
end
|
101
|
+
file.tail { |line| t[:queue] << line }
|
102
|
+
end
|
103
|
+
setup.wait mutex
|
104
|
+
end
|
105
|
+
@tailers.add ft
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
|
109
|
+
# Wait until new input is receіved on any of the tailers in the group. If
|
110
|
+
# so call +block+ with all of these trailers as an argument.
|
111
|
+
def wait_for_activity(&block)
|
112
|
+
loop do
|
113
|
+
pending = each_tailer.select(&:pending_lines?)
|
114
|
+
if pending.empty?
|
115
|
+
interval = each_file.map { |t| t.interval }.compact.min || 0.1
|
116
|
+
sleep interval
|
117
|
+
else
|
118
|
+
pending.each(&block)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class IO
|
2
|
+
module Tail
|
3
|
+
# This module is used to extend all lines received via one of the tailers
|
4
|
+
# of a File::Tail::Group.
|
5
|
+
module LineExtension
|
6
|
+
# The file as a File instance this line was read from.
|
7
|
+
def file
|
8
|
+
tailer.file
|
9
|
+
end
|
10
|
+
|
11
|
+
# This is the tailer this line was received from.
|
12
|
+
attr_reader :tailer
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class IO
|
2
|
+
module Tail
|
3
|
+
# This is an easy to use Logfile class that includes
|
4
|
+
# the File::Tail module.
|
5
|
+
#
|
6
|
+
# === Usage
|
7
|
+
# The unix command "tail -10f filename" can be emulated like that:
|
8
|
+
# File::Tail::Logfile.open(filename, :backward => 10) do |log|
|
9
|
+
# log.tail { |line| puts line }
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# Or a bit shorter:
|
13
|
+
# File::Tail::Logfile.tail(filename, :backward => 10) do |line|
|
14
|
+
# puts line
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# To skip the first 10 lines of the file do that:
|
18
|
+
# File::Tail::Logfile.open(filename, :forward => 10) do |log|
|
19
|
+
# log.tail { |line| puts line }
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# The unix command "head -10 filename" can be emulated like that:
|
23
|
+
# File::Tail::Logfile.open(filename, :return_if_eof => true) do |log|
|
24
|
+
# log.tail(10) { |line| puts line }
|
25
|
+
# end
|
26
|
+
class Logfile < File
|
27
|
+
|
28
|
+
# This method creates an File::Tail::Logfile object and
|
29
|
+
# yields to it, and closes it, if a block is given, otherwise it just
|
30
|
+
# returns it. The opts hash takes an option like
|
31
|
+
# * <code>:backward => 10</code> to go backwards
|
32
|
+
# * <code>:forward => 10</code> to go forwards
|
33
|
+
# in the logfile for 10 lines at the start. The buffersize
|
34
|
+
# for going backwards can be set with the
|
35
|
+
# * <code>:bufsiz => 8192</code> option.
|
36
|
+
# To define a callback, that will be called after a reopening occurs, use:
|
37
|
+
# * <code>:after_reopen => lambda { |file| p file }</code>
|
38
|
+
#
|
39
|
+
# Every attribute of File::Tail can be set with a <code>:attributename =>
|
40
|
+
# value</code> option.
|
41
|
+
def self.open(filename, opts = {}, &block) # :yields: file
|
42
|
+
file = new filename
|
43
|
+
opts.each do |o, v|
|
44
|
+
writer = o.to_s + "="
|
45
|
+
file.__send__(writer, v) if file.respond_to? writer
|
46
|
+
end
|
47
|
+
if opts.key?(:wind) or opts.key?(:rewind)
|
48
|
+
warn ":wind and :rewind options are deprecated, "\
|
49
|
+
"use :forward and :backward instead!"
|
50
|
+
end
|
51
|
+
if backward = opts[:backward] || opts[:rewind]
|
52
|
+
(args = []) << backward
|
53
|
+
args << opt[:bufsiz] if opts[:bufsiz]
|
54
|
+
file.backward(*args)
|
55
|
+
elsif forward = opts[:forward] || opts[:wind]
|
56
|
+
file.forward(forward)
|
57
|
+
end
|
58
|
+
if opts[:after_reopen]
|
59
|
+
file.after_reopen(&opts[:after_reopen])
|
60
|
+
end
|
61
|
+
if block_given?
|
62
|
+
begin
|
63
|
+
block.call file
|
64
|
+
ensure
|
65
|
+
file.close
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
else
|
69
|
+
file
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Like open, but yields to every new line encountered in the logfile in
|
74
|
+
# +block+.
|
75
|
+
def self.tail(filename, opts = {}, &block)
|
76
|
+
if ([ :forward, :backward ] & opts.keys).empty?
|
77
|
+
opts[:backward] = 0
|
78
|
+
end
|
79
|
+
open(filename, opts) do |log|
|
80
|
+
log.tail { |line| block.call line }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
class IO
|
2
|
+
# This module can be included in your own File subclasses or used to extend
|
3
|
+
# files you want to tail.
|
4
|
+
module Tail
|
5
|
+
# A Process that's run and tailed
|
6
|
+
class Process < IO::Tail::Tailable
|
7
|
+
|
8
|
+
attr_accessor :_command
|
9
|
+
attr_reader :_process
|
10
|
+
|
11
|
+
def initialize(command = nil)
|
12
|
+
super()
|
13
|
+
if command
|
14
|
+
@_command = command
|
15
|
+
self.reopen_tailable
|
16
|
+
end
|
17
|
+
end
|
18
|
+
# Taialble process should never have a EOF
|
19
|
+
# unless they are no longer tailable
|
20
|
+
def handle_EOFError
|
21
|
+
# Attempt to reopen
|
22
|
+
if @reopen_suspicious
|
23
|
+
raise ReopenException
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Ignore the mode
|
28
|
+
def reopen_tailable(mode = 'dummy')
|
29
|
+
@_process = IO.popen(@_command) if @_command
|
30
|
+
end
|
31
|
+
def readline
|
32
|
+
self._process.readline
|
33
|
+
end
|
34
|
+
# Used for testing purposes
|
35
|
+
def kill_inner
|
36
|
+
return if !self._process
|
37
|
+
killable = self._process.pid
|
38
|
+
$stdout.flush
|
39
|
+
begin
|
40
|
+
::Process.kill 'INT', killable
|
41
|
+
::Process.kill 'KILL', killable
|
42
|
+
rescue Exception => e
|
43
|
+
# Already killed ? Fine.
|
44
|
+
end
|
45
|
+
end
|
46
|
+
def close
|
47
|
+
return if !self._process
|
48
|
+
# We have to do that to stop the IO
|
49
|
+
self.kill_inner
|
50
|
+
begin
|
51
|
+
self._process.close
|
52
|
+
rescue Exception => e
|
53
|
+
# Ignore
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end # module Tail
|
58
|
+
end # class File
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class IO
|
2
|
+
module Tail
|
3
|
+
# This class supervises activity on a tailed fail and collects newly read
|
4
|
+
# lines until the Tail::Group fetches and processes them.
|
5
|
+
class Tailer < ::Thread
|
6
|
+
|
7
|
+
# True if there are any lines pending on this Tailer, false otherwise.
|
8
|
+
def pending_lines?
|
9
|
+
!queue.empty?
|
10
|
+
end
|
11
|
+
|
12
|
+
# Fetch all the pending lines from this Tailer and thereby remove them
|
13
|
+
# from the Tailer's queue.
|
14
|
+
def pending_lines
|
15
|
+
Array.new(queue.size) { queue.deq(true) }
|
16
|
+
end
|
17
|
+
|
18
|
+
alias stop exit # Stop tailing this file and remove it from its File::Tail::Group.
|
19
|
+
|
20
|
+
# Return true if the thread local variable +id+ is defined or if this
|
21
|
+
# object responds to the method +id+.
|
22
|
+
def respond_to?(id)
|
23
|
+
!self[id].nil? || super
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the thread local variable +id+ if it is defined.
|
27
|
+
def method_missing(id, *args, &block)
|
28
|
+
if args.empty? && !(value = self[id]).nil?
|
29
|
+
value
|
30
|
+
else
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__)
|
4
|
+
$: << File.join(File.dirname(__FILE__),'..', 'lib')
|
5
|
+
|
6
|
+
require 'test_helper'
|
7
|
+
require 'io-tail'
|
8
|
+
require 'timeout'
|
9
|
+
require 'thread'
|
10
|
+
require 'tempfile'
|
11
|
+
Thread.abort_on_exception = true
|
12
|
+
|
13
|
+
class FileTailGroupTest < Test::Unit::TestCase
|
14
|
+
|
15
|
+
def test_create_group
|
16
|
+
t, = make_file
|
17
|
+
g = IO::Tail::Group[t]
|
18
|
+
assert_equal t.path, g.each_tailer.first.file.path
|
19
|
+
assert_equal t.path, g.each_file.first.path
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_stop_group
|
23
|
+
t, = make_file
|
24
|
+
g = IO::Tail::Group[t]
|
25
|
+
assert_equal t.path, g.each_tailer.first.file.path
|
26
|
+
assert_equal t.path, g.each_file.first.path
|
27
|
+
g.stop
|
28
|
+
assert_nil g.each_file.first
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_add_file_to_group
|
32
|
+
g = IO::Tail::Group.new
|
33
|
+
t, = make_file
|
34
|
+
g.add_tailable t
|
35
|
+
assert_equal t.path, g.each_tailer.first.file.path
|
36
|
+
assert_equal t.path, g.each_file.first.path
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_add_filename_to_group
|
40
|
+
g = IO::Tail::Group.new
|
41
|
+
t, name = make_file
|
42
|
+
t.close
|
43
|
+
g.add name
|
44
|
+
assert_equal name, g.each_tailer.first.file.path
|
45
|
+
assert_equal t.path, g.each_file.first.path
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_add_generic_to_group
|
49
|
+
g = IO::Tail::Group.new
|
50
|
+
t1, n1 = make_file
|
51
|
+
t1.close
|
52
|
+
t2, n1 = make_file
|
53
|
+
g << n1
|
54
|
+
g << t2
|
55
|
+
assert g.each_tailer.any? { |t| t.file.path == n1 }
|
56
|
+
assert g.each_tailer.any? { |t| t.file.path == t2.path }
|
57
|
+
assert g.each_file.any? { |t| t.path == n1 }
|
58
|
+
assert g.each_file.any? { |t| t.path == t2.path }
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_tail_multiple_files
|
62
|
+
t1, = make_file
|
63
|
+
t1.max_interval = 0.1
|
64
|
+
t2, = make_file
|
65
|
+
t2.max_interval = 0.1
|
66
|
+
g = IO::Tail::Group[t1, t2]
|
67
|
+
q = Queue.new
|
68
|
+
t = Thread.new do
|
69
|
+
g.tail { |l| q << l }
|
70
|
+
end
|
71
|
+
t1.puts "foo"
|
72
|
+
assert_equal "foo\n", q.pop
|
73
|
+
t2.puts "bar"
|
74
|
+
assert_equal "bar\n", q.pop
|
75
|
+
ensure
|
76
|
+
t and t.exit
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def make_file
|
82
|
+
name = File.expand_path(::File.join(Dir.tmpdir, "tmp.#$$"))
|
83
|
+
file = File.open(name, 'w+')
|
84
|
+
return IO::Tail::File.new(file), name
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,315 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__)
|
4
|
+
$: << File.join(File.dirname(__FILE__),'..', 'lib')
|
5
|
+
|
6
|
+
require 'test_helper'
|
7
|
+
require 'io-tail'
|
8
|
+
require 'timeout'
|
9
|
+
require 'thread'
|
10
|
+
Thread.abort_on_exception = true
|
11
|
+
|
12
|
+
class FileTailTest < Test::Unit::TestCase
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@out = File.new("test.#$$", "wb")
|
16
|
+
append(@out, 100)
|
17
|
+
in_file = ::File.new(@out.path, "rb")
|
18
|
+
@in = IO::Tail::File.new(in_file)
|
19
|
+
@in.interval = 0.4
|
20
|
+
@in.max_interval = 0.8
|
21
|
+
@in.reopen_deleted = true # is default
|
22
|
+
@in.reopen_suspicious = true # is default
|
23
|
+
@in.suspicious_interval = 60
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_forward
|
27
|
+
[ 0, 1, 2, 10, 100 ].each do |lines|
|
28
|
+
@in.forward(lines)
|
29
|
+
assert_equal(100 - lines, count(@in))
|
30
|
+
end
|
31
|
+
@in.forward(101)
|
32
|
+
assert_equal(0, count(@in))
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_backward
|
36
|
+
[ 0, 1, 2, 10, 100 ].each do |lines|
|
37
|
+
@in.backward(lines)
|
38
|
+
assert_equal(lines, count(@in))
|
39
|
+
end
|
40
|
+
@in.backward(101)
|
41
|
+
assert_equal(100, count(@in))
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_backward_small_buffer
|
45
|
+
[ 0, 1, 2, 10, 100 ].each do |lines|
|
46
|
+
@in.backward(lines, 100)
|
47
|
+
assert_equal(lines, count(@in))
|
48
|
+
end
|
49
|
+
@in.backward(101, 100)
|
50
|
+
assert_equal(100, count(@in))
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_backward_small_buffer2
|
54
|
+
@in.default_bufsize = 100
|
55
|
+
[ 0, 1, 2, 10, 100 ].each do |lines|
|
56
|
+
@in.backward(lines)
|
57
|
+
assert_equal(lines, count(@in))
|
58
|
+
end
|
59
|
+
@in.backward(101)
|
60
|
+
assert_equal(100, count(@in))
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_tail_with_block_without_n
|
64
|
+
timeout(10) do
|
65
|
+
lines = []
|
66
|
+
@in.backward(1)
|
67
|
+
assert_raises(TimeoutError) do
|
68
|
+
timeout(1) { @in.tail { |l| lines << l } }
|
69
|
+
end
|
70
|
+
assert_equal(1, lines.size)
|
71
|
+
#
|
72
|
+
lines = []
|
73
|
+
@in.backward(10)
|
74
|
+
assert_raises(TimeoutError) do
|
75
|
+
timeout(1) { @in.tail { |l| lines << l } }
|
76
|
+
end
|
77
|
+
assert_equal(10, lines.size)
|
78
|
+
#
|
79
|
+
lines = []
|
80
|
+
@in.backward(100)
|
81
|
+
assert_raises(TimeoutError) do
|
82
|
+
timeout(1) { @in.tail { |l| lines << l } }
|
83
|
+
end
|
84
|
+
assert_equal(100, lines.size)
|
85
|
+
#
|
86
|
+
lines = []
|
87
|
+
@in.backward(101)
|
88
|
+
assert_raises(TimeoutError) do
|
89
|
+
timeout(1) { @in.tail { |l| lines << l } }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_tail_with_block_with_n
|
95
|
+
timeout(10) do
|
96
|
+
@in.backward(1)
|
97
|
+
lines = []
|
98
|
+
timeout(1) { @in.tail(0) { |l| lines << l } }
|
99
|
+
assert_equal(0, lines.size)
|
100
|
+
#
|
101
|
+
@in.backward(1)
|
102
|
+
lines = []
|
103
|
+
timeout(1) { @in.tail(1) { |l| lines << l } }
|
104
|
+
assert_equal(1, lines.size)
|
105
|
+
#
|
106
|
+
@in.backward(10)
|
107
|
+
lines = []
|
108
|
+
timeout(1) { @in.tail(10) { |l| lines << l } }
|
109
|
+
assert_equal(10, lines.size)
|
110
|
+
#
|
111
|
+
@in.backward(100)
|
112
|
+
lines = []
|
113
|
+
@in.backward(1)
|
114
|
+
assert_raises(TimeoutError) do
|
115
|
+
timeout(1) { @in.tail(2) { |l| lines << l } }
|
116
|
+
end
|
117
|
+
assert_equal(1, lines.size)
|
118
|
+
#
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_tail_without_block_with_n
|
123
|
+
timeout(10) do
|
124
|
+
@in.backward(1)
|
125
|
+
lines = []
|
126
|
+
timeout(1) { lines += @in.tail(0) }
|
127
|
+
assert_equal(0, lines.size)
|
128
|
+
#
|
129
|
+
@in.backward(1)
|
130
|
+
lines = []
|
131
|
+
timeout(1) { lines += @in.tail(1) }
|
132
|
+
assert_equal(1, lines.size)
|
133
|
+
#
|
134
|
+
@in.backward(10)
|
135
|
+
lines = []
|
136
|
+
timeout(1) { lines += @in.tail(10) }
|
137
|
+
assert_equal(10, lines.size)
|
138
|
+
#
|
139
|
+
@in.backward(100)
|
140
|
+
lines = []
|
141
|
+
@in.backward(1)
|
142
|
+
assert_raises(TimeoutError) do
|
143
|
+
timeout(1) { lines += @in.tail(2) }
|
144
|
+
end
|
145
|
+
assert_equal(0, lines.size)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_tail_withappend
|
150
|
+
@in.backward
|
151
|
+
lines = []
|
152
|
+
logger = Thread.new do
|
153
|
+
begin
|
154
|
+
timeout(1) { @in.tail { |l| lines << l } }
|
155
|
+
rescue TimeoutError
|
156
|
+
end
|
157
|
+
end
|
158
|
+
appender = Thread.new { append(@out, 10) }
|
159
|
+
appender.join
|
160
|
+
logger.join
|
161
|
+
assert_equal(10, lines.size)
|
162
|
+
end
|
163
|
+
|
164
|
+
def test_tail_truncated
|
165
|
+
@in.backward
|
166
|
+
lines = []
|
167
|
+
logger = Thread.new do
|
168
|
+
begin
|
169
|
+
timeout(1) do
|
170
|
+
@in.tail do |l|
|
171
|
+
lines << l
|
172
|
+
end
|
173
|
+
end
|
174
|
+
rescue TimeoutError
|
175
|
+
end
|
176
|
+
end
|
177
|
+
appender = Thread.new do
|
178
|
+
until logger.stop?
|
179
|
+
sleep 0.1
|
180
|
+
end
|
181
|
+
@out.close
|
182
|
+
File.truncate(@out.path, 0)
|
183
|
+
@out = File.new(@in._file.path, "ab")
|
184
|
+
append(@out, 10)
|
185
|
+
end
|
186
|
+
appender.join
|
187
|
+
logger.join
|
188
|
+
assert_equal(10, lines.size)
|
189
|
+
end
|
190
|
+
|
191
|
+
def test_tail_remove
|
192
|
+
return if File::PATH_SEPARATOR == ';' # Grmpf! Windows...
|
193
|
+
@in.backward
|
194
|
+
reopened = false
|
195
|
+
@in.after_reopen { |f| reopened = true }
|
196
|
+
lines = []
|
197
|
+
logger = Thread.new do
|
198
|
+
begin
|
199
|
+
timeout(2) do
|
200
|
+
@in.tail do |l|
|
201
|
+
lines << l
|
202
|
+
end
|
203
|
+
end
|
204
|
+
rescue TimeoutError
|
205
|
+
end
|
206
|
+
end
|
207
|
+
appender = Thread.new do
|
208
|
+
until logger.stop?
|
209
|
+
sleep 0.1
|
210
|
+
end
|
211
|
+
@out.close
|
212
|
+
File.unlink(@out.path)
|
213
|
+
@out = File.new(@in._file.path, "wb")
|
214
|
+
append(@out, 10)
|
215
|
+
end
|
216
|
+
appender.join
|
217
|
+
logger.join
|
218
|
+
assert_equal(10, lines.size)
|
219
|
+
assert reopened
|
220
|
+
end
|
221
|
+
|
222
|
+
def test_tail_remove2
|
223
|
+
return if File::PATH_SEPARATOR == ';' # Grmpf! Windows...
|
224
|
+
@in.backward
|
225
|
+
reopened = false
|
226
|
+
@in.after_reopen { |f| reopened = true }
|
227
|
+
lines = []
|
228
|
+
logger = Thread.new do
|
229
|
+
begin
|
230
|
+
timeout(2) do
|
231
|
+
@in.tail do |l|
|
232
|
+
lines << l
|
233
|
+
end
|
234
|
+
end
|
235
|
+
rescue TimeoutError
|
236
|
+
end
|
237
|
+
end
|
238
|
+
appender = Thread.new do
|
239
|
+
until logger.stop?
|
240
|
+
sleep 0.1
|
241
|
+
end
|
242
|
+
@out.close
|
243
|
+
File.unlink(@out.path)
|
244
|
+
@out = File.new(@in._file.path, "wb")
|
245
|
+
append(@out, 10)
|
246
|
+
sleep 1
|
247
|
+
append(@out, 10)
|
248
|
+
File.unlink(@out.path)
|
249
|
+
@out = File.new(@in._file.path, "wb")
|
250
|
+
append(@out, 10)
|
251
|
+
end
|
252
|
+
appender.join
|
253
|
+
logger.join
|
254
|
+
assert_equal(30, lines.size)
|
255
|
+
assert reopened
|
256
|
+
end
|
257
|
+
|
258
|
+
def test_tail_remove3
|
259
|
+
return if File::PATH_SEPARATOR == ';' # Grmpf! Windows...
|
260
|
+
@in.backward
|
261
|
+
reopened = false
|
262
|
+
@in.after_reopen { |f| reopened = true }
|
263
|
+
lines = []
|
264
|
+
logger = Thread.new do
|
265
|
+
begin
|
266
|
+
timeout(2) do
|
267
|
+
@in.tail(15) do |l|
|
268
|
+
lines << l
|
269
|
+
end
|
270
|
+
end
|
271
|
+
rescue TimeoutError
|
272
|
+
end
|
273
|
+
end
|
274
|
+
appender = Thread.new do
|
275
|
+
until logger.stop?
|
276
|
+
sleep 0.1
|
277
|
+
end
|
278
|
+
@out.close
|
279
|
+
File.unlink(@out.path)
|
280
|
+
@out = File.new(@in._file.path, "wb")
|
281
|
+
append(@out, 10)
|
282
|
+
sleep 1
|
283
|
+
append(@out, 10)
|
284
|
+
File.unlink(@out.path)
|
285
|
+
@out = File.new(@in._file.path, "wb")
|
286
|
+
append(@out, 10)
|
287
|
+
end
|
288
|
+
appender.join
|
289
|
+
logger.join
|
290
|
+
assert_equal(15, lines.size)
|
291
|
+
assert reopened
|
292
|
+
end
|
293
|
+
|
294
|
+
def teardown
|
295
|
+
@in.close
|
296
|
+
@out.close
|
297
|
+
File.unlink(@out.path)
|
298
|
+
end
|
299
|
+
|
300
|
+
private
|
301
|
+
|
302
|
+
def count(file)
|
303
|
+
n = 0
|
304
|
+
until file._file.eof?
|
305
|
+
file.readline
|
306
|
+
n += 1
|
307
|
+
end
|
308
|
+
return n
|
309
|
+
end
|
310
|
+
|
311
|
+
def append(file, n, size = 70)
|
312
|
+
(1..n).each { |x| file << "#{x} #{"A" * size}\n" }
|
313
|
+
file.flush
|
314
|
+
end
|
315
|
+
end
|