"""
Iterate over input lines in filesystem files.
"""
import os
import logging; log = logging.getLogger('pvl.syslog.tail')
class Tail (object) :
"""
Follow a file on the filesystem, reading lines until EOF, and re-opening if replaced.
Never blocks, no fileno() to poll. Just poll(timeout=POLL).
Not writable.
"""
POLL = 2.0
def __init__ (self, path, skip=None, **opts) :
log.debug("%s", path)
self.path = path
self.file = self.stat = None # closed
self.open()
if skip :
self.skip()
def _stat (self) :
"""
Return a key identifying the file at our path.
"""
st = os.stat(self.path)
stat = st.st_dev, st.st_ino
return stat
def _open (self) :
"""
Return the opened file.
"""
return open(self.path, 'r')
def open (self) :
"""
Re-opens our file when closed.
Raises ValueError if already open.
"""
if self.file is None :
# XXX: use fstat for "atomic" open+stat?
self.file = self._open()
self.stat = self._stat()
log.debug("%s: %s: %s", self, self.file, self.stat)
else :
raise ValueError("%s: open already open tail" % (self, ))
def changed (self) :
"""
Has the underlying file changed?
"""
return self._stat() != self.stat
def readline (self) :
"""
Reads a line from the file, without trailing \n.
Returns None on EOF.
"""
line = self.file.readline()
if not line :
line = None
else :
line = line.rstrip('\r\n')
log.debug("%s", line)
return line
def readlines (self, eof_mark=False) :
"""
Reads any available lines from the file.
Reopens the file on EOF if the underlying file changed.
eof_mark: yields a special None line when this happens.
"""
while True :
line = self.readline()
if line is not None :
yield line
elif self.changed() :
log.debug("EOF: reopen")
self.close()
self.open()
if eof_mark :
yield None # special token
# keep going
continue
else :
log.debug("EOF: wait")
break
__iter__ = readlines
def skip (self) :
"""
Skip any available lines.
"""
log.debug("%s", self)
for line in self.readlines() :
pass
def close (self) :
"""
Close our file, if open. Further operations raise ValueError.
log.warn's if already closed.
"""
if self.file :
log.debug("%s", self)
self.file.close()
self.file = None
else :
log.warn("%s: close on already closed tail", self)
def __str__ (self) :
return self.path