"Robert Klemme" <bob.news / gmx.net> writes: > "Lloyd Zusman" <ljz / asfast.com> schrieb im Newsbeitrag > news:zn5dbwke.fsf / asfast.com... >> >> [ ... ] >> >> 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__ -- Lloyd Zusman ljz / asfast.com God bless you.