Esempio n. 1
0
 def setUp(self):
     self._dir = TemporaryDirectory()
     self.dir = Path(self._dir.name)
     self.inotify = Inotify()
     self.watch = self.inotify.add_watch(
         self.dir,
         Mask.ACCESS | Mask.MODIFY | Mask.ATTRIB | Mask.CLOSE_WRITE
         | Mask.CLOSE_NOWRITE | Mask.OPEN | Mask.MOVED_FROM | Mask.MOVED_TO
         | Mask.CREATE | Mask.DELETE | Mask.DELETE_SELF | Mask.MOVE_SELF)
Esempio n. 2
0
    def __init__(self, path: Path):
        self.path = path
        # Store the inotify mask which is used throughout the watcher
        self.mask = Mask.MOVED_FROM  | \
                    Mask.MOVED_TO    | \
                    Mask.CREATE      | \
                    Mask.DELETE_SELF | \
                    Mask.IGNORED     | \
                    Mask.CLOSE_WRITE
        # Setup an inotify watching instance
        self.notifier = Inotify()

        # Queue of the various notifications to be sent at timestamps
        self.alarm_queue = []
        self.trigger = None
Esempio n. 3
0
 async def monitor(self):  # pragma: no cover
     """
     This function returns a coroutine to be set on the main loop.
     This function will async block waiting for changes to the config.
     On change event, reload the configuration.
     """
     with Inotify() as inotify:
         inotify.add_watch(self.fname, Mask.MODIFY)
         async for _ in inotify:
             await self.aread()
Esempio n. 4
0
async def watch_recursive(path: Path, mask: Mask) -> AsyncGenerator[Event, None]:
    with Inotify() as inotify:
        for directory in get_directories_recursive(path):
            print(f'INIT: watching {directory}')
            inotify.add_watch(directory, mask | Mask.MOVED_FROM | Mask.MOVED_TO | Mask.CREATE | Mask.DELETE_SELF | Mask.IGNORED)

        # Things that can throw this off:
        #
        # * Moving a watched directory out of the watch tree (will still
        #   generate events even when outside of directory tree)
        #
        # * Doing two changes on a directory or something before the program
        #   has a time to handle it (this will also throw off a lot of inotify
        #   code, though)
        #
        # * Moving a watched directory within a watched directory will get the
        #   wrong path.  This needs to use the cookie system to link events
        #   together and complete the move properly, which can still make some
        #   events get the wrong path if you get file events during the move or
        #   something silly like that, since MOVED_FROM and MOVED_TO aren't
        #   guaranteed to be contiguous.  That exercise is left up to the
        #   reader.
        #
        # * Trying to watch a path that doesn't exist won't automatically
        #   create it or anything of the sort.
        #
        # * Deleting and recreating or moving the watched directory won't do
        #   anything special, but it probably should.
        async for event in inotify:

            # Add subdirectories to watch if a new directory is added.  We do
            # this recursively here before processing events to make sure we
            # have complete coverage of existing and newly-created directories
            # by watching before recursing and adding, since we know
            # get_directories_recursive is depth-first and yields every
            # directory before iterating their children, we know we won't miss
            # anything.
            if Mask.CREATE in event.mask and event.path is not None and event.path.is_dir():
                for directory in get_directories_recursive(event.path):
                    print(f'EVENT: watching {directory}')
                    inotify.add_watch(directory, mask | Mask.MOVED_FROM | Mask.MOVED_TO | Mask.CREATE | Mask.DELETE_SELF | Mask.IGNORED)

            # If there is at least some overlap, assume the user wants this event.
            if event.mask & mask:
                yield event
            else:
                # Note that these events are needed for cleanup purposes.
                # We'll always get IGNORED events so the watch can be removed
                # from the inotify.  We don't need to do anything with the
                # events, but they do need to be generated for cleanup.
                # We don't need to pass IGNORED events up, because the end-user
                # doesn't have the inotify instance anyway, and IGNORED is just
                # used for management purposes.
                print(f'UNYIELDED EVENT: {event}')
Esempio n. 5
0
async def watch_directories(directories, handler):
    with Inotify() as inotify:
        for dirs in directories:
            inotify.add_watch(dirs, Mask.CREATE)

            for files in os.listdir(dirs):
                full_path = os.path.join(dirs, files)
                if os.path.isdir(full_path):
                    await handler.handle_create_event(full_path)

        async for event in inotify:
            await handler.handle_create_event(str(event.path))
