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
+18 -88
View File
@@ -196,94 +196,24 @@ fn reset_blocking(device: &Path) -> Result<()> {
}
}
/// Default upper bound on `scsi::usb_reset()`. Kernel USB stack
/// typically completes a port reset in under a second; 5 s is generous
/// while still catching a hang in the driver.
pub(crate) const DEFAULT_USB_RESET_TIMEOUT_SECS: u64 = 5;
/// USB-layer reset for USB-attached SCSI devices.
///
/// `pub(crate)` — outside callers reach recovery through the
/// higher-level `drive_has_disc` (poll-loop entry point) which folds
/// in the SCSI→USB escalation internally. See module-level docs for
/// the architectural reasoning.
///
/// **When to call this.** `scsi::reset()` resets at the SCSI layer; if
/// the wedge is at the USB Mass Storage interface *below* SCSI (the
/// classic mode where INQUIRY returns status `0xFF` because the kernel
/// got nothing back), SCSI commands never reach the device and SCSI
/// reset is meaningless. USB reset re-enumerates the device at the USB
/// layer — software equivalent of unplug-replug.
///
/// **Linux**: resolves `/dev/sgN` → `/dev/bus/usb/BBB/DDD` via sysfs,
/// then `USBDEVFS_RESET` ioctl. Same mechanism as the well-known
/// `usbreset.c` snippet.
///
/// **macOS**, **Windows**: returns `DeviceNotFound` for now (stub).
/// Real impls tracked for 0.13.3.
///
/// **Returns** `DeviceNotFound` when the sg device isn't USB-attached
/// (SATA / RAID / NVMe-passthrough sg nodes) so callers can detect the
/// fall-through case and know not to retry — USB reset is meaningless
/// for those drives.
///
/// Wraps the platform call in a thread + `recv_timeout` for the same
/// reason as `reset()` — kernel ioctls can hang and we don't want the
/// caller's poll loop wedged on it.
pub(crate) fn usb_reset(device: &Path) -> Result<()> {
usb_reset_with_timeout(
device,
std::time::Duration::from_secs(DEFAULT_USB_RESET_TIMEOUT_SECS),
)
}
/// USB reset with a caller-specified timeout. See [`usb_reset`].
pub(crate) fn usb_reset_with_timeout(device: &Path, timeout: std::time::Duration) -> Result<()> {
let device_owned = device.to_path_buf();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::Builder::new()
.name("usb-reset".into())
.spawn(move || {
let r = usb_reset_blocking(&device_owned);
let _ = tx.send(r);
})
.map_err(|_| Error::DeviceResetFailed {
path: device.display().to_string(),
})?;
match rx.recv_timeout(timeout) {
Ok(result) => result,
Err(_) => Err(Error::DeviceResetFailed {
path: device.display().to_string(),
}),
}
}
fn usb_reset_blocking(device: &Path) -> Result<()> {
#[cfg(target_os = "linux")]
{
linux::SgIoTransport::usb_reset(device)
}
#[cfg(target_os = "macos")]
{
macos::MacScsiTransport::usb_reset(device)
}
#[cfg(target_os = "windows")]
{
windows::SptiTransport::usb_reset(device)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = device;
Err(Error::DeviceNotFound {
path: device.display().to_string(),
})
}
}
// ── USB-layer recovery: rolled back in 0.13.4 ───────────────────────────────
//
// 0.13.1 0.13.3 exposed `scsi::usb_reset()` (`USBDEVFS_RESET` on Linux,
// `IOUSBDeviceInterface::ResetDevice` on macOS) and chained it into
// `drive_has_disc` recovery. Production testing on the LG BU40N USB BD-RE
// confirmed the USB stack resets succeed — dmesg logs
// `usb 3-2: reset high-speed USB device` and the device re-authorises —
// but the drive firmware below the USB bridge stays locked: LUN never
// re-enumerates, TUR still times out, the drive is unusable until
// physical unplug-replug or host reboot. Additional approaches tried
// and discarded: `authorized` 0→1 toggle, usb-storage driver
// unbind/rebind, forced SCSI host rescan, `STOP` + `START UNIT`.
//
// The APIs were removed so no caller can be misled into thinking a
// software-only recovery exists for this class of wedge. If a future
// hardware class surfaces where USB-layer recovery actually helps, the
// code should live here again, gated on a wedge signature — see git
// tag `v0.13.3` for the full implementation.
// ── Lightweight discovery + presence probes ─────────────────────────────────
//