Thomas Gläßle avatar Thomas Gläßle committed e30fe86

Separate notification service from business logic

Both notification and automounting utilities now use
udiskie.daemon.Daemon as event detector.

Comments (0)

Files changed (8)

 
 if __name__ == '__main__':
     import sys
-    import udiskie.automount
+    import udiskie.mount
 
-    sys.exit(udiskie.automount.cli(sys.argv[1:]))
+    sys.exit(udiskie.mount.cli(sys.argv[1:], allow_daemon=True))

udiskie/automount.py

 import logging
-import gobject
-
-import udiskie
-import udiskie.device
-import udiskie.mount
-import udiskie.notify
-
-class DeviceState:
-    def __init__(self, mounted, has_media):
-        self.mounted = mounted
-        self.has_media = has_media
 
 class AutoMounter:
+    """
+    Automatically mount newly added media.
+    """
+    def __init__(self, mounter):
+        self.log = logging.getLogger('udiskie.mount.AutoMounter')
+        self.mounter = mounter
 
-    def __init__(self, bus=None, filter_file=None, notify=None, prompt=None):
-        self.log = logging.getLogger('udiskie.mount.AutoMounter')
-        self.mounter = udiskie.mount.Mounter(bus, filter_file, notify, prompt)
-        self.bus = self.mounter.bus
+    def connect(self, daemon):
+        daemon.connect('device_added', self.device_added)
+        daemon.connect('media_added', self.media_added)
 
-        self.last_device_state = {}
+    def disconnect(self, daemon):
+        daemon.disconnect('device_added', self.device_added)
+        daemon.disconnect('media_added', self.media_added)
 
-        self.bus.add_signal_receiver(self.device_added,
-                                     signal_name='DeviceAdded',
-                                     bus_name='org.freedesktop.UDisks')
-        self.bus.add_signal_receiver(self.device_removed,
-                                     signal_name='DeviceRemoved',
-                                     bus_name='org.freedesktop.UDisks')
-        self.bus.add_signal_receiver(self.device_changed,
-                                     signal_name='DeviceChanged',
-                                     bus_name='org.freedesktop.UDisks')
+    def device_added(self, udevice):
+        self.mounter.add_device(udevice)
 
+    def media_added(self, udevice):
+        self.mounter.add_device(udevice)
 
-    def device_added(self, device):
-        self.log.debug('device added: %s' % (device,))
-        udiskie_device = udiskie.device.Device(self.bus, device)
-        self._store_device_state(udiskie_device)
-        self.mounter.add_device(udiskie_device)
-
-    def device_removed(self, device):
-        self.log.debug('device removed: %s' % (device,))
-        self._remove_device_state(udiskie.device.Device(self.bus, device))
-
-    def device_changed(self, device):
-        self.log.debug('device changed: %s' % (device,))
-
-        udiskie_device = udiskie.device.Device(self.bus, device)
-        last_state = self._get_device_state(udiskie_device)
-
-        if not last_state:
-            # First time we saw the device, try to mount it.
-            self.mounter.add_device(udiskie_device)
-        else:
-            media_added = False
-            if udiskie_device.has_media and not last_state.has_media:
-                media_added = True
-
-            if media_added and not last_state.mounted:
-                # Wasn't mounted before, but it has new media now.
-                self.mounter.add_device(udiskie_device)
-
-        self._store_device_state(udiskie_device)
-
-
-    def _store_device_state(self, device):
-        state = DeviceState(device.is_mounted,
-                            device.has_media)
-        self.last_device_state[device.device_path] = state
-
-    def _remove_device_state(self, device):
-        if device.device_path in self.last_device_state:
-            del self.last_device_state[device.device_path]
-
-    def _get_device_state(self, device):
-        return self.last_device_state.get(device.device_path)
-
-
-
-def cli(args):
-    import udiskie.mount
-    options, posargs = udiskie.mount.option_parser().parse_args(args)
-
-    # invoked as a mount tool
-    if options.all or len(posargs) > 0:
-        return udiskie.mount.cli(args)
-
-    # run as a daemon
-    else:
-        logging.basicConfig(level=options.log_level, format='%(message)s')
-
-        if options.suppress_notify:
-            notify = None
-        else:
-            notify = udiskie.notify.Notify('udiskie.mount')
-
-        import udiskie.prompt
-        prompt = udiskie.prompt.password(options.password_prompt)
-
-        mounter = AutoMounter(
-                bus=None, filter_file=options.filters,
-                notify=notify, prompt=prompt)
-        mounter.mounter.mount_present_devices()
-        return gobject.MainLoop().run()
-
-

