diff --git a/fail2ban/server/filterpyinotify.py b/fail2ban/server/filterpyinotify.py index faf01560..6394ecef 100644 --- a/fail2ban/server/filterpyinotify.py +++ b/fail2ban/server/filterpyinotify.py @@ -78,7 +78,8 @@ class FilterPyinotify(FileFilter): self.__modified = False # Pyinotify watch manager self.__monitor = pyinotify.WatchManager() - self.__watches = dict() + self.__watchFiles = dict() + self.__watchDirs = dict() self.__pending = dict() self.__pendingChkTime = 0 self.__pendingNextTime = 0 @@ -87,45 +88,56 @@ class FilterPyinotify(FileFilter): def callback(self, event, origin=''): logSys.log(7, "[%s] %sCallback for Event: %s", self.jailName, origin, event) path = event.pathname + # check watching of this path: + isWF = isWD = False + if path in self.__watchDirs: + isWD = True + elif path in self.__watchFiles: + isWF = True + # fix pyinotify behavior with '-unknown-path' (if target not watched also): + if (event.mask & pyinotify.IN_MOVE_SELF and + path.endswith('-unknown-path') and not isWF and not isWD + ): + path = path[:-len('-unknown-path')] + isWD = path in self.__watchDirs + assumeNoDir = False if event.mask & ( pyinotify.IN_CREATE | pyinotify.IN_MOVED_TO ): + # refresh watched dir (may be expected): + if isWD: + self._refreshWatcher(path, isDir=True) + return # skip directories altogether if event.mask & pyinotify.IN_ISDIR: logSys.debug("Ignoring creation of directory %s", path) return # check if that is a file we care about - if path not in self.__watches: + if not isWF: logSys.debug("Ignoring creation of %s we do not monitor", path) return - self._refreshFileWatcher(path) + self._refreshWatcher(path) elif event.mask & (pyinotify.IN_IGNORED | pyinotify.IN_MOVE_SELF | pyinotify.IN_DELETE_SELF): - # fix pyinotify behavior with '-unknown-path' (if target not watched also): - if (event.mask & pyinotify.IN_MOVE_SELF and path not in self.__watches and - path.endswith('-unknown-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) + assumeNoDir = event.mask & (pyinotify.IN_MOVE_SELF | pyinotify.IN_DELETE_SELF) + if isWD and (assumeNoDir or not os.path.isdir(path)): + self._addPending(path, event, isDir=True) + elif not isWF: + for logpath in self.__watchDirs: + if logpath.startswith(path + pathsep) and (assumeNoDir or not os.path.isdir(logpath)): + self._addPending(logpath, event, isDir=True) + # pending file: + for logpath in self.__watchFiles: + if logpath.startswith(path + pathsep) and (assumeNoDir or not os.path.isfile(logpath)): + self._addPending(logpath, event) + if isWF and not os.path.isfile(path): + self._addPending(path, event) + return # do nothing if idle: if self.idle: return + # be sure we process a file: + if not isWF: + logSys.debug("Ignoring event (%s) of %s we do not monitor", event.maskname, path) + return self._process_file(path) def _process_file(self, path): @@ -143,27 +155,36 @@ class FilterPyinotify(FileFilter): self.failManager.cleanup(MyTime.time()) self.__modified = False - def _addPendingFile(self, path, event): + def _addPending(self, path, event, isDir=False): if path not in self.__pending: - self.__pending[path] = self.sleeptime / 10; + self.__pending[path] = [self.sleeptime / 10, isDir]; + self.__pendingNextTime = 0 logSys.log(logging.MSG, "Log absence detected (possibly rotation) for %s, reason: %s of %s", path, event.maskname, event.pathname) - def _checkPendingFiles(self): + def _delPending(self, path): + try: + del self.__pending[path] + except KeyError: pass + + def _checkPending(self): if self.__pending: ntm = time.time() if ntm > self.__pendingNextTime: found = {} minTime = 60 - for path, retardTM in self.__pending.iteritems(): + for path, (retardTM, isDir) in self.__pending.iteritems(): if ntm - self.__pendingChkTime > retardTM: - if not os.path.isfile(path): # not found - prolong for next time + chkpath = os.path.isdir if isDir else os.path.isfile + if not chkpath(path): # not found - prolong for next time if retardTM < 60: retardTM *= 2 if minTime > retardTM: minTime = retardTM - self.__pending[path] = retardTM + self.__pending[path][0] = retardTM continue - found[path] = 1 - self._refreshFileWatcher(path) + logSys.log(logging.MSG, "Log presence detected for %s %s", + "directory" if isDir else "file", path) + found[path] = isDir + self._refreshWatcher(path, isDir=isDir) for path in found: try: del self.__pending[path] @@ -171,24 +192,32 @@ class FilterPyinotify(FileFilter): 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) + for path, isDir in found.iteritems(): + if not isDir: + self._process_file(path) - def _refreshFileWatcher(self, oldPath, newPath=None): + def _refreshWatcher(self, oldPath, newPath=None, isDir=False): + if not newPath: newPath = oldPath # 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) + # remove old one and then place a new one + if not isDir: + self._delFileWatcher(oldPath) + self._addFileWatcher(newPath) + else: + self._delDirWatcher(oldPath) + self._addDirWatcher(newPath) def _addFileWatcher(self, path): + # we need to watch also the directory for IN_CREATE + self._addDirWatcher(dirname(path)) + # add file watcher: wd = self.__monitor.add_watch(path, pyinotify.IN_MODIFY) - self.__watches.update(wd) + self.__watchFiles.update(wd) logSys.debug("Added file watcher for %s", path) def _delFileWatcher(self, path): try: - wdInt = self.__watches.pop(path) + wdInt = self.__watchFiles.pop(path) wd = self.__monitor.rm_watch(wdInt) if wd[wdInt]: logSys.debug("Removed file watcher for %s", path) @@ -197,21 +226,30 @@ class FilterPyinotify(FileFilter): pass return False + def _addDirWatcher(self, path_dir): + # Add watch for the directory: + if path_dir not in self.__watchDirs: + self.__watchDirs.update( + 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) + + def _delDirWatcher(self, path_dir): + # Remove watches for the directory: + try: + wdInt = self.__watchDirs.pop(path_dir) + self.__monitor.rm_watch(wdInt) + except KeyError: # pragma: no cover + pass + logSys.debug("Removed monitor for the parent directory %s", path_dir) + ## # Add a log file path # # @param path log file path def _addLogPath(self, path): - path_dir = dirname(path) - if not (path_dir in self.__watches): - # we need to watch also the directory for IN_CREATE - self.__watches.update( - 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) - self._addFileWatcher(path) self._process_file(path) @@ -223,18 +261,18 @@ class FilterPyinotify(FileFilter): def _delLogPath(self, path): if not self._delFileWatcher(path): logSys.error("Failed to remove watch on path: %s", path) + self._delPending(path) path_dir = dirname(path) - if not len([k for k in self.__watches - if k.startswith(path_dir + pathsep)]): + for k in self.__watchFiles: + if k.startswith(path_dir + pathsep): + path_dir = None + break + if path_dir: # Remove watches for the directory # since there is no other monitored file under this directory - try: - wdInt = self.__watches.pop(path_dir) - self.__monitor.rm_watch(wdInt) - except KeyError: # pragma: no cover - pass - logSys.debug("Removed monitor for the parent directory %s", path_dir) + self._delDirWatcher(path_dir) + self._delPending(path_dir) # pyinotify.ProcessEvent default handler: def __process_default(self, event): @@ -247,14 +285,16 @@ class FilterPyinotify(FileFilter): # slow check events while idle: def __check_events(self, *args, **kwargs): - # check pending files (logrotate ready): - self._checkPendingFiles() - if self.idle: if Utils.wait_for(lambda: not self.active or not self.idle, self.sleeptime * 10, self.sleeptime ): pass + + # check pending files/dirs (logrotate ready): + if not self.idle: + self._checkPending() + self.ticks += 1 return pyinotify.ThreadedNotifier.check_events(self.__notifier, *args, **kwargs)