Add a DriveDetach() method to properly power down USB hard disk enclosures
authorDavid Zeuthen <davidz@redhat.com>
Tue, 9 Jun 2009 00:13:01 +0000 (20:13 -0400)
committerDavid Zeuthen <davidz@redhat.com>
Tue, 9 Jun 2009 00:13:01 +0000 (20:13 -0400)
This work is inspired by this thread

 http://thread.gmane.org/gmane.linux.hotplug.devel/14079

that is referencing this blog entry

 http://elliotli.blogspot.com/2009/01/safely-remove-usb-hard-drive-in-linux.html

The only difference here is that instead of unbinding the usb driver
from the usb device, we unbind the usb-storage driver from the usb
interface in question. This is to better handle multi-function devices
(e.g. multiple USB interfaces).

Things like GVfs (ie. Nautilus) can use this new method to offer an
"Eject" option for USB enclosures that can invoke DriveDetach() on
this service. (In GVfs, such devices currently have no eject option
since they don't support removable media).

12 files changed:
configure.ac
doc/man/devkit-disks.xml
policy/org.freedesktop.devicekit.disks.policy.in
src/Makefile.am
src/devkit-disks-device-private.c
src/devkit-disks-device-private.h
src/devkit-disks-device.c
src/devkit-disks-device.h
src/job-drive-detach.c [new file with mode: 0644]
src/org.freedesktop.DeviceKit.Disks.Device.xml
tools/devkit-disks-bash-completion.sh
tools/devkit-disks.c

index 11824b0..5edf5ff 100644 (file)
@@ -117,6 +117,17 @@ ZLIB_LIBS="-lz"
 AC_SUBST(ZLIB_CFLAGS)
 AC_SUBST(ZLIB_LIBS)
 
+have_sgutils="false"
+AC_CHECK_LIB([sgutils2], [sg_ll_inquiry], have_sgutils="true")
+if test x$have_sgutils != "xtrue"; then
+   AC_MSG_ERROR([libsgutils2 is needed])
+fi
+SGUTILS_CFLAGS=""
+SGUTILS_LIBS="-lsgutils2"
+AC_SUBST(SGUTILS_CFLAGS)
+AC_SUBST(SGUTILS_LIBS)
+
+
 PKG_CHECK_MODULES(SQLITE3, [sqlite3])
 AC_SUBST(SQLITE3_CFLAGS)
 AC_SUBST(SQLITE3_LIBS)
index 28d5a8d..c55b2df 100644 (file)
 
       <varlistentry>
         <term>
+          <option>--detach</option>
+          <arg choice="plain"><replaceable>device_file</replaceable></arg>
+          <arg><option>--detach-options</option><arg choice="plain"><replaceable>options</replaceable></arg></arg>
+        </term>
+        <listitem>
+          <para>
+            Detaches (e.g. powering down the physical port the device
+            is connected to) the device represented
+            by <replaceable>device_file</replaceable> using a
+            comma-separated list
+            of <replaceable>options</replaceable>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>
           <option>--ata-smart-refresh</option>
           <arg choice="plain"><replaceable>device_file</replaceable></arg>
           <arg><option>--ata-smart-wakeup</option></arg>
index ffd0968..0227342 100644 (file)
@@ -80,7 +80,7 @@
   </action>
 
   <action id="org.freedesktop.devicekit.disks.drive-eject">
-    <_description>Eject a device</_description>
+    <_description>Eject media from a device</_description>
     <_message>Authentication is required to eject media from the device</_message>
     <defaults>
       <allow_any>no</allow_any>
     </defaults>
   </action>
 
+  <action id="org.freedesktop.devicekit.disks.drive-detach">
+    <_description>Detach a drive</_description>
+    <_message>Authentication is required to detach the drive</_message>
+    <defaults>
+      <allow_any>no</allow_any>
+      <allow_inactive>no</allow_inactive>
+      <allow_active>yes</allow_active>
+    </defaults>
+  </action>
+
   <action id="org.freedesktop.devicekit.disks.change">
     <_description>Modify a device</_description>
     <_message>Authentication is required to modify the device</_message>
index 307921f..8744d68 100644 (file)
@@ -93,6 +93,7 @@ libexec_PROGRAMS += devkit-disks-helper-mkfs                          \
                    devkit-disks-helper-fstab-mounter                   \
                    devkit-disks-helper-ata-smart-collect               \
                    devkit-disks-helper-ata-smart-selftest              \
+                   devkit-disks-helper-drive-detach                    \
                    $(NULL)
 
 libexec_SCRIPTS = devkit-disks-helper-change-luks-password
@@ -137,6 +138,10 @@ devkit_disks_helper_fstab_mounter_SOURCES = job-shared.h job-fstab-mounter.c
 devkit_disks_helper_fstab_mounter_CPPFLAGS = $(AM_CPPFLAGS)
 devkit_disks_helper_fstab_mounter_LDADD = $(GLIB_LIBS)
 
+devkit_disks_helper_drive_detach_SOURCES = job-shared.h job-drive-detach.c
+devkit_disks_helper_drive_detach_CPPFLAGS = $(AM_CPPFLAGS) $(LIBUDEV_CFLAGS) $(SGUTILS_CFLAGS)
+devkit_disks_helper_drive_detach_LDADD = $(LIBUDEV_LIBS) $(SGUTILS_LIBS) $(GLIB_LIBS)
+
 # TODO: move to udev
 udevhelperdir = $(slashlibdir)/udev
 udevhelper_PROGRAMS = devkit-disks-part-id devkit-disks-dm-export devkit-disks-probe-ata-smart
index 35c7826..46d6fac 100644 (file)
@@ -730,6 +730,16 @@ devkit_disks_device_set_drive_requires_eject (DevkitDisksDevice *device, gboolea
 }
 
 void
+devkit_disks_device_set_drive_can_detach (DevkitDisksDevice *device, gboolean value)
+{
+  if (G_UNLIKELY (device->priv->drive_can_detach != value))
+    {
+      device->priv->drive_can_detach = value;
+      emit_changed (device, "drive_can_detach");
+    }
+}
+
+void
 devkit_disks_device_set_optical_disc_is_blank (DevkitDisksDevice *device, gboolean value)
 {
   if (G_UNLIKELY (device->priv->optical_disc_is_blank != value))
index 622f569..8657df8 100644 (file)
@@ -153,6 +153,7 @@ struct DevkitDisksDevicePrivate
         char *drive_media;
         gboolean drive_is_media_ejectable;
         gboolean drive_requires_eject;
+        gboolean drive_can_detach;
 
         gboolean optical_disc_is_blank;
         gboolean optical_disc_is_appendable;
@@ -283,6 +284,7 @@ void devkit_disks_device_set_drive_media_compatibility (DevkitDisksDevice *devic
 void devkit_disks_device_set_drive_media (DevkitDisksDevice *device, const gchar *value);
 void devkit_disks_device_set_drive_is_media_ejectable (DevkitDisksDevice *device, gboolean value);
 void devkit_disks_device_set_drive_requires_eject (DevkitDisksDevice *device, gboolean value);
+void devkit_disks_device_set_drive_can_detach (DevkitDisksDevice *device, gboolean value);
 
 void devkit_disks_device_set_optical_disc_is_blank (DevkitDisksDevice *device, gboolean value);
 void devkit_disks_device_set_optical_disc_is_appendable (DevkitDisksDevice *device, gboolean value);
index 87630f3..7df2cda 100644 (file)
@@ -206,6 +206,7 @@ enum
         PROP_DRIVE_MEDIA,
         PROP_DRIVE_IS_MEDIA_EJECTABLE,
         PROP_DRIVE_REQUIRES_EJECT,
+        PROP_DRIVE_CAN_DETACH,
 
         PROP_OPTICAL_DISC_IS_BLANK,
         PROP_OPTICAL_DISC_IS_APPENDABLE,
@@ -512,6 +513,9 @@ get_property (GObject         *object,
        case PROP_DRIVE_REQUIRES_EJECT:
                g_value_set_boolean (value, device->priv->drive_requires_eject);
                break;
+       case PROP_DRIVE_CAN_DETACH:
+               g_value_set_boolean (value, device->priv->drive_can_detach);
+               break;
 
        case PROP_OPTICAL_DISC_IS_BLANK:
                g_value_set_boolean (value, device->priv->optical_disc_is_blank);
@@ -982,6 +986,10 @@ devkit_disks_device_class_init (DevkitDisksDeviceClass *klass)
                 object_class,
                 PROP_DRIVE_REQUIRES_EJECT,
                 g_param_spec_boolean ("drive-requires-eject", NULL, NULL, FALSE, G_PARAM_READABLE));
+        g_object_class_install_property (
+                object_class,
+                PROP_DRIVE_CAN_DETACH,
+                g_param_spec_boolean ("drive-can-detach", NULL, NULL, FALSE, G_PARAM_READABLE));
 
         g_object_class_install_property (
                 object_class,
@@ -2036,6 +2044,7 @@ update_info_drive (DevkitDisksDevice *device)
         const gchar *media_in_drive;
         gboolean drive_is_ejectable;
         gboolean drive_requires_eject;
+        gboolean drive_can_detach;
         gchar *decoded_string;
         guint n;
 
@@ -2126,6 +2135,13 @@ update_info_drive (DevkitDisksDevice *device)
 
         g_ptr_array_free (media_compat_array, TRUE);
 
+        /* right now, we only offer to detach USB devices */
+        drive_can_detach = FALSE;
+        if (g_strcmp0 (device->priv->drive_connection_interface, "usb") == 0) {
+                drive_can_detach = TRUE;
+        }
+        devkit_disks_device_set_drive_can_detach (device, drive_can_detach);
+
         return TRUE;
 }
 
@@ -5102,6 +5118,130 @@ devkit_disks_device_drive_eject (DevkitDisksDevice     *device,
 /*--------------------------------------------------------------------------------------------------------------*/
 
 static void
+drive_detach_completed_cb (DBusGMethodInvocation *context,
+                          DevkitDisksDevice *device,
+                          gboolean job_was_cancelled,
+                          int status,
+                          const char *stderr,
+                          const char *stdout,
+                          gpointer user_data)
+{
+        if (WEXITSTATUS (status) == 0 && !job_was_cancelled) {
+                /* TODO: probably wait for has_media to change to FALSE */
+                dbus_g_method_return (context);
+        } else {
+                if (job_was_cancelled) {
+                        throw_error (context,
+                                     DEVKIT_DISKS_ERROR_CANCELLED,
+                                     "Job was cancelled");
+                } else {
+                        throw_error (context,
+                                     DEVKIT_DISKS_ERROR_FAILED,
+                                     "Error detaching: helper exited with exit code %d: %s",
+                                     WEXITSTATUS (status),
+                                     stderr);
+                }
+        }
+}
+
+static void
+devkit_disks_device_drive_detach_authorized_cb (DevkitDisksDaemon     *daemon,
+                                               DevkitDisksDevice     *device,
+                                               DBusGMethodInvocation *context,
+                                               const gchar           *action_id,
+                                               guint                  num_user_data,
+                                               gpointer              *user_data_elements)
+{
+        gchar **options = user_data_elements[0];
+        int n;
+        char *argv[16];
+        GError *error;
+        char *mount_path;
+
+        error = NULL;
+        mount_path = NULL;
+
+        if (!device->priv->device_is_drive) {
+                throw_error (context, DEVKIT_DISKS_ERROR_FAILED,
+                             "Device is not a drive");
+                goto out;
+        }
+
+        if (!device->priv->device_is_media_available) {
+                throw_error (context, DEVKIT_DISKS_ERROR_FAILED,
+                             "No media in drive");
+                goto out;
+        }
+
+        if (devkit_disks_device_local_is_busy (device, TRUE, &error)) {
+                dbus_g_method_return_error (context, error);
+                g_error_free (error);
+                goto out;
+        }
+
+        for (n = 0; options[n] != NULL; n++) {
+                const char *option = options[n];
+                throw_error (context,
+                             DEVKIT_DISKS_ERROR_INVALID_OPTION,
+                             "Unknown option %s", option);
+                goto out;
+        }
+
+        n = 0;
+        argv[n++] = PACKAGE_LIBEXEC_DIR "/devkit-disks-helper-drive-detach";
+        argv[n++] = device->priv->device_file;
+        argv[n++] = device->priv->native_path;
+        argv[n++] = NULL;
+
+        if (!job_new (context,
+                      "DriveDetach",
+                      FALSE,
+                      device,
+                      argv,
+                      NULL,
+                      drive_detach_completed_cb,
+                      NULL,
+                      NULL)) {
+                goto out;
+        }
+
+out:
+        ;
+}
+
+gboolean
+devkit_disks_device_drive_detach (DevkitDisksDevice     *device,
+                                 char                 **options,
+                                 DBusGMethodInvocation *context)
+{
+        if (!device->priv->device_is_drive) {
+                throw_error (context, DEVKIT_DISKS_ERROR_FAILED,
+                             "Device is not a drive");
+                goto out;
+        }
+
+        if (!device->priv->device_is_media_available) {
+                throw_error (context, DEVKIT_DISKS_ERROR_FAILED,
+                             "No media in drive");
+                goto out;
+        }
+
+        devkit_disks_daemon_local_check_auth (device->priv->daemon,
+                                              device,
+                                              "org.freedesktop.devicekit.disks.drive-detach",
+                                              "DriveDetach",
+                                              devkit_disks_device_drive_detach_authorized_cb,
+                                              context,
+                                              1,
+                                              g_strdupv (options), g_strfreev);
+
+ out:
+        return TRUE;
+}
+
+/*--------------------------------------------------------------------------------------------------------------*/
+
+static void
 filesystem_check_completed_cb (DBusGMethodInvocation *context,
                                DevkitDisksDevice *device,
                                gboolean job_was_cancelled,
index 29e73b8..2da84ea 100644 (file)
@@ -183,6 +183,10 @@ gboolean devkit_disks_device_drive_uninhibit_polling (DevkitDisksDevice     *dev
 gboolean devkit_disks_device_drive_poll_media (DevkitDisksDevice     *device,
                                                DBusGMethodInvocation *context);
 
+gboolean devkit_disks_device_drive_detach (DevkitDisksDevice     *device,
+                                           char                 **options,
+                                           DBusGMethodInvocation *context);
+
 G_END_DECLS
 
 #endif /* __DEVKIT_DISKS_DEVICE_H__ */
diff --git a/src/job-drive-detach.c b/src/job-drive-detach.c
new file mode 100644 (file)
index 0000000..53725b4
--- /dev/null
@@ -0,0 +1,166 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2009 David Zeuthen <david@fubar.dk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include <scsi/sg_lib.h>
+#include <scsi/sg_cmds.h>
+
+#define LIBUDEV_I_KNOW_THE_API_IS_SUBJECT_TO_CHANGE
+#include <libudev.h>
+
+#include <glib.h>
+
+static void
+usage (void)
+{
+  g_printerr ("incorrect usage\n");
+}
+
+int
+main (int argc, char *argv[])
+{
+  int ret;
+  int sg_fd;
+  const gchar *device;
+  const gchar *sysfs_path;
+  struct udev *udev;
+  struct udev_device *udevice;
+  struct udev_device *udevice_usb;
+  gchar *unbind_path;
+  gchar *usb_interface_name;
+  size_t usb_interface_name_len;
+  FILE *f;
+
+  udev = NULL;
+  udevice = NULL;
+  udevice_usb = NULL;
+  usb_interface_name = NULL;
+  unbind_path = NULL;
+
+  ret = 1;
+  sg_fd = -1;
+
+  if (argc != 3)
+    {
+      usage ();
+      goto out;
+    }
+
+  device = argv[1];
+  sysfs_path = argv[2];
+
+  sg_fd = sg_cmds_open_device (device, 1 /* read_only */, 1);
+  if (sg_fd < 0)
+    {
+      g_printerr ("Cannot open %s: %m\n", device);
+      goto out;
+    }
+
+  if (sg_ll_sync_cache_10 (sg_fd,
+                           0,  /* sync_nv */
+                           0,  /* immed */
+                           0,  /* group */
+                           0,  /* lba */
+                           0,  /* count */
+                           1,  /* noisy */
+                           0   /* verbose */
+                           ) != 0)
+    {
+      g_printerr ("Error SYNCHRONIZE CACHE for %s: %m\n", device);
+      /* this is not a catastrophe, carry on */
+    }
+
+  if (sg_ll_start_stop_unit (sg_fd,
+                             0,  /* immed */
+                             0,  /* pc_mod__fl_num */
+                             0,  /* power_cond */
+                             0,  /* noflush__fl */
+                             0,  /* loej */
+                             0,  /* start */
+                             1,  /* noisy */
+                             0   /* verbose */
+                             ) != 0)
+    {
+      g_printerr ("Error STOP UNIT for %s: %m\n", device);
+      goto out;
+    }
+
+  /* OK, close the device */
+  sg_cmds_close_device (sg_fd);
+  sg_fd = -1;
+
+  /* Now unbind the usb-storage driver from the usb interface */
+  udev = udev_new ();
+  if (udev == NULL)
+    {
+      g_printerr ("Error initializing libudev: %m\n");
+      goto out;
+    }
+
+  udevice = udev_device_new_from_syspath (udev, sysfs_path);
+  if (udevice == NULL)
+    {
+      g_printerr ("No udev device for %s: %m\n", sysfs_path);
+      goto out;
+    }
+
+  udevice_usb = udev_device_get_parent_with_subsystem_devtype (udevice, "usb", "usb_interface");
+  if (udevice_usb == NULL)
+    {
+      g_printerr ("No usb parent interface for %s: %m\n", sysfs_path);
+      goto out;
+    }
+
+  usb_interface_name = g_path_get_basename (udev_device_get_devpath (udevice_usb));
+  usb_interface_name_len = strlen (usb_interface_name);
+
+  unbind_path = g_strdup_printf ("%s/driver/unbind", udev_device_get_syspath (udevice_usb));
+  f = fopen (unbind_path, "w");
+  if (f == NULL)
+    {
+      g_printerr ("Cannot open %s for writing: %m\n", unbind_path);
+      goto out;
+    }
+  if (fwrite (usb_interface_name, sizeof (char), usb_interface_name_len, f) < usb_interface_name_len)
+    {
+      g_printerr ("Error writing %s to %s: %m\n", unbind_path, usb_interface_name);
+      fclose (f);
+      goto out;
+    }
+  fclose (f);
+
+  ret = 0;
+
+ out:
+  g_free (usb_interface_name);
+  g_free (unbind_path);
+  if (sg_fd > 0)
+    sg_cmds_close_device (sg_fd);
+  if (udevice != NULL)
+    udev_device_unref (udevice);
+  if (udev != NULL)
+    udev_unref (udev);
+  return ret;
+}
index b006b3b..b650c94 100644 (file)
 
     <!-- ************************************************************ -->
 
+    <method name="DriveDetach">
+      <annotation name="org.freedesktop.DBus.GLib.Async" value=""/>
+      <arg name="options" direction="in" type="as">
+        <doc:doc><doc:summary>Detach options. Currently no options are recognized.</doc:summary></doc:doc>
+      </arg>
+
+      <doc:doc>
+        <doc:description>
+          <doc:para>
+            Detachs the device by e.g. powering down the physical port
+            it is connected to. Note that not all devices or ports are
+            capable of this. Check the
+            <doc:ref type="property" to="Device:drive-can-detach">drive-can-detach</doc:ref>
+            property before attempting to invoke this method.
+          </doc:para>
+        </doc:description>
+        <doc:permission>
+          The caller will need one of the following PolicyKit authorizations:
+          <doc:list>
+            <doc:item>
+              <doc:term>org.freedesktop.devicekit.disks.drive-detach</doc:term>
+              <doc:definition>To detach a device</doc:definition>
+            </doc:item>
+          </doc:list>
+        </doc:permission>
+        <doc:errors>
+          <doc:error name="&ERROR_NOT_AUTHORIZED;">if the caller lacks the appropriate PolicyKit authorization</doc:error>
+          <doc:error name="&ERROR_BUSY;">if the device or a dependent device (e.g. partition or cleartext luks device) is busy (e.g. mounted)</doc:error>
+          <doc:error name="&ERROR_FAILED;">if the operation failed</doc:error>
+          <doc:error name="&ERROR_CANCELLED;">if the job was cancelled</doc:error>
+          <doc:error name="&ERROR_INVALID_OPTION;">if an invalid or malformed option was given</doc:error>
+        </doc:errors>
+      </doc:doc>
+    </method>
+
+    <!-- ************************************************************ -->
+
     <method name="DriveAtaSmartRefreshData">
       <annotation name="org.freedesktop.DBus.GLib.Async" value=""/>
       <arg name="options" direction="in" type="as">
             is TRUE.
       </doc:para></doc:description></doc:doc>
     </property>
+    <property name="drive-can-detach" type="b" access="read">
+      <doc:doc><doc:description><doc:para>
+            TRUE only if the drive is capable of being detached by
+            e.g. powering down the port it is connected to.
+            This property is only valid if
+            <doc:ref type="property" to="Device:device-is-drive">device-is-drive</doc:ref>
+            is TRUE.
+      </doc:para></doc:description></doc:doc>
+    </property>
 
     <property name="optical-disc-is-blank" type="b" access="read">
       <doc:doc><doc:description><doc:para>
index e4fdbf5..662501e 100644 (file)
@@ -16,12 +16,14 @@ __devkit_disks() {
         COMPREPLY=($(compgen -W "$(devkit-disks --enumerate-device-files)" -- $cur))
     elif [ "${COMP_WORDS[$(($COMP_CWORD - 1))]}" = "--unmount" ] ; then
         COMPREPLY=($(compgen -W "$(devkit-disks --enumerate-device-files)" -- $cur))
+    elif [ "${COMP_WORDS[$(($COMP_CWORD - 1))]}" = "--detach" ] ; then
+        COMPREPLY=($(compgen -W "$(devkit-disks --enumerate-device-files)" -- $cur))
     elif [ "${COMP_WORDS[$(($COMP_CWORD - 1))]}" = "--ata-smart-refresh" ] ; then
         COMPREPLY=($(compgen -W "$(devkit-disks --enumerate-device-files)" -- $cur))
     elif [ "${COMP_WORDS[$(($COMP_CWORD - 1))]}" = "--ata-smart-simulate" ] ; then
         _filedir || return 0
     else
-        COMPREPLY=($(IFS=: compgen -W "--dump:--inhibit-polling:--inhibit-all-polling:--enumerate:--enumerate-device-files:--monitor:--monitor-detail:--show-info:--help:--mount:--mount-fstype:--mount-options:--unmount:--unmount-options:--ata-smart-refresh:--ata-smart-wakeup:--ata-smart-simulate" -- $cur))
+        COMPREPLY=($(IFS=: compgen -W "--dump:--inhibit-polling:--inhibit-all-polling:--enumerate:--enumerate-device-files:--monitor:--monitor-detail:--show-info:--help:--mount:--mount-fstype:--mount-options:--unmount:--unmount-options:--detach:--detach-options:--ata-smart-refresh:--ata-smart-wakeup:--ata-smart-simulate" -- $cur))
     fi
 }
 
index 41ffd6c..415e445 100644 (file)
@@ -65,6 +65,8 @@ static char         *opt_mount_fstype           = NULL;
 static char         *opt_mount_options          = NULL;
 static char         *opt_unmount                = NULL;
 static char         *opt_unmount_options        = NULL;
+static char         *opt_detach                 = NULL;
+static char         *opt_detach_options         = NULL;
 static char         *opt_ata_smart_refresh      = NULL;
 static gboolean      opt_ata_smart_wakeup       = FALSE;
 static char         *opt_ata_smart_simulate     = NULL;
@@ -171,6 +173,34 @@ do_unmount (const char *object_path,
 }
 
 static void
+do_detach (const char *object_path,
+           const char *options)
+{
+        DBusGProxy *proxy;
+        GError *error;
+        char **unmount_options;
+
+        unmount_options = NULL;
+        if (options != NULL)
+                unmount_options = g_strsplit (options, ",", 0);
+
+       proxy = dbus_g_proxy_new_for_name (bus,
+                                           "org.freedesktop.DeviceKit.Disks",
+                                           object_path,
+                                           "org.freedesktop.DeviceKit.Disks.Device");
+
+        error = NULL;
+        if (!org_freedesktop_DeviceKit_Disks_Device_drive_detach (proxy,
+                                                                  (const char **) options,
+                                                                  &error)) {
+                g_print ("Detach failed: %s\n", error->message);
+                g_error_free (error);
+        }
+
+        g_strfreev (unmount_options);
+}
+
+static void
 device_added_signal_handler (DBusGProxy *proxy, const char *object_path, gpointer user_data)
 {
   g_print ("added:     %s\n", object_path);
@@ -352,6 +382,7 @@ typedef struct
         char    *drive_media;
         gboolean drive_is_media_ejectable;
         gboolean drive_requires_eject;
+        gboolean drive_can_detach;
 
         gboolean optical_disc_is_blank;
         gboolean optical_disc_is_appendable;
@@ -549,6 +580,8 @@ collect_props (const char *key, const GValue *value, DeviceProperties *props)
                 props->drive_is_media_ejectable = g_value_get_boolean (value);
         else if (strcmp (key, "drive-requires-eject") == 0)
                 props->drive_requires_eject = g_value_get_boolean (value);
+        else if (strcmp (key, "drive-can-detach") == 0)
+                props->drive_can_detach = g_value_get_boolean (value);
 
         else if (strcmp (key, "optical-disc-is-blank") == 0)
                 props->optical_disc_is_blank = g_value_get_boolean (value);
@@ -1058,6 +1091,7 @@ do_show_info (const char *object_path)
                 g_print ("    model:                 %s\n", props->drive_model);
                 g_print ("    revision:              %s\n", props->drive_revision);
                 g_print ("    serial:                %s\n", props->drive_serial);
+                g_print ("    detachable:            %d\n", props->drive_can_detach);
                 g_print ("    ejectable:             %d\n", props->drive_is_media_ejectable);
                 g_print ("    require eject:         %d\n", props->drive_requires_eject);
                 g_print ("    media:                 %s\n", props->drive_media);
@@ -1546,12 +1580,14 @@ main (int argc, char **argv)
                 { "inhibit-all-polling", 0, 0, G_OPTION_ARG_NONE, &opt_inhibit_all_polling, "Inhibit all polling", NULL },
                 { "inhibit", 0, 0, G_OPTION_ARG_NONE, &opt_inhibit, "Inhibit the daemon", NULL },
 
-                { "mount", 0, 0, G_OPTION_ARG_STRING, &opt_mount, "Mount the device given by the object path", NULL },
+                { "mount", 0, 0, G_OPTION_ARG_STRING, &opt_mount, "Mount the given device", NULL },
                 { "mount-fstype", 0, 0, G_OPTION_ARG_STRING, &opt_mount_fstype, "Specify file system type", NULL },
                 { "mount-options", 0, 0, G_OPTION_ARG_STRING, &opt_mount_options, "Mount options separated by comma", NULL },
 
-                { "unmount", 0, 0, G_OPTION_ARG_STRING, &opt_unmount, "Unmount the device given by the object path", NULL },
+                { "unmount", 0, 0, G_OPTION_ARG_STRING, &opt_unmount, "Unmount the given device", NULL },
                 { "unmount-options", 0, 0, G_OPTION_ARG_STRING, &opt_unmount_options, "Unmount options separated by comma", NULL },
+                { "detach", 0, 0, G_OPTION_ARG_STRING, &opt_detach, "Detach the given device", NULL },
+                { "detach-options", 0, 0, G_OPTION_ARG_STRING, &opt_detach_options, "Detach options separated by comma", NULL },
                 { "ata-smart-refresh", 0, 0, G_OPTION_ARG_STRING, &opt_ata_smart_refresh, "Refresh ATA SMART data", NULL },
                 { "ata-smart-wakeup", 0, 0, G_OPTION_ARG_NONE, &opt_ata_smart_wakeup, "Wake up the disk if it is not awake", NULL },
                 { "ata-smart-simulate", 0, 0, G_OPTION_ARG_STRING, &opt_ata_smart_simulate, "Inject libatasmart BLOB for testing", NULL },
@@ -1678,6 +1714,11 @@ main (int argc, char **argv)
                 if (device_file == NULL)
                         goto out;
                 do_unmount (device_file, opt_unmount_options);
+        } else if (opt_detach != NULL) {
+                device_file = device_file_to_object_path (opt_detach);
+                if (device_file == NULL)
+                        goto out;
+                do_detach (device_file, opt_detach_options);
         } else if (opt_ata_smart_refresh != NULL) {
                 device_file = device_file_to_object_path (opt_ata_smart_refresh);
                 if (device_file == NULL)