udiskie/common.py

+"""
+Common utilities.
+"""
+__all__ = ['Properties', 'system_bus']
+import dbus
+
+
+DBUS_PROPS_INTERFACE = 'org.freedesktop.DBus.Properties'
+
+class Properties:
+    """
+    Dbus property map abstraction.
+
+    Properties of the object can be accessed as attributes.
+
+    """
+    def __init__(self, dbus_object, interface):
+        """Initialize a proxy object with standard dbus property interface."""
+        self.__proxy = dbus.Interface(
+                dbus_object,
+                dbus_interface=DBUS_PROPS_INTERFACE)
+        self.__interface = interface
+
+    def __getattr__(self, property):
+        """Retrieve the property via the dbus proxy."""
+        return self.__proxy.Get(self.__interface, property)
+
+def system_bus():
+    """
+    Connect to system bus.
+
+    When called for the first time, dbus will be initialized to use a glib
+    mainloop.
+
+    """
+    if not system_bus.initialized:
+        from dbus.mainloop.glib import DBusGMainLoop
+        DBusGMainLoop(set_as_default=True)
+        system_bus.initialized = True
+    return dbus.SystemBus()
+system_bus.initialized = False
+

udiskie/daemon.py

+"""
+Udisks event daemon module.
+
+Provides the class `Daemon` which listens to udisks events. When a change
+occurs this class detects what has changed and triggers an appropriate
+event.
+
+"""
+__all__ = ['Daemon']
+
+import gobject
+import logging
+import dbus
+
+from udiskie.device import Device, get_all_handleable
+
+
+class DeviceState:
+    """
+    State information struct for devices.
+    """
+    __slots__ = ['mounted', 'has_media', 'unlocked']
+
+    def __init__(self, mounted, has_media, unlocked):
+        self.mounted = mounted
+        self.has_media = has_media
+        self.unlocked = unlocked
+
+class Daemon:
+    """
+    Udisks listener daemon.
+
+    Listens to udisks events. When a change occurs this class detects what
+    has changed and triggers an appropriate event. Valid events are:
+
+        - device_added    / device_removed
+        - device_unlocked / device_locked
+        - device_mounted  / device_unmounted
+        - media_added     / media_removed
+        - device_changed
+
+    A very primitive mechanism that gets along without external
+    dependencies is used for event dispatching. The methods `connect` and
+    `disconnect` can be used to add or remove event handlers.
+
+    """
+    def __init__(self, bus):
+        """
+        Initialize object and start listening to udisks events.
+        """
+        self.log = logging.getLogger('udiskie.daemon.Daemon')
+        self.bus = bus
+        self.state = {}
+
+        self.event_handlers = {
+            'device_added': [],
+            'device_removed': [],
+            'device_mounted': [],
+            'device_unmounted': [],
+            'media_added': [],
+            'media_removed': [],
+            'device_unlocked': [],
+            'device_locked': [],
+            'device_changed': [self.on_device_changed]
+        }
+
+        for device in get_all_handleable(bus):
+            self._store_device_state(device)
+
+        self.bus.add_signal_receiver(
+                self._device_added,
+                signal_name='DeviceAdded',
+                bus_name='org.freedesktop.UDisks')
+        self.bus.add_signal_receiver(
+                self._device_removed,
+                signal_name='DeviceRemoved',
+                bus_name='org.freedesktop.UDisks')
+        self.bus.add_signal_receiver(
+                self._device_changed,
+                signal_name='DeviceChanged',
+                bus_name='org.freedesktop.UDisks')
+
+    def run(self):
+        """Run main loop."""
+        return gobject.MainLoop().run()
+
+    # events
+    def on_device_changed(self, udevice, old_state, new_state):
+        """Detect type of event and trigger appropriate event handlers."""
+        if old_state is None:
+            self.trigger('device_added', udevice)
+            return
+        d = {}
+        d['device_mounted'] = new_state.mounted and not old_state.mounted
+        d['device_unmounted'] = old_state.mounted and not new_state.mounted
+        d['media_added'] = new_state.has_media and not old_state.has_media
+        d['media_removed'] = old_state.has_media and not new_state.has_media
+        d['device_unlocked'] = new_state.unlocked and not old_state.unlocked
+        d['device_locked'] = old_state.unlocked and not new_state.unlocked
+        for event in d:
+            if d[event]:
+                self.trigger(event, udevice)
+
+    # event machinery
+    def trigger(self, event, device, *args):
+        """Trigger event handlers."""
+        self.log.debug('%s: %s' % (event, device))
+        for handler in self.event_handlers[event]:
+            handler(device, *args)
+
+    def connect(self, event, handler):
+        """Connect an event handler."""
+        self.event_handlers[event].append(handler)
+
+    def disconnect(self, event, handler):
+        """Disconnect an event handler."""
+        self.event_handlers.remove(handler)
+
+    # udisks event listeners
+    def _device_added(self, device_name):
+        try:
+            udevice = Device(self.bus, device_name)
+            if not udevice.is_handleable:
+                return
+            self._store_device_state(udevice)
+            self.trigger('device_added', udevice)
+        except dbus.exceptions.DBusException, err:
+            self.log.error('%s(%s): %s' % ('device_added', device_name, err))
+
+    def _device_removed(self, device_name):
+        try:
+            self.trigger('device_removed', device_name)
+            self._remove_device_state(device_name)
+        except dbus.exceptions.DBusException, err:
+            self.log.error('%s(%s): %s' % ('device_removed', device_name, err))
+
+    def _device_changed(self, device_name):
+        try:
+            udevice = Device(self.bus, device_name)
+            if not udevice.is_handleable:
+                return
+            old_state = self._get_device_state(udevice)
+            new_state = self._store_device_state(udevice)
+            self.trigger('device_changed', udevice, old_state, new_state)
+        except dbus.exceptions.DBusException, err:
+            self.log.error('%s(%s): %s' % ('device_changed', device_name, err))
+
+    # internal state keeping
+    def _store_device_state(self, device):
+        self.state[device.device_path] = DeviceState(
+            device.is_mounted,
+            device.has_media,
+            device.is_unlocked)
+        return self.state[device.device_path]
+
+    def _remove_device_state(self, device_name):
+        if device_name in self.state:
+            del self.state[device_name]
+
+    def _get_device_state(self, device):
+        return self.state.get(device.device_path)
+

