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.
@@ -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,8 @@
1
+ module IO::Tail
2
+ # IO::Tail version
3
+ VERSION = '0.0.1'
4
+ VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc:
5
+ VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
+ VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
7
+ VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
8
+ 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