2

I would like to eject a device by running a command (so it can be assigned to a key-shortcut for example).

I have used udisksctl and know how to eject a drive but this isn't very useful to assign to a key-binding because.

  • Errors are reported in the terminal (which I would like to be notified about).
  • Details on success are also printed to the terminal (which I want to ignore)

Is there a way to run a command that ejects a device that notifies of errors, as if I had pressed eject in nautilus?

3
  • The command you're looking for is udisksctl, you already have it. What exactly do you mean by error handling ? You want udisksctl to display a GUI popup if there's an error ? Please explain a bit Jul 21, 2017 at 4:44
  • Updated the question.
    – ideasman42
    Jul 21, 2017 at 5:01
  • OK. So I understood your question correctly. I can write an answer that can work around these problems Jul 21, 2017 at 5:05

3 Answers 3

2

Here's a python script that will gather all mounted USB partitions, and perform udisksctl unmount -b <DEV> on each one of them. The standard rules for making this a working shortcut apply: ensure that the script is executable and give full path to script as command.

As requested only errors will be displayed in GUI dialog, other output won't be shown.

#!/usr/bin/env python
import os
import subprocess
import sys

def run_cmd(cmdlist):
    """ utility: reusable function for running external commands """
    try:
        stdout = subprocess.check_output(cmdlist,stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError as cpe:
        # GtkDialog error should be ignored. 
        if not "GtkDialog mapped without a transient parent" in cpe.output:
            return error_dialog(cpe.output," ".join(cmdlist))

def error_dialog(err_msg,called_cmd):
    """ Displays graphical error dialog and exits the script"""
    subprocess.call(['zenity','--error','--text',err_msg,'--title',called_cmd])
    sys.exit(1)

def find_usb_partitions():
    """ Constructs a list of USB partitions """
    return tuple( os.path.realpath(os.path.join("/dev/disk/by-path",p))
                  for p in os.listdir("/dev/disk/by-path")
                  if 'usb' in p and 'part' in p
    )  

def find_mounted_devs():
    devs=[]
    with open('/proc/mounts') as mounts:
         for line in mounts:
             dev = line.split()[0]
             if dev.startswith('/dev/'):
                 devs.append(dev)     
    return devs

def main():
   parts=find_usb_partitions()
   devs=find_mounted_devs()
   mounted_parts=tuple(i for i in parts if i in devs)
   for part in mounted_parts:
       run_cmd(['udisksctl', 'unmount', '-b',part])
       run_cmd(['udisksctl', 'power-off', '-b',part])

if __name__ == '__main__': main()

Additional thoughts

While the script works, I find the idea of unmounting from shortcut slightly redundant. Such task requires at least some involvement from a user. If the goal is to be able to unmount without terminal, there are already ways to do that. For instance, if you are a Unity user you already have device icon on the launcher from which you can right click and eject the device. I've written Udisks-Indicator, which you can use to unmount partitions from a graphical indicator (and which uses some of the same ideas I've used in this answer). In either case, there are alternatives already, but I personally would caution against using shortcuts to unmount things. But then again, this is only my personal opinion.

2
  • Thanks! I needed to modify it to work for me though, included in own answer, feel free to use and I'll remove it.
    – ideasman42
    Jul 21, 2017 at 8:53
  • @ideasman42 oh, I am in no way against of people modifying my code, so feel free to keep it as stand-alone answer. As for the bash script that you originally had, I'd say include that as well, so that other users may benefit from it. I haven't read through it too attentively, but it looked like it had potential there Jul 21, 2017 at 9:28
1

This is a modified of @sergiy-kolodyazhnyy's excellent answer with the following changes:

  • Don't check for part in the name (some of my drives don't use partitions).
  • Use by-id instead of by-path so a less ambiguous p.startswith("usb-") can be used.
  • Power-off in a separate loop (multiple devices may be connected to the same power).
  • Check the device exists before powering off (again, linked power source).
  • Running commands (run_cmd) returns a boolean based on success.
  • Notify the final outcome (number of devices mounted/unmounted).
  • Use Python3 (subprocess output in bytes).

