Forward references?

L

Lloyd Zusman

Robert Klemme said:
Lloyd Zusman said:
[ ... ]

I refactored the code even more, based on our discussions and some ideas
of my own. If you're interested, I can privately email you the latest
version.

[x] interested [ ] not interested

Here it is. Let me know what you think. And thank you for the useful
and interesting discussion in the mailing list.

#!/usr/local/bin/ruby

# Do a 'tail -f' simultaneously multiple files, interspersing their
# output. Continue tailing any file that has been replaced by a new
# version, as in the following, over-simplified example:
#
# while :
# do
# something >>something.log &
# pid=$!
# # ... time passes ...
# rm -f something.log.old
# mv something.log something.log.old
# kill $pid
# done
#
# See the 'usage' routine, below, for a description of the command
# line options and arguments.

require 'sync'
require 'getoptlong'

$program = File.basename($0)

$stdout.extend(Sync_m)
$stdout.sync = 1

$stderr.sync = 1

$waitTime = 0.25

$defColumns = 80
$defLines = 80

$maxBlocksize = 1024

# Default values for flags that are set via the command line.
$tailf = true
$fnamePrefix = false

$opts = GetoptLong.new(
[ "--lines", "-l", GetoptLong::REQUIRED_ARGUMENT ],
[ "--exit", "-x", GetoptLong::NO_ARGUMENT ],
[ "--name", "-n", GetoptLong::NO_ARGUMENT ],
[ "--help", "-h", GetoptLong::NO_ARGUMENT ]
)

# My list of threads.
$fileThreads = [].extend(Sync_m)

# Main routine
def rtail

# Calculate the size of a screen so we can choose a reasonable
# number of lines to tail.

screenColumns = (ENV['COLUMNS'] == nil ? $defColumns : ENV['COLUMNS']).to_i
if screenColumns < 1 then
# In case ENV['COLUMNS'] was set to something <= 0
screenColumns = $defColumns
end

screenLines = (ENV['LINES'] == nil ? $defLines : ENV['LINES']).to_i
if screenLines < 1 then
# In case ENV['LINES'] was set to something <= 0
screenLines = $defLines
end

# One more full line than the maximum that the screen can hold ...

$backwards = screenColumns * (screenLines + 1)


# Parse and evaluate command-line options. Temporarily change
# $0 to be the basename prepended by a newline so that the error
# message that GetoptLong outputs looks good in the case where
# an invalid option was entered.

oldDollar0 = $0
$0 = "\n" + $program

begin

$opts.each do

|opt, arg|

case opt
when "--exit"
$tailf = false
when "--lines"
screenLines = arg.to_i + 0
when "--name"
$fnamePrefix = true
when "--help"
usage
# notreached
else
usage
# notreached
end
end

rescue
usage
# notreached
ensure
$0 = oldDollar0 # just in case we need $0 later on
end

if ARGV.length < 1 then
usage
# notreached
end

# Signal handler.
[ 'SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGTERM' ].each {
|sig|
trap(sig) {
abortThreads(Thread.list.reject {
|t|
t == Thread.main
})
raise "\n!!! aborted"
# notreached
}
}

# Start a thread to tail each file whose name appears on
# the command line. The threads for any file that cannot
# be opened for reading will die and will be reaped in
# the main loop, below.
ARGV.each {
|arg|
$fileThreads.synchronize {
$fileThreads << Thread.new(arg, $tailf, &$fileReadProc)
}
}

# Main loop: reap dead threads and exit once there are no more
# threads that are alive.
loop {
tcount = 0
$fileThreads.synchronize {
tcount = $fileThreads.length
}
if tcount < 1 then
break
else
# Don't eat up too much of my CPU time
waitFor($waitTime)
end
}

# Bye-bye
return 0
end

# This is a mixin for adding a textfile? method to a class that
# behaves like IO. It also adds an externally callable
# TextTester.text? method to test a block of data.

module TextTester

private
# List of items that I want to treat as being normal text
# characters. The first line adds a lot of European characters
# that are not normally considered to be text characters in
# the traditional routines that distinguish between text and
# binary files. This is used within the 'textfile?' method.
@@textpats = [ "^áéíóúàèìòùäëïöüøçñÁÉÍÓÚÀÈÌÒÙÄËÏÖÜØÇÑ¡¿",
"^ -~",
"^\b\f\t\r\n" ]

public

# This is my own, special-purpose test for text-ness. I don't want to
# treat certain European characters as binary. If the 'testsize'
# argument is non-nil, try to read a buffer of that size; otherwise,
# calculate the buffer size here. If the 'restorePosition' argument
# is true, make sure that the the position pointer within the IO
# handle gets repositioned back to its initial value after this test
# is performed.
#
# This method is callable directly from outside the module. Hence,
# I define it as self.text? here.

