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/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
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'io-tail'
4
+
5
+ filename = ARGV.pop or fail "Usage: #$0 number filename"
6
+ number = (ARGV.pop || 0).to_i.abs
7
+
8
+ IO::Tail::Logfile.open(filename) do |log|
9
+ log.backward(number).tail { |line| puts line }
10
+ end
data/lib/io-tail.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'io/tail'
2
+ require 'io/tail/process'
3
+ require 'io/tail/version'
4
+ require 'io/tail/logfile'
5
+ require 'io/tail/group'
6
+ require 'io/tail/tailer'
7
+ require 'io/tail/line_extension'
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