From 646c22ae9315f4abce6b9fb2110940c8be06091e Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:00:00 -0700 Subject: [PATCH] v0.13.34: open() just opens, drive_has_disc() is side-effect-free direct TUR, enumerate_sg_names skips unreadable type files, Disc::copy read_err_count fix --- Cargo.toml | 2 +- src/disc/mod.rs | 4 +- src/scsi/linux.rs | 106 ++++++++++++++++++++++++++-------------------- src/scsi/mod.rs | 6 +-- 4 files changed, 67 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 57fe7a4..2e1aa65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfreemkv" -version = "0.13.33" +version = "0.13.34" edition = "2024" rust-version = "1.86" license = "AGPL-3.0-only" diff --git a/src/disc/mod.rs b/src/disc/mod.rs index f1436c3..8aa4469 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -1284,7 +1284,7 @@ impl Disc { let copy_t0 = std::time::Instant::now(); let mut iter_count: u64 = 0; let mut read_ok_count: u64 = 0; - let read_err_count: u64 = 0; + let mut read_err_count: u64 = 0; let mut last_log_iter: u64 = 0; // Simple read strategy: start in block mode, drop to 1 sector on any // failure, then after BPT1_EXIT_THRESHOLD consecutive good single-sector @@ -1403,6 +1403,7 @@ impl Disc { } else if use_single { // Single sector mode — read failed, mark NonTrimmed let err = read_result.err().unwrap(); + read_err_count += 1; if err.is_marginal_read() || err.scsi_sense().is_some_and(|s| s.is_medium_error()) { @@ -1436,6 +1437,7 @@ impl Disc { } else { // Batch mode failed — drop to 1 sector and retry let err = read_result.err().unwrap(); + read_err_count += 1; if err.is_marginal_read() || err.scsi_sense().is_some_and(|s| s.is_medium_error()) { diff --git a/src/scsi/linux.rs b/src/scsi/linux.rs index d599bea..aa13e06 100644 --- a/src/scsi/linux.rs +++ b/src/scsi/linux.rs @@ -70,12 +70,9 @@ pub struct SgIoTransport { } impl SgIoTransport { - /// Open a SCSI device for use. Software-clean the kernel SG queue - /// (`reset()`) before opening so a previous killed process's queued - /// commands don't bleed into ours. + /// Open a SCSI device for use. pub fn open(device: &Path) -> Result { let device = Self::resolve_to_sg(device); - Self::reset(&device)?; let c_path = Self::to_c_path(&device); let fd = unsafe { libc::open( @@ -550,8 +547,8 @@ fn enumerate_sg_names() -> Vec { let type_path = format!("/sys/class/scsi_generic/{name}/device/type"); match std::fs::read_to_string(&type_path) { Ok(s) if s.trim() == SCSI_TYPE_OPTICAL => names.push(name), - Ok(_) => {} // not optical - Err(_) => names.push(name), // sysfs unreadable — let INQUIRY decide + Ok(_) => {} // not optical + Err(_) => {} } } } else { @@ -568,47 +565,64 @@ fn enumerate_sg_names() -> Vec { names } -/// `drive_has_disc` = single TEST UNIT READY. Any error (including the -/// wedge signature `ScsiError { status: 0xFF }` synthesised by `execute()` -/// when `host_status` is set) bubbles straight up to the caller. -/// -/// ## No in-library wedge recovery — and why -/// -/// Versions 0.13.1–0.13.3 layered `scsi::reset()` + `scsi::usb_reset()` -/// (`USBDEVFS_RESET`) escalation inside `drive_has_disc`. Production -/// testing on the LG BU40N USB BD-RE showed all three userspace recovery -/// ladders succeed at the USB transport level (the kernel logs -/// `usb 3-2: reset high-speed USB device`, the device re-authorises -/// and re-attaches on a fresh `scsi_host`) **but the drive firmware -/// below the USB bridge stays locked** — no LUN ever enumerates, TUR -/// never succeeds, /dev/sg* never reappears. Physical power-cycle -/// (unplug-replug or host reboot) is the only recovery. -/// -/// Methods tried and discarded: -/// - `SG_SCSI_RESET` (device-level SCSI bus reset) -/// - `STOP UNIT` / `START UNIT` CDB pair -/// - `USBDEVFS_RESET` ioctl on `/dev/bus/usb/BBB/DDD` -/// - `/sys/bus/usb/devices//authorized` 0→1 toggle -/// - `/sys/bus/usb/drivers/usb-storage/{unbind,bind}` driver rebind -/// - Forced `echo "- - -" > /sys/class/scsi_host/hostN/scan` -/// -/// Rolled back in 0.13.4. Callers (autorip, CLI) surface the error -/// directly and prompt the user to physically reconnect the drive. -/// If a future hardware class is found where USB-layer recovery -/// actually works, the escalation belongs here, gated on the wedge -/// signature — see git tag `v0.13.3` for the full implementation. +/// Send TEST UNIT READY directly — no transport, no reset, no side effects. pub(super) fn drive_has_disc(path: &Path) -> Result { - let mut transport = SgIoTransport::open(path)?; + let device = SgIoTransport::resolve_to_sg(path); + let c_path = SgIoTransport::to_c_path(&device); + let fd = unsafe { + libc::open( + c_path.as_ptr() as *const libc::c_char, + libc::O_RDWR | libc::O_NONBLOCK | libc::O_CLOEXEC, + ) + }; + if fd < 0 { + return SgIoTransport::open_error(&device); + } + 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(ref e) if e.scsi_sense().is_some_and(|s| s.is_not_ready()) => Ok(false), - Err(e) => Err(e), + let mut sense = [0u8; 32]; + let mut hdr: sg_io_hdr = unsafe { std::mem::zeroed() }; + hdr.interface_id = b'S' as i32; + hdr.dxfer_direction = SG_DXFER_NONE; + hdr.cmd_len = cdb.len() as u8; + hdr.mx_sb_len = sense.len() as u8; + hdr.dxfer_len = 0; + hdr.dxferp = std::ptr::null_mut(); + hdr.cmdp = cdb.as_ptr(); + hdr.sbp = sense.as_mut_ptr(); + hdr.timeout = crate::scsi::TUR_TIMEOUT_MS; + hdr.flags = SG_FLAG_Q_AT_HEAD; + + let ret = unsafe { libc::ioctl(fd, SG_IO as _, &mut hdr as *mut sg_io_hdr) }; + unsafe { libc::close(fd) }; + + if ret < 0 { + return Err(Error::IoError { + source: std::io::Error::last_os_error(), + }); + } + + let driver_status_real = hdr.driver_status & !super::DRIVER_SENSE; + if hdr.host_status != 0 || driver_status_real != 0 { + return Err(Error::ScsiError { + opcode: cdb[0], + status: super::SCSI_STATUS_TRANSPORT_FAILURE, + sense: None, + }); + } + + if hdr.status == 0 { + return Ok(true); + } + + let parsed = super::parse_sense(&sense, hdr.sb_len_wr); + if parsed.is_not_ready() { + Ok(false) + } else { + Err(Error::ScsiError { + opcode: cdb[0], + status: hdr.status, + sense: Some(parsed), + }) } } diff --git a/src/scsi/mod.rs b/src/scsi/mod.rs index b37c3e9..2d6391b 100644 --- a/src/scsi/mod.rs +++ b/src/scsi/mod.rs @@ -335,9 +335,9 @@ pub fn open(device: &Path) -> Result> { // platform reset in a thread+recv_timeout so a kernel-wedged ioctl // couldn't hang the caller. Removed in 0.13.6 along with the // SG_SCSI_RESET / STOP+START UNIT escalation that needed it. The -// remaining platform reset (Linux: SgIoTransport::reset, called only -// from SgIoTransport::open) does pure userspace state cleanup with -// bounded sleeps — no escape-hatch wrapper required. +// remaining platform reset (Linux: SgIoTransport::reset, available +// for explicit opt-in) does pure userspace state cleanup with bounded +// sleeps — no escape-hatch wrapper required. // ── USB-layer recovery: rolled back in 0.13.4 ─────────────────────────────── //