def self.text?(block, len = nil)
if len.nil? then
len = block.length
end
return (block.count(*@@textpats) < (len / 3.0) and block.count("\x00") < 1)
end

def textfile?(testsize = nil, restorePosition = true)
begin
if restorePosition then
pos = self.pos
else
pos = nil
end
if testsize.nil? then
testsize = [ self.stat.blocksize,
self.stat.bytesize,
$maxBlocksize ].min
end
block = self.read(testsize)
len = block.length
if len < 1 then
return true # Provisionally treat a zero-length file as a text file.
end

# I need to call text? both inside and outside of this module.
# Therefore, I have to define that method as self.text?, which
# requires me to explicitly reference it off of TextTester here.
result = TextTester.text?(block, len)
unless pos.nil?
self.seek(pos, IO::SEEK_SET)
end
return result
rescue
return false
end
end
end

# Add the test for a text file into the IO class.

class IO
include TextTester
end


# Do a timed 'wait'.

def waitFor(duration)
startTime = Time.now.to_f
select(nil, nil, nil, duration)
Thread.pass
# We could be back here long before 'duration' has passed.
# The loop below makes sure that we wait at least as long
# as this specified interval.
while (elapsed = (Time.now.to_f - startTime)) < duration
select(nil, nil, nil, 0.001)
Thread.pass
end
# Return the actual amount of time that elapsed. This is
# guaranteed to be >= 'duration'.
return elapsed
end


# We make sure that $stdout is synchronized so that lines of
# data coming from different threads don't garble each other.

def syncwrite(text)
begin
$stdout.synchronize(Sync::EX) {
$stdout.write(text)
}
rescue
# Fall back to normal, non-sync writing
$stdout.write(text)
end
end


# Decide whether to output a block as is, or with a prefix
# at the beginning of each line. In the "as is" case, just
# send the whole block to 'syncwrite'; otherwise, split into
# lines and prepend the prefix before outputting. In other
# words, we only incur the cost of splitting the block when
# we absolutely have to.

def output(item)
prefix, block = item
if prefix.nil? or prefix.length < 1 then
syncwrite(block)
else
block.split(/\r*\n/).each {
|line|
syncwrite(prefix + line + "\n")
}
end
end


# Remove a group of threads from the list and kill each one.

def abortThreads(tlist)
$fileThreads.synchronize {
tlist.each {
|t|
$fileThreads.delete(t)
t.kill
}
}
end


# Remove myself from the thread list and kill myself.
def abortMyself
abortThreads([Thread.current])
# notreached
end


# Close the specified IO handle and kill the containing thread
# if this fails.

def closeOrDie(f)
begin
f.close()
rescue
output([nil, "!!! unable to close file: #{item}\n"])
abortMyself()
# notreached
end
end


# This is the main thread proc for tailing a given file.

$fileReadProc = Proc.new do
|item, follow|

# Open the file, make sure it's a text file, read the last bit
# at the end, and output it. Kill the containing thread if any
# of this fails.

begin
f = File.open(item, 'r')
rescue
output([nil, "!!! unable to open: #{item}\n"])
abortMyself()
# notreached
end

# Get some info about the open file
begin
f.sync = true
bytesize = f.stat.size
blocksize = f.stat.blksize
inode = f.stat.ino
rescue
f.close
output([nil, "!!! unable to stat: #{item}\n"])
abortMyself()
# notreached
end

# Blocksize will be nil or zero if the device being opened
# is not a disk file. Bytesize will also be nil in this case.
if blocksize.nil? or blocksize < 1 or bytesize.nil? then
f.close
output([nil, "!!! invalid device: #{item}\n"])
abortMyself()
# notreached
end

# Test for text-ness using one blocksize unit, or the length
# of the file if that is smaller. This is done in two statements
# because we need to use 'blocksize' by itself, further down in
# this procedure.
blocksize = [ blocksize, $maxBlocksize ].min
testsize = [ blocksize, bytesize ].min
unless f.textfile?(testsize, false) then
f.close
output([nil, "!!! not a text file: #{item}\n"])
abortMyself()
# notreached
end

# Set the optional output line prefix.
if $fnamePrefix then
prefix = File.basename(item) + ': '
else
prefix = nil
end

textTestLength = 0

# Position to a suitable point near the end of the file,
# and then read and output the data from that point until
# the end.
begin
if bytesize > $backwards then
pos = bytesize - $backwards
else
pos = 0
end
f.seek(pos, IO::SEEK_SET)
if pos > 0 then
f.gets # discard possible line fragment
end
readSoFar = f.read
textTestLength = readSoFar.length
output([prefix, readSoFar])
rescue
end