Esempio n. 6
0
 async def cfg_mods_worker(self, reload_one=True):
     """ Reloading configs on change. Reload only appropriate config by default.
         watcher: watcher for cfg. """
     config_extension = '.py'
     while True:
         with Inotify() as inotify:
             inotify.add_watch(
                 f'{Misc.i3path()}/cfg/', Mask.MODIFY)
             async for event in inotify:
                 changed_mod = str(event.name).removesuffix(config_extension)
                 if changed_mod in self.mods:
                     binpath = f'{Misc.i3path()}/bin/'
                     subprocess.run(
                         [f'{binpath}/create_config.py'],
                         stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
                         cwd=binpath, check=False
                     )
                     if reload_one:
                         self.mods[changed_mod].bindings['reload']()
                     else:
                         for mod in self.mods:
                             self.mods[mod].bindings['reload']()
Esempio n. 7
0
    async def watch_file_changes(self):
        with Inotify() as inotify:
            for directory, _, _ in os.walk(os.getcwd()):
                # TODO How to best ignore things like .git, .pytest_cache etc?
                path = pathlib.Path(directory)
                if any(
                        p.startswith(".") or p == "__pycache__"
                        for p in path.parts):
                    continue

                inotify.add_watch(
                    directory,
                    Mask.MODIFY
                    | Mask.CREATE
                    | Mask.DELETE
                    | Mask.MOVE
                    | Mask.ONLYDIR
                    | Mask.MASK_CREATE,
                )

            async for event in inotify:
                if event.name.suffix == ".py":
                    self.start_test_run()
Esempio n. 8
0
async def run_daemon() -> None:
    log.debug("Running daemon on D-Bus system bus.")
    bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
    interface = ChargeDaemon()
    log.debug(f"Exporting interface '{DBUS_NAME}' to path '{DBUS_PATH}'")
    bus.export(DBUS_PATH, interface)
    if (
        not (reply := await bus.request_name(DBUS_NAME))
        == RequestNameReply.PRIMARY_OWNER
    ):
        log.critical(
            f"Unable to acquire primary ownership of bus name '{DBUS_NAME}'. In state: {reply.name}"
        )
        raise SystemExit(1)
    log.debug("Daemon running...")
    inotify = Inotify()
    inotify.add_watch(path.dirname(interface.controller.bat_path), Mask.CLOSE_WRITE)
    inotify.add_watch(CONFIG_DIR, Mask.CLOSE_WRITE)
    tasks = asyncio.gather(
        bus.wait_for_disconnect(), iterate_threshold_events(inotify, interface)
    )

    def sig_handler(signum, frame) -> None:
        log.debug(f"Received signal: {signal.Signals(signum).name}")
        tasks.cancel()
        bus.disconnect()
        if interface.notifier:
            interface.notifier.close()
        inotify.close()
        raise SystemExit(0)
Esempio n. 9
0
    def watch_photos(self):
        watching_libraries = {}

        with Inotify() as inotify:

            @sync_to_async
            def get_libraries():
                return {
                    l.path: l.library_id
                    for l in LibraryPath.objects.filter(type='St',
                                                        backend_type='Lo')
                }

            @sync_to_async
            def record_photo_async(photo_path, library_id, event_mask):
                record_photo(photo_path, library_id, event_mask)

            async def check_libraries():
                while True:
                    await asyncio.sleep(1)

                    current_libraries = await get_libraries()

                    for path, id in current_libraries.items():
                        if path not in watching_libraries:
                            print('Watching new path:', path)
                            watch = inotify.add_watch(
                                path, Mask.MODIFY | Mask.CREATE | Mask.DELETE
                                | Mask.CLOSE | Mask.MOVE)
                            watching_libraries[path] = (id, watch)

                    for path, (id, watch) in watching_libraries.items():
                        if path not in current_libraries:
                            print('Removing old path:', path)
                            inotify.rm_watch(watch)

                    await asyncio.sleep(4)

            async def handle_inotify_events():
                async for event in inotify:
                    if event.mask in [
                            Mask.CLOSE_WRITE, Mask.MOVED_TO, Mask.DELETE,
                            Mask.MOVED_FROM
                    ]:
                        photo_path = event.path
                        library_id = None
                        for potential_library_path, (
                                potential_library_id,
                                _) in watching_libraries.items():
                            if str(photo_path).startswith(
                                    potential_library_path):
                                library_id = potential_library_id

                        if event.mask in [Mask.DELETE, Mask.MOVED_FROM]:
                            print(
                                f'Removing photo "{photo_path}" from library "{library_id}"'
                            )
                            await record_photo_async(
                                photo_path, library_id,
                                str(event.mask).split('.')[1])
                        else:
                            if imghdr.what(photo_path) or not subprocess.run(
                                ['dcraw', '-i', photo_path]).returncode:
                                print(
                                    f'Adding photo "{photo_path}" to library "{library_id}"'
                                )
                                await record_photo_async(
                                    photo_path, library_id,
                                    str(event.mask).split('.')[1])

            loop = asyncio.get_event_loop()
            loop.create_task(check_libraries())
            loop.create_task(handle_inotify_events())

            try:
                loop.run_forever()
            except KeyboardInterrupt:
                print('Shutting down')
            finally:
                loop.run_until_complete(loop.shutdown_asyncgens())
                loop.close()
