From 08be717dc582fe91f89e20efdf8c9747e6763cb4 Mon Sep 17 00:00:00 2001 From: Matt Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:58:46 +0000 Subject: [PATCH] Drive halt flag, sector events, binary search light recovery (3x5s) --- src/drive/mod.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++--- src/error.rs | 5 ++++ src/event.rs | 16 +++++++++++ src/mux/disc.rs | 62 ++++++++++++++++++++++++++++++------------ 4 files changed, 133 insertions(+), 21 deletions(-) diff --git a/src/drive/mod.rs b/src/drive/mod.rs index face1be..a13b226 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -16,6 +16,7 @@ mod macos; mod windows; use crate::error::{Error, Result}; +use crate::event::{Event, EventKind}; use crate::identity::DriveId; use crate::platform::mt1959::Mt1959; use crate::platform::PlatformDriver; @@ -23,6 +24,8 @@ use crate::profile::{self, DriveProfile}; use crate::scsi::ScsiTransport; use crate::sector::SectorReader; use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; /// Physical state of the drive tray and disc. #[derive(Debug, Clone, Copy, PartialEq)] @@ -59,8 +62,11 @@ pub struct Drive { 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, + /// Halt flag — when set, Drive::read() bails at the next check point. + halt: Arc, + /// Event handler — fires during read recovery. + event_fn: Option>, } impl Drive { @@ -87,9 +93,41 @@ impl Drive { drive_id, device_path: device.to_string_lossy().to_string(), recovery_bytes_remaining: 0, + halt: Arc::new(AtomicBool::new(false)), + event_fn: None, }) } + /// Get a clone of the halt flag. Set to true to interrupt Drive::read(). + pub fn halt_flag(&self) -> Arc { + self.halt.clone() + } + + /// Halt the drive — Drive::read() will bail at the next check point. + pub fn halt(&self) { + self.halt.store(true, Ordering::Relaxed); + } + + /// Clear the halt flag for the next operation. + pub fn clear_halt(&self) { + self.halt.store(false, Ordering::Relaxed); + } + + /// Set an event handler for read recovery events. + pub fn on_event(&mut self, f: impl Fn(Event) + Send + 'static) { + self.event_fn = Some(Box::new(f)); + } + + fn emit(&self, kind: EventKind) { + if let Some(ref f) = self.event_fn { + f(Event { kind }); + } + } + + fn is_halted(&self) -> bool { + self.halt.load(Ordering::Relaxed) + } + /// Close the drive cleanly. Unlocks tray, flushes SCSI state, closes fd. /// Also runs automatically on Drop as a safety net. pub fn close(self) { @@ -489,16 +527,27 @@ impl Drive { self.recovery_bytes_remaining = self.recovery_bytes_remaining.saturating_sub(bytes_read); if self.recovery_bytes_remaining == 0 { + self.emit(EventKind::SpeedChange { speed_kbs: 0xFFFF }); self.set_speed(0xFFFF); } } return Ok(result.bytes_transferred); } - // Phase 1: gentle — sleep 30s, retry. 5 times. + // Read failed — enter recovery + self.emit(EventKind::ReadError { + sector: lba as u64, + error: Error::DiscRead { sector: lba as u64 }, + }); + self.emit(EventKind::SpeedChange { speed_kbs: 0 }); self.set_speed(0); - for _ in 0..5 { + // Phase 1: gentle — sleep 30s, retry. 5 times. + for attempt in 1..=5u32 { + if self.is_halted() { + return Err(Error::Halted); + } + self.emit(EventKind::Retry { attempt }); std::thread::sleep(std::time::Duration::from_secs(30)); if let Ok(result) = self.scsi.as_mut().execute( @@ -507,11 +556,16 @@ impl Drive { buf, 30_000, ) { + self.emit(EventKind::SectorRecovered { sector: lba as u64 }); self.recovery_bytes_remaining = RECOVERY_WINDOW; return Ok(result.bytes_transferred); } } + if self.is_halted() { + return Err(Error::Halted); + } + // Phase 2: fresh start — close, reset, open, init. let device = std::path::PathBuf::from(&self.device_path); std::thread::sleep(std::time::Duration::from_secs(5)); @@ -522,8 +576,16 @@ impl Drive { let _ = self.wait_ready(); self.set_speed(0); + if self.is_halted() { + return Err(Error::Halted); + } + // Phase 3: gentle again on fresh connection — sleep 30s, retry. 5 times. - for _ in 0..5 { + for attempt in 6..=10u32 { + if self.is_halted() { + return Err(Error::Halted); + } + self.emit(EventKind::Retry { attempt }); std::thread::sleep(std::time::Duration::from_secs(30)); if let Ok(result) = self.scsi.as_mut().execute( @@ -532,6 +594,7 @@ impl Drive { buf, 30_000, ) { + self.emit(EventKind::SectorRecovered { sector: lba as u64 }); self.recovery_bytes_remaining = RECOVERY_WINDOW; return Ok(result.bytes_transferred); } diff --git a/src/error.rs b/src/error.rs index f813bbf..449616f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -41,6 +41,7 @@ pub const E_IO_ERROR: u16 = 5000; // Disc format (6xxx) pub const E_DISC_READ: u16 = 6000; +pub const E_HALTED: u16 = 6010; pub const E_MPLS_PARSE: u16 = 6001; pub const E_CLPI_PARSE: u16 = 6002; pub const E_UDF_NOT_FOUND: u16 = 6003; @@ -134,6 +135,8 @@ pub enum Error { DiscRead { sector: u64, }, + /// Drive was halted by caller. + Halted, MplsParse, ClpiParse, UdfNotFound { @@ -215,6 +218,7 @@ impl Error { Error::ScsiError { .. } => E_SCSI_ERROR, Error::IoError { .. } => E_IO_ERROR, Error::DiscRead { .. } => E_DISC_READ, + Error::Halted => E_HALTED, Error::MplsParse => E_MPLS_PARSE, Error::ClpiParse => E_CLPI_PARSE, Error::UdfNotFound { .. } => E_UDF_NOT_FOUND, @@ -304,6 +308,7 @@ impl std::fmt::Display for Error { } Error::IoError { source } => write!(f, "E{}: {}", self.code(), source), Error::DiscRead { sector } => write!(f, "E{}: {}", self.code(), sector), + Error::Halted => write!(f, "E{}", self.code()), Error::UdfNotFound { path } => write!(f, "E{}: {}", self.code(), path), Error::DiscTitleRange { index, count } => { write!(f, "E{}: {}/{}", self.code(), index, count) diff --git a/src/event.rs b/src/event.rs index 822472e..6d652cb 100644 --- a/src/event.rs +++ b/src/event.rs @@ -80,6 +80,22 @@ pub enum EventKind { sector_count: u64, }, + /// Binary search isolated and recovered a marginal sector. + SectorRecovered { + sector: u64, + }, + + /// Sector unreadable, zero-filled (skip mode). + SectorSkipped { + sector: u64, + }, + + /// Binary search activated — batch failed, isolating bad sector. + BinarySearch { + sector: u64, + batch_size: u16, + }, + /// Operation complete. Complete { /// Total bytes written. diff --git a/src/mux/disc.rs b/src/mux/disc.rs index 6702628..ef6cb04 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -6,6 +6,7 @@ //! Read-only. For disc→ISO (raw sector copy), use `Disc::copy()`. use crate::disc::{detect_max_batch_sectors, Disc, DiscTitle, Extent, ScanOptions}; +use crate::event::{Event, EventKind}; use crate::sector::SectorReader; use std::io; @@ -34,6 +35,7 @@ pub struct DiscStream { batch_sectors: u16, pub errors: u64, pub skip_errors: bool, + event_fn: Option>, eof: bool, // PES output @@ -100,6 +102,7 @@ impl DiscStream { batch_sectors, errors: 0, skip_errors: false, + event_fn: None, eof: false, ts_demuxer, ps_demuxer, @@ -109,6 +112,17 @@ impl DiscStream { } } + /// Set event handler for sector-level events (binary search, skip, recover). + pub fn on_event(&mut self, f: impl Fn(Event) + Send + 'static) { + self.event_fn = Some(Box::new(f)); + } + + fn emit(&self, kind: EventKind) { + if let Some(ref f) = self.event_fn { + f(Event { kind }); + } + } + /// Skip decryption — return raw encrypted bytes. pub fn set_raw(&mut self) { self.decrypt_keys = crate::decrypt::DecryptKeys::None; @@ -120,29 +134,39 @@ impl DiscStream { } /// Binary search to isolate failing sectors within a batch. - /// Reads good regions in sub-batches, handles bad sectors individually. + /// Good sub-batches read fast. Bad sectors get 3 retries × 5s — max 15s per sector. + /// No full Drive::read() recovery — that only runs on the initial batch attempt. fn read_with_binary_search(&mut self, lba: u32, count: u16) -> io::Result<()> { if count <= 1 { - // Single sector — read with full recovery (Tier 2) + // Single sector — light recovery: 3 attempts, 5s sleep between let offset = self.buf_valid; - match self.reader.read_sectors(lba, 1, &mut self.read_buf[offset..offset + 2048]) { - Ok(_) => { - self.buf_valid += 2048; + for attempt in 0..3u32 { + if attempt > 0 { + std::thread::sleep(std::time::Duration::from_secs(5)); } - Err(e) => { - if self.skip_errors { - self.read_buf[offset..offset + 2048].fill(0); - self.buf_valid += 2048; - self.errors += 1; - } else { - return Err(e.into()); - } + if self + .reader + .read_sectors_recover(lba, 1, &mut self.read_buf[offset..offset + 2048], false) + .is_ok() + { + self.emit(EventKind::SectorRecovered { sector: lba as u64 }); + self.buf_valid += 2048; + return Ok(()); } } - return Ok(()); + // 3 attempts failed + if self.skip_errors { + self.emit(EventKind::SectorSkipped { sector: lba as u64 }); + self.read_buf[offset..offset + 2048].fill(0); + self.buf_valid += 2048; + self.errors += 1; + return Ok(()); + } else { + return Err(crate::error::Error::DiscRead { sector: lba as u64 }.into()); + } } - // Try this sub-batch as a whole first + // Try this sub-batch as a whole (fast read, no recovery) let bytes = count as usize * 2048; let offset = self.buf_valid; if self @@ -150,14 +174,18 @@ impl DiscStream { .read_sectors_recover(lba, count, &mut self.read_buf[offset..offset + bytes], false) .is_ok() { - // Sub-batch succeeded with fast read — all good self.buf_valid += bytes; return Ok(()); } // Sub-batch failed — split in half and recurse + self.emit(EventKind::BinarySearch { + sector: lba as u64, + batch_size: count, + }); + let half = count / 2; - let half = half - (half % 3).min(half); // align to 3-sector BD-TS boundary + let half = half - (half % 3).min(half); let half = half.max(1); let remainder = count - half;