# If we have made it here, we've read the last bit of the file
# and have output it. Now, if we're not in 'follow' mode, we
# just exit.
unless follow then
f.close
abortMyself()
# notreached
end

# We only arrive here if we're in 'follow' mode. In this case,
# we keep looping to test if there is any more data to output.
loop {
#
# The file might have been closed due to it having disappeared
# or having changed names. If so, reopen it.
#
if f.closed? then
begin
f = File.open(item, 'r')
f.sync = true
textTextLength = 0
readSoFar = ''
inode = f.stat.ino
output([nil, "!!! reopened: #{item}\n"])
# Fall through to the EOF test.
rescue
output([nil, "!!! disappeared: #{item}\n"])
begin
f.close
rescue
end
abortMyself()
# notreached
end
else # file is not closed
#
# File was not previously closed, so we can test to see if it
# has changed or disappeared.
#
# Get the current inode of the file. This is needed to test
# whether or not the file has disappeared and whether or not there
# is a new file by the same name. This is not 100-percent
# conclusive, since a new file might accidentally end up with the
# same inode of an older, deleted file.
#
begin
newinode = File.stat(item).ino
# Fall through to the EOF test.
rescue
# If we're here, the file has disappeared. Close the handle,
# wait a bit, and then try to reopen it.
closeOrDie(f)
waitFor($waitTime)
redo # go back and iterate again
# notreached
end

if newinode != inode then
# If we're here, the file was replaced by a new file of
# the same name. Close the handle, wait a bit, and then
# try to reopen it.
closeOrDie(f)
waitFor($waitTime)
redo # go back and iterate again
# notreached
end

end # f.closed? ... else ...

# The only way that we can get to this point is if the file is
# properly open and it hasn't been deleted or replaced.

if f.eof? then
# If we're here, we're at EOF. Reset the EOF indicator and
# try again.
f.seek(0, IO::SEEK_CUR)
waitFor($waitTime)
redo # go back and iterate again
# notreached
end

# If we're here, we're not at EOF.

if f.pos < f.stat.size then

# If we're here, more data was added to the file since the last
# time we checked. Output this data, relinquish control to
# other threads, and then repeat the loop.
#
# If we haven't yet tested a full block's worth of bytes
# for text-ness, continue that test here.

data = f.read
if textTestLength < blocksize then
len = data.length
textTestLength += len
readSoFar << data
if len > 0 and not TextTester.text?(readSoFar) then
# If we're here, it's not a text file after all.
closeOrDie(f)
output([nil, "!!! not a text file: #{item}\n"])
abortMyself()
# notreached
end
end
output([prefix, data])
Thread.pass
redo # go back and iterate again
# notreached
end

# If we're here, the file hasn't changed since last time.
# Wait a bit so as to not eat up too much CPU time.

waitFor($waitTime)

} # end of loop

end # end of thread proc


# Print a usage message and exit.
def usage
raise <<EOD

usage: #{$program} [ options ] file [ ... ]

options:

--help, -h print this usage message

--lines=<n>, -l <n> tail <n> lines of each file (default #{$defLines})

--exit, -x exit after showing initial tail

--name, -n prepend file basename on each line that is output

EOD
# notreached

end


# Run it
begin
result = rtail
rescue Exception => e
$stderr.puts(e)
result = 1
end
exit(result)

__END__
 
L

Lloyd Zusman

Lloyd Zusman said:
[ ... ]

Here it is. Let me know what you think. And thank you for the useful
and interesting discussion in the mailing list.

[ ... ]

OOPS! I apologize to the list for the bandwidth. I forgot to remove
the Newsgroups: line when I emailed this to Robert (I post via gnus
and access this mailing list via gmane).
 
L

Lloyd Zusman

Clifford Heath said:
Except of course that you are in fact doing the exact opposite
of synchronizing - you're *asynchronizing*. Or enforcing MUTual
EXclusion... names mean different things to different people,
you seem to have absorbed the inverted (Java?) meaning of
"synchronize" :).

Yes, I guess I have. But then again, each of these mixins (Mutex_m,
MonitorMixin, Sync_m) has a method called "synchronize" which is used in
pretty much the same way as the Java keyword by the same name ...

Java:

synchronize(object) {
... do stuff ...
}

Ruby:

object.extend(Mutex_m)

object.synchronize {
... do stuff ...
}

Nice little program BTW, thanks for posting it, if inadvertently.

Thanks!
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,054
Latest member
TrimKetoBoost

Latest Threads

Top