Esempio n. 10
0
class IcalAlarmWatcher:
    def __init__(self, path: Path):
        self.path = path
        # Store the inotify mask which is used throughout the watcher
        self.mask = Mask.MOVED_FROM  | \
                    Mask.MOVED_TO    | \
                    Mask.CREATE      | \
                    Mask.DELETE_SELF | \
                    Mask.IGNORED     | \
                    Mask.CLOSE_WRITE
        # Setup an inotify watching instance
        self.notifier = Inotify()

        # Queue of the various notifications to be sent at timestamps
        self.alarm_queue = []
        self.trigger = None

    async def init_alarms(self, path: Path):
        """Add the alarms for all of the ics files under a directory
        (recursively) to the alarm queue"""
        for ics in path.glob('**/*.ics'):
            await self.insert_alarm(ics)

    async def add_listeners(self, path: Path):
        """Add the required listeners for a directory and its children"""
        self.notifier.add_watch(path, self.mask)

        for child in self.get_subdirs(path):
            self.notifier.add_watch(child, self.mask)

    async def listen(self):
        """Listen for changes in files, making sure to track newly created
        files and update the ICS alarms as changes come in"""
        # Add listeners for the root directory and all of its children
        await self.init_alarms(self.path)
        await self.add_listeners(self.path)

        # Prime the first alarm to trigger a notification
        self.prime()

        # Loop through the inotify listeners to check for changes to files
        async for event in self.notifier:
            # Listen for new dirs, and add listeners for them
            if Mask.CREATE in event.mask and event.path.is_dir():
                await self.init_alarms(event.path)
                await self.add_listeners(event.path)
                self.prime()
            
            # A file changed, so regenerate the alarm queue and trigger
            # the new 'next alarm'
            elif not event.path.is_dir():
                await self.insert_alarm(event.path)
                self.prime()

    async def insert_alarm(self, path: Path):
        """Insert the alarms given by the file at the specified path
        into the alarm queue as a Notification object"""
        # Remove old alarms related to this file from the queue
        self.alarm_queue = list(
                filter(lambda x: x[-1] != path, self.alarm_queue))

        # Read content from the file
        try:
            async with aiofiles.open(path, mode='r') as f:
                content = await f.read()
        except FileNotFoundError:
            return

        # Parse the content into a calendar
        try:
            c = Calendar(content)
        except NotImplementedError:
            # TODO: Make sure this handles multiple calendars in a single file
            return

        # Loop trough the alarms in the calendar
        for event in c.events:
            for alarm in event.alarms:
                title = event.name
                details = event.description

                if not title:
                    title = "iCalendar alarm"
                if not details:
                    details = "Event happening soon"

                # If the alarm trigger is a timedelta prior to start,
                # convert it to an absolute time
                if isinstance(alarm.trigger, timedelta):
                    alarm_time = event.begin + alarm.trigger
                else:
                    alarm_time = alarm.trigger

                # Append this alarm to the queue
                alarm_time = datetime.fromtimestamp(alarm_time.timestamp)
                notification = Notification(title, details)
                heapq.heappush(self.alarm_queue, (alarm_time, notification, path))

    def prime(self):
        """Clear any alarm triggers already in memory, and use the most recent
        alarm in the alarm_queue to setup a new notification event"""
        # Cancel the current alarm so the new one can be triggered on its own
        if self.trigger is not None:
            self.trigger.cancel()

        # Go through the alarm queue and remove notifications that should
        # have been triggered some time ago
        while self.alarm_queue and self.alarm_queue[0][0] < datetime.now():
            heapq.heappop(self.alarm_queue)

        if not self.alarm_queue:
            return

        # Obtain the data of the next alarm to be triggered
        (timestamp, notification, path) = self.alarm_queue[-1]

        # Small function that triggers the notification for an alarm,
        # and then clears it from the queue
        def _notify_and_pop():
            notification.notify()
            heapq.heappop(self.alarm_queue)

        self.trigger = Trigger(timestamp, _notify_and_pop)
        self.trigger()

    @classmethod
    def get_subdirs(cls, path: Path) -> Generator[Path, None, None]:
        """Recursively list all directories under path"""
        for child in path.iterdir():
            if child.is_dir():
                yield child
                yield from cls.get_subdirs(child)
