Drive halt flag, sector events, binary search light recovery (3x5s)
This commit is contained in:
+67
-4
@@ -16,6 +16,7 @@ mod macos;
|
|||||||
mod windows;
|
mod windows;
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
use crate::event::{Event, EventKind};
|
||||||
use crate::identity::DriveId;
|
use crate::identity::DriveId;
|
||||||
use crate::platform::mt1959::Mt1959;
|
use crate::platform::mt1959::Mt1959;
|
||||||
use crate::platform::PlatformDriver;
|
use crate::platform::PlatformDriver;
|
||||||
@@ -23,6 +24,8 @@ use crate::profile::{self, DriveProfile};
|
|||||||
use crate::scsi::ScsiTransport;
|
use crate::scsi::ScsiTransport;
|
||||||
use crate::sector::SectorReader;
|
use crate::sector::SectorReader;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Physical state of the drive tray and disc.
|
/// Physical state of the drive tray and disc.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
@@ -59,8 +62,11 @@ pub struct Drive {
|
|||||||
pub drive_id: DriveId,
|
pub drive_id: DriveId,
|
||||||
device_path: String,
|
device_path: String,
|
||||||
/// Bytes remaining in the min-speed recovery window.
|
/// 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,
|
recovery_bytes_remaining: u64,
|
||||||
|
/// Halt flag — when set, Drive::read() bails at the next check point.
|
||||||
|
halt: Arc<AtomicBool>,
|
||||||
|
/// Event handler — fires during read recovery.
|
||||||
|
event_fn: Option<Box<dyn Fn(Event) + Send>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drive {
|
impl Drive {
|
||||||
@@ -87,9 +93,41 @@ impl Drive {
|
|||||||
drive_id,
|
drive_id,
|
||||||
device_path: device.to_string_lossy().to_string(),
|
device_path: device.to_string_lossy().to_string(),
|
||||||
recovery_bytes_remaining: 0,
|
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<AtomicBool> {
|
||||||
|
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.
|
/// Close the drive cleanly. Unlocks tray, flushes SCSI state, closes fd.
|
||||||
/// Also runs automatically on Drop as a safety net.
|
/// Also runs automatically on Drop as a safety net.
|
||||||
pub fn close(self) {
|
pub fn close(self) {
|
||||||
@@ -489,16 +527,27 @@ impl Drive {
|
|||||||
self.recovery_bytes_remaining =
|
self.recovery_bytes_remaining =
|
||||||
self.recovery_bytes_remaining.saturating_sub(bytes_read);
|
self.recovery_bytes_remaining.saturating_sub(bytes_read);
|
||||||
if self.recovery_bytes_remaining == 0 {
|
if self.recovery_bytes_remaining == 0 {
|
||||||
|
self.emit(EventKind::SpeedChange { speed_kbs: 0xFFFF });
|
||||||
self.set_speed(0xFFFF);
|
self.set_speed(0xFFFF);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Ok(result.bytes_transferred);
|
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);
|
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));
|
std::thread::sleep(std::time::Duration::from_secs(30));
|
||||||
|
|
||||||
if let Ok(result) = self.scsi.as_mut().execute(
|
if let Ok(result) = self.scsi.as_mut().execute(
|
||||||
@@ -507,11 +556,16 @@ impl Drive {
|
|||||||
buf,
|
buf,
|
||||||
30_000,
|
30_000,
|
||||||
) {
|
) {
|
||||||
|
self.emit(EventKind::SectorRecovered { sector: lba as u64 });
|
||||||
self.recovery_bytes_remaining = RECOVERY_WINDOW;
|
self.recovery_bytes_remaining = RECOVERY_WINDOW;
|
||||||
return Ok(result.bytes_transferred);
|
return Ok(result.bytes_transferred);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.is_halted() {
|
||||||
|
return Err(Error::Halted);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 2: fresh start — close, reset, open, init.
|
// Phase 2: fresh start — close, reset, open, init.
|
||||||
let device = std::path::PathBuf::from(&self.device_path);
|
let device = std::path::PathBuf::from(&self.device_path);
|
||||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
@@ -522,8 +576,16 @@ impl Drive {
|
|||||||
let _ = self.wait_ready();
|
let _ = self.wait_ready();
|
||||||
self.set_speed(0);
|
self.set_speed(0);
|
||||||
|
|
||||||
|
if self.is_halted() {
|
||||||
|
return Err(Error::Halted);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 3: gentle again on fresh connection — sleep 30s, retry. 5 times.
|
// 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));
|
std::thread::sleep(std::time::Duration::from_secs(30));
|
||||||
|
|
||||||
if let Ok(result) = self.scsi.as_mut().execute(
|
if let Ok(result) = self.scsi.as_mut().execute(
|
||||||
@@ -532,6 +594,7 @@ impl Drive {
|
|||||||
buf,
|
buf,
|
||||||
30_000,
|
30_000,
|
||||||
) {
|
) {
|
||||||
|
self.emit(EventKind::SectorRecovered { sector: lba as u64 });
|
||||||
self.recovery_bytes_remaining = RECOVERY_WINDOW;
|
self.recovery_bytes_remaining = RECOVERY_WINDOW;
|
||||||
return Ok(result.bytes_transferred);
|
return Ok(result.bytes_transferred);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ pub const E_IO_ERROR: u16 = 5000;
|
|||||||
|
|
||||||
// Disc format (6xxx)
|
// Disc format (6xxx)
|
||||||
pub const E_DISC_READ: u16 = 6000;
|
pub const E_DISC_READ: u16 = 6000;
|
||||||
|
pub const E_HALTED: u16 = 6010;
|
||||||
pub const E_MPLS_PARSE: u16 = 6001;
|
pub const E_MPLS_PARSE: u16 = 6001;
|
||||||
pub const E_CLPI_PARSE: u16 = 6002;
|
pub const E_CLPI_PARSE: u16 = 6002;
|
||||||
pub const E_UDF_NOT_FOUND: u16 = 6003;
|
pub const E_UDF_NOT_FOUND: u16 = 6003;
|
||||||
@@ -134,6 +135,8 @@ pub enum Error {
|
|||||||
DiscRead {
|
DiscRead {
|
||||||
sector: u64,
|
sector: u64,
|
||||||
},
|
},
|
||||||
|
/// Drive was halted by caller.
|
||||||
|
Halted,
|
||||||
MplsParse,
|
MplsParse,
|
||||||
ClpiParse,
|
ClpiParse,
|
||||||
UdfNotFound {
|
UdfNotFound {
|
||||||
@@ -215,6 +218,7 @@ impl Error {
|
|||||||
Error::ScsiError { .. } => E_SCSI_ERROR,
|
Error::ScsiError { .. } => E_SCSI_ERROR,
|
||||||
Error::IoError { .. } => E_IO_ERROR,
|
Error::IoError { .. } => E_IO_ERROR,
|
||||||
Error::DiscRead { .. } => E_DISC_READ,
|
Error::DiscRead { .. } => E_DISC_READ,
|
||||||
|
Error::Halted => E_HALTED,
|
||||||
Error::MplsParse => E_MPLS_PARSE,
|
Error::MplsParse => E_MPLS_PARSE,
|
||||||
Error::ClpiParse => E_CLPI_PARSE,
|
Error::ClpiParse => E_CLPI_PARSE,
|
||||||
Error::UdfNotFound { .. } => E_UDF_NOT_FOUND,
|
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::IoError { source } => write!(f, "E{}: {}", self.code(), source),
|
||||||
Error::DiscRead { sector } => write!(f, "E{}: {}", self.code(), sector),
|
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::UdfNotFound { path } => write!(f, "E{}: {}", self.code(), path),
|
||||||
Error::DiscTitleRange { index, count } => {
|
Error::DiscTitleRange { index, count } => {
|
||||||
write!(f, "E{}: {}/{}", self.code(), index, count)
|
write!(f, "E{}: {}/{}", self.code(), index, count)
|
||||||
|
|||||||
@@ -80,6 +80,22 @@ pub enum EventKind {
|
|||||||
sector_count: u64,
|
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.
|
/// Operation complete.
|
||||||
Complete {
|
Complete {
|
||||||
/// Total bytes written.
|
/// Total bytes written.
|
||||||
|
|||||||
+42
-14
@@ -6,6 +6,7 @@
|
|||||||
//! Read-only. For disc→ISO (raw sector copy), use `Disc::copy()`.
|
//! Read-only. For disc→ISO (raw sector copy), use `Disc::copy()`.
|
||||||
|
|
||||||
use crate::disc::{detect_max_batch_sectors, Disc, DiscTitle, Extent, ScanOptions};
|
use crate::disc::{detect_max_batch_sectors, Disc, DiscTitle, Extent, ScanOptions};
|
||||||
|
use crate::event::{Event, EventKind};
|
||||||
use crate::sector::SectorReader;
|
use crate::sector::SectorReader;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ pub struct DiscStream {
|
|||||||
batch_sectors: u16,
|
batch_sectors: u16,
|
||||||
pub errors: u64,
|
pub errors: u64,
|
||||||
pub skip_errors: bool,
|
pub skip_errors: bool,
|
||||||
|
event_fn: Option<Box<dyn Fn(Event) + Send>>,
|
||||||
eof: bool,
|
eof: bool,
|
||||||
|
|
||||||
// PES output
|
// PES output
|
||||||
@@ -100,6 +102,7 @@ impl DiscStream {
|
|||||||
batch_sectors,
|
batch_sectors,
|
||||||
errors: 0,
|
errors: 0,
|
||||||
skip_errors: false,
|
skip_errors: false,
|
||||||
|
event_fn: None,
|
||||||
eof: false,
|
eof: false,
|
||||||
ts_demuxer,
|
ts_demuxer,
|
||||||
ps_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.
|
/// Skip decryption — return raw encrypted bytes.
|
||||||
pub fn set_raw(&mut self) {
|
pub fn set_raw(&mut self) {
|
||||||
self.decrypt_keys = crate::decrypt::DecryptKeys::None;
|
self.decrypt_keys = crate::decrypt::DecryptKeys::None;
|
||||||
@@ -120,29 +134,39 @@ impl DiscStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Binary search to isolate failing sectors within a batch.
|
/// 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<()> {
|
fn read_with_binary_search(&mut self, lba: u32, count: u16) -> io::Result<()> {
|
||||||
if count <= 1 {
|
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;
|
let offset = self.buf_valid;
|
||||||
match self.reader.read_sectors(lba, 1, &mut self.read_buf[offset..offset + 2048]) {
|
for attempt in 0..3u32 {
|
||||||
Ok(_) => {
|
if attempt > 0 {
|
||||||
self.buf_valid += 2048;
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3 attempts failed
|
||||||
if self.skip_errors {
|
if self.skip_errors {
|
||||||
|
self.emit(EventKind::SectorSkipped { sector: lba as u64 });
|
||||||
self.read_buf[offset..offset + 2048].fill(0);
|
self.read_buf[offset..offset + 2048].fill(0);
|
||||||
self.buf_valid += 2048;
|
self.buf_valid += 2048;
|
||||||
self.errors += 1;
|
self.errors += 1;
|
||||||
} else {
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(());
|
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 bytes = count as usize * 2048;
|
||||||
let offset = self.buf_valid;
|
let offset = self.buf_valid;
|
||||||
if self
|
if self
|
||||||
@@ -150,14 +174,18 @@ impl DiscStream {
|
|||||||
.read_sectors_recover(lba, count, &mut self.read_buf[offset..offset + bytes], false)
|
.read_sectors_recover(lba, count, &mut self.read_buf[offset..offset + bytes], false)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
// Sub-batch succeeded with fast read — all good
|
|
||||||
self.buf_valid += bytes;
|
self.buf_valid += bytes;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub-batch failed — split in half and recurse
|
// 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 = 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 half = half.max(1);
|
||||||
let remainder = count - half;
|
let remainder = count - half;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user