Example #1
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)
Example #2
0
    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)

    signal.signal(signal.SIGTERM, sig_handler)
Example #3
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))