pvl/syslog/tail.py
author Tero Marttila <tero.marttila@aalto.fi>
Mon, 28 Jul 2014 13:32:41 +0300
changeset 35 4c7905e1cad7
parent 2 5a8a32cbc944
permissions -rw-r--r--
version 0.5.1: bugfix for working around conflicting -c/--config options
"""
    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