Fix extract_scsi_context() and Error::scsi_sense() to handle Error::DiscRead in addition to Error::ScsiError, so is_marginal_read() works for DiscRead errors and disc::copy() can properly route MEDIUM ERROR as a marginal (bad sector) instead of bailing. Closes: (internal)#20260427
715 lines
22 KiB
Rust
715 lines
22 KiB
Rust
//! Drive session — open, identify, and read from optical drives.
|
||
//!
|
||
//! 4. `probe_disc()` — probe disc surface. Drive learns optimal speeds.
|
||
|
||
pub(crate) fn extract_scsi_context(e: &Error) -> (u8, Option<crate::scsi::ScsiSense>) {
|
||
match e {
|
||
Error::ScsiError { status, sense, .. } => (*status, *sense),
|
||
Error::DiscRead { status, sense, .. } => (status.unwrap_or(0), *sense),
|
||
_ => (0, None),
|
||
}
|
||
}
|
||
|
||
pub mod capture;
|
||
|
||
// Per-platform discovery helpers (the `pub(crate)` `find_drives` /
|
||
// equivalents). Crate-public so `scsi/{linux,macos,windows}.rs` can
|
||
// reuse the existing enumeration logic when shaping `DriveInfo`.
|
||
#[cfg(target_os = "linux")]
|
||
pub(crate) mod linux;
|
||
#[cfg(target_os = "macos")]
|
||
pub(crate) mod macos;
|
||
#[cfg(windows)]
|
||
pub(crate) mod windows;
|
||
|
||
use crate::error::{Error, Result};
|
||
use crate::event::{Event, EventKind};
|
||
use crate::identity::DriveId;
|
||
use crate::platform::PlatformDriver;
|
||
use crate::platform::mt1959::Mt1959;
|
||
use crate::profile::{self, DriveProfile};
|
||
use crate::scsi::ScsiTransport;
|
||
use crate::sector::SectorReader;
|
||
use std::path::Path;
|
||
use std::sync::Arc;
|
||
use std::sync::atomic::{AtomicBool, Ordering};
|
||
|
||
/// Physical state of the drive tray and disc.
|
||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||
pub enum DriveStatus {
|
||
/// Tray is open
|
||
TrayOpen,
|
||
/// Tray closed, no disc
|
||
NoDisc,
|
||
/// Tray closed, disc present and ready
|
||
DiscPresent,
|
||
/// Drive is loading or spinning up
|
||
NotReady,
|
||
/// Could not determine status
|
||
Unknown,
|
||
}
|
||
|
||
// SCSI opcodes used in drive control
|
||
const SCSI_TEST_UNIT_READY: u8 = 0x00;
|
||
const SCSI_START_STOP_UNIT: u8 = 0x1B;
|
||
const SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL: u8 = 0x1E;
|
||
const SCSI_GET_EVENT_STATUS: u8 = 0x4A;
|
||
const SCSI_MODE_SENSE: u8 = 0x5A;
|
||
const SCSI_REPORT_KEY: u8 = 0xA4;
|
||
|
||
/// Optical disc drive session -- open, identify, unlock, and read.
|
||
pub struct Drive {
|
||
scsi: Box<dyn ScsiTransport>,
|
||
driver: Option<Box<dyn PlatformDriver>>,
|
||
pub profile: Option<DriveProfile>,
|
||
pub platform: Option<profile::Platform>,
|
||
pub drive_id: DriveId,
|
||
device_path: String,
|
||
/// Halt flag — when set, Drive::read() bails at the next check point.
|
||
halt: Arc<AtomicBool>,
|
||
/// Event handler — fires for read errors and library-level state changes.
|
||
event_fn: Option<Box<dyn Fn(Event) + Send>>,
|
||
}
|
||
|
||
impl Drive {
|
||
pub fn open(device: &Path) -> Result<Self> {
|
||
let mut transport = crate::scsi::open(device)?;
|
||
let profiles = profile::load_bundled()?;
|
||
let drive_id = DriveId::from_drive(transport.as_mut())?;
|
||
|
||
let m = profile::find_by_drive_id(&profiles, &drive_id);
|
||
let (driver, platform, profile) = match m {
|
||
Some(m) => (
|
||
create_driver(m.platform, &m.profile).ok(),
|
||
Some(m.platform),
|
||
Some(m.profile),
|
||
),
|
||
None => (None, None, None),
|
||
};
|
||
|
||
Ok(Drive {
|
||
scsi: transport,
|
||
driver,
|
||
platform,
|
||
profile,
|
||
drive_id,
|
||
device_path: device.to_string_lossy().to_string(),
|
||
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));
|
||
}
|
||
|
||
#[allow(dead_code)] // public on_event registration kept; Drive currently
|
||
// has no internal emission sites after the 0.13.6 recovery strip.
|
||
// DiscStream is the BytesRead source. Plan to drop on_event in 0.14.
|
||
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)
|
||
}
|
||
|
||
/// Halt-aware SCSI execute. Returns `Err(Halted)` if the flag is set
|
||
/// before the command dispatches or by the time it completes. The only
|
||
/// path to talk to the drive in the recovery hot loop; keeps Drive::read
|
||
/// free of explicit halt checks.
|
||
fn checked_exec(
|
||
&mut self,
|
||
cdb: &[u8],
|
||
dir: crate::scsi::DataDirection,
|
||
buf: &mut [u8],
|
||
timeout_ms: u32,
|
||
) -> Result<crate::scsi::ScsiResult> {
|
||
if self.is_halted() {
|
||
return Err(Error::Halted);
|
||
}
|
||
let r = self.scsi.as_mut().execute(cdb, dir, buf, timeout_ms)?;
|
||
if self.is_halted() {
|
||
return Err(Error::Halted);
|
||
}
|
||
Ok(r)
|
||
}
|
||
|
||
/// 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();
|
||
}
|
||
|
||
/// Whether this drive has a known profile (unlock parameters available).
|
||
pub fn has_profile(&self) -> bool {
|
||
self.profile.is_some()
|
||
}
|
||
|
||
/// Access the SCSI transport for direct commands (used by CSS/AACS auth).
|
||
pub fn scsi_mut(&mut self) -> &mut dyn ScsiTransport {
|
||
self.scsi.as_mut()
|
||
}
|
||
|
||
pub fn wait_ready(&mut self) -> Result<()> {
|
||
let tur = [SCSI_TEST_UNIT_READY, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||
|
||
for _ in 0..60 {
|
||
let mut buf = [0u8; 0];
|
||
if self
|
||
.scsi
|
||
.as_mut()
|
||
.execute(&tur, crate::scsi::DataDirection::None, &mut buf, 5_000)
|
||
.is_ok()
|
||
{
|
||
return Ok(());
|
||
}
|
||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||
}
|
||
Err(Error::DeviceNotReady {
|
||
path: self.device_path.clone(),
|
||
})
|
||
}
|
||
|
||
/// Query the physical state of the drive — disc present, tray open, etc.
|
||
/// Uses GET EVENT STATUS NOTIFICATION which works regardless of firmware state.
|
||
pub fn drive_status(&mut self) -> DriveStatus {
|
||
// GET EVENT STATUS NOTIFICATION: polled, media event class (0x10)
|
||
let cdb = [
|
||
SCSI_GET_EVENT_STATUS,
|
||
0x01,
|
||
0x00,
|
||
0x00,
|
||
0x10,
|
||
0x00,
|
||
0x00,
|
||
0x00,
|
||
0x08,
|
||
0x00,
|
||
];
|
||
let mut buf = [0u8; 8];
|
||
match self.scsi.as_mut().execute(
|
||
&cdb,
|
||
crate::scsi::DataDirection::FromDevice,
|
||
&mut buf,
|
||
5_000,
|
||
) {
|
||
Ok(r) if r.bytes_transferred >= 6 => {
|
||
let media_status = buf[5];
|
||
// 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
|
||
0x02 => DriveStatus::DiscPresent, // tray closed, disc present
|
||
0x03 => DriveStatus::DiscPresent, // tray closed, disc present
|
||
_ => DriveStatus::Unknown,
|
||
}
|
||
}
|
||
_ => {
|
||
// Fallback: try TUR
|
||
let tur = [SCSI_TEST_UNIT_READY, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||
let mut empty = [0u8; 0];
|
||
match self.scsi.as_mut().execute(
|
||
&tur,
|
||
crate::scsi::DataDirection::None,
|
||
&mut empty,
|
||
5_000,
|
||
) {
|
||
Ok(_) => DriveStatus::DiscPresent,
|
||
Err(ref e)
|
||
if e.scsi_sense()
|
||
.is_some_and(|s| s.is_not_ready() || s.is_unit_attention()) =>
|
||
{
|
||
DriveStatus::NotReady
|
||
}
|
||
_ => DriveStatus::Unknown,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn platform_name(&self) -> &str {
|
||
match self.platform {
|
||
Some(ref p) => p.name(),
|
||
None => "Unknown",
|
||
}
|
||
}
|
||
|
||
pub fn device_path(&self) -> &str {
|
||
&self.device_path
|
||
}
|
||
|
||
/// Initialize drive — unlock + firmware upload.
|
||
/// Optional. Adds features: removes riplock, enables UHD reads, speed control.
|
||
pub fn init(&mut self) -> Result<()> {
|
||
match self.driver {
|
||
Some(ref mut d) => d.init(self.scsi.as_mut()),
|
||
None => Err(Error::UnsupportedDrive {
|
||
vendor_id: self.drive_id.vendor_id.trim().to_string(),
|
||
product_id: self.drive_id.product_id.trim().to_string(),
|
||
product_revision: self.drive_id.product_revision.trim().to_string(),
|
||
}),
|
||
}
|
||
}
|
||
|
||
/// Probe disc surface so the drive firmware learns optimal read speeds
|
||
/// per region. After this the host reads at max speed and the drive
|
||
/// manages zones internally.
|
||
pub fn probe_disc(&mut self) -> Result<()> {
|
||
match self.driver {
|
||
Some(ref mut d) => d.probe_disc(self.scsi.as_mut()),
|
||
None => Err(Error::UnsupportedDrive {
|
||
vendor_id: self.drive_id.vendor_id.trim().to_string(),
|
||
product_id: self.drive_id.product_id.trim().to_string(),
|
||
product_revision: self.drive_id.product_revision.trim().to_string(),
|
||
}),
|
||
}
|
||
}
|
||
|
||
/// Query a specific GET CONFIGURATION feature by code.
|
||
/// 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,
|
||
];
|
||
let mut buf = vec![0u8; 256];
|
||
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 {
|
||
None
|
||
}
|
||
}
|
||
|
||
/// Read REPORT KEY RPC state (region playback control).
|
||
pub fn report_key_rpc_state(&mut self) -> Option<Vec<u8>> {
|
||
let cdb = [
|
||
SCSI_REPORT_KEY,
|
||
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
|
||
}
|
||
}
|
||
|
||
/// Read MODE SENSE page data.
|
||
pub fn mode_sense_page(&mut self, page: u8) -> Option<Vec<u8>> {
|
||
let cdb = [
|
||
SCSI_MODE_SENSE,
|
||
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
|
||
}
|
||
}
|
||
|
||
/// 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
|
||
}
|
||
}
|
||
|
||
pub fn is_ready(&self) -> bool {
|
||
match self.driver {
|
||
Some(ref d) => d.is_ready(),
|
||
None => false,
|
||
}
|
||
}
|
||
|
||
/// Read sectors from the disc. Single-shot — no inline retries, no
|
||
/// SCSI reset.
|
||
///
|
||
/// `recovery=true` uses [`crate::scsi::READ_RECOVERY_TIMEOUT_MS`] (60 s,
|
||
/// matches sg_dd) for the `Disc::patch` pass; `recovery=false` uses
|
||
/// [`crate::scsi::READ_TIMEOUT_MS`] (30 s, matches the kernel's
|
||
/// `/sys/block/sr*/device/timeout` default) for `Disc::copy`'s fast
|
||
/// skip-forward sweep. Both budgets are generous enough that the drive
|
||
/// can finish ECC recovery on a marginal sector — pre-0.13.21 this was
|
||
/// 1.5 s on the fast path which forced the kernel mid-layer to time
|
||
/// out and escalate while we waited anyway. On any failure returns
|
||
/// `Err(DiscRead)` immediately; orchestration (`Disc::patch` multi-pass,
|
||
/// `DiscStream` adaptive batch halving) handles retry policy.
|
||
///
|
||
/// Inline retry phases (5× gentle + reset+reopen + 5× more) were
|
||
/// removed in 0.13.6. Per
|
||
/// `(internal)/postmortems/2026-04-25-stop-wedge-and-zero-kbs.md`,
|
||
/// the inline reset on the LG BU40N (Initio bridge) wedged drive
|
||
/// firmware without ever recovering a sector. The remaining recovery
|
||
/// layers (Disc::patch multi-pass, DiscStream batch halving) do not
|
||
/// touch the wedge-prone reset path.
|
||
pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8], recovery: bool) -> Result<usize> {
|
||
let timeout_ms = if recovery {
|
||
crate::scsi::READ_RECOVERY_TIMEOUT_MS
|
||
} else {
|
||
crate::scsi::READ_TIMEOUT_MS
|
||
};
|
||
let cdb = [
|
||
crate::scsi::SCSI_READ_10,
|
||
0x00,
|
||
(lba >> 24) as u8,
|
||
(lba >> 16) as u8,
|
||
(lba >> 8) as u8,
|
||
lba as u8,
|
||
0x00,
|
||
(count >> 8) as u8,
|
||
count as u8,
|
||
0x00,
|
||
];
|
||
|
||
match self.checked_exec(
|
||
&cdb,
|
||
crate::scsi::DataDirection::FromDevice,
|
||
buf,
|
||
timeout_ms,
|
||
) {
|
||
Ok(result) => Ok(result.bytes_transferred),
|
||
Err(Error::Halted) => Err(Error::Halted),
|
||
Err(e) => {
|
||
let (status, sense) = extract_scsi_context(&e);
|
||
Err(Error::DiscRead {
|
||
sector: lba as u64,
|
||
status: Some(status),
|
||
sense,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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,
|
||
];
|
||
let mut buf = [0u8; 8];
|
||
self.scsi.as_mut().execute(
|
||
&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)
|
||
}
|
||
|
||
pub fn set_speed(&mut self, speed_kbs: u16) {
|
||
let cdb = crate::scsi::build_set_cd_speed(speed_kbs);
|
||
let mut dummy = [0u8; 0];
|
||
let _ = self.scsi_execute(&cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000);
|
||
}
|
||
|
||
/// Lock the tray so the disc cannot be ejected during a rip.
|
||
pub fn lock_tray(&mut self) {
|
||
let prevent = [
|
||
SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL,
|
||
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);
|
||
}
|
||
|
||
/// Unlock the tray so the user can manually eject the disc.
|
||
pub fn unlock_tray(&mut self) {
|
||
let allow = [
|
||
SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL,
|
||
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);
|
||
}
|
||
|
||
/// Eject the disc tray. Unlocks first, then ejects.
|
||
pub fn eject(&mut self) -> Result<()> {
|
||
self.unlock_tray();
|
||
let eject_cdb = [SCSI_START_STOP_UNIT, 0, 0, 0, 0x02, 0];
|
||
let mut buf = [0u8; 0];
|
||
self.scsi.as_mut().execute(
|
||
&eject_cdb,
|
||
crate::scsi::DataDirection::None,
|
||
&mut buf,
|
||
30_000,
|
||
)?;
|
||
Ok(())
|
||
}
|
||
|
||
pub fn scsi_execute(
|
||
&mut self,
|
||
cdb: &[u8],
|
||
direction: crate::scsi::DataDirection,
|
||
buf: &mut [u8],
|
||
timeout_ms: u32,
|
||
) -> Result<crate::scsi::ScsiResult> {
|
||
self.scsi.as_mut().execute(cdb, direction, buf, timeout_ms)
|
||
}
|
||
}
|
||
|
||
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],
|
||
recovery: bool,
|
||
) -> Result<usize> {
|
||
self.read(lba, count, buf, recovery)
|
||
}
|
||
}
|
||
|
||
/// Find the first optical drive on this system and open it.
|
||
///
|
||
/// For just listing drives without opening (e.g. UI sidebar), use
|
||
/// `scsi::list_drives()` — that returns `DriveInfo` (path + identity)
|
||
/// without the cost of running every drive's profile + identity probe.
|
||
pub fn find_drive() -> Option<Drive> {
|
||
discover_drives()
|
||
.into_iter()
|
||
.find_map(|(path, _)| Drive::open(std::path::Path::new(&path)).ok())
|
||
}
|
||
|
||
/// Halt-aware sleep primitive — wakes within ~100 ms of `halt` flipping
|
||
/// to true. Kept for the unit tests that cover the slicing behaviour;
|
||
/// production code paths no longer sleep on the recovery hot path
|
||
/// (recovery loop removed in 0.13.6).
|
||
#[cfg(test)]
|
||
fn sleep_until_halted(halt: &AtomicBool, total: std::time::Duration) -> Result<()> {
|
||
const SLICE: std::time::Duration = std::time::Duration::from_millis(100);
|
||
let deadline = std::time::Instant::now() + total;
|
||
loop {
|
||
if halt.load(Ordering::Relaxed) {
|
||
return Err(Error::Halted);
|
||
}
|
||
let now = std::time::Instant::now();
|
||
if now >= deadline {
|
||
return Ok(());
|
||
}
|
||
let remaining = deadline - now;
|
||
std::thread::sleep(remaining.min(SLICE));
|
||
}
|
||
}
|
||
|
||
/// Internal: discover drive paths + IDs without opening full Drive objects.
|
||
fn discover_drives() -> Vec<(String, DriveId)> {
|
||
#[cfg(target_os = "linux")]
|
||
{
|
||
linux::find_drives()
|
||
}
|
||
#[cfg(target_os = "macos")]
|
||
{
|
||
macos::find_drives()
|
||
}
|
||
#[cfg(windows)]
|
||
{
|
||
windows::find_drives()
|
||
}
|
||
}
|
||
|
||
/// 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")]
|
||
{
|
||
linux::resolve_device(path)
|
||
}
|
||
#[cfg(target_os = "macos")]
|
||
{
|
||
macos::resolve_device(path)
|
||
}
|
||
#[cfg(windows)]
|
||
{
|
||
windows::resolve_device(path)
|
||
}
|
||
}
|
||
|
||
fn create_driver(
|
||
platform: profile::Platform,
|
||
profile: &DriveProfile,
|
||
) -> Result<Box<dyn PlatformDriver>> {
|
||
match platform {
|
||
profile::Platform::Mt1959A => Ok(Box::new(Mt1959::new(profile.clone(), false))),
|
||
profile::Platform::Mt1959B => Ok(Box::new(Mt1959::new(profile.clone(), true))),
|
||
profile::Platform::Renesas => Err(Error::PlatformNotImplemented {
|
||
platform: "renesas".to_string(),
|
||
}),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod halt_tests {
|
||
use super::*;
|
||
use std::time::{Duration, Instant};
|
||
|
||
#[test]
|
||
fn sleep_until_halted_completes_when_not_halted() {
|
||
let flag = AtomicBool::new(false);
|
||
let t0 = Instant::now();
|
||
let r = sleep_until_halted(&flag, Duration::from_millis(150));
|
||
assert!(r.is_ok());
|
||
assert!(t0.elapsed() >= Duration::from_millis(140));
|
||
}
|
||
|
||
#[test]
|
||
fn sleep_until_halted_returns_immediately_if_preflagged() {
|
||
let flag = AtomicBool::new(true);
|
||
let t0 = Instant::now();
|
||
let r = sleep_until_halted(&flag, Duration::from_secs(10));
|
||
assert!(matches!(r, Err(Error::Halted)));
|
||
// Must wake within one slice (100 ms) — the whole point of the
|
||
// primitive is that a 30 s sleep doesn't block Stop.
|
||
assert!(t0.elapsed() < Duration::from_millis(200));
|
||
}
|
||
|
||
#[test]
|
||
fn sleep_until_halted_wakes_mid_sleep() {
|
||
let flag = Arc::new(AtomicBool::new(false));
|
||
let f2 = flag.clone();
|
||
let t0 = Instant::now();
|
||
std::thread::spawn(move || {
|
||
std::thread::sleep(Duration::from_millis(150));
|
||
f2.store(true, Ordering::Relaxed);
|
||
});
|
||
let r = sleep_until_halted(&flag, Duration::from_secs(10));
|
||
assert!(matches!(r, Err(Error::Halted)));
|
||
let waited = t0.elapsed();
|
||
// Flag flipped at ~150 ms; we wake within one 100 ms slice → <300 ms.
|
||
assert!(waited < Duration::from_millis(350), "waited {waited:?}");
|
||
assert!(waited >= Duration::from_millis(140), "waited {waited:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn sleep_until_halted_zero_duration_is_noop_when_not_halted() {
|
||
let flag = AtomicBool::new(false);
|
||
let r = sleep_until_halted(&flag, Duration::ZERO);
|
||
assert!(r.is_ok());
|
||
}
|
||
}
|