v0.13.4: roll back wedge recovery + add sysfs identity fallback

USB/SCSI recovery escalation in drive_has_disc (0.13.1-0.13.3) tested
on LG BU40N USB BD-RE: USBDEVFS_RESET, authorized toggle, driver
unbind/rebind, SCSI host rescan — all succeed at the USB transport
layer but the drive firmware below the bridge stays locked. Only
physical unplug-replug clears it. Rolled back so consumers can
surface the real failure to the user.

New: list_drives falls back to sysfs-cached vendor/model/rev from
/sys/class/scsi_generic/sgN/device/ when live INQUIRY returns empty,
so wedged drives still show their identity in UIs.

Removed: scsi::usb_reset, usb_reset_with_timeout, per-platform
usb_reset methods, recover_then_probe, is_wedge_signature. Breadcrumb
comment in scsi/linux.rs::drive_has_disc points at v0.13.3 tag for
the full implementation if future hardware needs it back.

Linux/macOS/Windows pass-through symmetric; 233 tests passing.
This commit is contained in:
2026-04-24 19:53:44 -07:00
parent 5f495d7a0b
commit 1ffb1d3542
6 changed files with 162 additions and 481 deletions
+16 -184
View File
@@ -277,147 +277,8 @@ impl MacScsiTransport {
std::thread::sleep(std::time::Duration::from_secs(2));
Ok(())
}
/// USB-layer reset on macOS via `IOUSBDeviceInterface::ResetDevice`.
///
/// Mirrors the Linux `USBDEVFS_RESET` path: walk from the BSD-named
/// SCSI service up the IORegistry plane to the parent `IOUSBDevice`,
/// query its `IOUSBDeviceInterface`, call `ResetDevice()`. Software
/// equivalent of unplug-replug — the only thing that recovers a
/// kernel-level USB Mass Storage wedge on macOS.
///
/// Returns `DeviceNotFound` when the device isn't USB-attached
/// (Thunderbolt/SATA/internal SuperDrive over PCIe — they don't
/// have an `IOUSBDevice` ancestor) so the caller's escalation can
/// fall through cleanly. `DeviceResetFailed` on actual reset
/// failures.
pub fn usb_reset(device: &Path) -> Result<()> {
let bsd_name =
device
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| Error::DeviceNotFound {
path: device.display().to_string(),
})?;
let service = find_scsi_service(bsd_name)?;
let usb_service = walk_to_usb_device(service);
unsafe { IOObjectRelease(service) };
let usb_service = usb_service.ok_or_else(|| Error::DeviceNotFound {
path: device.display().to_string(),
})?;
// Get IOUSBDeviceInterface from the USB device service.
let mut plugin: ComRef = std::ptr::null_mut();
let mut score: i32 = 0;
let kr = unsafe {
IOCreatePlugInInterfaceForService(
usb_service,
&K_IO_USB_DEVICE_USER_CLIENT_TYPE_ID,
&K_IO_CFPLUGIN_INTERFACE_ID,
&mut plugin,
&mut score,
)
};
unsafe { IOObjectRelease(usb_service) };
if kr != K_IO_RETURN_SUCCESS || plugin.is_null() {
return Err(Error::DeviceResetFailed {
path: device.display().to_string(),
});
}
// QueryInterface for IOUSBDeviceInterface.
let mut device_iface: ComRef = std::ptr::null_mut();
let hr = unsafe {
type QiFn = unsafe extern "C" fn(ComRef, *const [u8; 16], *mut ComRef) -> i32;
let qi: QiFn = vtable_fn(plugin, 1);
qi(plugin, &K_IO_USB_DEVICE_INTERFACE_ID, &mut device_iface)
};
com_release(plugin);
if hr != 0 || device_iface.is_null() {
return Err(Error::DeviceResetFailed {
path: device.display().to_string(),
});
}
// Call ResetDevice() — vtable index 11 in IOUSBDeviceInterface.
// Verified against IOUSBLib.h headers (Apple OSS).
let kr = unsafe {
type ResetFn = unsafe extern "C" fn(ComRef) -> IOReturn;
let f: ResetFn = vtable_fn(device_iface, K_IO_USB_DEVICE_RESET_VTABLE_INDEX);
f(device_iface)
};
com_release(device_iface);
if kr != K_IO_RETURN_SUCCESS {
Err(Error::DeviceResetFailed {
path: device.display().to_string(),
})
} else {
Ok(())
}
}
}
/// `kIOUSBDeviceUserClientTypeID` — IOKit plugin type for accessing a
/// USB device through user-space (the gateway to `IOUSBDeviceInterface`).
const K_IO_USB_DEVICE_USER_CLIENT_TYPE_ID: [u8; 16] = [
0x9D, 0xC7, 0xB7, 0x80, 0x9E, 0xC0, 0x11, 0xD4, 0xA5, 0x4F, 0x00, 0x0A, 0x27, 0x05, 0x28, 0x61,
];
/// `kIOUSBDeviceInterfaceID` — `IOUSBDeviceInterface` (revision 0).
/// Sufficient for `ResetDevice()` which has been at vtable index 11
/// since the original interface revision.
const K_IO_USB_DEVICE_INTERFACE_ID: [u8; 16] = [
0x5C, 0x81, 0x87, 0xD0, 0x9E, 0xF3, 0x11, 0xD4, 0x8B, 0x45, 0x00, 0x0A, 0x27, 0x05, 0x28, 0x61,
];
/// Vtable index of `IOUSBDeviceInterface::ResetDevice`. Per IOUSBLib.h:
/// the interface inherits from IOCFPlugInInterface which occupies slots
/// 0..2 (QueryInterface, AddRef, Release), then IOUSBDeviceInterface
/// methods start at slot 3. ResetDevice is the 9th IOUSBDevice-specific
/// method → slot 3 + 8 = 11.
const K_IO_USB_DEVICE_RESET_VTABLE_INDEX: usize = 11;
/// Walk up the IORegistry plane from a SCSI peripheral service to the
/// parent `IOUSBDevice` (if any). Mirrors the Linux sysfs walk in
/// `linux::SgIoTransport::resolve_usb_device`. Returns the IOService
/// for the USB device (caller owns the reference; release with
/// `IOObjectRelease`), or `None` for non-USB-attached drives.
fn walk_to_usb_device(start: IOObject) -> Option<IOObject> {
let mut current = start;
// Retain the start so we can release uniformly each loop iteration.
unsafe {
let kr = IOObjectRetain(current);
if kr != K_IO_RETURN_SUCCESS {
return None;
}
}
for _ in 0..K_USB_PARENT_WALK_LIMIT {
if unsafe { IOObjectConformsTo(current, c"IOUSBDevice".as_ptr() as *const u8) } != 0 {
return Some(current);
}
let mut parent: IOObject = 0;
let kr = unsafe {
IORegistryEntryGetParentEntry(current, c"IOService".as_ptr() as *const u8, &mut parent)
};
unsafe { IOObjectRelease(current) };
if kr != K_IO_RETURN_SUCCESS || parent == 0 {
return None;
}
current = parent;
}
unsafe { IOObjectRelease(current) };
None
}
/// Maximum IORegistry parent-chain depth searched for a USB ancestor.
/// Real chains for USB-attached optical drives are 6-10 entries deep
/// (IOMedia → BlockStorageDriver → SCSIPeripheralDeviceNub →
/// SCSIProtocolEmulator → IOUSBInterface → IOUSBDevice → ...). 32 is
/// generous; if we don't find it by then, the device isn't USB.
const K_USB_PARENT_WALK_LIMIT: u32 = 32;
/// Enumerate optical drives on macOS. Mirrors `drive::macos::find_drives`
/// (which iterates `/dev/disk0..15` + INQUIRY + filters peripheral
/// type 5). Same logic, exposed through the new `DriveInfo` shape so
@@ -467,58 +328,29 @@ const K_INQUIRY_TYPE_MASK: u8 = 0x1F;
/// SCSI peripheral type 5 = "CD-ROM device" (covers DVD, BD-ROM, BD-RE).
const K_SCSI_TYPE_OPTICAL: u8 = 0x05;
/// TEST UNIT READY probe on macOS. Same shape as the Linux impl —
/// open transport, run TUR, classify response. The macOS path doesn't
/// surface the Linux `0xff`-status wedge pattern (IOKit returns its
/// own error codes), so wedge-detection here is sense-key based: a
/// transport-level error during TUR escalates to SCSI reset → USB
/// reset. Most macOS drives auto-recover at the SCSI-reset stage.
/// TEST UNIT READY probe on macOS. Any non-"not ready" error bubbles up
/// to the caller — in-library wedge recovery was rolled back in 0.13.4
/// after USB-layer resets failed to recover the LG BU40N on Linux; the
/// macOS impl mirrors that choice for symmetry. See the Linux
/// `drive_has_disc` in `scsi/linux.rs` for the full rationale.
pub(super) fn drive_has_disc(path: &Path) -> Result<bool> {
match probe_tur(path) {
Ok(present) => Ok(present),
let mut transport = MacScsiTransport::open(path)?;
let cdb = [crate::scsi::SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0];
let mut buf = [0u8; 0];
match transport.execute(
&cdb,
crate::scsi::DataDirection::None,
&mut buf,
crate::scsi::TUR_TIMEOUT_MS,
) {
Ok(_) => Ok(true),
Err(Error::ScsiError { sense_key, .. }) if sense_key == K_SENSE_KEY_NOT_READY => Ok(false),
Err(_) => recover_then_probe(path),
Err(e) => Err(e),
}
}
const K_SENSE_KEY_NOT_READY: u8 = 2;
fn probe_tur(path: &Path) -> Result<bool> {
let mut transport = MacScsiTransport::open(path)?;
let cdb = [crate::scsi::SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0];
let mut buf = [0u8; 0];
transport
.execute(
&cdb,
crate::scsi::DataDirection::None,
&mut buf,
crate::scsi::TUR_TIMEOUT_MS,
)
.map(|_| true)
}
fn recover_then_probe(path: &Path) -> Result<bool> {
let _ = super::reset(path);
if let Ok(present) = probe_tur(path) {
return Ok(present);
}
if super::usb_reset(path).is_ok() {
std::thread::sleep(std::time::Duration::from_secs(K_USB_RESET_SETTLE_SECS));
if let Ok(present) = probe_tur(path) {
return Ok(present);
}
}
Err(Error::DeviceResetFailed {
path: path.display().to_string(),
})
}
const K_USB_RESET_SETTLE_SECS: u64 = 2;
unsafe extern "C" {
fn IOObjectRetain(object: IOObject) -> IOReturn;
}
impl Drop for MacScsiTransport {
fn drop(&mut self) {
if self.exclusive {