udiskie/device.py

 import os
 import dbus
 
-DBUS_PROPS_INTERFACE = 'org.freedesktop.DBus.Properties'
+from udiskie.common import Properties as DbusProperties
+
+
 UDISKS_INTERFACE = 'org.freedesktop.UDisks'
 UDISKS_DEVICE_INTERFACE = 'org.freedesktop.UDisks.Device'
 
 UDISKS_OBJECT = 'org.freedesktop.UDisks'
 UDISKS_OBJECT_PATH = '/org/freedesktop/UDisks'
 
-class DbusProperties:
-    """
-    Dbus property map abstraction.
-
-    Properties of the object can be accessed as attributes.
-
-    """
-    def __init__(self, dbus_object, interface):
-        """Initialize a proxy object with standard dbus property interface."""
-        self.__proxy = dbus.Interface(
-                dbus_object,
-                dbus_interface=DBUS_PROPS_INTERFACE)
-        self.__interface = interface
-
-    def __getattr__(self, property):
-        """Retrieve the property via the dbus proxy."""
-        return self.__proxy.Get(self.__interface, property)
 
 class Device:
+    """
+    Wrapper class for org.freedesktop.UDisks.Device proxy objects.
+    """
     def __init__(self, bus, device_path):
         self.log = logging.getLogger('udiskie.device.Device')
         self.bus = bus
         return self.method.LuksUnlock(password, options)
 
 
-
 def get_all(bus):
+    """Enumerate all device objects currently known to udisks."""
     udisks = bus.get_object(UDISKS_OBJECT, UDISKS_OBJECT_PATH)
     for path in udisks.EnumerateDevices(dbus_interface=UDISKS_INTERFACE):
         yield Device(bus, path)
 
+def get_all_handleable(bus):
+    """Enumerate all handleable devices currently known to udisks."""
+    for device in get_all(bus):
+        if device.is_handleable:
+            yield device
+
 def get_device(bus, path):
