pvl/syslog/tail.py
author Tero Marttila <terom@paivola.fi>
Wed, 24 Oct 2012 21:02:33 +0300
changeset 31 3e6d0feb115c
child 46 0bdbbda4cdea
permissions -rw-r--r--
pvl.syslog: import from pvl-collectd
"""
    `tail -f` style continuous file reads.

    Checks if the file was renamed on EOF, and reopens if so.
"""

import os

import logging; log = logging.getLogger('pvl.collectd.tail')

class TailFile (object) :
    """
        A file on the filesystem, that is appended to.
    """

    def __init__ (self, path) :
        self.path = path
        self._file = None
        self._id = None

        self._open()
    
    @property
    def file (self) :
        """
            The underlying file objct, if opened.
        """

        if self._file is None :
            raise ValueError("I/O operation on closed file: {0}".format(self.path))

        return self._file

    @property
    def fd (self) :
        return self.file.fileno()

    def fileno (self) :
        """
            The underlying OS fd.
        """

        return self.fd

    def _stat (self) :
        """
            Return a key identifying the file at our path.
        """

        st = os.stat(self.path)

        return st.st_dev, st.st_ino

    def _open (self) :
        assert self._file is None

        self._file = open(self.path, 'r')
        self._id = self._stat()

    def _close (self) :
        assert self._file is not None

        self._file.close()
        self._file = None

    def _reopen (self) :
        """
            Re-open, in case the file changed..
        """

        self._close()
        self._open()

    def _changed (self) :
        """
            Has the underlying file changed?
        """

        return self._stat() != self._id

    def poll (self, timeout) :
        """
            XXX: not really implemented...
        """

        import time

        time.sleep(timeout)
    
    def readline (self) :
        """
            Reads a line from the file.

            Raises EOF on end-of-file.
        """

        line = self.file.readline()

        # eof?
        if not line :
            raise EOFError()
        
        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 :
            try :
                line = self.readline()

            except EOFError :
                if self._changed() :
                    log.debug("EOF: file changed: reopen")
                    self._reopen()
                    
                    if eof_mark :
                        # special token
                        yield None

                else :
                    log.debug("EOF: wait")
                    # done reading
                    return

            else :
                yield line.strip()

    __iter__ = readlines

    def close (self) :
        """
            Close the fifo.
        """

        if self._file is None :
            raise ValueError("File already closed: {0}".format(self.path))

        self._close()

    def __del__ (self) :
        """
            Cleanup
        """

        if self._file is not None :
            self._close()