io-tail 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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