From 2f52188f778c7403abbbc102a7828e6d69eb626a Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:32:22 +0000 Subject: [PATCH] Drive recovery, reset on open, simplified DiscStream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SgIoTransport::reset() — open/close/TUR/escalate on every open - Drive::read() — single read method with error recovery (min speed, sleep 30s, retry, phase 1/2/3 escalation) - Removed read_timeout, read_sectors, read_range — one read() method - DiscStream simplified — no on_error/on_success/Recovery, delegates all error handling to Drive::read() - IsoStream no longer decrypts — streams return raw bytes, pipeline handles decryption - reset() on all platforms (Linux real, Windows/macOS stubs) - Watchdog thread removed — kernel handles USB timeouts --- src/drive/mod.rs | 293 +++++++++++++++++---- src/mux/disc.rs | 626 ++++++++++++++++++-------------------------- src/mux/iso.rs | 33 ++- src/scsi/linux.rs | 209 +++++++++++++-- src/scsi/macos.rs | 18 ++ src/scsi/mod.rs | 27 +- src/scsi/windows.rs | 49 ++++ 7 files changed, 803 insertions(+), 452 deletions(-) diff --git a/src/drive/mod.rs b/src/drive/mod.rs index 9889fb5..597734e 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -39,6 +39,9 @@ pub enum DriveStatus { Unknown, } +/// Recovery state after a read error — stay at min speed for N bytes. +const RECOVERY_WINDOW: u64 = 500 * 1024 * 1024; // 500 MB + /// Optical disc drive session -- open, identify, unlock, and read. pub struct Drive { scsi: Box, @@ -47,6 +50,9 @@ pub struct Drive { pub platform: Option, pub drive_id: DriveId, device_path: String, + /// Bytes remaining in the min-speed recovery window. + /// After a read error, we stay at min speed for RECOVERY_WINDOW bytes. + recovery_bytes_remaining: u64, } impl Drive { @@ -72,9 +78,26 @@ impl Drive { profile, drive_id, device_path: device.to_string_lossy().to_string(), + recovery_bytes_remaining: 0, }) } + /// Close the drive cleanly. Unlocks tray, flushes SCSI state, closes fd. + /// Also runs automatically on Drop as a safety net. + pub fn close(self) { + // cleanup() runs here via Drop + } + + /// Shared cleanup — called by Drop (and thus by close). + fn cleanup(&mut self) { + self.unlock_tray(); + } + + // NOTE: Debug aid — remove after fd issue is resolved + pub fn device_path_owned(&self) -> String { + self.device_path.clone() + } + /// Whether this drive has a known profile (unlock parameters available). pub fn has_profile(&self) -> bool { self.profile.is_some() @@ -132,8 +155,8 @@ impl Drive { // Bits 1-0: door/tray state // Bit 1: media present, Bit 0: tray open match media_status & 0x03 { - 0x00 => DriveStatus::NoDisc, // tray closed, no disc - 0x01 => DriveStatus::TrayOpen, // tray open + 0x00 => DriveStatus::NoDisc, // tray closed, no disc + 0x01 => DriveStatus::TrayOpen, // tray open 0x02 => DriveStatus::DiscPresent, // tray closed, disc present 0x03 => DriveStatus::DiscPresent, // tray closed, disc present _ => DriveStatus::Unknown, @@ -175,19 +198,24 @@ impl Drive { // 1. Unlock + stop/start self.unlock_tray(); let stop = [0x1Bu8, 0x00, 0x00, 0x00, 0x00, 0x00]; - let _ = self.scsi.as_mut().execute( - &stop, crate::scsi::DataDirection::None, &mut buf, 5_000, - ); + let _ = + self.scsi + .as_mut() + .execute(&stop, crate::scsi::DataDirection::None, &mut buf, 5_000); std::thread::sleep(std::time::Duration::from_millis(500)); let start = [0x1Bu8, 0x00, 0x00, 0x00, 0x01, 0x00]; - let _ = self.scsi.as_mut().execute( - &start, crate::scsi::DataDirection::None, &mut buf, 5_000, - ); + let _ = + self.scsi + .as_mut() + .execute(&start, crate::scsi::DataDirection::None, &mut buf, 5_000); std::thread::sleep(std::time::Duration::from_millis(2000)); - if self.scsi.as_mut().execute( - &tur, crate::scsi::DataDirection::None, &mut buf, 5_000, - ).is_ok() { + if self + .scsi + .as_mut() + .execute(&tur, crate::scsi::DataDirection::None, &mut buf, 5_000) + .is_ok() + { return Ok(()); } @@ -196,14 +224,17 @@ impl Drive { // counts as success: the drive is functional, just needs disc reinserted. self.unlock_tray(); let eject = [0x1Bu8, 0x00, 0x00, 0x00, 0x02, 0x00]; - let _ = self.scsi.as_mut().execute( - &eject, crate::scsi::DataDirection::None, &mut buf, 30_000, - ); + let _ = + self.scsi + .as_mut() + .execute(&eject, crate::scsi::DataDirection::None, &mut buf, 30_000); std::thread::sleep(std::time::Duration::from_millis(2000)); - match self.scsi.as_mut().execute( - &tur, crate::scsi::DataDirection::None, &mut buf, 5_000, - ) { + match self + .scsi + .as_mut() + .execute(&tur, crate::scsi::DataDirection::None, &mut buf, 5_000) + { Ok(_) => return Ok(()), Err(Error::ScsiError { sense_key: 2, .. }) => return Ok(()), // tray open = valid _ => {} @@ -214,7 +245,10 @@ impl Drive { self.init()?; std::thread::sleep(std::time::Duration::from_millis(1000)); match self.scsi.as_mut().execute( - &tur, crate::scsi::DataDirection::None, &mut buf, 5_000, + &tur, + crate::scsi::DataDirection::None, + &mut buf, + 5_000, ) { Ok(_) => return Ok(()), Err(Error::ScsiError { sense_key: 2, .. }) => return Ok(()), @@ -269,13 +303,28 @@ impl Drive { /// Returns the feature data (without the 8-byte header), or None if not available. pub fn get_config_feature(&mut self, feature_code: u16) -> Option> { let cdb = [ - crate::scsi::SCSI_GET_CONFIGURATION, 0x02, - (feature_code >> 8) as u8, feature_code as u8, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + crate::scsi::SCSI_GET_CONFIGURATION, + 0x02, + (feature_code >> 8) as u8, + feature_code as u8, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, ]; let mut buf = vec![0u8; 256]; - let r = self.scsi.as_mut() - .execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?; + let r = self + .scsi + .as_mut() + .execute( + &cdb, + crate::scsi::DataDirection::FromDevice, + &mut buf, + 5_000, + ) + .ok()?; if r.bytes_transferred > 8 { Some(buf[8..r.bytes_transferred].to_vec()) } else { @@ -285,29 +334,67 @@ impl Drive { /// Read REPORT KEY RPC state (region playback control). pub fn report_key_rpc_state(&mut self) -> Option> { - let cdb = [0xA4u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00]; + let cdb = [ + 0xA4u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00, + ]; let mut buf = vec![0u8; 8]; - let r = self.scsi.as_mut() - .execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?; - if r.bytes_transferred > 0 { Some(buf[..r.bytes_transferred].to_vec()) } else { None } + let r = self + .scsi + .as_mut() + .execute( + &cdb, + crate::scsi::DataDirection::FromDevice, + &mut buf, + 5_000, + ) + .ok()?; + if r.bytes_transferred > 0 { + Some(buf[..r.bytes_transferred].to_vec()) + } else { + None + } } /// Read MODE SENSE page data. pub fn mode_sense_page(&mut self, page: u8) -> Option> { let cdb = [0x5Au8, 0x00, page, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00]; let mut buf = vec![0u8; 252]; - let r = self.scsi.as_mut() - .execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?; - if r.bytes_transferred > 0 { Some(buf[..r.bytes_transferred].to_vec()) } else { None } + let r = self + .scsi + .as_mut() + .execute( + &cdb, + crate::scsi::DataDirection::FromDevice, + &mut buf, + 5_000, + ) + .ok()?; + if r.bytes_transferred > 0 { + Some(buf[..r.bytes_transferred].to_vec()) + } else { + None + } } /// Read vendor-specific READ BUFFER data. pub fn read_buffer(&mut self, mode: u8, buffer_id: u8, length: u16) -> Option> { let cdb = crate::scsi::build_read_buffer(mode, buffer_id, 0, length as u32); let mut buf = vec![0u8; length as usize]; - let r = self.scsi.as_mut() - .execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?; - if r.bytes_transferred > 0 { Some(buf[..r.bytes_transferred].to_vec()) } else { None } + let r = self + .scsi + .as_mut() + .execute( + &cdb, + crate::scsi::DataDirection::FromDevice, + &mut buf, + 5_000, + ) + .ok()?; + if r.bytes_transferred > 0 { + Some(buf[..r.bytes_transferred].to_vec()) + } else { + None + } } pub fn is_ready(&self) -> bool { @@ -317,8 +404,16 @@ impl Drive { } } - /// Read sectors from the disc. Raw SCSI READ(10). + /// Read sectors from the disc with automatic error recovery. + /// + /// On failure: drops to min speed, waits with escalating patience + /// (5s, 10s, 15s, 30s, 60s), resets drive between attempts. + /// After recovery, stays at min speed for 500 MB before ramping up. + /// + /// Returns Err only after all attempts exhausted — user should clean + /// the disc and resume. pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { + let timeout_ms = if self.recovery_bytes_remaining > 0 { 30_000 } else { 10_000 }; let cdb = [ crate::scsi::SCSI_READ_10, 0x00, @@ -331,24 +426,112 @@ impl Drive { count as u8, 0x00, ]; - let result = self.scsi.as_mut().execute( - &cdb, - crate::scsi::DataDirection::FromDevice, - buf, - 30_000, - )?; - Ok(result.bytes_transferred) + + // Normal read + match self.scsi.as_mut().execute( + &cdb, crate::scsi::DataDirection::FromDevice, buf, timeout_ms, + ) { + Ok(result) => { + if self.recovery_bytes_remaining > 0 { + let bytes_read = count as u64 * 2048; + self.recovery_bytes_remaining = + self.recovery_bytes_remaining.saturating_sub(bytes_read); + if self.recovery_bytes_remaining == 0 { + eprintln!("[drive] recovery window complete — resuming full speed"); + self.set_speed(0xFFFF); + } + } + return Ok(result.bytes_transferred); + } + Err(e) => { + eprintln!("[drive] read error at LBA {} count {} — {}", lba, count, e); + } + } + + // Phase 1: gentle — sleep 30s, retry. 5 times. + // No intervention, just patience. + self.set_speed(0); + + for attempt in 1..=5 { + eprintln!("[drive] phase 1 retry {}/5 at LBA {} — sleep 30s", attempt, lba); + std::thread::sleep(std::time::Duration::from_secs(30)); + + match self.scsi.as_mut().execute( + &cdb, crate::scsi::DataDirection::FromDevice, buf, 30_000, + ) { + Ok(result) => { + eprintln!("[drive] phase 1 retry {}/5 OK at LBA {}", attempt, lba); + self.recovery_bytes_remaining = RECOVERY_WINDOW; + return Ok(result.bytes_transferred); + } + Err(e) => { + eprintln!("[drive] phase 1 retry {}/5 FAILED at LBA {} — {}", attempt, lba, e); + } + } + } + + // Phase 2: fresh start — close, reset, open, init. Like restarting the app. + eprintln!("[drive] phase 2: fresh start at LBA {}", lba); + let device = std::path::PathBuf::from(&self.device_path); + std::thread::sleep(std::time::Duration::from_secs(5)); + let _ = crate::scsi::reset(&device); + std::thread::sleep(std::time::Duration::from_secs(5)); + self.scsi = match crate::scsi::open(&device) { + Ok(s) => s, + Err(e) => { + eprintln!("[drive] reopen failed: {}", e); + return Err(e); + } + }; + let _ = self.init(); + let _ = self.wait_ready(); + self.set_speed(0); + + // Phase 3: gentle again on fresh connection — sleep 30s, retry. 5 times. + for attempt in 1..=5 { + eprintln!("[drive] phase 3 retry {}/5 at LBA {} — sleep 30s", attempt, lba); + std::thread::sleep(std::time::Duration::from_secs(30)); + + match self.scsi.as_mut().execute( + &cdb, crate::scsi::DataDirection::FromDevice, buf, 30_000, + ) { + Ok(result) => { + eprintln!("[drive] phase 3 retry {}/5 OK at LBA {}", attempt, lba); + self.recovery_bytes_remaining = RECOVERY_WINDOW; + return Ok(result.bytes_transferred); + } + Err(e) => { + eprintln!("[drive] phase 3 retry {}/5 FAILED at LBA {} — {}", attempt, lba, e); + } + } + } + + // Both phases failed. Give up. + eprintln!("[drive] FAILED LBA {} count {} — all recovery exhausted", lba, count); + self.recovery_bytes_remaining = RECOVERY_WINDOW; + Err(Error::DiscRead { sector: lba as u64 }) } /// Read the disc capacity in sectors (2048 bytes each). pub fn read_capacity(&mut self) -> Result { let cdb = [ - crate::scsi::SCSI_READ_CAPACITY, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, + crate::scsi::SCSI_READ_CAPACITY, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, ]; let mut buf = [0u8; 8]; self.scsi.as_mut().execute( - &cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000, + &cdb, + crate::scsi::DataDirection::FromDevice, + &mut buf, + 5_000, )?; let last_lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]); Ok(last_lba + 1) @@ -364,18 +547,20 @@ impl Drive { pub fn lock_tray(&mut self) { let prevent = [0x1Eu8, 0x00, 0x00, 0x00, 0x01, 0x00]; let mut buf = [0u8; 0]; - let _ = self.scsi.as_mut().execute( - &prevent, crate::scsi::DataDirection::None, &mut buf, 5_000, - ); + let _ = + self.scsi + .as_mut() + .execute(&prevent, crate::scsi::DataDirection::None, &mut buf, 5_000); } /// Unlock the tray so the user can manually eject the disc. pub fn unlock_tray(&mut self) { let allow = [0x1Eu8, 0x00, 0x00, 0x00, 0x00, 0x00]; let mut buf = [0u8; 0]; - let _ = self.scsi.as_mut().execute( - &allow, crate::scsi::DataDirection::None, &mut buf, 5_000, - ); + let _ = + self.scsi + .as_mut() + .execute(&allow, crate::scsi::DataDirection::None, &mut buf, 5_000); } /// Eject the disc tray. Unlocks first, then ejects. @@ -403,6 +588,13 @@ impl Drive { } } +impl Drop for Drive { + fn drop(&mut self) { + self.cleanup(); + // SgIoTransport::drop() runs next, calling libc::close(fd) + } +} + impl SectorReader for Drive { fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { self.read(lba, count, buf) @@ -441,6 +633,7 @@ fn discover_drives() -> Vec<(String, DriveId)> { } /// Resolve a device path to its raw SCSI device, with optional warning message. +#[allow(dead_code)] pub(crate) fn resolve_device(path: &str) -> Result<(String, Option)> { #[cfg(target_os = "linux")] { diff --git a/src/mux/disc.rs b/src/mux/disc.rs index 3eb7c2d..ccb1028 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -1,273 +1,307 @@ -//! DiscStream — read BD-TS data from an optical disc drive. +//! DiscStream — read sectors from an optical disc drive. //! -//! Read-only stream. Wraps Drive + Disc. -//! Handles drive init, AACS decryption, and sector reading. +//! `DiscStream::open()` does the full init sequence: +//! drive open → wait_ready → init → probe_disc → scan //! -//! Reading state (extent index, offset, batch size, error recovery) is stored -//! directly on the struct so that successive `read()` calls advance through -//! the disc instead of restarting from byte 0. +//! Then reads title extents or full-disc sequentially. +//! No decryption — that's a caller concern. use super::IOStream; use crate::disc::{ - detect_max_batch_sectors, ContentFormat, Disc, DiscTitle, Extent, MIN_BATCH_SECTORS, - RAMP_BATCH_AFTER, RAMP_SPEED_AFTER, SLOW_SPEED_AFTER, + detect_max_batch_sectors, Disc, DiscTitle, Extent, ScanOptions, }; use crate::drive::Drive; -use crate::error::Error; -use crate::speed::DriveSpeed; +use crate::error::{Error, Result}; +use crate::event::{Event, EventKind}; use std::io::{self, Read, Write}; +use std::path::Path; -/// Options for opening a disc stream. -#[derive(Default)] -pub struct DiscOptions { - /// Device path (e.g. "/dev/sg4"). None = auto-detect. - pub device: Option, - /// KEYDB.cfg path. None = search standard locations. - pub keydb_path: Option, - /// Which title to read (0-based). None = longest title. - pub title_index: Option, -} - -/// Optical disc stream. Read-only — yields decrypted BD-TS bytes. +/// Optical disc stream. Read-only — yields raw sector bytes. /// -/// Embeds the reading state that `ContentReader` would normally hold, so that -/// successive `read()` calls advance through the disc correctly. +/// Created from an initialized Drive + title extents or full-disc mode. +/// Error recovery (batch reduction, retry, zero-fill) is handled internally. pub struct DiscStream { - disc_title: DiscTitle, - disc: Disc, - session: Drive, - // Read buffer: holds one decoded batch - batch_buf: Vec, - batch_pos: usize, - eof: bool, + drive: Drive, + title: DiscTitle, - // ── Reading state (replaces ContentReader) ── - extents: Vec, + // What to read + mode: ReadMode, + + // Position + current_lba: u32, current_extent: usize, current_offset: u32, - #[allow(dead_code)] - content_format: ContentFormat, - decrypt_keys: crate::decrypt::DecryptKeys, - unit_key_idx: usize, + + // Buffer read_buf: Vec, - /// Current batch size in sectors (adapts on errors) + buf_valid: usize, + buf_cursor: usize, + + // Batch size for reads batch_sectors: u16, - /// Maximum batch size detected from kernel limits - max_batch_sectors: u16, - /// Consecutive successful batch reads - ok_streak: u32, - /// Consecutive errors at current position - error_streak: u32, - /// Total read errors encountered - pub errors: u32, + pub errors: u64, + eof: bool, +} + +enum ReadMode { + /// Read title extents (for MKV, M2TS, etc.) + Extents(Vec), + /// Read LBA 0 to capacity (for ISO) + Sequential { capacity: u32 }, +} + +/// Result of opening a DiscStream. +pub struct DiscOpenResult { + pub stream: DiscStream, + pub disc: Disc, } impl DiscStream { - /// Open the disc drive and scan disc metadata. - pub fn open(opts: DiscOptions) -> Result { - let mut session = match opts.device { - Some(ref d) => Drive::open(d)?, + /// Open a disc drive, init, scan, and prepare to read a title. + /// + /// Steps (each does one thing): + /// 1. Drive::open (or find_drive) + /// 2. wait_ready + /// 3. init (non-fatal) + /// 4. probe_disc (non-fatal) + /// 5. Disc::scan + /// + /// Pass an event callback for status reporting, or None. + pub fn open( + device: Option<&Path>, + keydb_path: Option<&str>, + title_index: usize, + on_event: Option<&dyn Fn(Event)>, + ) -> Result { + let emit = |kind: EventKind| { + if let Some(cb) = &on_event { + cb(Event { kind }); + } + }; + + // 1. Open + let mut drive = match device { + Some(d) => Drive::open(d)?, None => crate::drive::find_drive().ok_or_else(|| Error::DeviceNotFound { path: String::new(), })?, }; - session.wait_ready()?; - let _ = session.init(); - let _ = session.probe_disc(); + emit(EventKind::DriveOpened { + device: drive.device_path().to_string(), + }); - let scan_opts = match opts.keydb_path { - Some(ref kp) => crate::disc::ScanOptions::with_keydb(kp.clone()), - None => crate::disc::ScanOptions::default(), + // 2. Wait + let _ = drive.wait_ready(); + emit(EventKind::DriveReady); + + // 3. Init + let init_ok = drive.init().is_ok(); + emit(EventKind::InitComplete { success: init_ok }); + + // 4. Probe + let probe_ok = drive.probe_disc().is_ok(); + emit(EventKind::ProbeComplete { success: probe_ok }); + + // 5. Scan + let scan_opts = match keydb_path { + Some(kp) => ScanOptions::with_keydb(kp), + None => ScanOptions::default(), }; - let disc = Disc::scan(&mut session, &scan_opts)?; + let disc = Disc::scan(&mut drive, &scan_opts)?; + emit(EventKind::ScanComplete { + titles: disc.titles.len(), + }); - let title_index = opts.title_index.unwrap_or(0); if title_index >= disc.titles.len() { return Err(Error::DiscTitleRange { index: title_index, count: disc.titles.len(), }); } - let disc_title = disc.titles[title_index].clone(); - let extents = disc_title.extents.clone(); - let content_format = disc_title.content_format; - let decrypt_keys = disc.decrypt_keys(); - let max_batch = detect_max_batch_sectors(session.device_path()); + let title = disc.titles[title_index].clone(); + let stream = Self::title(drive, title); - Ok(Self { - disc_title, - disc, - session, - batch_buf: Vec::new(), - batch_pos: 0, - eof: false, - extents, + Ok(DiscOpenResult { stream, disc }) + } + + /// Create a stream that reads a title's extents. + /// Use this when you already have an initialized Drive. + pub fn title(drive: Drive, title: DiscTitle) -> Self { + let max_batch = detect_max_batch_sectors(drive.device_path()); + let extents = title.extents.clone(); + Self::new(drive, title, ReadMode::Extents(extents), max_batch) + } + + /// Create a stream that reads the full disc sequentially (for ISO). + pub fn full_disc(drive: Drive, title: DiscTitle, capacity: u32) -> Self { + let max_batch = detect_max_batch_sectors(drive.device_path()); + Self::new(drive, title, ReadMode::Sequential { capacity }, max_batch) + } + + /// Resume a full disc read from a given LBA (for ISO resume). + /// Use after checking an existing partial file: + /// start_lba = (file_size / 2048) - safety_margin + pub fn full_disc_resume(drive: Drive, title: DiscTitle, capacity: u32, start_lba: u32) -> Self { + let max_batch = detect_max_batch_sectors(drive.device_path()); + let mut stream = Self::new(drive, title, ReadMode::Sequential { capacity }, max_batch); + stream.current_lba = start_lba; + stream + } + + /// Set SCSI read timeout (default 30s). + + fn new(drive: Drive, title: DiscTitle, mode: ReadMode, max_batch: u16) -> Self { + Self { + drive, + title, + mode, + current_lba: 0, current_extent: 0, current_offset: 0, - content_format, - decrypt_keys, - unit_key_idx: 0, read_buf: Vec::with_capacity(max_batch as usize * 2048), + buf_valid: 0, + buf_cursor: 0, batch_sectors: max_batch, - max_batch_sectors: max_batch, - ok_streak: 0, - error_streak: 0, errors: 0, - }) - } - - /// Get the full Disc (for listing all titles, etc.) - pub fn disc(&self) -> &Disc { - &self.disc - } - - /// Read sectors from the drive into `self.read_buf`. - fn read_sectors(&mut self, lba: u32, count: u16) -> Result<(), Error> { - self.session.read(lba, count, &mut self.read_buf)?; - Ok(()) - } - - /// Fill the internal read buffer with the next batch of sectors, - /// handling error recovery (halve batch, slow drive, retry, skip). - /// - /// Returns `true` if data was read, `false` at end-of-title. - fn fill_buffer(&mut self) -> Result { - loop { - if self.current_extent >= self.extents.len() { - return Ok(false); - } - - let ext_start = self.extents[self.current_extent].start_lba; - let ext_sectors = self.extents[self.current_extent].sector_count; - let remaining = ext_sectors.saturating_sub(self.current_offset); - - // Align to 3 sectors (one aligned unit) - let sectors_to_read = remaining.min(self.batch_sectors as u32) as u16; - let sectors_to_read = sectors_to_read - (sectors_to_read % 3); - if sectors_to_read == 0 { - self.current_extent += 1; - self.current_offset = 0; - continue; - } - - let lba = ext_start + self.current_offset; - let byte_count = sectors_to_read as usize * 2048; - self.read_buf.resize(byte_count, 0); - - match self.read_sectors(lba, sectors_to_read) { - Ok(_) => { - self.current_offset += sectors_to_read as u32; - self.error_streak = 0; - - if self.current_offset >= ext_sectors { - self.current_extent += 1; - self.current_offset = 0; - } - - // Ramp up batch size after consecutive successes - self.ok_streak += 1; - if self.batch_sectors < self.max_batch_sectors - && self.ok_streak >= RAMP_BATCH_AFTER - { - self.batch_sectors = (self.batch_sectors * 2).min(self.max_batch_sectors); - self.ok_streak = 0; - } - - // Restore max speed after sustained success at full batch - if self.batch_sectors == self.max_batch_sectors - && self.ok_streak >= RAMP_SPEED_AFTER - { - self.session.set_speed(0xFFFF); - self.ok_streak = 0; - } - - return Ok(true); - } - Err(_) => { - self.errors += 1; - self.error_streak += 1; - self.ok_streak = 0; - - // First error: re-init (drive may have re-locked) - if self.error_streak == 1 { - let _ = self.session.init(); - let _ = self.session.probe_disc(); - } - - // Repeated errors: slow down - if self.error_streak >= SLOW_SPEED_AFTER { - self.session.set_speed(DriveSpeed::BD2x.to_kbps()); - self.error_streak = 0; - } - - if self.batch_sectors > MIN_BATCH_SECTORS { - self.batch_sectors = (self.batch_sectors / 2).max(MIN_BATCH_SECTORS); - std::thread::sleep(std::time::Duration::from_millis(100)); - } else { - // At minimum batch -- retry once with longer pause - std::thread::sleep(std::time::Duration::from_millis(500)); - self.read_buf.resize(MIN_BATCH_SECTORS as usize * 2048, 0); - if self.read_sectors(lba, MIN_BATCH_SECTORS).is_ok() { - self.error_streak = 0; - self.current_offset += MIN_BATCH_SECTORS as u32; - if self.current_offset >= ext_sectors { - self.current_extent += 1; - self.current_offset = 0; - } - return Ok(true); - } - // Still failing -- skip this unit (zero-fill) - self.current_offset += 3; - if self.current_offset >= ext_sectors { - self.current_extent += 1; - self.current_offset = 0; - } - self.read_buf.resize(crate::aacs::ALIGNED_UNIT_LEN, 0); - self.read_buf.fill(0); - return Ok(true); - } - } - } + eof: false, } } - /// Decrypt the contents of `self.read_buf` in-place and copy the - /// decrypted data into `self.batch_buf`. - fn decrypt_and_buffer(&mut self) { - let total_bytes = self.read_buf.len(); - crate::decrypt::decrypt_sectors( - &mut self.read_buf[..total_bytes], - &self.decrypt_keys, - self.unit_key_idx, - ); - - // Swap buffers instead of copying — the old batch_buf becomes - // read_buf and will be overwritten on the next read. - std::mem::swap(&mut self.batch_buf, &mut self.read_buf); - self.batch_pos = 0; + /// Lock the tray. + pub fn lock_tray(&mut self) { + self.drive.lock_tray(); } + + /// Unlock the tray. + pub fn unlock_tray(&mut self) { + self.drive.unlock_tray(); + } + + /// Recover the drive (for batch: switch to another title). + pub fn into_drive(self) -> Drive { + self.drive + } + + // ── Fill ───────────────────────────────────────────────────────────── + + fn fill(&mut self) -> bool { + match &self.mode { + ReadMode::Extents(_) => self.fill_extents(), + ReadMode::Sequential { .. } => self.fill_sequential(), + } + } + + fn fill_extents(&mut self) -> bool { + let (ext_start, ext_sectors) = match &self.mode { + ReadMode::Extents(exts) => { + if self.current_extent >= exts.len() { + return false; + } + ( + exts[self.current_extent].start_lba, + exts[self.current_extent].sector_count, + ) + } + _ => unreachable!(), + }; + + let remaining = ext_sectors.saturating_sub(self.current_offset); + let sectors = remaining.min(self.batch_sectors as u32) as u16; + let sectors = sectors - (sectors % 3); + if sectors == 0 { + self.current_extent += 1; + self.current_offset = 0; + return self.fill_extents(); // next extent + } + + let lba = ext_start + self.current_offset; + let bytes = sectors as usize * 2048; + self.read_buf.resize(bytes, 0); + + // Drive handles all error recovery internally. + match self.drive.read( + lba, + sectors, + &mut self.read_buf[..bytes], + ) { + Ok(_) => { + self.buf_valid = bytes; + self.buf_cursor = 0; + self.current_offset += sectors as u32; + if self.current_offset >= ext_sectors { + self.current_extent += 1; + self.current_offset = 0; + } + true + } + Err(_) => false, // drive gone — EOF + } + } + + fn fill_sequential(&mut self) -> bool { + let capacity = match &self.mode { + ReadMode::Sequential { capacity } => *capacity, + _ => unreachable!(), + }; + + if self.current_lba >= capacity { + return false; + } + + let remaining = capacity - self.current_lba; + let count = remaining.min(self.batch_sectors as u32) as u16; + let bytes = count as usize * 2048; + self.read_buf.resize(bytes, 0); + + // Drive handles all error recovery internally — + // retries, speed changes, zero-fill on unreadable sectors. + match self.drive.read( + self.current_lba, + count, + &mut self.read_buf[..bytes], + ) { + Ok(_) => { + self.buf_valid = bytes; + self.buf_cursor = 0; + self.current_lba += count as u32; + true + } + Err(_) => false, // drive gone — EOF + } + } + } +// ── IOStream ───────────────────────────────────────────────────────────────── + impl IOStream for DiscStream { fn info(&self) -> &DiscTitle { - &self.disc_title + &self.title } + fn finish(&mut self) -> io::Result<()> { + self.drive.unlock_tray(); Ok(()) } + fn total_bytes(&self) -> Option { - Some(self.disc_title.size_bytes) + match &self.mode { + ReadMode::Extents(extents) => { + Some(extents.iter().map(|e| e.sector_count as u64 * 2048).sum()) + } + ReadMode::Sequential { capacity } => Some(*capacity as u64 * 2048), + } } } impl Read for DiscStream { fn read(&mut self, buf: &mut [u8]) -> io::Result { - // Drain buffer first - if self.batch_pos < self.batch_buf.len() { - let n = (self.batch_buf.len() - self.batch_pos).min(buf.len()); - buf[..n].copy_from_slice(&self.batch_buf[self.batch_pos..self.batch_pos + n]); - self.batch_pos += n; + // Drain current buffer + if self.buf_cursor < self.buf_valid { + let n = (self.buf_valid - self.buf_cursor).min(buf.len()); + buf[..n].copy_from_slice(&self.read_buf[self.buf_cursor..self.buf_cursor + n]); + self.buf_cursor += n; return Ok(n); } @@ -275,24 +309,16 @@ impl Read for DiscStream { return Ok(0); } - // Fill the read buffer with the next batch of sectors - let has_data = self - .fill_buffer() - .map_err(|e| io::Error::other(e.to_string()))?; - - if !has_data { + // Fill next batch + if self.fill() { + let n = self.buf_valid.min(buf.len()); + buf[..n].copy_from_slice(&self.read_buf[..n]); + self.buf_cursor = n; + Ok(n) + } else { self.eof = true; - return Ok(0); + Ok(0) } - - // Decrypt in-place and move to batch_buf - self.decrypt_and_buffer(); - - // Now drain into the caller's buffer - let n = self.batch_buf.len().min(buf.len()); - buf[..n].copy_from_slice(&self.batch_buf[..n]); - self.batch_pos = n; - Ok(n) } } @@ -307,145 +333,3 @@ impl Write for DiscStream { Ok(()) } } - -#[cfg(test)] -mod tests { - use crate::disc::Extent; - - /// Build a minimal DiscStream with fake extents for testing state advancement. - /// We cannot call `DiscStream::open()` without a real drive, so we construct - /// one manually and then call the internal `fill_buffer` / `read` path - /// through a helper that simulates the session reads. - /// - /// Instead we test the state-machine logic directly: given a set of extents - /// and a current_extent/current_offset, verify that repeated reads advance - /// through the extents correctly. - #[test] - fn state_advances_across_extents() { - // Simulate two extents of 6 sectors each (2 aligned units each). - let extents = [ - Extent { - start_lba: 100, - sector_count: 6, - }, - Extent { - start_lba: 200, - sector_count: 6, - }, - ]; - - // Walk through the extents manually using the same arithmetic - // that fill_buffer uses, and verify we visit every sector. - let batch_sectors: u16 = 6; - let mut current_extent: usize = 0; - let mut current_offset: u32 = 0; - let mut lbas_read = Vec::new(); - - while current_extent < extents.len() { - let ext_start = extents[current_extent].start_lba; - let ext_sectors = extents[current_extent].sector_count; - let remaining = ext_sectors.saturating_sub(current_offset); - let sectors_to_read = remaining.min(batch_sectors as u32) as u16; - let sectors_to_read = sectors_to_read - (sectors_to_read % 3); - if sectors_to_read == 0 { - current_extent += 1; - current_offset = 0; - continue; - } - let lba = ext_start + current_offset; - lbas_read.push((lba, sectors_to_read)); - current_offset += sectors_to_read as u32; - if current_offset >= ext_sectors { - current_extent += 1; - current_offset = 0; - } - } - - assert_eq!(lbas_read.len(), 2, "should read two batches"); - assert_eq!(lbas_read[0], (100, 6), "first batch starts at LBA 100"); - assert_eq!(lbas_read[1], (200, 6), "second batch starts at LBA 200"); - } - - /// Verify that small extents that are not aligned to 3 sectors are skipped - /// (moved past) rather than causing an infinite loop. - #[test] - fn unaligned_extent_is_skipped() { - let extents = [ - Extent { - start_lba: 50, - sector_count: 2, // < 3, cannot form an aligned unit - }, - Extent { - start_lba: 300, - sector_count: 9, - }, - ]; - - let batch_sectors: u16 = 9; - let mut current_extent: usize = 0; - let mut current_offset: u32 = 0; - let mut lbas_read = Vec::new(); - - while current_extent < extents.len() { - let ext_start = extents[current_extent].start_lba; - let ext_sectors = extents[current_extent].sector_count; - let remaining = ext_sectors.saturating_sub(current_offset); - let sectors_to_read = remaining.min(batch_sectors as u32) as u16; - let sectors_to_read = sectors_to_read - (sectors_to_read % 3); - if sectors_to_read == 0 { - current_extent += 1; - current_offset = 0; - continue; - } - let lba = ext_start + current_offset; - lbas_read.push((lba, sectors_to_read)); - current_offset += sectors_to_read as u32; - if current_offset >= ext_sectors { - current_extent += 1; - current_offset = 0; - } - } - - assert_eq!(lbas_read.len(), 1, "only second extent is readable"); - assert_eq!(lbas_read[0], (300, 9)); - } - - /// Verify that multiple reads from the same extent produce advancing offsets. - #[test] - fn multiple_batches_within_one_extent() { - let extents = [Extent { - start_lba: 1000, - sector_count: 18, // 6 aligned units = 3 batches of 6 sectors - }]; - - let batch_sectors: u16 = 6; - let mut current_extent: usize = 0; - let mut current_offset: u32 = 0; - let mut lbas_read = Vec::new(); - - while current_extent < extents.len() { - let ext_start = extents[current_extent].start_lba; - let ext_sectors = extents[current_extent].sector_count; - let remaining = ext_sectors.saturating_sub(current_offset); - let sectors_to_read = remaining.min(batch_sectors as u32) as u16; - let sectors_to_read = sectors_to_read - (sectors_to_read % 3); - if sectors_to_read == 0 { - current_extent += 1; - current_offset = 0; - continue; - } - let lba = ext_start + current_offset; - lbas_read.push((lba, sectors_to_read)); - current_offset += sectors_to_read as u32; - if current_offset >= ext_sectors { - current_extent += 1; - current_offset = 0; - } - } - - assert_eq!(lbas_read.len(), 3, "three batches from one extent"); - assert_eq!(lbas_read[0], (1000, 6)); - assert_eq!(lbas_read[1], (1006, 6)); - assert_eq!(lbas_read[2], (1012, 6)); - } -} diff --git a/src/mux/iso.rs b/src/mux/iso.rs index d49591e..e68abe7 100644 --- a/src/mux/iso.rs +++ b/src/mux/iso.rs @@ -9,6 +9,7 @@ use super::isowriter::IsoWriter; use super::IOStream; +use crate::decrypt::DecryptKeys; use crate::disc::{Disc, DiscTitle, ScanOptions}; use crate::error::{Error, Result}; use crate::sector::SectorReader; @@ -71,6 +72,8 @@ pub struct IsoStream { buf_pos: usize, buf_len: usize, eof: bool, + /// Decrypt on read — auto-detected from disc scan. + decrypt_keys: DecryptKeys, // Write side iso_writer: Option>>, write_started: bool, @@ -85,17 +88,26 @@ impl IsoStream { let disc = Disc::scan_image(&mut reader, capacity, opts) .map_err(|e| io::Error::other(e.to_string()))?; - let idx = title_index - .unwrap_or(0) - .min(disc.titles.len().saturating_sub(1)); - let disc_title = if disc.titles.is_empty() { + if disc.titles.is_empty() { return Err(io::Error::new( io::ErrorKind::NotFound, "no titles found in ISO image", )); - } else { - disc.titles[idx].clone() - }; + } + let idx = title_index.unwrap_or(0); + if idx >= disc.titles.len() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "title {} out of range (disc has {})", + idx + 1, + disc.titles.len() + ), + )); + } + let disc_title = disc.titles[idx].clone(); + + let decrypt_keys = disc.decrypt_keys(); let extents: Vec<(u32, u32)> = disc_title .extents @@ -115,6 +127,7 @@ impl IsoStream { buf_pos: 0, buf_len: 0, eof: false, + decrypt_keys, iso_writer: None, write_started: false, }) @@ -130,6 +143,7 @@ impl IsoStream { Ok(IsoStream { disc_title: DiscTitle::empty(), disc: None, + decrypt_keys: DecryptKeys::None, reader: None, extents: Vec::new(), extent_idx: 0, @@ -188,8 +202,11 @@ impl IsoStream { reader .read_sectors(lba, count, &mut self.batch_buf) .map_err(|e| io::Error::other(e.to_string()))?; + + let bytes = count as usize * SECTOR_SIZE as usize; + self.buf_pos = 0; - self.buf_len = count as usize * SECTOR_SIZE as usize; + self.buf_len = bytes; self.sectors_remaining -= count as u32; if self.sectors_remaining == 0 { diff --git a/src/scsi/linux.rs b/src/scsi/linux.rs index 410f681..c2fd29c 100644 --- a/src/scsi/linux.rs +++ b/src/scsi/linux.rs @@ -5,9 +5,13 @@ use crate::error::{Error, Result}; use std::path::Path; const SG_IO: u32 = 0x2285; +const SG_SCSI_RESET: u32 = 0x2284; +const SG_SCSI_RESET_DEVICE: i32 = 1; const SG_DXFER_NONE: i32 = -1; const SG_DXFER_TO_DEV: i32 = -2; const SG_DXFER_FROM_DEV: i32 = -3; +const SG_FLAG_DIRECT_IO: u32 = 1; +const SG_FLAG_Q_AT_HEAD: u32 = 0x10; #[repr(C)] #[allow(non_camel_case_types)] @@ -41,13 +45,12 @@ pub struct SgIoTransport { } impl SgIoTransport { + /// Open a SCSI device for use. Resets the drive first to ensure + /// a known good state, then opens a fresh fd for commands. pub fn open(device: &Path) -> Result { - use std::os::unix::ffi::OsStrExt; - let path_bytes = device.as_os_str().as_bytes(); - let mut c_path = Vec::with_capacity(path_bytes.len() + 1); - c_path.extend_from_slice(path_bytes); - c_path.push(0); - + let device = Self::resolve_to_sg(device); + Self::reset(&device)?; + let c_path = Self::to_c_path(&device); let fd = unsafe { libc::open( c_path.as_ptr() as *const libc::c_char, @@ -55,29 +58,183 @@ impl SgIoTransport { ) }; if fd < 0 { - let err = std::io::Error::last_os_error(); - return Err(if err.kind() == std::io::ErrorKind::PermissionDenied { - Error::DevicePermission { - path: format!( - "{}: permission denied (try running as root)", - device.display() - ), - } - } else { - Error::DeviceNotFound { - path: device.display().to_string(), - } - }); + return Self::open_error(&device); } Ok(SgIoTransport { fd }) } + + /// Reset the drive to a known good state — equivalent to unplug/replug. + /// After reset, the drive is clean and no fd is held open. + /// + /// ## Why each step exists + /// + /// When a process is killed (SIGKILL/kill -9) mid-SG_IO ioctl, two things + /// go wrong: (1) the kernel's SG driver may have stale pending commands + /// queued for the dead process's fd, and (2) the drive firmware may still + /// be mid-operation (seeking, reading, processing a vendor command). + /// + /// A new process opening the same /dev/sg* device gets a fresh fd, but the + /// kernel doesn't automatically abort the dead process's commands — the + /// drive can appear hung on the first SCSI command. + /// + /// Additionally, killed processes skip Drop, so the tray may be locked + /// via PREVENT MEDIUM REMOVAL with no process alive to unlock it. + /// + /// ## Sequence + /// + /// 1. **open** — allocates kernel SG state for this fd + /// 2. **close** — triggers kernel cleanup: aborts any pending SG_IO + /// commands associated with this fd. The key operation — + /// the kernel's sg_release() cancels queued commands. + /// 3. **sleep 2s** — the drive firmware needs time to finish/abort whatever + /// it was doing when the previous process died. Without + /// this, the next command may block on drive-internal state. + /// 4. **open** — fresh fd with no stale commands in the kernel queue + /// 5. **unlock** — ALLOW MEDIUM REMOVAL (CDB 0x1E, prevent=0). Clears + /// any tray lock left by a killed process that never + /// ran its Drop/cleanup. + /// 6. **TUR** — TEST UNIT READY (CDB 0x00) with 3s timeout. If the + /// drive responds, it's in a good state. + /// 7. **escalate** — if TUR fails: + /// a. SG_SCSI_RESET (device level) — kernel sends a SCSI + /// bus reset to the device, clearing all firmware state. + /// b. STOP + START UNIT (CDB 0x1B) — power-cycles the + /// drive's logical unit, like pressing the eject button + /// and reinserting. + /// 8. **close** — release the fd. Drive is clean, nobody holds it. + pub fn reset(device: &Path) -> Result<()> { + let c_path = Self::to_c_path(device); + + // Step 1-2: open + close — flush stale kernel SG_IO state + let probe_fd = unsafe { + libc::open( + c_path.as_ptr() as *const libc::c_char, + libc::O_RDWR | libc::O_NONBLOCK, + ) + }; + if probe_fd >= 0 { + unsafe { libc::close(probe_fd) }; + } + + // Step 3: let drive settle + std::thread::sleep(std::time::Duration::from_secs(2)); + + // Step 4: open clean fd + let fd = unsafe { + libc::open( + c_path.as_ptr() as *const libc::c_char, + libc::O_RDWR | libc::O_NONBLOCK, + ) + }; + if fd < 0 { + return Self::open_error(device); + } + + // Step 5: unlock tray + let _ = Self::raw_command(fd, &[0x1E, 0, 0, 0, 0, 0], 3_000); + + // Step 6: TUR — if drive responds, we're done + if Self::raw_command(fd, &[0, 0, 0, 0, 0, 0], 3_000).is_err() { + // Step 7: escalate — SG_SCSI_RESET + let mut reset_type: i32 = SG_SCSI_RESET_DEVICE; + unsafe { libc::ioctl(fd, SG_SCSI_RESET as _, &mut reset_type) }; + std::thread::sleep(std::time::Duration::from_secs(3)); + + if Self::raw_command(fd, &[0, 0, 0, 0, 0, 0], 3_000).is_err() { + // STOP + START + let _ = Self::raw_command(fd, &[0x1B, 0, 0, 0, 0x00, 0], 3_000); + std::thread::sleep(std::time::Duration::from_secs(1)); + let _ = Self::raw_command(fd, &[0x1B, 0, 0, 0, 0x01, 0], 3_000); + std::thread::sleep(std::time::Duration::from_secs(3)); + let _ = Self::raw_command(fd, &[0, 0, 0, 0, 0, 0], 3_000); + } + } + + // Step 8: close — drive is clean + unsafe { libc::close(fd) }; + Ok(()) + } + + fn open_error(device: &Path) -> Result { + let err = std::io::Error::last_os_error(); + Err(if err.kind() == std::io::ErrorKind::PermissionDenied { + Error::DevicePermission { + path: format!("{}: permission denied (try running as root)", device.display()), + } + } else { + Error::DeviceNotFound { + path: device.display().to_string(), + } + }) + } + + /// Send a raw SCSI command on an fd. Used by reset() before the + /// transport is constructed. + fn raw_command(fd: i32, cdb: &[u8], timeout_ms: u32) -> std::result::Result<(), ()> { + 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().min(16) 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 = 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) }; + if ret < 0 || hdr.status != 0 { + Err(()) + } else { + Ok(()) + } + } + + fn to_c_path(device: &Path) -> Vec { + use std::os::unix::ffi::OsStrExt; + let path_bytes = device.as_os_str().as_bytes(); + let mut c_path = Vec::with_capacity(path_bytes.len() + 1); + c_path.extend_from_slice(path_bytes); + c_path.push(0); + c_path + } + + /// Resolve /dev/sr* -> /dev/sg* via sysfs. If already sg, returns as-is. + /// Falls back to the original path if resolution fails. + fn resolve_to_sg(device: &Path) -> std::path::PathBuf { + let dev_name = match device.file_name().and_then(|n| n.to_str()) { + Some(n) => n, + None => return device.to_path_buf(), + }; + + if dev_name.starts_with("sg") { + return device.to_path_buf(); + } + + if dev_name.starts_with("sr") { + let sg_dir = format!("/sys/class/block/{}/device/scsi_generic", dev_name); + if let Ok(mut entries) = std::fs::read_dir(&sg_dir) { + if let Some(Ok(entry)) = entries.next() { + let sg_name = entry.file_name(); + return std::path::PathBuf::from(format!( + "/dev/{}", + sg_name.to_string_lossy() + )); + } + } + } + + device.to_path_buf() + } } impl Drop for SgIoTransport { fn drop(&mut self) { - unsafe { - libc::close(self.fd); - } + // Unlock tray before closing — don't leave it locked + let _ = Self::raw_command(self.fd, &[0x1E, 0, 0, 0, 0, 0], 3_000); + unsafe { libc::close(self.fd) }; } } @@ -117,6 +274,14 @@ impl ScsiTransport for SgIoTransport { hdr.cmdp = cdb.as_ptr(); hdr.sbp = sense.as_mut_ptr(); hdr.timeout = timeout_ms; + if dxfer_direction == SG_DXFER_FROM_DEV + && data.len() >= 4096 + && (data.as_ptr() as usize) % 4096 == 0 + { + hdr.flags = SG_FLAG_DIRECT_IO | SG_FLAG_Q_AT_HEAD; + } else { + hdr.flags = SG_FLAG_Q_AT_HEAD; + } let ret = unsafe { libc::ioctl(self.fd, SG_IO as _, &mut hdr as *mut sg_io_hdr) }; diff --git a/src/scsi/macos.rs b/src/scsi/macos.rs index 5c38e5e..d34acb8 100644 --- a/src/scsi/macos.rs +++ b/src/scsi/macos.rs @@ -248,6 +248,24 @@ impl MacScsiTransport { exclusive: true, }) } + + /// Reset the drive to a known good state. + /// On macOS, we open the device, release exclusive access, wait for + /// the system to reclaim it, then the next open() re-acquires. + /// IOKit's USB layer handles device-level resets internally when the + /// exclusive access is released and re-acquired. + /// + /// NOTE: untested — macOS reset may need IOUSBDeviceInterface::ResetDevice() + /// for USB drives. This is a best-effort implementation. + pub fn reset(device: &Path) -> Result<()> { + // Opening and immediately dropping triggers release of exclusive access + // which forces IOKit to reset the device state. + if let Ok(transport) = Self::open(device) { + drop(transport); // Drop releases exclusive access + closes plugin + } + std::thread::sleep(std::time::Duration::from_secs(2)); + Ok(()) + } } impl Drop for MacScsiTransport { diff --git a/src/scsi/mod.rs b/src/scsi/mod.rs index b3580d4..e574dc5 100644 --- a/src/scsi/mod.rs +++ b/src/scsi/mod.rs @@ -60,7 +60,7 @@ pub trait ScsiTransport: Send { ) -> Result; } -// ── Platform-agnostic open ────────────────────────────────────────────────── +// ── Platform-agnostic open / reset ────────────────────────────────────────── /// Open a SCSI transport for the given device path. /// Selects the right backend for the current platform. @@ -88,6 +88,31 @@ pub fn open(device: &Path) -> Result> { } } +/// Reset a SCSI device to a known good state. Platform-specific. +/// On Linux: open/close fd cycle + TUR + SG_SCSI_RESET escalation. +pub fn reset(device: &Path) -> Result<()> { + #[cfg(target_os = "linux")] + { + linux::SgIoTransport::reset(device) + } + + #[cfg(target_os = "macos")] + { + macos::MacScsiTransport::reset(device) + } + + #[cfg(target_os = "windows")] + { + windows::SptiTransport::reset(device) + } + + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { + let _ = device; + Ok(()) + } +} + // ── CDB builders (platform-agnostic) ──────────────────────────────────────── /// SCSI INQUIRY response. diff --git a/src/scsi/windows.rs b/src/scsi/windows.rs index a29e980..92767eb 100644 --- a/src/scsi/windows.rs +++ b/src/scsi/windows.rs @@ -136,6 +136,55 @@ impl SptiTransport { Ok(SptiTransport { handle }) } + + /// Reset the drive to a known good state. + /// Opens the device, sends IOCTL_STORAGE_RESET_DEVICE to reset + /// the USB/SCSI bus, then closes. Same concept as SG_SCSI_RESET on Linux. + pub fn reset(device: &Path) -> Result<()> { + const IOCTL_STORAGE_RESET_DEVICE: u32 = 0x002D1004; + + let dev_str = device.to_str().ok_or_else(|| Error::DeviceNotFound { + path: device.display().to_string(), + })?; + let win_path = normalize_device_path(dev_str); + let wide: Vec = win_path.encode_utf16().chain(std::iter::once(0)).collect(); + + // Open + let handle = unsafe { + CreateFileW( + wide.as_ptr(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + std::ptr::null(), + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + std::ptr::null(), + ) + }; + if handle == INVALID_HANDLE_VALUE { + return Ok(()); // can't open — skip reset, not fatal + } + + // Send device reset + let mut returned: u32 = 0; + unsafe { + DeviceIoControl( + handle, + IOCTL_STORAGE_RESET_DEVICE, + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + 0, + &mut returned, + std::ptr::null_mut(), + ); + } + + // Close and wait for drive to settle + unsafe { CloseHandle(handle) }; + std::thread::sleep(std::time::Duration::from_secs(2)); + Ok(()) + } } impl Drop for SptiTransport {