Drive recovery, reset on open, simplified DiscStream

- 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
This commit is contained in:
MattJackson
2026-04-14 23:32:22 +00:00
parent 252e58cab0
commit 2f52188f77
7 changed files with 803 additions and 452 deletions
+241 -48
View File
@@ -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<dyn ScsiTransport>,
@@ -47,6 +50,9 @@ pub struct Drive {
pub platform: Option<profile::Platform>,
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()
@@ -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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<usize> {
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<u32> {
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<usize> {
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<String>)> {
#[cfg(target_os = "linux")]
{
+234 -350
View File
@@ -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<std::path::PathBuf>,
/// KEYDB.cfg path. None = search standard locations.
pub keydb_path: Option<std::path::PathBuf>,
/// Which title to read (0-based). None = longest title.
pub title_index: Option<usize>,
}
/// 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<u8>,
batch_pos: usize,
eof: bool,
drive: Drive,
title: DiscTitle,
// ── Reading state (replaces ContentReader) ──
extents: Vec<Extent>,
// 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<u8>,
/// 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<Extent>),
/// 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<Self, Error> {
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<DiscOpenResult> {
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,
})
eof: false,
}
}
/// Get the full Disc (for listing all titles, etc.)
pub fn disc(&self) -> &Disc {
&self.disc
/// Lock the tray.
pub fn lock_tray(&mut self) {
self.drive.lock_tray();
}
/// 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(())
/// Unlock the tray.
pub fn unlock_tray(&mut self) {
self.drive.unlock_tray();
}
/// 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<bool, Error> {
loop {
if self.current_extent >= self.extents.len() {
return Ok(false);
/// Recover the drive (for batch: switch to another title).
pub fn into_drive(self) -> Drive {
self.drive
}
let ext_start = self.extents[self.current_extent].start_lba;
let ext_sectors = self.extents[self.current_extent].sector_count;
// ── 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);
// 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 {
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;
continue;
return self.fill_extents(); // next extent
}
let lba = ext_start + self.current_offset;
let byte_count = sectors_to_read as usize * 2048;
self.read_buf.resize(byte_count, 0);
let bytes = sectors as usize * 2048;
self.read_buf.resize(bytes, 0);
match self.read_sectors(lba, sectors_to_read) {
// Drive handles all error recovery internally.
match self.drive.read(
lba,
sectors,
&mut self.read_buf[..bytes],
) {
Ok(_) => {
self.current_offset += sectors_to_read as u32;
self.error_streak = 0;
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;
}
// 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);
}
}
true
}
Err(_) => false, // drive gone — EOF
}
}
/// 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,
);
fn fill_sequential(&mut self) -> bool {
let capacity = match &self.mode {
ReadMode::Sequential { capacity } => *capacity,
_ => unreachable!(),
};
// 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;
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<u64> {
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<usize> {
// 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 {
self.eof = true;
return 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;
// 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;
Ok(0)
}
}
}
@@ -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));
}
}
+25 -8
View File
@@ -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<IsoWriter<io::BufWriter<File>>>,
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 {
+181 -16
View File
@@ -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<Self> {
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 {
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<T>(device: &Path) -> Result<T> {
let err = std::io::Error::last_os_error();
return Err(if err.kind() == std::io::ErrorKind::PermissionDenied {
Err(if err.kind() == std::io::ErrorKind::PermissionDenied {
Error::DevicePermission {
path: format!(
"{}: permission denied (try running as root)",
device.display()
),
path: format!("{}: permission denied (try running as root)", device.display()),
}
} else {
Error::DeviceNotFound {
path: device.display().to_string(),
}
});
})
}
Ok(SgIoTransport { fd })
/// 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<u8> {
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) };
+18
View File
@@ -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 {
+26 -1
View File
@@ -60,7 +60,7 @@ pub trait ScsiTransport: Send {
) -> Result<ScsiResult>;
}
// ── 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<Box<dyn ScsiTransport>> {
}
}
/// 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.
+49
View File
@@ -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<u16> = 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 {