Update: made this more generic tool:
udisksctl_usb_all [--mount, --unmount, --unmount-power-off]
So it can be used for all 3 operations.

#!/usr/bin/env python3
import os
import subprocess
import sys


def run_cmd(cmdlist):
    ''' utility: reusable function for running external commands '''
    try:
        stdout = subprocess.check_output(cmdlist, stderr=subprocess.STDOUT)
        return True
    except subprocess.CalledProcessError as cpe:
        print(cpe.output)
        # GtkDialog error should be ignored.
        if not b'GtkDialog mapped without a transient parent' in cpe.output:
            return error_dialog(cpe.output, ' '.join(cmdlist))
        return False


def error_dialog(err_msg, called_cmd):
    ''' Displays graphical error dialog and exits the script'''
    print(called_cmd)
    subprocess.call(['zenity', '--error', '--text', err_msg, '--title', called_cmd])
    sys.exit(1)


def find_usb_partitions():
    ''' Constructs a list of USB partitions '''
    return tuple(
        os.path.realpath(os.path.join('/dev/disk/by-id', p))
        for p in os.listdir('/dev/disk/by-id')
        if p.startswith('usb-')
    )


def find_mounted_devs():
    devs = []
    with open('/proc/mounts') as mounts:
        for line in mounts:
            if line.startswith('/dev/'):
                devs.append(line.split(' ', 1)[0])
    return devs


def unmount_all(power_off=False):
    unmount_len = 0
    parts = find_usb_partitions()
    devs = find_mounted_devs()
    mounted_parts = tuple(i for i in parts if i in devs)
    for part in mounted_parts:
        if run_cmd(['udisksctl', 'unmount', '-b', part]):
            unmount_len += 1

    if power_off:
        # Some drives may be linked regarding power, check each exists first.
        for part in mounted_parts:
            if os.path.exists(part):
                run_cmd(['udisksctl', 'power-off', '-b', part])

    if unmount_len:
        run_cmd(['notify-send', 'Unmounted {} file-systems!'.format(unmount_len)])
    else:
        run_cmd(['notify-send', 'Nothing to unmount!'])


def mount_all():
    mount_len = 0
    parts = find_usb_partitions()
    devs = find_mounted_devs()

    # only include non-numbered devices if numbered ones don't exist
    # don't try to mout /dev/sdc if /dev/sdc1 exists.
    parts_numbered = set()
    for p in parts:
        p_strip = p.rstrip('0123456789')
        if p != p_strip:
            parts_numbered.add(p_strip)
    parts = tuple(p for p in parts if p not in parts_numbered)
    del parts_numbered

    unmounted_parts = tuple(i for i in parts if i not in devs)
    for part in unmounted_parts:
        if run_cmd(['udisksctl', 'mount', '-b', part]):
            mount_len += 1

    if mount_len:
        run_cmd(['notify-send', 'Mounted {} file-systems!'.format(mount_len)])
    else:
        run_cmd(['notify-send', 'Nothing to mount!'])


if __name__ == '__main__':
    if "--mount" in sys.argv:
        mount_all()
    elif "--unmount" in sys.argv:
        unmount_all(power_off=False)
    elif "--unmount-power-off" in sys.argv:
        unmount_all(power_off=True)
    else:
        print("Expected one of ['--mount', '--unmount' or '--unmount-power-off'] to be passed.")
0

One alternative is to use umount command with: sudo umount /media/user/your-usb-drive

For determining the right drive you can use the mount command: mount | grep usb

2
  • 1
    The problem with this is calling sudo from a key-binding isn't practical, also I don't think notifications would be handled usefully.
    – ideasman42
    Jul 21, 2017 at 6:44
  • 1
    Lines with usb in them are not guaranteed to show up in the output of mount | grep 'usb', only in rare cases where name of the USB device or mountpoint has that. Also, what ideasman42 said - calling sudo from shortcut wont work because you 1) either have to hard-code your password into the script which is not secure, or 2) ask user for password each and every time, which over time becomes annoying to most users ( not to me personally, but for most users). sudo only prompts in stdin so you also have to somehow raise a GUI popup asking for password, which is another task Jul 21, 2017 at 8:02

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .