Fixed detection of directory-based log-rotation of pyinotify backend.

If directory moved and the target is not watched path, so the monitoring of it could not be continued.

Now fixed with pending files await a monitoring if there (resp. its directories) appears again (respawn).

Closes gh-1769
pull/1778/head
sebres 2017-05-12 16:51:08 +02:00
parent 31627b796b
commit e340d0d2b2
3 changed files with 124 additions and 17 deletions

View File

@ -895,7 +895,8 @@ class FileFilter(Filter):
# see http://python.org/dev/peps/pep-3151/ # see http://python.org/dev/peps/pep-3151/
except IOError as e: except IOError as e:
logSys.error("Unable to open %s", filename) logSys.error("Unable to open %s", filename)
logSys.exception(e) if e.errno != 2:
logSys.exception(e)
return False return False
except OSError as e: # pragma: no cover - requires race condition to tigger this except OSError as e: # pragma: no cover - requires race condition to tigger this
logSys.error("Error opening %s", filename) logSys.error("Error opening %s", filename)

View File

@ -25,13 +25,14 @@ __license__ = "GPL"
import logging import logging
from distutils.version import LooseVersion from distutils.version import LooseVersion
import os
from os.path import dirname, sep as pathsep from os.path import dirname, sep as pathsep
import pyinotify import pyinotify
from .failmanager import FailManagerEmpty from .failmanager import FailManagerEmpty
from .filter import FileFilter from .filter import FileFilter
from .mytime import MyTime from .mytime import MyTime, time
from .utils import Utils from .utils import Utils
from ..helpers import getLogger from ..helpers import getLogger
@ -52,6 +53,11 @@ except Exception as e:
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
# Override pyinotify default logger/init-handler:
def _pyinotify_logger_init():
return logSys
pyinotify._logger_init = _pyinotify_logger_init
pyinotify.log = logSys
## ##
# Log reader class. # Log reader class.
@ -73,6 +79,9 @@ class FilterPyinotify(FileFilter):
# Pyinotify watch manager # Pyinotify watch manager
self.__monitor = pyinotify.WatchManager() self.__monitor = pyinotify.WatchManager()
self.__watches = dict() self.__watches = dict()
self.__pending = dict()
self.__pendingChkTime = 0
self.__pendingNextTime = 0
logSys.debug("Created FilterPyinotify") logSys.debug("Created FilterPyinotify")
def callback(self, event, origin=''): def callback(self, event, origin=''):
@ -84,15 +93,36 @@ class FilterPyinotify(FileFilter):
logSys.debug("Ignoring creation of directory %s", path) logSys.debug("Ignoring creation of directory %s", path)
return return
# check if that is a file we care about # check if that is a file we care about
if not path in self.__watches: if path not in self.__watches:
logSys.debug("Ignoring creation of %s we do not monitor", path) logSys.debug("Ignoring creation of %s we do not monitor", path)
return return
else: self._refreshFileWatcher(path)
# we need to substitute the watcher with a new one, so first elif event.mask & (pyinotify.IN_IGNORED | pyinotify.IN_MOVE_SELF | pyinotify.IN_DELETE_SELF):
# remove old one # fix pyinotify behavior with '-unknown-path' (if target not watched also):
self._delFileWatcher(path) if (event.mask & pyinotify.IN_MOVE_SELF and path not in self.__watches and
# place a new one path.endswith('-unknown-path')
self._addFileWatcher(path) ):
path = path[:-len('-unknown-path')]
# watch was removed for some reasons (log-rotate?):
if not os.path.isfile(path):
for log in self.getLogs():
logpath = log.getFileName()
if logpath.startswith(path):
# check exists (rotated):
if event.mask & pyinotify.IN_MOVE_SELF or not os.path.isfile(logpath):
self._addPendingFile(logpath, event)
else:
path = logpath
break
if path not in self.__watches:
logSys.debug("Ignoring event of %s we do not monitor", path)
return
if not os.path.isfile(path):
if self.containsLogPath(path):
self._addPendingFile(path, event)
logSys.debug("Ignoring watching/rotation event (%s) for %s", event.maskname, path)
return
self._refreshFileWatcher(path)
# do nothing if idle: # do nothing if idle:
if self.idle: if self.idle:
return return
@ -113,6 +143,44 @@ class FilterPyinotify(FileFilter):
self.failManager.cleanup(MyTime.time()) self.failManager.cleanup(MyTime.time())
self.__modified = False self.__modified = False
def _addPendingFile(self, path, event):
if path not in self.__pending:
self.__pending[path] = self.sleeptime / 10;
logSys.log(logging.MSG, "Log absence detected (possibly rotation) for %s, reason: %s of %s",
path, event.maskname, event.pathname)
def _checkPendingFiles(self):
if self.__pending:
ntm = time.time()
if ntm > self.__pendingNextTime:
found = {}
minTime = 60
for path, retardTM in self.__pending.iteritems():
if ntm - self.__pendingChkTime > retardTM:
if not os.path.isfile(path): # not found - prolong for next time
if retardTM < 60: retardTM *= 2
if minTime > retardTM: minTime = retardTM
self.__pending[path] = retardTM
continue
found[path] = 1
self._refreshFileWatcher(path)
for path in found:
try:
del self.__pending[path]
except KeyError: pass
self.__pendingChkTime = time.time()
self.__pendingNextTime = self.__pendingChkTime + minTime
# process now because we'he missed it in monitoring:
for path in found:
self._process_file(path)
def _refreshFileWatcher(self, oldPath, newPath=None):
# we need to substitute the watcher with a new one, so first
# remove old one
self._delFileWatcher(oldPath)
# place a new one
self._addFileWatcher(newPath or oldPath)
def _addFileWatcher(self, path): def _addFileWatcher(self, path):
wd = self.__monitor.add_watch(path, pyinotify.IN_MODIFY) wd = self.__monitor.add_watch(path, pyinotify.IN_MODIFY)
self.__watches.update(wd) self.__watches.update(wd)
@ -139,7 +207,9 @@ class FilterPyinotify(FileFilter):
if not (path_dir in self.__watches): if not (path_dir in self.__watches):
# we need to watch also the directory for IN_CREATE # we need to watch also the directory for IN_CREATE
self.__watches.update( self.__watches.update(
self.__monitor.add_watch(path_dir, pyinotify.IN_CREATE | pyinotify.IN_MOVED_TO)) self.__monitor.add_watch(path_dir, pyinotify.IN_CREATE |
pyinotify.IN_MOVED_TO | pyinotify.IN_MOVE_SELF |
pyinotify.IN_DELETE_SELF | pyinotify.IN_ISDIR))
logSys.debug("Added monitor for the parent directory %s", path_dir) logSys.debug("Added monitor for the parent directory %s", path_dir)
self._addFileWatcher(path) self._addFileWatcher(path)
@ -177,6 +247,9 @@ class FilterPyinotify(FileFilter):
# slow check events while idle: # slow check events while idle:
def __check_events(self, *args, **kwargs): def __check_events(self, *args, **kwargs):
# check pending files (logrotate ready):
self._checkPendingFiles()
if self.idle: if self.idle:
if Utils.wait_for(lambda: not self.active or not self.idle, if Utils.wait_for(lambda: not self.active or not self.idle,
self.sleeptime * 10, self.sleeptime self.sleeptime * 10, self.sleeptime
@ -209,6 +282,7 @@ class FilterPyinotify(FileFilter):
super(FilterPyinotify, self).stop() super(FilterPyinotify, self).stop()
# Stop the notifier thread # Stop the notifier thread
self.__notifier.stop() self.__notifier.stop()
self.__notifier.stop = lambda *args: 0; # prevent dual stop
## ##
# Wait for exit with cleanup. # Wait for exit with cleanup.

View File

@ -43,7 +43,7 @@ from ..server.failmanager import FailManagerEmpty
from ..server.ipdns import DNSUtils, IPAddr from ..server.ipdns import DNSUtils, IPAddr
from ..server.mytime import MyTime from ..server.mytime import MyTime
from ..server.utils import Utils, uni_decode from ..server.utils import Utils, uni_decode
from .utils import setUpMyTime, tearDownMyTime, mtimesleep, LogCaptureTestCase from .utils import setUpMyTime, tearDownMyTime, mtimesleep, with_tmpdir, LogCaptureTestCase
from .dummyjail import DummyJail from .dummyjail import DummyJail
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
@ -942,17 +942,21 @@ def get_monitor_failures_testcase(Filter_):
skip=3, mode='w') skip=3, mode='w')
self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assert_correct_last_attempt(GetFailures.FAILURES_01)
def test_move_file(self): def _wait4failures(self, count=2):
# if we move file into a new location while it has been open already
self.file.close()
self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name,
n=14, mode='w')
# Poll might need more time # Poll might need more time
self.assertTrue(self.isEmpty(_maxWaitTime(5)), self.assertTrue(self.isEmpty(_maxWaitTime(5)),
"Queue must be empty but it is not: %s." "Queue must be empty but it is not: %s."
% (', '.join([str(x) for x in self.jail.queue]))) % (', '.join([str(x) for x in self.jail.queue])))
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
Utils.wait_for(lambda: self.filter.failManager.getFailTotal() == 2, _maxWaitTime(10)) Utils.wait_for(lambda: self.filter.failManager.getFailTotal() >= count, _maxWaitTime(10))
self.assertEqual(self.filter.failManager.getFailTotal(), count)
def test_move_file(self):
# if we move file into a new location while it has been open already
self.file.close()
self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name,
n=14, mode='w')
self._wait4failures()
self.assertEqual(self.filter.failManager.getFailTotal(), 2) self.assertEqual(self.filter.failManager.getFailTotal(), 2)
# move aside, but leaving the handle still open... # move aside, but leaving the handle still open...
@ -967,6 +971,34 @@ def get_monitor_failures_testcase(Filter_):
self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assert_correct_last_attempt(GetFailures.FAILURES_01)
self.assertEqual(self.filter.failManager.getFailTotal(), 6) self.assertEqual(self.filter.failManager.getFailTotal(), 6)
@with_tmpdir
def test_move_dir(self, tmp):
self.file.close()
self.filter.delLogPath(self.name)
# if we rename parent dir into a new location (simulate directory-base log rotation)
tmpsub1 = os.path.join(tmp, "1")
tmpsub2 = os.path.join(tmp, "2")
os.mkdir(tmpsub1)
self.name = os.path.join(tmpsub1, os.path.basename(self.name))
os.close(os.open(self.name, os.O_CREAT|os.O_APPEND)); # create empty file
self.filter.addLogPath(self.name, autoSeek=False)
self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name,
skip=12, n=1, mode='w')
self.file.close()
self._wait4failures(1)
# rotate whole directory: rename directory 1 as 2:
os.rename(tmpsub1, tmpsub2)
os.mkdir(tmpsub1)
self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name,
skip=12, n=1, mode='w')
self.file.close()
self._wait4failures(2)
# stop before tmpdir deleted (just prevents many monitor events)
self.filter.stop()
def _test_move_into_file(self, interim_kill=False): def _test_move_into_file(self, interim_kill=False):
# if we move a new file into the location of an old (monitored) file # if we move a new file into the location of an old (monitored) file
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, _copy_lines_between_files(GetFailures.FILENAME_01, self.name,