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
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# vim: set filetype=ruby et sw=2 ts=2:
|
2
|
+
|
3
|
+
require 'gem_hadar'
|
4
|
+
|
5
|
+
|
6
|
+
$: << File.join(File.dirname(__FILE__),'lib')
|
7
|
+
require 'io/tail/version'
|
8
|
+
|
9
|
+
|
10
|
+
GemHadar do
|
11
|
+
name 'io-tail'
|
12
|
+
path_name 'io/tail'
|
13
|
+
author 'Pierre Baillet'
|
14
|
+
email 'pierre@baillet.name'
|
15
|
+
homepage "http://github.com/octplane/ruby-#{name}"
|
16
|
+
summary "#{path_name.camelize} for Ruby"
|
17
|
+
description 'Library to tail files and process in Ruby'
|
18
|
+
test_dir 'tests'
|
19
|
+
ignore '.*.sw[pon]', 'pkg', 'Gemfile.lock', 'coverage', '*.rbc'
|
20
|
+
readme 'README.rdoc'
|
21
|
+
version IO::Tail::VERSION
|
22
|
+
executables 'rtail'
|
23
|
+
|
24
|
+
dependency 'tins', '~>0.3'
|
25
|
+
|
26
|
+
development_dependency 'test-unit', '~>2.4.0'
|
27
|
+
|
28
|
+
install_library do
|
29
|
+
cd 'lib' do
|
30
|
+
libdir = CONFIG["sitelibdir"]
|
31
|
+
|
32
|
+
dest = File.join(libdir, 'file')
|
33
|
+
mkdir_p(dest)
|
34
|
+
dest = File.join(libdir, path_name)
|
35
|
+
install(path_name + '.rb', dest + '.rb', :verbose => true)
|
36
|
+
mkdir_p(dest)
|
37
|
+
for file in Dir[File.join(path_name, '*.rb')]
|
38
|
+
install(file, dest, :verbose => true)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
bindir = CONFIG["bindir"]
|
42
|
+
install('bin/rtail', bindir, :verbose => true, :mode => 0755)
|
43
|
+
end
|
44
|
+
end
|
data/bin/rtail
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'io-tail'
|
5
|
+
require 'tins/go'
|
6
|
+
include Tins::GO
|
7
|
+
require 'thread'
|
8
|
+
Thread.abort_on_exception = true
|
9
|
+
|
10
|
+
$opt = go 'n:m:Mh'
|
11
|
+
if $opt['h']
|
12
|
+
puts <<EOT
|
13
|
+
Usage: #{File.basename($0)} [OPTS] PATHES
|
14
|
+
|
15
|
+
OPTS are
|
16
|
+
-n NUMBER show the last NUMBER of lines in the tailed files
|
17
|
+
-m PATTERN only tail files matching PATTERN, e. g. '*.log'
|
18
|
+
-M prefix every line with the logfile name
|
19
|
+
-h to display this help
|
20
|
+
|
21
|
+
EOT
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
|
25
|
+
dirs, logfiles = ARGV.partition { |path| File.directory?(path) }
|
26
|
+
|
27
|
+
$n = ($opt['n'] || 0).to_i
|
28
|
+
$logfiles = IO::Tail::Group.new
|
29
|
+
|
30
|
+
def add_logfiles(logfiles)
|
31
|
+
logfiles = logfiles.map { |l| File.expand_path(l) }
|
32
|
+
$opt['m'] and logfiles =
|
33
|
+
logfiles.select { |l| !$opt['m'] || File.fnmatch?($opt['m'], File.basename(l)) }
|
34
|
+
for l in logfiles
|
35
|
+
$logfiles.each_file.any? { |f| l == f.path } and next
|
36
|
+
warn "Tailing '#{l}'."
|
37
|
+
$logfiles.add_filename l, $n
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
add_logfiles logfiles
|
42
|
+
|
43
|
+
t = Thread.new do
|
44
|
+
$logfiles.tail do |line|
|
45
|
+
if $opt['M']
|
46
|
+
puts "#{line.file.path}: #{line}"
|
47
|
+
else
|
48
|
+
puts line
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
loop do
|
55
|
+
logfiles = []
|
56
|
+
for d in dirs
|
57
|
+
logfiles.concat Dir[File.join(d, '*')].select { |x|
|
58
|
+
File.file?(x) || File.symlink?(x)
|
59
|
+
}
|
60
|
+
end
|
61
|
+
add_logfiles logfiles
|
62
|
+
sleep 1
|
63
|
+
end
|
64
|
+
rescue Interrupt
|
65
|
+
warn " *** Interrupted *** "
|
66
|
+
end
|
data/examples/pager.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# A poor man's pager... :)
|
3
|
+
|
4
|
+
require 'io-tail'
|
5
|
+
|
6
|
+
filename = ARGV.shift or fail "Usage: #$0 filename [height]"
|
7
|
+
height = (ARGV.shift || ENV['LINES'] || 23).to_i - 1
|
8
|
+
|
9
|
+
IO::Tail::Logfile.open(filename, :break_if_eof => true) do |log|
|
10
|
+
begin
|
11
|
+
log.tail(height) { |line| puts line }
|
12
|
+
print "Press return key to continue!" ; gets
|
13
|
+
print ""
|
14
|
+
redo
|
15
|
+
rescue File::Tail::BreakException
|
16
|
+
end
|
17
|
+
end
|
data/examples/tail.rb
ADDED
data/lib/io-tail.rb
ADDED
data/lib/io/tail.rb
ADDED
@@ -0,0 +1,363 @@
|
|
1
|
+
class IO
|
2
|
+
module Tail
|
3
|
+
|
4
|
+
# This is the base class of all exceptions that are raised
|
5
|
+
# in IO::Tail.
|
6
|
+
class TailException < Exception; end
|
7
|
+
|
8
|
+
# The DeletedException is raised if a file is
|
9
|
+
# deleted while tailing it.
|
10
|
+
class DeletedException < TailException; end
|
11
|
+
|
12
|
+
# The ReturnException is raised and caught
|
13
|
+
# internally to implement "tail -10" behaviour.
|
14
|
+
class ReturnException < TailException; end
|
15
|
+
|
16
|
+
# The BreakException is raised if the <code>break_if_eof</code>
|
17
|
+
# attribute is set to a true value and the end of tailed file
|
18
|
+
# is reached.
|
19
|
+
class BreakException < TailException; end
|
20
|
+
|
21
|
+
# The ReopenException is raised internally if File::Tail
|
22
|
+
# gets suspicious something unusual has happend to
|
23
|
+
# the tailed file, e. g., it was rotated away. The exception
|
24
|
+
# is caught and an attempt to reopen it is made.
|
25
|
+
class ReopenException < TailException
|
26
|
+
attr_reader :mode
|
27
|
+
|
28
|
+
# Creates an ReopenException object. The mode defaults to
|
29
|
+
# <code>:bottom</code> which indicates that the file
|
30
|
+
# should be tailed beginning from the end. <code>:top</code>
|
31
|
+
# indicates, that it should be tailed from the beginning from the
|
32
|
+
# start.
|
33
|
+
def initialize(mode = :bottom)
|
34
|
+
super(self.class.name)
|
35
|
+
@mode = mode
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Tailable
|
40
|
+
|
41
|
+
# The maximum interval File::Tail sleeps, before it tries
|
42
|
+
# to take some action like reading the next few lines
|
43
|
+
# or reopening the file.
|
44
|
+
attr_accessor :max_interval
|
45
|
+
|
46
|
+
# The start value of the sleep interval. This value
|
47
|
+
# goes against <code>max_interval</code> if the tailed
|
48
|
+
# file is silent for a sufficient time.
|
49
|
+
attr_accessor :interval
|
50
|
+
|
51
|
+
# If this attribute is set to a true value, File::Tail persists
|
52
|
+
# on reopening a deleted file waiting <code>max_interval</code> seconds
|
53
|
+
# between the attempts. This is useful if logfiles are
|
54
|
+
# moved away while rotation occurs but are recreated at
|
55
|
+
# the same place after a while. It defaults to true.
|
56
|
+
attr_accessor :reopen_deleted
|
57
|
+
|
58
|
+
# If this attribute is set to a true value, File::Tail
|
59
|
+
# attempts to reopen it's tailed file after
|
60
|
+
# <code>suspicious_interval</code> seconds of silence.
|
61
|
+
attr_accessor :reopen_suspicious
|
62
|
+
|
63
|
+
# The callback is called with _self_ as an argument after a reopen has
|
64
|
+
# occured. This allows a tailing script to find out, if a logfile has been
|
65
|
+
# rotated.
|
66
|
+
def after_reopen(&block)
|
67
|
+
@after_reopen = block
|
68
|
+
end
|
69
|
+
|
70
|
+
# This attribute is the interval in seconds before File::Tail
|
71
|
+
# gets suspicious that something has happend to its tailed file
|
72
|
+
# and an attempt to reopen it is made.
|
73
|
+
#
|
74
|
+
# If the attribute <code>reopen_suspicious</code> is
|
75
|
+
# set to a non true value, suspicious_interval is
|
76
|
+
# meaningless. It defaults to 60 seconds.
|
77
|
+
attr_accessor :suspicious_interval
|
78
|
+
|
79
|
+
# If this attribute is set to a true value, File::Fail's tail method
|
80
|
+
# raises a BreakException if the end of the file is reached.
|
81
|
+
attr_accessor :break_if_eof
|
82
|
+
|
83
|
+
# If this attribute is set to a true value, File::Fail's tail method
|
84
|
+
# just returns if the end of the file is reached.
|
85
|
+
attr_accessor :return_if_eof
|
86
|
+
|
87
|
+
# Default buffer size, that is used while going backward from a file's end.
|
88
|
+
# This defaults to nil, which means that File::Tail attempts to derive this
|
89
|
+
# value from the filesystem block size.
|
90
|
+
attr_accessor :default_bufsize
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
# This method tails this file and yields to the given block for
|
95
|
+
# every new line that is read.
|
96
|
+
# If no block is given an array of those lines is
|
97
|
+
# returned instead. (In this case it's better to use a
|
98
|
+
# reasonable value for <code>n</code> or set the
|
99
|
+
# <code>return_if_eof</code> or <code>break_if_eof</code>
|
100
|
+
# attribute to a true value to stop the method call from blocking.)
|
101
|
+
#
|
102
|
+
# If the argument <code>n</code> is given, only the next <code>n</code>
|
103
|
+
# lines are read and the method call returns. Otherwise this method
|
104
|
+
# call doesn't return, but yields to block for every new line read from
|
105
|
+
# this file for ever.
|
106
|
+
def tail(n = nil, &block) # :yields: line
|
107
|
+
@n = n
|
108
|
+
result = []
|
109
|
+
array_result = false
|
110
|
+
unless block
|
111
|
+
block = lambda { |line| result << line }
|
112
|
+
array_result = true
|
113
|
+
end
|
114
|
+
preset_attributes unless @lines
|
115
|
+
loop do
|
116
|
+
begin
|
117
|
+
restat
|
118
|
+
read_line(&block)
|
119
|
+
redo
|
120
|
+
rescue ReopenException => e
|
121
|
+
handle_ReopenException(e, &block)
|
122
|
+
rescue ReturnException
|
123
|
+
return array_result ? result : nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Override me
|
129
|
+
def close
|
130
|
+
raise "Not implemented"
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def preset_attributes
|
136
|
+
@reopen_deleted = true if @reopen_deleted.nil?
|
137
|
+
@reopen_suspicious = true if @reopen_suspicious.nil?
|
138
|
+
@break_if_eof = false if @break_if_eof.nil?
|
139
|
+
@return_if_eof = false if @return_if_eof.nil?
|
140
|
+
@max_interval ||= 10
|
141
|
+
@interval ||= @max_interval
|
142
|
+
@suspicious_interval ||= 60
|
143
|
+
@lines = 0
|
144
|
+
@no_read = 0
|
145
|
+
end
|
146
|
+
|
147
|
+
def read_line(&block)
|
148
|
+
if @n
|
149
|
+
until @n == 0
|
150
|
+
block.call readline
|
151
|
+
@lines += 1
|
152
|
+
@no_read = 0
|
153
|
+
@n -= 1
|
154
|
+
output_debug_information
|
155
|
+
end
|
156
|
+
raise ReturnException
|
157
|
+
else
|
158
|
+
block.call readline
|
159
|
+
@lines += 1
|
160
|
+
@no_read = 0
|
161
|
+
output_debug_information
|
162
|
+
end
|
163
|
+
rescue EOFError
|
164
|
+
handle_EOFError
|
165
|
+
rescue Errno::ENOENT, Errno::ESTALE, Errno::EBADF
|
166
|
+
raise ReopenException
|
167
|
+
end
|
168
|
+
|
169
|
+
# Oy, override me !
|
170
|
+
def readline
|
171
|
+
raise "Not implemented"
|
172
|
+
end
|
173
|
+
|
174
|
+
# You can override this method in order to handle EOF error while reading
|
175
|
+
def handle_EOFError
|
176
|
+
raise ReopenException if @reopen_suspicious and
|
177
|
+
@no_read > @suspicious_interval
|
178
|
+
raise BreakException if @break_if_eof
|
179
|
+
raise ReturnException if @return_if_eof
|
180
|
+
sleep_interval
|
181
|
+
end
|
182
|
+
|
183
|
+
# You should overfide this method to behave correctly when a Reopen Exception is thrown
|
184
|
+
def handle_ReopenException(ex, &block)
|
185
|
+
reopen_tailable(ex.mode)
|
186
|
+
@after_reopen.call self if @after_reopen
|
187
|
+
end
|
188
|
+
# You have to implement this method in order to detect underlying IO change
|
189
|
+
def restat
|
190
|
+
# Nothing
|
191
|
+
end
|
192
|
+
|
193
|
+
def sleep_interval
|
194
|
+
if @lines > 0
|
195
|
+
# estimate how much time we will spend on waiting for next line
|
196
|
+
@interval = (@interval.to_f / @lines)
|
197
|
+
@lines = 0
|
198
|
+
else
|
199
|
+
# exponential backoff if logfile is quiet
|
200
|
+
@interval *= 2
|
201
|
+
end
|
202
|
+
if @interval > @max_interval
|
203
|
+
# max. wait @max_interval
|
204
|
+
@interval = @max_interval
|
205
|
+
end
|
206
|
+
output_debug_information
|
207
|
+
sleep @interval
|
208
|
+
@no_read += @interval
|
209
|
+
end
|
210
|
+
|
211
|
+
# You can override me too.
|
212
|
+
def output_debug_information
|
213
|
+
$DEBUG or return
|
214
|
+
STDERR.puts({
|
215
|
+
:lines => @lines,
|
216
|
+
:interval => @interval,
|
217
|
+
:no_read => @no_read,
|
218
|
+
:n => @n,
|
219
|
+
}.inspect)
|
220
|
+
self
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
|
227
|
+
class File < Tailable
|
228
|
+
attr_reader :_file
|
229
|
+
|
230
|
+
def initialize(file_or_filename = nil)
|
231
|
+
super()
|
232
|
+
if file_or_filename.is_a?(String)
|
233
|
+
@_file = ::File.new(file_or_filename, 'rb')
|
234
|
+
elsif file_or_filename.is_a?(::File)
|
235
|
+
@_file = file_or_filename
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def close
|
240
|
+
self._file.close
|
241
|
+
end
|
242
|
+
|
243
|
+
def path
|
244
|
+
self._file.path
|
245
|
+
end
|
246
|
+
|
247
|
+
def puts o
|
248
|
+
self._file.puts o
|
249
|
+
end
|
250
|
+
|
251
|
+
# Skip the first <code>n</code> lines of this file. The default is to don't
|
252
|
+
# skip any lines at all and start at the beginning of this file.
|
253
|
+
def forward(n = 0)
|
254
|
+
self._file.seek(0, ::File::SEEK_SET)
|
255
|
+
while n > 0 and not self._file.eof?
|
256
|
+
self._file.readline
|
257
|
+
n -= 1
|
258
|
+
end
|
259
|
+
self
|
260
|
+
end
|
261
|
+
# Rewind the last <code>n</code> lines of this file, starting
|
262
|
+
# from the end. The default is to start tailing directly from the
|
263
|
+
# end of the file.
|
264
|
+
#
|
265
|
+
# The additional argument <code>bufsize</code> is
|
266
|
+
# used to determine the buffer size that is used to step through
|
267
|
+
# the file backwards. It defaults to the block size of the
|
268
|
+
# filesystem this file belongs to or 8192 bytes if this cannot
|
269
|
+
# be determined.
|
270
|
+
def backward(n = 0, bufsize = nil)
|
271
|
+
if n <= 0
|
272
|
+
self._file.seek(0, ::File::SEEK_END)
|
273
|
+
return self
|
274
|
+
end
|
275
|
+
bufsize ||= default_bufsize || self._file.stat.blksize || 8192
|
276
|
+
size = self._file.stat.size
|
277
|
+
begin
|
278
|
+
if bufsize < size
|
279
|
+
self._file.seek(0, ::File::SEEK_END)
|
280
|
+
while n > 0 and self._file.tell > 0 do
|
281
|
+
start = self._file.tell
|
282
|
+
self._file.seek(-bufsize, ::File::SEEK_CUR)
|
283
|
+
buffer = self._file.read(bufsize)
|
284
|
+
n -= buffer.count("\n")
|
285
|
+
self._file.seek(-bufsize, ::File::SEEK_CUR)
|
286
|
+
end
|
287
|
+
else
|
288
|
+
self._file.seek(0, ::File::SEEK_SET)
|
289
|
+
buffer = self._file.read(size)
|
290
|
+
n -= buffer.count("\n")
|
291
|
+
self._file.seek(0, ::File::SEEK_SET)
|
292
|
+
end
|
293
|
+
rescue Errno::EINVAL
|
294
|
+
size = self._file.tell
|
295
|
+
retry
|
296
|
+
end
|
297
|
+
pos = -1
|
298
|
+
while n < 0 # forward if we are too far back
|
299
|
+
pos = buffer.index("\n", pos + 1)
|
300
|
+
n += 1
|
301
|
+
end
|
302
|
+
self._file.seek(pos + 1, ::File::SEEK_CUR)
|
303
|
+
self
|
304
|
+
end
|
305
|
+
|
306
|
+
# On EOF, we seek to position 0
|
307
|
+
# Next read will reopen the file if it has changed
|
308
|
+
def handle_EOFError
|
309
|
+
self._file.seek(0, ::File::SEEK_CUR)
|
310
|
+
super
|
311
|
+
end
|
312
|
+
|
313
|
+
def handle_ReopenException(ex, &block)
|
314
|
+
# Something wrong with the file, attempt to go until its end
|
315
|
+
# or at least to read n lines and then proceed to reopening
|
316
|
+
until self._file.eof? || @n == 0
|
317
|
+
block.call self._file.readline
|
318
|
+
@n -= 1 if @n
|
319
|
+
end
|
320
|
+
super
|
321
|
+
end
|
322
|
+
|
323
|
+
def readline
|
324
|
+
self._file.readline
|
325
|
+
end
|
326
|
+
|
327
|
+
def restat
|
328
|
+
stat = ::File.stat(self._file.path)
|
329
|
+
if @stat
|
330
|
+
if stat.ino != @stat.ino or stat.dev != @stat.dev
|
331
|
+
@stat = nil
|
332
|
+
raise ReopenException.new(:top)
|
333
|
+
end
|
334
|
+
if stat.size < @stat.size
|
335
|
+
@stat = nil
|
336
|
+
raise ReopenException.new(:top)
|
337
|
+
end
|
338
|
+
else
|
339
|
+
@stat = stat
|
340
|
+
end
|
341
|
+
rescue Errno::ENOENT, Errno::ESTALE
|
342
|
+
raise ReopenException
|
343
|
+
end
|
344
|
+
|
345
|
+
def reopen_tailable(mode)
|
346
|
+
$DEBUG and $stdout.print "Reopening '#{path}', mode = #{mode}.\n"
|
347
|
+
@no_read = 0
|
348
|
+
self._file.reopen(::File.new(self._file.path, 'rb'))
|
349
|
+
if mode == :bottom
|
350
|
+
backward
|
351
|
+
end
|
352
|
+
rescue Errno::ESTALE, Errno::ENOENT
|
353
|
+
if @reopen_deleted
|
354
|
+
sleep @max_interval
|
355
|
+
retry
|
356
|
+
else
|
357
|
+
raise DeletedException
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|