diff options
author | David Zeuthen <zeuthen@gmail.com> | 2014-01-26 12:05:40 -0800 |
---|---|---|
committer | David Zeuthen <zeuthen@gmail.com> | 2014-01-26 12:11:03 -0800 |
commit | fcdd8f48b6ac9b1b6da82fdf5f59230fc2ea6feb (patch) | |
tree | b6656eeede9f154cda24debadeb3ce93bd96987c | |
parent | 4ce586f81b6097f32c54c781b008798e5b70b9f8 (diff) |
Send SCSI START STOP UNIT when powering down a drive
It turns out that some disks / enclosures will not spin down when the
power to the USB upstream port is removed. Therefore, issue the START
STOP UNIT command just before removing power. As this command does
fail on some devices [1] make the failure non-fatal. Also add some
logging - even on successful paths - as it's useful for the admin to
see when a drive has been powered down.
This was reported in bug 71802.
https://bugs.freedesktop.org/show_bug.cgi?id=71802
[1] : in fact, START STOP UNIT fails on one of my external USB drives
but works on another.
Signed-off-by: David Zeuthen <zeuthen@gmail.com>
-rw-r--r-- | src/udiskslinuxdrive.c | 183 |
1 files changed, 176 insertions, 7 deletions
diff --git a/src/udiskslinuxdrive.c b/src/udiskslinuxdrive.c index 170ba27..ed541ff 100644 --- a/src/udiskslinuxdrive.c +++ b/src/udiskslinuxdrive.c @@ -25,6 +25,12 @@ #include <sys/stat.h> #include <sys/ioctl.h> #include <fcntl.h> +#include <inttypes.h> +#include <errno.h> +#include <linux/bsg.h> +#include <scsi/scsi.h> +#include <scsi/sg.h> +#include <scsi/scsi_ioctl.h> #include <pwd.h> #include <grp.h> @@ -1192,6 +1198,122 @@ handle_set_configuration (UDisksDrive *_drive, /* ---------------------------------------------------------------------------------------------------- */ +/* TODO: move to udisksscsi.[ch] similar what we do for ATA with udisksata.[ch] */ + +static gboolean +send_scsi_command_sync (gint fd, + guint8 *cdb, + gsize cdb_len, + GError **error) +{ + struct sg_io_v4 io_v4; + uint8_t sense[32]; + gboolean ret = FALSE; + gint rc; + gint timeout_msec = 30000; /* 30 seconds */ + + g_return_val_if_fail (fd != -1, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* See http://sg.danny.cz/sg/sg_io.html and http://www.tldp.org/HOWTO/SCSI-Generic-HOWTO/index.html + * for detailed information about how the SG_IO ioctl work + */ + + memset (sense, 0, sizeof (sense)); + memset (&io_v4, 0, sizeof (io_v4)); + io_v4.guard = 'Q'; + io_v4.protocol = BSG_PROTOCOL_SCSI; + io_v4.subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD; + io_v4.request_len = cdb_len; + io_v4.request = (uintptr_t) cdb; + io_v4.max_response_len = sizeof (sense); + io_v4.response = (uintptr_t) sense; + io_v4.timeout = timeout_msec; + + rc = ioctl (fd, SG_IO, &io_v4); + if (rc != 0) + { + /* could be that the driver doesn't do version 4, try version 3 */ + if (errno == EINVAL) + { + struct sg_io_hdr io_hdr; + memset (&io_hdr, 0, sizeof (struct sg_io_hdr)); + io_hdr.interface_id = 'S'; + io_hdr.cmdp = (unsigned char*) cdb; + io_hdr.cmd_len = cdb_len; + io_hdr.dxfer_direction = SG_DXFER_NONE; + io_hdr.sbp = sense; + io_hdr.mx_sb_len = sizeof (sense); + io_hdr.timeout = timeout_msec; + + rc = ioctl (fd, SG_IO, &io_hdr); + if (rc != 0) + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "SGIO v3 ioctl failed (v4 not supported): %m"); + goto out; + } + else + { + if (!(io_hdr.status == 0 && + io_hdr.host_status == 0 && + io_hdr.driver_status == 0)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Non-GOOD SCSI status from SGIO v3 ioctl: " + "status=%d host_status=%d driver_status=%d", + io_hdr.status, + io_hdr.host_status, + io_hdr.driver_status); + goto out; + } + } + } + else + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), + "SGIO v4 ioctl failed: %m"); + goto out; + } + } + else + { + if (!(io_v4.device_status == 0 && + io_v4.transport_status == 0 && + io_v4.driver_status == 0)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Non-GOOD SCSI status from SGIO v4 ioctl: " + "device_status=%d transport_status=%d driver_status=%d", + io_v4.device_status, + io_v4.transport_status, + io_v4.driver_status); + goto out; + } + } + + ret = TRUE; + + out: + return ret; +} + +static gboolean +send_scsi_start_stop_command_sync (gint fd, + GError **error) +{ + uint8_t cdb[6]; + + /* SBC3 (SCSI Block Commands), 5.20 START STOP UNIT command + */ + memset (cdb, 0, sizeof cdb); + cdb[0] = 0x1b; /* OPERATION CODE: START STOP UNIT */ + + return send_scsi_command_sync (fd, cdb, sizeof cdb, error); +} + +/* ---------------------------------------------------------------------------------------------------- */ + static gboolean handle_power_off (UDisksDrive *_drive, GDBusMethodInvocation *invocation, @@ -1216,6 +1338,7 @@ handle_power_off (UDisksDrive *_drive, gid_t caller_gid; pid_t caller_pid; GList *sibling_objects = NULL, *l; + gint fd = -1; object = udisks_daemon_util_dup_object (drive, &error); if (object == NULL) @@ -1324,10 +1447,10 @@ handle_power_off (UDisksDrive *_drive, { UDisksBlock *block_to_sync = UDISKS_BLOCK (l->data); const gchar *device_file; - gint fd; + gint device_fd; device_file = udisks_block_get_device (block_to_sync); - fd = open (device_file, O_RDONLY|O_NONBLOCK|O_EXCL); - if (fd == -1) + device_fd = open (device_file, O_RDONLY|O_NONBLOCK|O_EXCL); + if (device_fd == -1) { g_dbus_method_invocation_return_error (invocation, UDISKS_ERROR, @@ -1336,7 +1459,7 @@ handle_power_off (UDisksDrive *_drive, device_file); goto out; } - if (fsync (fd) != 0) + if (fsync (device_fd) != 0) { g_dbus_method_invocation_return_error (invocation, UDISKS_ERROR, @@ -1345,7 +1468,7 @@ handle_power_off (UDisksDrive *_drive, device_file); goto out; } - if (close (fd) != 0) + if (close (device_fd) != 0) { g_dbus_method_invocation_return_error (invocation, UDISKS_ERROR, @@ -1356,9 +1479,45 @@ handle_power_off (UDisksDrive *_drive, } } - escaped_device = udisks_daemon_util_escape_and_quote (udisks_block_get_device (block)); + /* Send the "SCSI START STOP UNIT" command to request that the unit + * be stopped but don't treat failure as fatal. In fact some + * USB-attached hard-disks fails with this command, probably due to + * the SCSI/SATA translation layer. + */ + fd = open (udisks_block_get_device (block), O_RDONLY|O_NONBLOCK|O_EXCL); + if (fd == -1) + { + g_dbus_method_invocation_return_error (invocation, + UDISKS_ERROR, + UDISKS_ERROR_FAILED, + "Error opening %s: %m", + udisks_block_get_device (block)); + goto out; + } + if (!send_scsi_start_stop_command_sync (fd, &error)) + { + udisks_warning ("Ignoring SCSI command START STOP UNIT failure (%s) on %s", + error->message, + udisks_block_get_device (block)); + g_clear_error (&error); + } + else + { + udisks_notice ("Powering off %s - successfully sent SCSI command START STOP UNIT", + udisks_block_get_device (block)); + } + if (close (fd) != 0) + { + g_dbus_method_invocation_return_error (invocation, + UDISKS_ERROR, + UDISKS_ERROR_FAILED, + "Error closing %s: %m", + udisks_block_get_device (block)); + goto out; + } + fd = -1; - /* TODO: Send the eject? Send SCSI START STOP UNIT? */ + escaped_device = udisks_daemon_util_escape_and_quote (udisks_block_get_device (block)); device = udisks_linux_drive_object_get_device (object, TRUE /* get_hw */); if (device == NULL) { @@ -1405,10 +1564,20 @@ handle_power_off (UDisksDrive *_drive, } } fclose (f); + udisks_notice ("Powered off %s - successfully wrote to sysfs path %s", + udisks_block_get_device (block), + remove_path); udisks_drive_complete_power_off (UDISKS_DRIVE (drive), invocation); out: + if (fd != -1) + { + if (close (fd) != 0) + { + udisks_warning ("Error closing device: %m"); + } + } g_list_free_full (blocks_to_sync, g_object_unref); g_list_free_full (sibling_objects, g_object_unref); g_free (remove_path); |