+    """Get a device proxy by device name or any mount path of the device."""
     logger = logging.getLogger('udiskie.device.get_device')
     for device in get_all(bus):
         if os.path.samefile(path, device.device_file):
 
 import udiskie.device
 import udiskie.match
+import udiskie.prompt
 import udiskie.notify
-import udiskie.prompt
+import udiskie.automount
+import udiskie.daemon
+
+from udiskie.common import system_bus
 
 
 class Mounter:
     CONFIG_PATH = 'udiskie/filters.conf'
 
-    def __init__(self, bus=None, filter_file=None, notify=None, prompt=None):
+    def __init__(self, bus, filter_file=None, prompt=None):
         self.log = logging.getLogger('udiskie.mount.Mounter')
-
-        if not bus:
-            from dbus.mainloop.glib import DBusGMainLoop
-            DBusGMainLoop(set_as_default=True)
-            self.bus = dbus.SystemBus()
-        else:
-            self.bus = bus
+        self.bus = bus
 
         if not filter_file:
             filter_file = os.path.join(xdg_config_home, self.CONFIG_PATH)
         self.filters = udiskie.match.FilterMatcher((filter_file,))
 
-        if not notify:
-            self.notify = lambda ctx: lambda *args: True
-        else:
-            self.notify = lambda ctx: getattr(notify, ctx)
-
         if not prompt:
             self.prompt = lambda text, title: None
         else:
             return None
 
         mount_paths = ', '.join(device.mount_paths)
-        self.notify('mount')(device.device_file, mount_paths)
 
         return True
 
             self.log.error('failed to unlock device %s:\n%s'
                                         % (device, dbus_err))
             return None
-
-        self.notify('unlock')(device.device_file)
         return True
 
     def add_device(self, device):
 
     def mount_present_devices(self):
         """Mount handleable devices that are already present."""
-        for device in udiskie.device.get_all(self.bus):
+        for device in udiskie.device.get_all_handleable(self.bus):
             self.add_device(device)
 
 
                       metavar='MODULE', help="replace password prompt")
     return parser
 
-def cli(args):
+def cli(args, allow_daemon=False):
     parser = option_parser()
     options, posargs = parser.parse_args(args)
     logging.basicConfig(level=options.log_level, format='%(message)s')
 
-    if options.suppress_notify:
-        notify = None
-    else:
+    # establish connection to system bus
+    bus = system_bus()
+
+    # create a mounter
+    prompt = udiskie.prompt.password(options.password_prompt)
+    mounter = Mounter(bus=bus, filter_file=options.filters, prompt=prompt)
+
+    # run udiskie daemon if needed
+    run_daemon = allow_daemon and not options.all and len(posargs) == 0
+    if run_daemon:
+        daemon = udiskie.daemon.Daemon(bus)
+
+    if run_daemon and not options.suppress_notify:
         notify = udiskie.notify.Notify('udiskie.mount')
+        notify.connect(daemon)
 
-    prompt = udiskie.prompt.password(options.password_prompt)
-
-    mounter = Mounter(
-            bus=None, filter_file=options.filters,
-            notify=notify, prompt=prompt)
+    if run_daemon:
+        automount = udiskie.automount.AutoMounter(mounter)
+        automount.connect(daemon)
 
     # mount all present devices
     if options.all:
             if device:
                 mounter.add_device(device)
 
+    # run in daemon mode
+    elif run_daemon:
+        mounter.mount_present_devices()
+        return daemon.run()
+
     # print command line options
     else:
         parser.print_usage()

udiskie/notify.py

 import gio
 
 class Notify:
+    """
+    Notification tool.
+
+    Can be connected to udisks daemon in order to automatically issue
+    notifications when system status has changed.
+
+    """
     def __init__(self, name):
+        """Initialize notifier."""
         pynotify.init(name)
 
-    def mount(self, device, path):
+    def connect(self, daemon):
+        """Connect to udisks daemon."""
+        daemon.connect('device_mounted', self.mount)
+        daemon.connect('device_unmounted', self.umount)
+        daemon.connect('device_unlocked', self.unlock)
+        daemon.connect('device_locked', self.lock)
+
+    def disconnect(self, daemon):
+        """Disconnect from udisks daemon."""
+        daemon.disconnect('device_mounted', self.mount)
+        daemon.disconnect('device_unmounted', self.umount)
+        daemon.disconnect('device_unlocked', self.unlock)
+        daemon.disconnect('device_locked', self.lock)
+
+    # event handlers:
+    def mount(self, device):
         try:
+            device_file = device.device_file
+            mount_path = device.mount_paths[0]
             pynotify.Notification('Device mounted',
-                                  '%s mounted on %s' % (device, path),
+                                  '%s mounted on %s' % (device_file, mount_path),
                                   'drive-removable-media').show()
         except gio.Error:
             pass
 
     def umount(self, device):
         try:
+            device_file = device.device_file
             pynotify.Notification('Device unmounted',
-                                  '%s unmounted' % (device,),
+                                  '%s unmounted' % (device_file,),
                                   'drive-removable-media').show()
         except gio.Error:
             pass
 
     def lock(self, device):
         try:
+            device_file = device.device_file
             pynotify.Notification('Device locked',
-                                  '%s locked' % (device,),
+                                  '%s locked' % (device_file,),
                                   'drive-removable-media').show()
         except gio.Error:
             pass
 
     def unlock(self, device):
         try:
+            device_file = device.device_file
             pynotify.Notification('Device unlocked',
-                                  '%s unlocked' % (device,),
+                                  '%s unlocked' % (device_file,),
                                   'drive-removable-media').show()
         except gio.Error:
             pass
+

udiskie/umount.py

 import dbus
 
 import udiskie.device
-import udiskie.notify
 
-def unmount_device(device, notify):
+def unmount_device(device):
     """
     Unmount a Device.
 
         logger.error('failed to unmount device %s: %s' % (device,
                                                             dbus_err))
         return None
-    notify('umount')(device.device_file)
     return True
 
-def lock_device(device, notify):
+def lock_device(device):
     """
     Lock device.
 
     except dbus.exceptions.DBusException, dbus_err:
         logger.error('failed to lock device %s: %s' % (device, dbus_err))
         return None
-    notify('lock')(device.device_file)
     return True
 
-def remove_device(device, notify):
+def remove_device(device):
     """Unmount or lock the device depending on device type."""
     logger = logging.getLogger('udiskie.umount.remove_device')
     if not device.is_handleable:
         logger.debug('skipping unhandled device %s' % (device,))
         return False
     if device.is_filesystem:
-        return unmount_device(device, notify)
+        return unmount_device(device)
     elif device.is_crypto:
-        return lock_device(device, notify)
+        return lock_device(device)
 
-def lock_slave(device, notify):
+def lock_slave(device):
     """
     Lock the luks slave of this device.
 
     slave = udiskie.device.Device(device.bus, slave_path)
     if slave.is_luks_cleartext_slave:
         return False
-    return lock_device(slave, notify)
+    return lock_device(slave)
 
 
-def unmount(path, notify):
+def unmount(path):
     """Unmount or lock a filesystem
 
     The filesystem must match the criteria for a filesystem mountable by
     device = udiskie.device.get_device(bus, path)
     if device:
         logger.debug('found device owning "%s": "%s"' % (path, device))
-        if remove_device(device, notify):
+        if remove_device(device):
             return device
     return None
 
 
-def unmount_all(notify):
+def unmount_all():
     """Unmount all filesystems handleable by udiskie."""
-
     unmounted = []
     bus = dbus.SystemBus()
     for device in udiskie.device.get_all(bus):
-        if unmount_device(device, notify):
+        if unmount_device(device):
             unmounted.append(device)
     return unmounted
 
     (options, posargs) = option_parser().parse_args(args)
     logging.basicConfig(level=options.log_level, format='%(message)s')
 
-    if options.suppress_notify:
-        notify = lambda ctx: lambda *args: True
-    else:
-        notify_ = udiskie.notify.Notify('udiskie.umount')
-        notify = lambda ctx: getattr(notify_, ctx)
-
     if options.all:
-        unmounted = unmount_all(notify)
+        unmounted = unmount_all()
     else:
         if len(posargs) == 0:
             logger.warn('No devices provided for unmount')
 
         unmounted = []
         for path in posargs:
-            device = unmount(os.path.normpath(path), notify)
+            device = unmount(os.path.normpath(path))
             if device:
                 unmounted.append(device)
 
     # automatically lock unused luks slaves of unmounted devices
     for device in unmounted:
-        lock_slave(device, notify)
+        lock_slave(device)
 
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.