Esempio n. 11
0
class TestSyncInotify(unittest.TestCase):
    def watch_events(self):
        '''Watch events until an IGNORED is received for the main watch, then
        return the events.'''
        events = []
        with self.inotify as inotify:
            for event in inotify:
                events.append(event)
                if Mask.IGNORED in event and event.watch is self.watch:
                    return events

    def gather_events(self, function):
        '''Run the function and then watch events until you can return the
        result.'''

        try:
            function()
        finally:
            self.inotify.rm_watch(self.watch)

        return self.watch_events()

    def setUp(self):
        self._dir = TemporaryDirectory()
        self.dir = Path(self._dir.name)
        self.inotify = Inotify()
        self.watch = self.inotify.add_watch(
            self.dir,
            Mask.ACCESS | Mask.MODIFY | Mask.ATTRIB | Mask.CLOSE_WRITE
            | Mask.CLOSE_NOWRITE | Mask.OPEN | Mask.MOVED_FROM | Mask.MOVED_TO
            | Mask.CREATE | Mask.DELETE | Mask.DELETE_SELF | Mask.MOVE_SELF)

    def tearDown(self):
        self._dir.cleanup()

    def test_diriterated(self):
        def test():
            list(self.dir.iterdir())

        events = self.gather_events(test)
        self.assertTrue(all(event.watch is self.watch for event in events))
        self.assertTrue(
            any(Mask.ISDIR | Mask.OPEN in event and event.path == self.dir
                for event in events))
        self.assertTrue(
            any(Mask.ISDIR | Mask.ACCESS in event and event.path == self.dir
                for event in events))
        self.assertTrue(
            any(Mask.ISDIR
                | Mask.CLOSE_NOWRITE in event and event.path == self.dir
                for event in events))
        self.assertTrue(
            any(Mask.IGNORED in event and event.path == self.dir
                for event in events))

    def test_foo_opened_and_closed(self):
        def test():
            with open(self.dir / 'foo', 'w') as file:
                pass
            with open(self.dir / 'foo', 'r') as file:
                pass

        events = self.gather_events(test)
        self.assertTrue(all(event.watch is self.watch for event in events))
        self.assertTrue(
            any(Mask.CREATE in event and event.path == self.dir / 'foo'
                for event in events))
        self.assertTrue(
            any(Mask.OPEN in event and event.path == self.dir / 'foo'
                for event in events))
        self.assertTrue(
            any(Mask.CLOSE_WRITE in event and event.path == self.dir / 'foo'
                for event in events))
        self.assertTrue(
            any(Mask.CLOSE_NOWRITE in event and event.path == self.dir / 'foo'
                for event in events))

    def test_foo_deleted(self):
        def test():
            with open(self.dir / 'foo', 'w') as file:
                pass

            (self.dir / 'foo').unlink()

        events = self.gather_events(test)
        self.assertTrue(all(event.watch is self.watch for event in events))
        self.assertTrue(
            any(Mask.DELETE in event and event.path == self.dir / 'foo'
                for event in events))

    def test_foo_write(self):
        def test():
            with open(self.dir / 'foo', 'w') as file:
                file.write('test')

        events = self.gather_events(test)
        self.assertTrue(all(event.watch is self.watch for event in events))
        self.assertTrue(
            any(Mask.CREATE in event and event.path == self.dir / 'foo'
                for event in events))
        self.assertTrue(
            any(Mask.OPEN in event and event.path == self.dir / 'foo'
                for event in events))
        self.assertTrue(
            any(Mask.MODIFY in event and event.path == self.dir / 'foo'
                for event in events))
        self.assertTrue(
            any(Mask.CLOSE_WRITE in event and event.path == self.dir / 'foo'
                for event in events))

    def test_foo_moved(self):
        def test():
            with open(self.dir / 'foo', 'w') as file:
                pass

            (self.dir / 'foo').rename(self.dir / 'bar')

        events = self.gather_events(test)
        self.assertTrue(all(event.watch is self.watch for event in events))
        self.assertTrue(
            any(Mask.MOVED_FROM in event and event.path == self.dir / 'foo'
                for event in events))
        self.assertTrue(
            any(Mask.MOVED_TO in event and event.path == self.dir / 'bar'
                for event in events))
        self.assertEqual(
            next(event.cookie for event in events if Mask.MOVED_FROM in event),
            next(event.cookie for event in events if Mask.MOVED_TO in event),
        )

    def test_foo_attrib(self):
        def test():
            with open(self.dir / 'foo', 'w') as file:
                pass

            (self.dir / 'foo').chmod(0o777)

        events = self.gather_events(test)
        self.assertTrue(all(event.watch is self.watch for event in events))
        self.assertTrue(
            any(Mask.ATTRIB in event and event.path == self.dir / 'foo'
                for event in events))

    def test_onlydir_error(self):
        with open(self.dir / 'foo', 'w'):
            pass

        # Will not raise error
        self.inotify.add_watch(self.dir / 'foo', Mask.ATTRIB)

        with self.assertRaises(InotifyError):
            self.inotify.add_watch(self.dir / 'foo',
                                   Mask.ATTRIB | Mask.ONLYDIR)

    def test_nonexist_error(self):
        with self.assertRaises(InotifyError):
            self.inotify.add_watch(self.dir / 'foo',
                                   Mask.ATTRIB | Mask.ONLYDIR)

        with self.assertRaises(InotifyError):
            self.inotify.add_watch(self.dir / 'foo', Mask.ATTRIB)

    def test_move_self(self):
        with open(self.dir / 'foo', 'w'):
            pass

        watch = self.inotify.add_watch(self.dir / 'foo', Mask.MOVE_SELF)

        def test():
            (self.dir / 'foo').rename(self.dir / 'bar')

        events = self.gather_events(test)
        self.assertTrue(
            any(Mask.MOVE_SELF in event and event.path == self.dir /
                'foo' and event.watch is watch for event in events))

    def test_delete_self(self):
        with open(self.dir / 'foo', 'w'):
            pass

        watch = self.inotify.add_watch(self.dir / 'foo', Mask.DELETE_SELF)

        def test():
            (self.dir / 'foo').unlink()

        events = self.gather_events(test)

        self.assertTrue(
            any(Mask.DELETE_SELF in event and event.path == self.dir /
                'foo' and event.watch is watch for event in events))
        self.assertTrue(
            any(Mask.IGNORED in event and event.path == self.dir /
                'foo' and event.watch is watch for event in events))
        self.assertTrue(
            any(Mask.IGNORED in event and event.path == self.dir
                for event in events))

    def test_oneshot(self):
        with open(self.dir / 'foo', 'w'):
            pass

        watch = self.inotify.add_watch(self.dir / 'foo',
                                       Mask.CREATE | Mask.OPEN | Mask.ONESHOT)

        def test():
            with open(self.dir / 'foo', 'r'):
                pass
            (self.dir / 'foo').unlink()

        events = self.gather_events(test)

        # We check for name is None because only the first event will have a watch value
        self.assertTrue(
            any(Mask.OPEN in event and event.name is None
                and event.path == self.dir / 'foo' and event.watch is watch
                for event in events))
        # The oneshot has already expired, so this should not exist
        self.assertFalse(
            any(Mask.DELETE in event and event.name is None
                for event in events))
