diff --git a/.coverage b/.coverage index ac029bf..b733f24 100644 --- a/.coverage +++ b/.coverage @@ -1 +1 @@ -!coverage.py: This is a private format, don't read it directly!{"lines":{"/home/dustin/development/python/pyinotify/inotify/test_support.py":[1,2,3,4,5,7,9,11,13,14,16,17,19,21,22],"/home/dustin/development/python/pyinotify/inotify/adapters.py":[1,2,3,4,5,6,8,10,11,15,16,20,22,23,25,26,27,28,31,32,35,36,39,40,41,42,43,44,46,47,49,51,53,56,59,60,61,63,65,66,67,69,70,72,73,75,76,78,84,85,88,89,91,92,93,95,97,98,99,100,101,102,104,105,107,110,112,115,117,118,121,123,124,126,132,134,135,136,138,139,141,142,145,148,150,152,153,154,161,162,163,165,170,171,172,176,177,191,194,195,196,197,199,200,201,202,204,208,209,210,216,217,219,221,230,231,232,234,235,238,239,242,245,247,250,252,253,256,259,261,264,265,267,268,269,271,273,275,276,278,280,281,282,283,285,287,288,289,292,294,295,298,299,301,302,303,305,307,308,310,312,313,314,315,317,319,327,328],"/home/dustin/development/python/pyinotify/inotify/library.py":[8,1,2,4,5],"/home/dustin/development/python/pyinotify/inotify/calls.py":[1,2,4,6,8,11,12,18,25,32,33,37,39,40,41,43,45,46,47,49,51,52,53,55,56],"/home/dustin/development/python/pyinotify/inotify/__init__.py":[1],"/home/dustin/development/python/pyinotify/inotify/constants.py":[3,4,8,9,10,11,12,13,14,15,16,17,18,19,23,24,30,34,35,36,40,41,42,43,44,46,47,48,52,53,54,55,56,57,58,59,60,61,62,63,67,68,69,73,74,75,76,77]}} \ No newline at end of file +!coverage.py: This is a private format, don't read it directly!{"lines":{"/home/eohm/github/Elias481/PyInotify/inotify/calls.py":[1,2,4,6,8,11,12,18,25,32,33,37,39,40,41,43,45,46,47,49,51,52,53,55,56],"/home/eohm/github/Elias481/PyInotify/inotify/test_support.py":[1,2,3,4,5,7,9,11,13,14,16,17,19,21,22],"/home/eohm/github/Elias481/PyInotify/inotify/constants.py":[3,4,8,9,10,11,12,13,14,15,16,17,18,19,23,24,30,34,35,36,40,41,42,43,44,46,47,48,52,53,54,55,56,57,58,59,60,61,62,63,67,68,69,73,74,75,76,77],"/home/eohm/github/Elias481/PyInotify/inotify/__init__.py":[1],"/home/eohm/github/Elias481/PyInotify/inotify/library.py":[1,2,4,5,8],"/home/eohm/github/Elias481/PyInotify/inotify/adapters.py":[1,2,3,4,5,6,8,10,11,15,16,20,25,27,28,30,31,32,33,36,37,40,41,44,45,50,51,52,53,54,55,57,58,60,61,63,65,68,71,72,73,75,77,78,79,81,82,89,93,95,96,98,99,101,103,104,105,107,108,109,110,111,112,113,114,117,126,127,130,132,140,141,142,143,144,145,147,148,150,153,155,158,159,162,164,165,167,173,175,176,177,179,180,181,183,184,187,190,192,194,195,196,197,199,200,201,204,205,213,215,216,217,221,222,236,239,240,242,243,244,246,247,248,252,255,257,258,259,260,262,265,270,271,272,283,284,286,288,297,298,299,301,302,305,306,309,319,321,324,326,327,330,333,334,335,339,341,342,349,351,356,357,359,360,361,363,365,367,368,370,372,373,374,375,377,379,380,381,384,386,387,390,391,393,394,395,397,399,400,402,404,405,406,407,409,411,419,420]}} \ No newline at end of file diff --git a/inotify/adapters.py b/inotify/adapters.py index 8a67347..895b34e 100644 --- a/inotify/adapters.py +++ b/inotify/adapters.py @@ -100,30 +100,42 @@ def add_watch(self, path_unicode, mask=inotify.constants.IN_ALL_EVENTS): return wd + def _remove_watch(self, wd, path, superficial=False): + _LOGGER.debug("Removing watch for watch-handle (%d): [%s]", + wd, path) + + if superficial is not None: + del self.__watches[path] + del self.__watches_r[wd] + inotify.adapters._LOGGER.debug(".. removed from adaptor") + if superficial is not False: + return + inotify.calls.inotify_rm_watch(self.__inotify_fd, wd) + _LOGGER.debug(".. removed from inotify") + + def remove_watch(self, path, superficial=False): """Remove our tracking information and call inotify to stop watching the given path. When a directory is removed, we'll just have to remove our tracking since inotify already cleans-up the watch. + With superficial set to None it is also possible to remove only inotify + watch to be able to wait for the final IN_IGNORED event received for + the wd (useful for example in threaded applications). """ wd = self.__watches.get(path) if wd is None: + _LOGGER.warning("Path not in watch list: [%s]", path) return - - _LOGGER.debug("Removing watch for watch-handle (%d): [%s]", - wd, path) - - del self.__watches[path] - - self.remove_watch_with_id(wd) + self._remove_watch(wd, path, superficial) def remove_watch_with_id(self, wd, superficial=False): - del self.__watches_r[wd] - - if superficial is False: - _LOGGER.debug("Removing watch for watch-handle (%d).", wd) - - inotify.calls.inotify_rm_watch(self.__inotify_fd, wd) + """Same as remove_watch but does the same by id""" + path = self.__watches_r.get(wd) + if path is None: + _LOGGER.warning("Watchdescriptor not in watch list: [%d]", wd) + return + self._remove_watch(wd, path, superficial) def _get_event_names(self, event_type): names = [] @@ -261,10 +273,15 @@ def __init__(self, mask=inotify.constants.IN_ALL_EVENTS, # No matter what we actually received as the mask, make sure we have # the minimum that we require to curate our list of watches. + # + # todo: we really should have two masks... the combined one (requested|needed) + # and the user specified mask for the events he wants to receive from tree... self._mask = mask | \ inotify.constants.IN_ISDIR | \ inotify.constants.IN_CREATE | \ - inotify.constants.IN_DELETE + inotify.constants.IN_MOVED_TO | \ + inotify.constants.IN_DELETE | \ + inotify.constants.IN_MOVED_FROM self._i = Inotify(block_duration_s=block_duration_s) @@ -290,6 +307,13 @@ def event_gen(self, ignore_missing_new_folders=False, **kwargs): ) and \ ( os.path.exists(full_path) is True or + # todo: as long as the "Path already being watche/not in watch list" warnings + # instead of exceptions are in place, it should really be default to also log + # only a warning if target folder does not exists in tree autodiscover mode. + # - but probably better to implement that with try/catch around add_watch + # when errno fix is merged and also this should normally not be an argument + # to event_gen but to InotifyTree(s) constructor (at least set default there) + # to not steal someones use case to specify this differently for each event_call?? ignore_missing_new_folders is False ): _LOGGER.debug("A directory has been created. We're " @@ -299,7 +323,7 @@ def event_gen(self, ignore_missing_new_folders=False, **kwargs): self._i.add_watch(full_path, self._mask) - if header.mask & inotify.constants.IN_MOVED_FROM: + if header.mask & inotify.constants.IN_DELETE: _LOGGER.debug("A directory has been removed. We're " "being recursive, but it would have " "automatically been deregistered: [%s]", @@ -309,17 +333,18 @@ def event_gen(self, ignore_missing_new_folders=False, **kwargs): self._i.remove_watch(full_path, superficial=True) elif header.mask & inotify.constants.IN_MOVED_FROM: _LOGGER.debug("A directory has been renamed. We're " - "being recursive, but it would have " - "automatically been deregistered: [%s]", - full_path) - - self._i.remove_watch(full_path, superficial=True) - elif header.mask & inotify.constants.IN_MOVED_TO: - _LOGGER.debug("A directory has been renamed. We're " - "adding a watch on it (because we're " - "being recursive): [%s]", full_path) - - self._i.add_watch(full_path, self._mask) + "being recursive, we will remove watch " + "from it and re-add with IN_MOVED_TO " + "if target parent dir is within " + "our tree: [%s]", full_path) + + try: + self._i.remove_watch(full_path, superficial=False) + except inotify.calls.InotifyError as ex: + # for the unlikely case the moved diretory is deleted + # and automatically unregistered before we try to + # unregister.... + pass yield event diff --git a/tests/test_inotify.py b/tests/test_inotify.py index 0f87524..e1f4b37 100644 --- a/tests/test_inotify.py +++ b/tests/test_inotify.py @@ -2,6 +2,7 @@ import os import unittest +import shutil import inotify.constants import inotify.adapters @@ -349,6 +350,112 @@ def test__automatic_new_watches_on_existing_paths(self): self.assertEquals(events, expected) + def test__moving_readded_folder(self): + #test for https://github.com/dsoprea/PyInotify/issues/46 + #doing no checks of genereated events as current master does + #not generate events that should really be expected in this case + #avoid having to adjust this - also not implement chcking for expected + #wd assignment now.. + #just check for no exception and expected watches in the end + #emulate slow mkdir/rmdir/rename... (because of another unfixed bug and + #because this is needed to reproduces issue) + with inotify.test_support.temp_path() as path: + path1 = os.path.join(path, 'org_folder') + path2 = os.path.join(path, 'ren_folder') + + i = inotify.adapters.InotifyTree(path) + os.mkdir(path1) + events = self.__read_all_events(i) + os.rmdir(path1) + events = self.__read_all_events(i) + os.mkdir(path1) + events = self.__read_all_events(i) + os.rename(path1, path2) + events = self.__read_all_events(i) + + watches = i._i._Inotify__watches + watches_reverse = i._i._Inotify__watches_r + + watches_expect = sorted((path,path2)) + watches_reg_names = sorted(watches.keys()) + watches_reg_check = dict((value, key) for key, value in watches.items()) + + self.assertEquals(watches_expect, watches_reg_names) + self.assertEquals(watches_reg_check, watches_reverse) + + def test__readd_deleted_folder(self): + #test for https://github.com/dsoprea/PyInotify/issues/51 + #doing no checks the directory-discovery events as current master does + #not generate events that should really be expected in this case + #avoid having to adjust this - also not implement chcking for expected + #wd assignment now.. + #just check for no exception, file creation events and expected watches + #at the end. emulate slow succession of filesystem actions... (because + #of another unfixed bug and because this is needed to reproduces issue) + with inotify.test_support.temp_path() as path: + path1 = os.path.join(path, 'folder') + file1 = os.path.join(path1, 'file1') + file2 = os.path.join(path1, 'file2') + + i = inotify.adapters.InotifyTree(path) + os.mkdir(path1) + events = self.__read_all_events(i) + with open(file1, 'w'): + pass + with open(file2, 'w'): + pass + events = self.__read_all_events(i) + + expected = [ + (inotify.adapters._INOTIFY_EVENT(wd=2, mask=256, cookie=0, len=16), ['IN_CREATE'], path1, 'file1'), + (inotify.adapters._INOTIFY_EVENT(wd=2, mask=32, cookie=0, len=16), ['IN_OPEN'], path1, 'file1'), + (inotify.adapters._INOTIFY_EVENT(wd=2, mask=8, cookie=0, len=16), ['IN_CLOSE_WRITE'], path1, 'file1'), + (inotify.adapters._INOTIFY_EVENT(wd=2, mask=256, cookie=0, len=16), ['IN_CREATE'], path1, 'file2'), + (inotify.adapters._INOTIFY_EVENT(wd=2, mask=32, cookie=0, len=16), ['IN_OPEN'], path1, 'file2'), + (inotify.adapters._INOTIFY_EVENT(wd=2, mask=8, cookie=0, len=16), ['IN_CLOSE_WRITE'], path1, 'file2'), + ] + self.assertEquals(events, expected) + + shutil.rmtree(path1) + events = self.__read_all_events(i) + + #could do the following asserts here to prove the the assumption of amigian74 in + #his 5th point in issue 51 ("everything until now works fine") false, but that is + #not target of this test, also it is not his reposibility to verify this... + #so to get same issue he describes it's just a comment... + #self.assertEquals(len(i._i._Inotify__watches), 1) + #self.assertEquals(len(i._i._Inotify__watches_r), 1) + #self.assertNotIn(path1, i._i._Inotify__watches) + + os.mkdir(path1) + events = self.__read_all_events(i) + with open(file1, 'w'): + pass + with open(file2, 'w'): + pass + events = self.__read_all_events(i) + + watches = i._i._Inotify__watches + watches_reverse = i._i._Inotify__watches_r + + watches_expect = sorted((path,path1)) + watches_reg_names = sorted(watches.keys()) + watches_reg_check = dict((value, key) for key, value in watches.items()) + + self.assertEquals(watches_expect, watches_reg_names) + self.assertEquals(watches_reg_check, watches_reverse) + + wd = watches[path1] + expected = [ + (inotify.adapters._INOTIFY_EVENT(wd=wd, mask=256, cookie=0, len=16), ['IN_CREATE'], path1, 'file1'), + (inotify.adapters._INOTIFY_EVENT(wd=wd, mask=32, cookie=0, len=16), ['IN_OPEN'], path1, 'file1'), + (inotify.adapters._INOTIFY_EVENT(wd=wd, mask=8, cookie=0, len=16), ['IN_CLOSE_WRITE'], path1, 'file1'), + (inotify.adapters._INOTIFY_EVENT(wd=wd, mask=256, cookie=0, len=16), ['IN_CREATE'], path1, 'file2'), + (inotify.adapters._INOTIFY_EVENT(wd=wd, mask=32, cookie=0, len=16), ['IN_OPEN'], path1, 'file2'), + (inotify.adapters._INOTIFY_EVENT(wd=wd, mask=8, cookie=0, len=16), ['IN_CLOSE_WRITE'], path1, 'file2'), + ] + self.assertEquals(events, expected) + class TestInotifyTrees(unittest.TestCase): def __init__(self, *args, **kwargs):