def __init__(self, argv=None): """ Parse command line options, read config and initialize members. :param list argv: command line parameters """ # parse program options (retrieve log level and config file name): args = docopt(self.usage, version=self.name + ' ' + self.version) default_opts = self.option_defaults program_opts = self.program_options(args) # initialize logging configuration: log_level = program_opts.get('log_level', default_opts['log_level']) if log_level <= logging.DEBUG: fmt = _('%(levelname)s [%(asctime)s] %(name)s: %(message)s') else: fmt = _('%(message)s') logging.basicConfig(level=log_level, format=fmt) # parse config options config_file = OptionalValue('--config')(args) config = udiskie.config.Config.from_file(config_file) options = {} options.update(default_opts) options.update(config.program_options) options.update(program_opts) # initialize instance variables self.config = config self.options = options self._init(config, options)
def get_backend(clsname, version=None): """ Return UDisks service. :param str clsname: requested service object :param int version: requested UDisks backend version :returns: UDisks service wrapper object :raises dbus.DBusException: if unable to connect to UDisks dbus service. :raises ValueError: if the version is invalid If ``version`` has a false truth value, try to connect to UDisks1 and fall back to UDisks2 if not available. """ if not version: from udiskie.dbus import DBusException try: return get_backend(clsname, 2) except DBusException: log = logging.getLogger(__name__) log.warning(_('Failed to connect UDisks2 dbus service..\n' 'Falling back to UDisks1.')) return get_backend(clsname, 1) elif version == 1: import udiskie.udisks1 return getattr(udiskie.udisks1, clsname)() elif version == 2: import udiskie.udisks2 return getattr(udiskie.udisks2, clsname)() else: raise ValueError(_("UDisks version not supported: {0}!", version))
def device_unmounted(self, device): """ Show 'Device unmounted' notification. :param device: device object """ self._show_notification('device_unmounted', _('Device unmounted'), _('{0.id_label} unmounted', device), 'drive-removable-media')
def device_unlocked(self, device): """ Show 'Device unlocked' notification. :param device: device object """ self._show_notification('device_unlocked', _('Device unlocked'), _('{0.device_presentation} unlocked', device), 'drive-removable-media')
def device_unmounted(self, device): """ Show 'Device unmounted' notification. :param device: device object """ self._show_notification( 'device_unmounted', _('Device unmounted'), _('{0.id_label} unmounted', device), 'drive-removable-media')
def device_unlocked(self, device): """ Show 'Device unlocked' notification. :param device: device object """ self._show_notification( 'device_unlocked', _('Device unlocked'), _('{0.device_presentation} unlocked', device), 'drive-removable-media')
def device_mounted(self, device): """ Show 'Device mounted' notification with 'Browse directory' button. :param device: device object """ browse_action = ('browse', _('Browse directory'), self._mounter.browse, device) self._show_notification( 'device_mounted', _('Device mounted'), _('{0.id_label} mounted on {0.mount_paths[0]}', device), 'drive-removable-media', self._mounter._browser and browse_action)
def device_added(self, device): """ Show 'Device added' notification. :param device: device object """ device_file = device.device_presentation if (device.is_drive or device.is_toplevel) and device_file: self._show_notification( 'device_added', _('Device added'), _('device appeared on {0.device_presentation}', device), 'drive-removable-media')
def device_removed(self, device): """ Show 'Device removed' notification. :param device: device object """ device_file = device.device_presentation if (device.is_drive or device.is_toplevel) and device_file: self._show_notification( 'device_removed', _('Device removed'), _('device disappeared on {0.device_presentation}', device), 'drive-removable-media')
def get_password_gui(device): """Get the password to unlock a device from GUI.""" text = _('Enter password for {0.device_presentation}: ', device) try: return password_dialog('udiskie', text) except RuntimeError: return None
def add(self, device, recursive=False): """ Mount or unlock the device depending on its type. :param device: device object, block device path or mount path :param bool recursive: recursively mount and unlock child devices :returns: whether all attempted operations succeeded :rtype: bool """ if device.is_filesystem: success = self.mount(device) elif device.is_crypto: success = self.unlock(device) if success and recursive: # TODO: update device success = self.add(device.luks_cleartext_holder, recursive=True) elif recursive and device.is_partition_table: success = True for dev in self.get_all_handleable(): if dev.is_partition and dev.partition_slave == device: success = self.add(dev, recursive=True) and success else: self._log.info(_('not adding {0}: unhandled device', device)) return False return success
def _branchmenu(self, groups): """ Create a menu from the given node. :param Branch groups: contains information about the menu :returns: a new menu object holding all groups of the node :rtype: Gtk.Menu """ def make_action_callback(node): return lambda _: node.action() menu = Gtk.Menu() separate = False for group in groups: if len(group) > 0: if separate: menu.append(Gtk.SeparatorMenuItem()) separate = True for node in group: if isinstance(node, Action): menu.append(self._menuitem( node.label, self._icons.get_icon(node.method, Gtk.IconSize.MENU), make_action_callback(node))) elif isinstance(node, Branch): menu.append(self._menuitem( node.label, icon=None, onclick=self._branchmenu(node.groups))) else: raise ValueError(_("Invalid node!")) return menu
def __init__(self, match, value): """ Construct an instance. :param dict match: device attributes :param list value: value """ self._log = logging.getLogger(__name__) self._match = match.copy() # the use of keys() makes deletion inside the loop safe: for k in self._match.keys(): if k not in self.VALID_PARAMETERS: self._log.warn(_('Unknown matching attribute: {!r}', k)) del self._match[k] self._value = value self._log.debug(_('{0} created', self))
def _branchmenu(self, groups): """ Create a menu from the given node. :param Branch groups: contains information about the menu :returns: a new menu object holding all groups of the node :rtype: Gtk.Menu """ menu = Gtk.Menu() separate = False for group in groups: if len(group) > 0: if separate: menu.append(Gtk.SeparatorMenuItem()) separate = True for node in group: if isinstance(node, Action): menu.append(self._actionitem( node.method, feed=[node.label], bind=[node.device])) elif isinstance(node, Branch): menu.append(self._menuitem( node.label, icon=None, onclick=self._branchmenu(node.groups))) else: raise ValueError(_("Invalid node!")) return menu
def browser(browser_name='xdg-open'): """ Create a browse-directory function. :param str browser_name: file manager program name :returns: one-parameter open function :rtype: callable """ if not browser_name: return None executable = find_executable(browser_name) if executable is None: # Why not raise an exception? -I think it is more convenient (for # end users) to have a reasonable default, without enforcing it. logging.getLogger(__name__).warn( _("Can't find file browser: {0!r}. " "You may want to change the value for the '-b' option.", browser_name)) return None def browse(path): return subprocess.Popen([executable, path]) return browse
def browser(browser_name='xdg-open'): """ Create a browse-directory function. :param str browser_name: file manager program name :returns: one-parameter open function :rtype: callable """ if not browser_name: return None executable = find_executable(browser_name) if executable is None: # Why not raise an exception? -I think it is more convenient (for # end users) to have a reasonable default, without enforcing it. logging.getLogger(__name__).warn( _( "Can't find file browser: {0!r}. " "You may want to change the value for the '-b' option.", browser_name)) return None def browse(path): return subprocess.Popen([executable, path]) return browse
def get_password_tty(device): """Get the password to unlock a device from terminal.""" text = _('Enter password for {0.device_presentation}: ', device) try: return getpass.getpass(text) except EOFError: print("") return None
def lock(self, device): """ Lock device if unlocked. :param device: device object, block device path or mount path :returns: whether the device is locked :rtype: bool """ if not self.is_handleable(device) or not device.is_crypto: self._log.warn(_('not locking {0}: unhandled device', device)) return False if not device.is_unlocked: self._log.info(_('not locking {0}: not unlocked', device)) return True self._log.debug(_('locking {0}', device)) device.lock() self._log.info(_('locked {0}', device)) return True
def unmount(self, device): """ Unmount a Device if mounted. :param device: device object, block device path or mount path :returns: whether the device is unmounted :rtype: bool """ if not self.is_handleable(device) or not device.is_filesystem: self._log.warn(_('not unmounting {0}: unhandled device', device)) return False if not device.is_mounted: self._log.info(_('not unmounting {0}: not mounted', device)) return True self._log.debug(_('unmounting {0}', device)) device.unmount() self._log.info(_('unmounted {0}', device)) return True
def browse(self, device): """ Browse device. :param device: device object, block device path or mount path :returns: success :rtype: bool """ if not device.is_mounted: self._log.error(_("not browsing {0}: not mounted", device)) return False if not self._browser: self._log.error(_("not browsing {0}: no program", device)) return False self._log.debug(_('opening {0} on {0.mount_paths[0]}', device)) self._browser(device.mount_paths[0]) self._log.info(_('opened {0} on {0.mount_paths[0]}', device)) return True
def require_Gtk(): """ Make sure Gtk is properly initialized. :raises RuntimeError: if Gtk can not be properly initialized """ # if we attempt to create any GUI elements with no X server running the # program will just crash, so let's make a way to catch this case: if not Gtk.init_check(None)[0]: raise RuntimeError(_("X server not connected!"))
def job_failed(self, device, action, message): """ Show 'Job failed' notification with 'Retry' button. :param device: device object """ device_file = device.device_presentation or device.object_path if message: text = _('failed to {0} {1}:\n{2}', action, device_file, message) else: text = _('failed to {0} device {1}.', action, device_file) try: retry = getattr(self._mounter, action) except AttributeError: retry_action = None else: retry_action = ('retry', _('Retry'), retry, device) self._show_notification('job_failed', _('Job failed'), text, 'drive-removable-media', retry_action)
def wrapper(self, device_or_path, *args, **kwargs): if isinstance(device_or_path, basestring): device = self.udisks.find(device_or_path) if device: self._log.debug(_('found device owning "{0}": "{1}"', device_or_path, device)) else: self._log.error(_('no device found owning "{0}"', device_or_path)) return False else: device = device_or_path try: return fn(self, device, *args, **kwargs) except device.Exception: err = sys.exc_info()[1] self._log.error(_('failed to {0} {1}: {2}', fn.__name__, device, err.message)) self._set_error(device, fn.__name__, err.message) return False
def value(self, device): """ Get the associated value. :param Device device: matched device If :meth:`match` is False for the device, the return value of this method is undefined. """ self._log.debug(_('{0} used for {1}', self, device.object_path)) return self._value
def wrapper(self, device_or_path, *args, **kwargs): if isinstance(device_or_path, basestring): device = self.udisks.find(device_or_path) if device: self._log.debug( _('found device owning "{0}": "{1}"', device_or_path, device)) else: self._log.error( _('no device found owning "{0}"', device_or_path)) return False else: device = device_or_path try: return fn(self, device, *args, **kwargs) except device.Exception: err = sys.exc_info()[1] self._log.error( _('failed to {0} {1}: {2}', fn.__name__, device, err.message)) self._set_error(device, fn.__name__, err.message) return False
def mount(self, device): """ Mount the device if not already mounted. :param device: device object, block device path or mount path :returns: whether the device is mounted. :rtype: bool """ if not self.is_handleable(device) or not device.is_filesystem: self._log.warn(_('not mounting {0}: unhandled device', device)) return False if device.is_mounted: self._log.info(_('not mounting {0}: already mounted', device)) return True fstype = str(device.id_type) options = self._mount_options(device) kwargs = dict(fstype=fstype, options=options) self._log.debug(_('mounting {0} with {1}', device, kwargs)) mount_path = device.mount(**kwargs) self._log.info(_('mounted {0} on {1}', device, mount_path)) return True
def unlock(self, device): """ Unlock the device if not already unlocked. :param device: device object, block device path or mount path :returns: whether the device is unlocked :rtype: bool """ if not self.is_handleable(device) or not device.is_crypto: self._log.warn(_('not unlocking {0}: unhandled device', device)) return False if device.is_unlocked: self._log.info(_('not unlocking {0}: already unlocked', device)) return True if not self._prompt: self._log.error(_('not unlocking {0}: no password prompt', device)) return False password = self._prompt(device) if password is None: self._log.debug(_('not unlocking {0}: cancelled by user', device)) return False self._log.debug(_('unlocking {0}', device)) device.unlock(password) self._log.info(_('unlocked {0}', device)) return True
def find(self, path): """ Get a device proxy by device name or any mount path of the device. This searches through all accessible devices and compares device path as well as mount pathes. """ for device in self: if device.is_file(path): return device logger = logging.getLogger(__name__) logger.warn(_('Device not found: {0}', path)) return None
def detach(self, device, force=False): """ Detach a device after unmounting all its mounted filesystems. :param device: device object, block device path or mount path :param bool force: remove child devices before trying to detach :returns: whether the operation succeeded :rtype: bool """ if not self.is_handleable(device): self._log.warn(_('not detaching {0}: unhandled device')) return False drive = device.root if not drive.is_detachable: self._log.warn(_('not detaching {0}: drive not detachable', drive)) return False if force: self.remove(drive, force=True) self._log.debug(_('detaching {0}', device)) drive.detach() self._log.info(_('detached {0}', device)) return True
def job_failed(self, device, action, message): """ Show 'Job failed' notification with 'Retry' button. :param device: device object """ device_file = device.device_presentation or device.object_path if message: text = _('failed to {0} {1}:\n{2}', action, device_file, message) else: text = _('failed to {0} device {1}.', action, device_file) try: retry = getattr(self._mounter, action) except AttributeError: retry_action = None else: retry_action = ('retry', _('Retry'), retry, device) self._show_notification( 'job_failed', _('Job failed'), text, 'drive-removable-media', retry_action)
def remove(self, device, force=False, detach=False, eject=False, lock=False): """ Unmount or lock the device depending on device type. :param device: device object, block device path or mount path :param bool force: recursively remove all child devices :param bool detach: detach the root drive :param bool eject: remove media from the root drive :param bool lock: lock the associated LUKS cleartext slave :returns: whether all attempted operations succeeded :rtype: bool """ if device.is_filesystem: success = self.unmount(device) elif device.is_crypto: if force and device.is_unlocked: self.remove(device.luks_cleartext_holder, force=True) success = self.lock(device) elif force and (device.is_partition_table or device.is_drive): success = True for child in self.get_all_handleable(): if _is_parent_of(device, child): success = self.remove(child, force=True, detach=detach, eject=eject, lock=lock) and success else: self._log.info(_('not removing {0}: unhandled device', device)) success = False # if these operations work, everything is fine, we can return True: if lock and device.is_luks_cleartext: device = device.luks_cleartext_slave success = self.lock(device) if eject: success = self.eject(device) if detach: success = self.detach(device) return success
def _device_job_changed(self, object_path, job_in_progress, job_id, job_initiated_by_user, job_is_cancellable, job_percentage): """ Detect type of event and trigger appropriate event handlers. Internal method. """ try: if job_id: action = self._action_mapping[job_id] else: action = self._jobs[object_path] except KeyError: # this can happen # a) at startup, when we only see the completion of a job # b) when we get notified about a job, which we don't handle return # NOTE: The here used heuristic is prone to raise conditions. if job_in_progress: # Cache the action name for later use: self._jobs[object_path] = action else: del self._jobs[object_path] device = self[object_path] if self._check_success[action](device): event = self._event_mapping[action] self.trigger(event, device) else: # get and delete message, if available: message = self._errors[action].pop(object_path, "") self.trigger('job_failed', device, action, message) log = logging.getLogger(__name__) log.info(_('{0} operation failed for device: {1}', job_id, object_path))
def _device_job_changed(self, object_path, job_in_progress, job_id, job_initiated_by_user, job_is_cancellable, job_percentage): """ Detect type of event and trigger appropriate event handlers. Internal method. """ try: if job_id: action = self._action_mapping[job_id] else: action = self._jobs[object_path] except KeyError: # this can happen # a) at startup, when we only see the completion of a job # b) when we get notified about a job, which we don't handle return # NOTE: The here used heuristic is prone to raise conditions. if job_in_progress: # Cache the action name for later use: self._jobs[object_path] = action else: del self._jobs[object_path] device = self[object_path] if self._check_success[action](device): event = self._event_mapping[action] self.trigger(event, device) else: # get and delete message, if available: message = self._errors[action].pop(object_path, "") self.trigger('job_failed', device, action, message) log = logging.getLogger(__name__) log.info( _('{0} operation failed for device: {1}', job_id, object_path))
def _check(self, args): """Exit in case of multiple exclusive arguments.""" if sum(bool(args[arg]) for arg in self._mapping) > 1: raise DocoptExit(_('These options are mutually exclusive: {0}', ', '.join(self._mapping)))
def trigger(self, event, device, *args): self._log.debug(_("+++ {0}: {1}", event, device)) super(Daemon, self).trigger(event, device, *args)
class _EntryPoint(object): """ Abstract base class for program entry points. Concrete implementations need to implement :meth:`run` and extend :meth:`finalize_options` to be usable with :meth:`main`. Furthermore the docstring of any concrete implementation must be usable with docopt. :ivar:`name` must be set to the name of the CLI utility. """ option_defaults = { 'log_level': logging.INFO, 'udisks_version': None, } option_rules = { 'log_level': Choice({ '--verbose': logging.DEBUG, '--quiet': logging.ERROR}), 'udisks_version': Choice({ '--udisks-auto': 0, '--use-udisks1': 1, '--use-udisks2': 2}), } usage_remarks = _(""" Note, that the options in the individual groups are mutually exclusive. The config file can be a JSON or preferrably a YAML file. For an example, see the MAN page (or doc/udiskie.8.txt in the repository). """) def __init__(self, argv=None): """ Parse command line options, read config and initialize members. :param list argv: command line parameters """ # parse program options (retrieve log level and config file name): args = docopt(self.usage, version=self.name + ' ' + self.version) default_opts = self.option_defaults program_opts = self.program_options(args) # initialize logging configuration: log_level = program_opts.get('log_level', default_opts['log_level']) if log_level <= logging.DEBUG: fmt = _('%(levelname)s [%(asctime)s] %(name)s: %(message)s') else: fmt = _('%(message)s') logging.basicConfig(level=log_level, format=fmt) # parse config options config_file = OptionalValue('--config')(args) config = udiskie.config.Config.from_file(config_file) options = {} options.update(default_opts) options.update(config.program_options) options.update(program_opts) # initialize instance variables self.config = config self.options = options self._init(config, options) def program_options(self, args): """ Fully initialize Daemon object. :param dict args: arguments as parsed by docopt :returns: options from command line :rtype: dict """ options = {} for name, rule in self.option_rules.items(): val = rule(args) if val is not None: options[name] = val return options @classmethod def main(cls, argv=None): """ Run program. :param list argv: command line parameters :returns: program exit code :rtype: int """ return cls(argv).run() @property def version(self): """Get the version from setuptools metadata.""" return udiskie.__version__ @property def usage(self): """Get the full usage string.""" return inspect.cleandoc(self.__doc__ + self.usage_remarks) @property def name(self): """Get the name of the CLI utility.""" raise NotImplementedError() def _init(self, config, options): """ Fully initialize Daemon object. :param Config config: configuration object :param options: program options """ raise NotImplementedError() def run(self): """ Run main program logic. :param options: program options :returns: exit code :rtype: int """ raise NotImplementedError()
def __str__(self): return _('{0}(match={1!r}, value={2!r})', self.__class__.__name__, self._match, self._value)
def _create_statusicon(self): """Return a new Gtk.StatusIcon.""" statusicon = Gtk.StatusIcon() statusicon.set_from_gicon(self._icons.get_gicon('media')) statusicon.set_tooltip_text(_("udiskie")) return statusicon
class UdiskieMenu(object): """ Builder for udiskie menus. Objects of this class generate action menus when being called. """ _menu_labels = { 'browse': _('Browse {0}'), 'mount': _('Mount {0}'), 'unmount': _('Unmount {0}'), 'unlock': _('Unlock {0}'), 'lock': _('Lock {0}'), 'eject': _('Eject {0}'), 'detach': _('Unpower {0}'), 'quit': _('Quit'), } def __init__(self, mounter, icons, actions={}): """ Initialize a new menu maker. :param object mounter: mount operation provider :param Icons icons: icon provider :param dict actions: actions for menu items :returns: a new menu maker :rtype: cls Required keys for the ``_menu_labels``, ``_menu_icons`` and ``actions`` dictionaries are: - browse Open mount location - mount Mount a device - unmount Unmount a device - unlock Unlock a LUKS device - lock Lock a LUKS device - eject Eject a drive - detach Detach (power down) a drive - quit Exit the application NOTE: If using a main loop other than ``Gtk.main`` the 'quit' action must be customized. """ self._icons = icons self._mounter = mounter _actions = actions.copy() setdefault(_actions, { 'browse': mounter.browse, 'mount': mounter.mount, 'unmount': mounter.unmount, 'unlock': mounter.unlock, 'lock': partial(mounter.remove, force=True), 'eject': partial(mounter.eject, force=True), 'detach': partial(mounter.detach, force=True), 'quit': Gtk.main_quit, }) self._actions = _actions def __call__(self): """ Create menu for udiskie mount operations. :returns: a new menu :rtype: Gtk.Menu """ # create actions items menu = self._branchmenu(self._prepare_menu(self.detect()).groups) # append menu item for closing the application if self._actions.get('quit'): if len(menu) > 0: menu.append(Gtk.SeparatorMenuItem()) menu.append(self._actionitem('quit')) return menu def detect(self): """ Detect all currently known devices. :returns: root of device hierarchy :rtype: Node """ root = Node(None, [], None, "", []) device_nodes = dict(map(self._device_node, self._mounter.get_all_handleable())) # insert child devices as branches into their roots: for object_path, node in device_nodes.items(): device_nodes.get(node.root, root).branches.append(node) return root def _branchmenu(self, groups): """ Create a menu from the given node. :param Branch groups: contains information about the menu :returns: a new menu object holding all groups of the node :rtype: Gtk.Menu """ menu = Gtk.Menu() separate = False for group in groups: if len(group) > 0: if separate: menu.append(Gtk.SeparatorMenuItem()) separate = True for node in group: if isinstance(node, Action): menu.append(self._actionitem( node.method, feed=[node.label], bind=[node.device])) elif isinstance(node, Branch): menu.append(self._menuitem( node.label, icon=None, onclick=self._branchmenu(node.groups))) else: raise ValueError(_("Invalid node!")) return menu def _menuitem(self, label, icon, onclick): """ Create a generic menu item. :param str label: text :param Gtk.Image icon: icon (may be ``None``) :param onclick: onclick handler, either a callable or Gtk.Menu :returns: the menu item object :rtype: Gtk.MenuItem """ if icon is None: item = Gtk.MenuItem() else: item = Gtk.ImageMenuItem() item.set_image(icon) # I don't really care for the "show icons only for nouns, not # for verbs" policy: item.set_always_show_image(True) if label is not None: item.set_label(label) if isinstance(onclick, Gtk.Menu): item.set_submenu(onclick) else: item.connect('activate', onclick) return item def _actionitem(self, action, feed=(), bind=()): """ Create a menu item for the specified action. :param str action: name of the action :param tuple feed: parameters for the label text :param tuple bind: parameters for the onclick handler :returns: the menu item object :rtype: Gtk.MenuItem """ return self._menuitem( self._menu_labels[action].format(*feed), self._icons.get_icon(action, Gtk.IconSize.MENU), lambda _: self._actions[action](*bind)) def _get_device_methods(self, device): """Return an iterable over all available methods the device has.""" if device.is_filesystem: if device.is_mounted: yield 'browse' yield 'unmount' else: yield 'mount' elif device.is_crypto: if device.is_unlocked: yield 'lock' else: yield 'unlock' if device.is_ejectable and device.has_media: yield 'eject' if device.is_detachable: yield 'detach' def _device_node(self, device): """Create an empty menu node for the specified device.""" label = device.id_label or device.device_presentation # determine available methods methods = [method for method in self._get_device_methods(device) if self._actions[method]] # find the root device: if device.is_partition: root = device.partition_slave.object_path elif device.is_luks_cleartext: root = device.luks_cleartext_slave.object_path else: root = None # in this first step leave branches empty return device.object_path, Node(root, [], device, label, methods) def _prepare_menu(self, node): """ Prepare the menu hierarchy from the given device tree. :param Node node: root node of device hierarchy :returns: menu hierarchy :rtype: Branch """ return Branch( label=node.label, groups=[ [self._prepare_menu(branch) for branch in node.branches], [Action(node.label, node.device, method) for method in node.methods], ])