Esempio n. 12
0
    def watch_photos(self):
        """Management command to watch photo directory and create photo records in database."""
        watching_libraries = {}

        with Inotify() as inotify:

            @sync_to_async
            def get_libraries():
                return {l.path: l.library_id for l in LibraryPath.objects.filter(type='St', backend_type='Lo')}

            @sync_to_async
            def record_photo_async(photo_path, library_id, event_mask):
                record_photo(photo_path, library_id, event_mask)

            @sync_to_async
            def move_or_rename_photo_async(photo_old_path, photo_new_path, library_id):
                move_or_rename_photo(photo_old_path, photo_new_path, library_id)

            @sync_to_async
            def delete_child_dir_all_photos_async(photo_path, library_id):
                delete_child_dir_all_photos(photo_path, library_id)

            def get_directories_recursive(path: Path) -> Generator[Path, None, None]:
                """ Recursively list all directories under path, including path itself, if
                it's a directory.

                The path itself is always yielded before its children are iterated, so you
                can pre-process a path (by watching it with inotify) before you get the
                directory listing.

                Passing a non-directory won't raise an error or anything, it'll just yield
                nothing.
                """

                if path.is_dir():
                    yield path
                    for child in path.iterdir():
                        yield from get_directories_recursive(child)

            async def check_libraries():
                while True:
                    await asyncio.sleep(1)

                    current_libraries = await get_libraries()

                    for path, id in current_libraries.items():
                        if path not in watching_libraries:
                            for directory in get_directories_recursive(Path(path)):
                                logger.info(f'Watching new path: {directory}')
                                watch = inotify.add_watch(directory, Mask.MODIFY | Mask.CREATE | Mask.DELETE | Mask.CLOSE | Mask.MOVE)
                                watching_libraries[path] = (id, watch)

                    for path, (id, watch) in watching_libraries.items():
                        if path not in current_libraries:
                            logger.info(f'Removing old path: {path}')
                            inotify.rm_watch(watch)

                    await asyncio.sleep(4)

            async def handle_inotify_events():
                async for event in inotify:
                    if 'moved_from_attr_dict' in locals() and moved_from_attr_dict:
                        for potential_library_path, (potential_library_id, _) in watching_libraries.items():
                            if str(event.path).startswith(potential_library_path):
                                library_id = potential_library_id
                        photo_moved_from_path = moved_from_attr_dict.get('moved_from_path')
                        photo_moved_from_cookie = moved_from_attr_dict.get('moved_from_cookie')
                        moved_from_attr_dict = {}
                        if event.mask.name == 'MOVED_TO' and photo_moved_from_cookie == event.cookie:
                            logger.info(f'Moving or renaming the photo "{str(event.path)}" from library "{library_id}"')
                            await move_or_rename_photo_async(photo_moved_from_path, event.path, library_id)
                        else:
                            logger.info(f'Removing photo "{str(photo_moved_from_path)}" from library "{library_id}"')
                            await record_photo_async(photo_moved_from_path, library_id, 'MOVED_FROM')
                    elif Mask.CREATE in event.mask and event.path is not None and event.path.is_dir():
                        current_libraries = await get_libraries()
                        for path, id in current_libraries.items():
                            for directory in get_directories_recursive(event.path):
                                logger.info(f'Watching newly created child directory: {directory}')
                                watch = inotify.add_watch(directory, Mask.MODIFY | Mask.CREATE | Mask.DELETE | Mask.CLOSE | Mask.MOVE)
                                watching_libraries[path] = (id, watch)

                    elif event.mask in [Mask.CLOSE_WRITE, Mask.MOVED_TO, Mask.DELETE, Mask.MOVED_FROM] or event.mask.value == 1073741888:
                        photo_path = event.path
                        library_id = None
                        for potential_library_path, (potential_library_id, _) in watching_libraries.items():
                            if str(photo_path).startswith(potential_library_path):
                                library_id = potential_library_id
                        if event.mask in [Mask.DELETE, Mask.MOVED_FROM]:
                            if event.mask.name == 'MOVED_FROM':
                                moved_from_attr_dict = {
                                    'moved_from_path': event.path,
                                    'moved_from_cookie': event.cookie}
                            else:
                                logger.info(f'Removing photo "{photo_path}" from library "{library_id}"')
                                await record_photo_async(photo_path, library_id, event.mask.name)
                        elif event.mask.value == 1073741888:
                            logger.info(f'Delete child directory with its all photos "{photo_path}" to library "{library_id}"')
                            await delete_child_dir_all_photos_async(photo_path, library_id)
                        else:
                            logger.info(f'Adding photo "{photo_path}" to library "{library_id}"')
                            await record_photo_async(photo_path, library_id, event.mask.name)

            loop = asyncio.get_event_loop()
            loop.create_task(check_libraries())
            loop.create_task(handle_inotify_events())

            try:
                loop.run_forever()
            except KeyboardInterrupt:
                logger.info('Shutting down')
            finally:
                loop.run_until_complete(loop.shutdown_asyncgens())
                loop.close()