Stop documenting the recovery API that 1.6.0 deleted
Disc::sweep, Disc::patch, Disc::copy, SweepOptions and PatchOptions have zero occurrences in src/ — recovery moved to freemkv-engine — but they were still documented in 30 places across README.md, TROUBLESHOOTING.md, six files under docs/, seven src/ doc comments and a Cargo.toml comment. README.md is the crate's GitHub front page and carried a full multi-pass code example that cannot compile. Two of the src/ references were intra-doc LINKS to deleted items ([`disc::Disc::copy`], [`disc::Disc::patch`] in scsi/mod.rs). They produced no warning on a normal `cargo doc` only because they sit on pub(crate) items; `--document-private-items` reports both, and they are gone now. The README example is deleted rather than rewritten against the engine's API: libfreemkv documenting a downstream crate's API on its own front page is the drift that produced this, and it cannot even depend on it. The src/ references become plain code spans naming freemkv_engine::recovery::* — deliberately not links, for the same reason. docs/rip-recovery.md was 202 lines about relocated code. It now documents only what this crate owns — Drive::read, SenseFamily, DiscStream's adaptive batch halving — plus the read-path design constraints, which belong with the code that enforces them, and points at freemkv-engine/src/recovery/ for the strategy. api-design.md's module tree is regenerated from the real src/disc/ and src/drive/ layouts instead of hand-patched; it had listed sweep.rs, patch.rs, mapfile.rs and read_error.rs, none of which exist. Three stale facts surfaced while rewriting and are corrected: the read timeouts are 10 s / 60 s, not the documented 1.5 s / 30 s; Drive::reset and SgIoTransport::reset no longer exist at all, so "no SCSI reset from any read path" is now stated as the stronger fact it has become; and verify_title, listed as a progress-emitting operation, was removed entirely. CHANGELOG.md keeps its references — those are the historical record of the releases that shipped the API.
This commit is contained in:
+2
-1
@@ -42,7 +42,8 @@ base64 = "0.22.1"
|
|||||||
# parser, not a hand-rolled scanner: the XPL is genuine XML (comments, varied
|
# parser, not a hand-rolled scanner: the XPL is genuine XML (comments, varied
|
||||||
# attribute order, self-closing tags).
|
# attribute order, self-closing tags).
|
||||||
roxmltree = "0.20"
|
roxmltree = "0.20"
|
||||||
# Trace-level instrumentation for Disc::copy + SgIoTransport::execute. Permitted
|
# Trace-level instrumentation for the read/transport path (SgIoTransport::execute
|
||||||
|
# and friends). Permitted
|
||||||
# under CLAUDE.md ("Acceptable strings: debug/trace logging"). Consumers (autorip)
|
# under CLAUDE.md ("Acceptable strings: debug/trace logging"). Consumers (autorip)
|
||||||
# wire a tracing subscriber and pipe events into the JSONL debug log.
|
# wire a tracing subscriber and pipe events into the JSONL debug log.
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|||||||
@@ -55,48 +55,15 @@ output.finish()?;
|
|||||||
|
|
||||||
### Multi-pass recovery rip
|
### Multi-pass recovery rip
|
||||||
|
|
||||||
For damaged discs the library exposes two flat verbs — `Disc::sweep` for the
|
Recovery moved OUT of this crate in 1.6.0. The sweep/patch strategy, the
|
||||||
forward Pass 1 and `Disc::patch` for retrying bad ranges. The library never
|
ddrescue mapfile, damage classification and the multipass loop now live in the
|
||||||
loops; the multipass policy is the caller's job. See
|
`freemkv-engine` crate as `freemkv_engine::recovery::{copy, sweep, patch}`.
|
||||||
[`docs/rip-recovery.md`](docs/rip-recovery.md).
|
|
||||||
|
|
||||||
```rust
|
libfreemkv keeps the layers underneath: the raw single-shot read
|
||||||
use libfreemkv::{SweepOptions, PatchOptions};
|
(`Drive::read`) and the SCSI-fact translation (`SenseFamily`) that the engine's
|
||||||
use libfreemkv::disc::{mapfile, mapfile_path_for};
|
strategy is built on. The dependency runs engine → libfreemkv, so this crate
|
||||||
use std::path::Path;
|
cannot call into it; front-ends get recovery from the engine directly. See
|
||||||
|
[`docs/rip-recovery.md`](docs/rip-recovery.md) for what stayed here.
|
||||||
let iso = Path::new("disc.iso");
|
|
||||||
|
|
||||||
// Pass 1: disc → ISO. Skip-on-error, zero-fill, write the sidecar mapfile.
|
|
||||||
disc.sweep(&mut drive, iso, &SweepOptions {
|
|
||||||
decrypt: true,
|
|
||||||
resume: false,
|
|
||||||
batch_sectors: None,
|
|
||||||
skip_on_error: true,
|
|
||||||
progress: None,
|
|
||||||
halt: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Pass 2..N: retry every non-finished range. Idempotent.
|
|
||||||
loop {
|
|
||||||
let map = mapfile::Mapfile::load(&mapfile_path_for(iso))?;
|
|
||||||
let stats = map.stats();
|
|
||||||
if stats.bytes_pending + stats.bytes_unreadable == 0 { break; }
|
|
||||||
|
|
||||||
let outcome = disc.patch(&mut drive, iso, &PatchOptions {
|
|
||||||
decrypt: true,
|
|
||||||
block_sectors: None,
|
|
||||||
full_recovery: true,
|
|
||||||
reverse: true,
|
|
||||||
wedged_threshold: 50,
|
|
||||||
progress: None,
|
|
||||||
halt: None,
|
|
||||||
})?;
|
|
||||||
if outcome.bytes_recovered_this_pass == 0 { break; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mux from the ISO via the normal stream pipeline (no drive involvement).
|
|
||||||
```
|
|
||||||
|
|
||||||
## What It Does
|
## What It Does
|
||||||
|
|
||||||
@@ -114,7 +81,7 @@ loop {
|
|||||||
| Stream | Input | Output | Transport |
|
| Stream | Input | Output | Transport |
|
||||||
|--------|-------|--------|-----------|
|
|--------|-------|--------|-----------|
|
||||||
| DiscStream | Yes | -- | Optical drive via SCSI |
|
| DiscStream | Yes | -- | Optical drive via SCSI |
|
||||||
| IsoStream | Yes | -- | Blu-ray ISO image file (read via stream pipeline; written via `Disc::sweep()`) |
|
| IsoStream | Yes | -- | Blu-ray ISO image file (read via stream pipeline; written by `freemkv_engine::recovery`) |
|
||||||
| MkvStream | Yes | Yes | Matroska container |
|
| MkvStream | Yes | Yes | Matroska container |
|
||||||
| M2tsStream | Yes | Yes | BD transport stream with FMKV metadata header |
|
| M2tsStream | Yes | Yes | BD transport stream with FMKV metadata header |
|
||||||
| NetworkStream | Yes (listen) | Yes (connect) | TCP with FMKV metadata header |
|
| NetworkStream | Yes (listen) | Yes (connect) | TCP with FMKV metadata header |
|
||||||
|
|||||||
+2
-2
@@ -141,8 +141,8 @@ If your machine has a free SATA port, use it.
|
|||||||
|
|
||||||
freemkv uses a three-layer recovery model. See [`docs/rip-recovery.md`](docs/rip-recovery.md) for full details.
|
freemkv uses a three-layer recovery model. See [`docs/rip-recovery.md`](docs/rip-recovery.md) for full details.
|
||||||
|
|
||||||
- **Pass 1 (Disc::copy):** Fast sweep with 64 KB reads. On failure, zero-fills the block and skips forward. Writes a ddrescue-format mapfile for later retry.
|
- **Pass 1 (`freemkv_engine::recovery::copy`):** Fast sweep with 64 KB reads. On failure, zero-fills the block and skips forward. Writes a ddrescue-format mapfile for later retry.
|
||||||
- **Pass 2+ (Disc::patch):** Targeted re-reads of bad ranges with a long 30-second timeout per CDB. The drive firmware performs its own ECC and laser power retries within that window.
|
- **Pass 2+ (`freemkv_engine::recovery::patch`):** Targeted re-reads of bad ranges with a long 60-second timeout per CDB. The drive firmware performs its own ECC and laser power retries within that window.
|
||||||
- **In-stream (DiscStream):** Adaptive batch halving -- reduces request size on failure to isolate bad sectors within a larger block.
|
- **In-stream (DiscStream):** Adaptive batch halving -- reduces request size on failure to isolate bad sectors within a larger block.
|
||||||
|
|
||||||
This means a disc with some bad sectors will still produce a usable ISO. The damaged areas are zero-filled in pass 1 and retried in subsequent passes. Structure-protected sectors (deliberate unreadable regions from copy protection) will never yield, which is expected.
|
This means a disc with some bad sectors will still produce a usable ISO. The damaged areas are zero-filled in pass 1 and retried in subsequent passes. Structure-protected sectors (deliberate unreadable regions from copy protection) will never yield, which is expected.
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ Technical documentation for [libfreemkv](https://github.com/freemkv/libfreemkv),
|
|||||||
|----------|---------------|
|
|----------|---------------|
|
||||||
| [Architecture](architecture.md) | Module map, design principles, error codes, platform support |
|
| [Architecture](architecture.md) | Module map, design principles, error codes, platform support |
|
||||||
| [Drive Access](drive-access.md) | Drive, SCSI transport, profiles, unlock, why raw mode is needed |
|
| [Drive Access](drive-access.md) | Drive, SCSI transport, profiles, unlock, why raw mode is needed |
|
||||||
| [Rip Recovery](rip-recovery.md) | Three-layer recovery model: Disc::patch, single-shot Drive::read, DiscStream batch halving |
|
| [Rip Recovery](rip-recovery.md) | What this crate owns of the recovery model: single-shot Drive::read, SenseFamily, DiscStream batch halving (the strategy itself moved to freemkv-engine in 1.6.0) |
|
||||||
| [AACS Encryption](aacs.md) | Key resolution (4 paths), content decryption, bus encryption, SCSI handshake |
|
| [AACS Encryption](aacs.md) | Key resolution (4 paths), content decryption, bus encryption, SCSI handshake |
|
||||||
| [UDF Filesystem](udf.md) | UDF 2.50 with metadata partitions, pointer chain, how files are read from disc |
|
| [UDF Filesystem](udf.md) | UDF 2.50 with metadata partitions, pointer chain, how files are read from disc |
|
||||||
| [MPLS Playlists](mpls.md) | Playlist format, play items, STN stream table, coding types |
|
| [MPLS Playlists](mpls.md) | Playlist format, play items, STN stream table, coding types |
|
||||||
|
|||||||
+10
-7
@@ -170,17 +170,20 @@ libfreemkv/src/
|
|||||||
│ ├── writeback_file.rs WritebackFile (was crate::io::Writer)
|
│ ├── writeback_file.rs WritebackFile (was crate::io::Writer)
|
||||||
│ └── writeback.rs sync_file_range pipeline
|
│ └── writeback.rs sync_file_range pipeline
|
||||||
├── drive/ Drive (open, init, single-shot read)
|
├── drive/ Drive (open, init, single-shot read)
|
||||||
│ ├── mod.rs Drive struct, init, read (single-shot), reset, eject
|
│ ├── mod.rs Drive struct, init, read (single-shot), eject
|
||||||
│ ├── capture.rs Raw drive SCSI capture (INQUIRY/GET_CONFIG) for contribution
|
│ ├── capture.rs Raw drive SCSI capture (INQUIRY/GET_CONFIG) for contribution
|
||||||
│ ├── linux.rs Linux drive discovery
|
│ ├── linux.rs Linux drive discovery
|
||||||
│ ├── macos.rs macOS drive discovery
|
│ ├── macos.rs macOS drive discovery
|
||||||
│ └── windows.rs Windows drive discovery
|
│ └── windows.rs Windows drive discovery
|
||||||
├── disc/ Disc (scan, titles, AACS setup, sweep, patch)
|
├── disc/ Disc (scan, titles, AACS setup, per-format parsing)
|
||||||
│ ├── mod.rs Disc struct, scan, titles, formats; Disc::copy + Disc::sweep (Pass 1)
|
│ ├── mod.rs Disc struct, scan, titles, formats
|
||||||
│ ├── sweep.rs Pass 1 internal helpers (pub(super))
|
│ ├── bluray.rs Blu-ray / UHD scanning (MPLS/CLPI-driven)
|
||||||
│ ├── patch.rs Disc::patch (Pass N retry over mapfile)
|
│ ├── dvd.rs DVD-Video scanning (IFO-driven)
|
||||||
│ ├── mapfile.rs ddrescue-format mapfile
|
│ ├── hddvd.rs HD-DVD scanning
|
||||||
│ └── read_error.rs ReadCtx / ReadAction state machine
|
│ ├── extract.rs Per-extent content extraction
|
||||||
|
│ ├── encrypt.rs Encrypted-range mapping for content reads
|
||||||
|
│ ├── dvd_audio_probe.rs DVD audio-stream probing
|
||||||
|
│ └── pgs_forced_probe.rs PGS forced-subtitle probing
|
||||||
├── scsi/ SCSI transport (Linux SG_IO, macOS IOKit, Windows SPTI)
|
├── scsi/ SCSI transport (Linux SG_IO, macOS IOKit, Windows SPTI)
|
||||||
├── unlock.rs Unlocker trait + registry (pluggable unlock seam)
|
├── unlock.rs Unlocker trait + registry (pluggable unlock seam)
|
||||||
├── aacs/ AACS decryption (handshake, keys, keydb, decrypt)
|
├── aacs/ AACS decryption (handshake, keys, keydb, decrypt)
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ After open:
|
|||||||
- `eject()` -- eject tray
|
- `eject()` -- eject tray
|
||||||
|
|
||||||
Recovery is layered above `Drive::read`, not inside it. Layer 1
|
Recovery is layered above `Drive::read`, not inside it. Layer 1
|
||||||
(`Disc::patch`) handles bad-range retry by replaying the ddrescue mapfile.
|
(`freemkv_engine::recovery::patch`, in the engine crate) handles bad-range
|
||||||
|
retry by replaying the ddrescue mapfile.
|
||||||
Layer 3 (`DiscStream::fill_extents` adaptive batch sizer) handles in-loop
|
Layer 3 (`DiscStream::fill_extents` adaptive batch sizer) handles in-loop
|
||||||
request-size adaptation. Inline recovery (gentle retry → SCSI reset → retry)
|
request-size adaptation. Inline recovery (gentle retry → SCSI reset → retry)
|
||||||
was removed in 0.13.6 — see [`rip-recovery.md`](rip-recovery.md) and
|
was removed in 0.13.6 — see [`rip-recovery.md`](rip-recovery.md) and
|
||||||
|
|||||||
+2
-2
@@ -79,7 +79,7 @@ Insert disc
|
|||||||
│ Or: read sectors → decrypt → raw bytes (for ISO output)
|
│ Or: read sectors → decrypt → raw bytes (for ISO output)
|
||||||
│ Drive::read() is single-shot. DiscStream::fill_extents adapts the
|
│ Drive::read() is single-shot. DiscStream::fill_extents adapts the
|
||||||
│ batch size on failure (halve / probe-up). Bad-range retry is layer
|
│ batch size on failure (halve / probe-up). Bad-range retry is layer
|
||||||
│ 1 above this — Disc::patch re-runs against the mapfile.
|
│ 1 above this — freemkv_engine::recovery::patch re-runs against the mapfile.
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
PES frames → output stream (MKV, M2TS, network, etc.)
|
PES frames → output stream (MKV, M2TS, network, etc.)
|
||||||
@@ -123,7 +123,7 @@ output.finish()?;
|
|||||||
| aacs/ | [aacs.md](aacs.md) | Key resolution + content decrypt + bus handshake |
|
| aacs/ | [aacs.md](aacs.md) | Key resolution + content decrypt + bus handshake |
|
||||||
| css/ | -- | DVD CSS cipher |
|
| css/ | -- | DVD CSS cipher |
|
||||||
| decrypt.rs | -- | Unified decrypt dispatcher (AACS/CSS/None) |
|
| decrypt.rs | -- | Unified decrypt dispatcher (AACS/CSS/None) |
|
||||||
| disc/ | [rip-recovery.md](rip-recovery.md) | Disc::scan + Disc::sweep + Disc::patch + mapfile |
|
| disc/ | [rip-recovery.md](rip-recovery.md) | Disc::scan (sweep/patch/mapfile moved to freemkv-engine in 1.6.0) |
|
||||||
| labels/ | -- | BD-J stream labels (5 format parsers) |
|
| labels/ | -- | BD-J stream labels (5 format parsers) |
|
||||||
| mux/ | -- | Stream implementations (7 stream types) |
|
| mux/ | -- | Stream implementations (7 stream types) |
|
||||||
| pes.rs | -- | PES frame types + FrameSource / FrameSink traits |
|
| pes.rs | -- | PES frame types + FrameSource / FrameSink traits |
|
||||||
|
|||||||
@@ -58,15 +58,16 @@ selects the per-CDB timeout:
|
|||||||
|
|
||||||
| `recovery` | Timeout | Used by |
|
| `recovery` | Timeout | Used by |
|
||||||
|------------|----------|------------------------------------------|
|
|------------|----------|------------------------------------------|
|
||||||
| `false` | 1.5 s | `Disc::sweep` fast skip-forward pass, `DiscStream::fill_extents` |
|
| `false` | 10 s | `freemkv_engine::recovery::sweep` fast skip-forward pass, `DiscStream::fill_extents` |
|
||||||
| `true` | 30 s | `Disc::patch` retry pass over the mapfile |
|
| `true` | 60 s | `freemkv_engine::recovery::patch` retry pass over the mapfile |
|
||||||
|
|
||||||
On any SCSI failure or timeout, `read` returns `Err(DiscRead)` immediately.
|
On any SCSI failure or timeout, `read` returns `Err(DiscRead)` immediately.
|
||||||
There are no inline retries, no SCSI reset, no Phase 1/2/3 escalation.
|
There are no inline retries, no SCSI reset, no Phase 1/2/3 escalation.
|
||||||
|
|
||||||
Recovery is layered above `Drive::read`:
|
Recovery is layered above `Drive::read`:
|
||||||
|
|
||||||
- **Layer 1 — `Disc::patch`** loops over the ddrescue mapfile and re-issues
|
- **Layer 1 — `freemkv_engine::recovery::patch`** (in the engine crate, not
|
||||||
|
here) loops over the ddrescue mapfile and re-issues
|
||||||
`read(.., recovery=true)` against each non-`+` range.
|
`read(.., recovery=true)` against each non-`+` range.
|
||||||
- **Layer 3 — `DiscStream::fill_extents`** halves the request size on
|
- **Layer 3 — `DiscStream::fill_extents`** halves the request size on
|
||||||
failure, retries at the same LBA, and probes back up on a clean-read
|
failure, retries at the same LBA, and probes back up on a clean-read
|
||||||
|
|||||||
+64
-168
@@ -1,136 +1,38 @@
|
|||||||
# Rip recovery — three-layer architecture
|
# Rip recovery — what libfreemkv owns
|
||||||
|
|
||||||
`libfreemkv` supports a multi-stage rip model for damaged or protection-bearing
|
**Recovery strategy moved OUT of this crate in 1.6.0.** The forward sweep, the
|
||||||
discs: a fast forward sweep that tolerates read failures, in-loop request-size
|
targeted retry pass, the ddrescue mapfile, damage classification and the
|
||||||
adaptation that survives transient drive trouble without bailing, and targeted
|
multipass loop now live in the **`freemkv-engine`** crate as
|
||||||
retry passes against a persistent bad-range map. The stream pipeline
|
`freemkv_engine::recovery::{copy, sweep, patch}`. The dependency runs
|
||||||
(`DiscStream` + `input`/`output`) operates against the resulting ISO image, so
|
engine → libfreemkv, so this crate cannot call into the engine; front-ends
|
||||||
the mux stage never touches the drive.
|
(`freemkv` CLI, autorip) get recovery from the engine directly.
|
||||||
|
|
||||||
Recovery is layered cleanly. Each layer has one responsibility and does not
|
What stayed here are the two layers underneath the strategy: the single-shot
|
||||||
reach into the others.
|
read primitive, and the in-stream request-size adaptation that sits in front of
|
||||||
|
it. This document covers those, plus the design constraints they encode — the
|
||||||
|
constraints are the reason the strategy above them looks the way it does, so
|
||||||
|
they belong with the code that enforces them.
|
||||||
|
|
||||||
|
For the strategy itself — damage-jump thresholds, pass ordering, mapfile status
|
||||||
|
state machine, wedge detection — read `freemkv-engine/src/recovery/`.
|
||||||
|
|
||||||
| Layer | Where it lives | What it does |
|
| Layer | Where it lives | What it does |
|
||||||
|-------|---------------|--------------|
|
|-------|---------------|--------------|
|
||||||
| 1 — Bad-range retry | `Disc::patch` (one pass over the mapfile per call) | Re-reads non-`+` ranges with the long timeout. Idempotent; caller invokes N times. |
|
| 1 — Bad-range retry | **`freemkv-engine`** (`recovery::patch`) | Re-reads non-`+` ranges with the long timeout. Idempotent; caller invokes N times. |
|
||||||
| 2 — Single-shot primitive | `Drive::read` in `src/drive/mod.rs` | One CDB, one timeout, one result. No inline retries, no SCSI reset. |
|
| 2 — Single-shot primitive | `Drive::read` in `src/drive/mod.rs` | One CDB, one timeout, one result. No inline retries, no SCSI reset. |
|
||||||
| 3 — In-loop request adaptation | `DiscStream::fill_extents` adaptive batch sizer | Halves the batch on failure, retries at the same LBA, walks back up on a clean-read streak. |
|
| 3 — In-loop request adaptation | `DiscStream::fill_extents` in `src/mux/disc.rs` | Halves the batch on failure, retries at the same LBA, walks back up on a clean-read streak. |
|
||||||
|
|
||||||
The library exposes flat verbs; the caller drives the multipass loop. Autorip
|
Layer 2 also translates drive facts: [`SenseFamily`](../src/scsi/mod.rs)
|
||||||
runs `Disc::sweep` once, then loops `Disc::patch` until either the mapfile is
|
classifies SCSI sense data into the categories the engine's strategy routes on
|
||||||
clean or the configured retry budget is exhausted, then hands the ISO off to
|
(marginal vs. hardware vs. not-ready). Getting that classification wrong
|
||||||
the mux pipeline. The `freemkv` CLI does the same shape, but as of 1.6.0 the
|
silently misroutes recovery, which is why it lives next to the transport rather
|
||||||
loop itself (including the multi-title rip loop) lives one layer up, in the
|
than in the strategy.
|
||||||
shared `freemkv-engine` crate, with a terminal-output progress sink plugged
|
|
||||||
into it as the `Sink`. Layer 3 runs inside any consumer of `DiscStream`
|
|
||||||
(direct PES pipeline, ISO playback, etc.) without caller involvement.
|
|
||||||
|
|
||||||
Three primitives compose the disc-side flow:
|
Layer 3 runs inside any consumer of `DiscStream` — direct PES pipeline, ISO
|
||||||
|
playback — without caller involvement, and applies whether or not the engine's
|
||||||
|
recovery is in play.
|
||||||
|
|
||||||
| Primitive | What it does |
|
## In-stream — adaptive batch halving (`DiscStream::fill_extents`)
|
||||||
|---------------------------|-----------------------------------------------------------------------|
|
|
||||||
| `Disc::sweep` | disc → ISO, one forward pass. Writes a sidecar `.mapfile`. Opt-in skip-on-error. |
|
|
||||||
| `Disc::patch` | Re-reads bad ranges from the drive. One pass per call; caller invokes N times. |
|
|
||||||
| `DiscStream` (ISO source) | Reads sectors from the ISO, feeds decrypt → demux → codec → mux. |
|
|
||||||
|
|
||||||
## Data model
|
|
||||||
|
|
||||||
### Mapfile
|
|
||||||
|
|
||||||
Format: [ddrescue](https://www.gnu.org/software/ddrescue/manual/ddrescue_manual.html)-compatible
|
|
||||||
plain text, greppable, tool-interoperable. Flushed to disk on every `record()`
|
|
||||||
so a crashed rip loses at most one block.
|
|
||||||
|
|
||||||
```
|
|
||||||
# Rescue Logfile. Created by libfreemkv v0.13.6
|
|
||||||
# Current pos / status / pass / pass_time
|
|
||||||
0x000000000 ? 1 0
|
|
||||||
# pos size status
|
|
||||||
0x000000000 0x12a35d000 +
|
|
||||||
0x12a35d000 0x000003000 -
|
|
||||||
0x12a360000 0x009c4a000 +
|
|
||||||
0x12d00a000 0x000064000 *
|
|
||||||
```
|
|
||||||
|
|
||||||
Status characters match ddrescue:
|
|
||||||
|
|
||||||
| Char | Meaning |
|
|
||||||
|------|----------------------------------------------------|
|
|
||||||
| `?` | Not yet attempted |
|
|
||||||
| `*` | Fast-pass failed; needs edge-trim |
|
|
||||||
| `/` | Trimmed; interior needs sector scrape |
|
|
||||||
| `-` | Unreadable this session |
|
|
||||||
| `+` | Finished (good) |
|
|
||||||
|
|
||||||
Position and size are hex byte offsets into the ISO.
|
|
||||||
|
|
||||||
### `SweepOptions` and `PatchOptions`
|
|
||||||
|
|
||||||
The library no longer dispatches between sweep and patch internally — the
|
|
||||||
caller picks the verb explicitly per pass. The two option structs are flat
|
|
||||||
and have no overlap:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
SweepOptions {
|
|
||||||
decrypt: true,
|
|
||||||
resume: false,
|
|
||||||
batch_sectors: None,
|
|
||||||
skip_on_error: true, // damage-jump + zero-fill on read failure
|
|
||||||
progress: Some(&reporter),
|
|
||||||
halt: Some(flag),
|
|
||||||
}
|
|
||||||
|
|
||||||
PatchOptions {
|
|
||||||
decrypt: true,
|
|
||||||
block_sectors: None,
|
|
||||||
full_recovery: true,
|
|
||||||
reverse: true, // walk bad ranges high → low LBA
|
|
||||||
wedged_threshold: 50,
|
|
||||||
progress: Some(&reporter),
|
|
||||||
halt: Some(flag),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Caller-orchestrated dispatch (the policy `Disc::copy` used to embed):
|
|
||||||
|
|
||||||
- No mapfile → `sweep` (fresh Pass 1).
|
|
||||||
- Mapfile with `?` ranges → `sweep` with `resume: true`.
|
|
||||||
- Mapfile covers full disc, only `*` / `/` / `-` ranges → `patch`.
|
|
||||||
- Mapfile clean → done; no further pass needed.
|
|
||||||
|
|
||||||
Each consumer (autorip, `freemkv` CLI) implements the loop in roughly five
|
|
||||||
lines of `Mapfile::stats()` checks.
|
|
||||||
|
|
||||||
## Algorithm
|
|
||||||
|
|
||||||
### Pass 1 — fast sweep (`Disc::sweep`)
|
|
||||||
|
|
||||||
1. Read one ECC block (32 sectors for UHD, 16 for BD/DVD) at the current LBA.
|
|
||||||
2. On success: write data to ISO, mark `+`, advance.
|
|
||||||
3. On failure (with `multipass`): zero-fill, mark `*`, advance.
|
|
||||||
4. Track a sliding window of the last 16 ECC block results. When ≥12% are failures
|
|
||||||
→ **damage-jump**: skip ahead by `1024×batch×multiplier` sectors (64 MB base for
|
|
||||||
UHD). Double the multiplier on each jump (64→128→256→512 MB...). Zero-fill the gap as `*`.
|
|
||||||
5. On 16 consecutive good reads: reset jump multiplier to 1, restore max read speed.
|
|
||||||
6. Speed control: damage zone entry → minimum speed, exit → maximum speed.
|
|
||||||
7. Only transport failures (USB bridge crash) abort the pass.
|
|
||||||
|
|
||||||
Pass 1 completes when every byte has been visited (either `+` or `*`).
|
|
||||||
|
|
||||||
### Pass 2+ — patch (`Disc::patch`)
|
|
||||||
|
|
||||||
`Disc::patch` reads the mapfile and iterates every non-`+` range. Default: **reverse** mode
|
|
||||||
(walks ranges from highest LBA to lowest, within each range from end to start).
|
|
||||||
|
|
||||||
1. Issue a single-sector read with 60 s timeout (`recovery=true`). Drive firmware
|
|
||||||
does its own ECC recovery inside that window.
|
|
||||||
2. On success: write the good bytes into the ISO, mark `+`.
|
|
||||||
3. On failure with non-marginal SCSI sense: bail immediately (drive won't produce data).
|
|
||||||
4. On failure with marginal sense: mark `-`, continue.
|
|
||||||
5. Update the mapfile after every block — crash-safe resume.
|
|
||||||
6. Wedged-drive exit: 50 consecutive failures with zero recovery → bail this pass.
|
|
||||||
|
|
||||||
### In-stream — adaptive batch halving (`DiscStream::fill_extents`)
|
|
||||||
|
|
||||||
When a consumer reads a `DiscStream` directly (no ISO intermediate),
|
When a consumer reads a `DiscStream` directly (no ISO intermediate),
|
||||||
`fill_extents` runs an adaptive sizer in front of `Drive::read`:
|
`fill_extents` runs an adaptive sizer in front of `Drive::read`:
|
||||||
@@ -144,59 +46,53 @@ When a consumer reads a `DiscStream` directly (no ISO intermediate),
|
|||||||
`EventKind::SectorSkipped`) when `skip_errors` is set, otherwise return
|
`EventKind::SectorSkipped`) when `skip_errors` is set, otherwise return
|
||||||
`Err(DiscRead)`.
|
`Err(DiscRead)`.
|
||||||
|
|
||||||
This is layer 3. It exists so a transient single-sector glitch in a 32-sector
|
This exists so a transient single-sector glitch inside a 32-sector batch can be
|
||||||
batch can be isolated and read individually without the caller needing to
|
isolated and read individually without the caller implementing retry logic. See
|
||||||
implement retry logic.
|
[`src/event.rs`](../src/event.rs) for the emitted events.
|
||||||
|
|
||||||
## Design choices
|
## Design choices
|
||||||
|
|
||||||
**`Drive::read` is single-shot.** No inline retry phases, no SCSI reset,
|
These are constraints on the read path, enforced here and relied on by the
|
||||||
no eject cycle. The `recovery` flag controls only the per-CDB timeout
|
engine's strategy.
|
||||||
(1.5 s vs. 30 s); on any failure it returns `Err(DiscRead)` immediately.
|
|
||||||
Inline recovery (5× gentle retry → close + SCSI reset + reopen → 5× more)
|
|
||||||
was removed in 0.13.6. See the stop-wedge postmortem (2026-04-25) for rationale:
|
|
||||||
the inline reset on the LG BU40N (Initio USB-SATA bridge)
|
|
||||||
wedged drive firmware below the bridge without ever recovering a sector,
|
|
||||||
and the gentle-retry phase produced long stretches of 0 KB/s with no
|
|
||||||
recoveries to show for it. Recovery responsibility is now layered: layer 1
|
|
||||||
handles ranges, layer 3 handles request size, neither touches the
|
|
||||||
wedge-prone reset path.
|
|
||||||
|
|
||||||
**No `MODE SELECT` to disable drive retries.** Neither ddrescue
|
**`Drive::read` is single-shot.** No inline retry phases, no SCSI reset, no
|
||||||
nor any consumer ripper does this. Drive firmware has access to raw analog signal, laser
|
eject cycle. The `recovery` flag controls only the per-CDB timeout (10 s vs.
|
||||||
power control, and drive-specific ECC tuning that userspace can't replicate —
|
60 s); on any failure it returns `Err(DiscRead)` immediately. Inline recovery
|
||||||
disabling it throws away recovery headroom on marginal sectors. We fail fast
|
(5× gentle retry → close + SCSI reset + reopen → 5× more) was removed in
|
||||||
via short SG_IO timeouts in pass 1 and let the firmware work the long timeout
|
0.13.6. See the stop-wedge postmortem (2026-04-25) for rationale: the inline
|
||||||
in pass 2 / patch.
|
reset on the LG BU40N (Initio USB-SATA bridge) wedged drive firmware below the
|
||||||
|
bridge without ever recovering a sector, and the gentle-retry phase produced
|
||||||
|
long stretches of 0 KB/s with nothing to show for it. Recovery responsibility
|
||||||
|
is layered instead: layer 1 handles ranges, layer 3 handles request size,
|
||||||
|
neither touches the wedge-prone reset path.
|
||||||
|
|
||||||
**No SCSI reset from any retry path.** `SgIoTransport::reset` (Linux) is
|
**No `MODE SELECT` to disable drive retries.** Neither ddrescue nor any
|
||||||
trimmed to a kernel SG_IO state flush plus ALLOW MEDIUM REMOVAL — the
|
consumer ripper does this. Drive firmware has access to raw analog signal,
|
||||||
`SG_SCSI_RESET` ioctl and STOP/START UNIT escalation were removed in 0.13.6.
|
laser power control and drive-specific ECC tuning that userspace cannot
|
||||||
The macOS reset (which had been a no-op) was removed entirely. The top-level
|
replicate — disabling it throws away recovery headroom on marginal sectors. The
|
||||||
`scsi::reset()` / `reset_with_timeout()` / `reset_blocking()` wrappers were
|
fast pass fails quickly via short SG_IO timeouts and lets the firmware work the
|
||||||
also removed (no callers). The remaining `Drive::reset()` is only invoked
|
long timeout during retry.
|
||||||
explicitly by callers that need an eject-cycle escape hatch — it is never
|
|
||||||
reached from a read path.
|
|
||||||
|
|
||||||
**ISO intermediate, even for single-pass.** Pass 1 always writes an ISO. The
|
**No SCSI reset from any read path.** There is no reset escape hatch on
|
||||||
mux stage reads the ISO via `FileSectorSource`. For single-pass (no retries),
|
`Drive` at all: the `SG_SCSI_RESET` ioctl and STOP/START UNIT escalation went in
|
||||||
this adds ~2-3 min (local disk mux) but gains resumability across crashes,
|
0.13.6, the macOS reset (always a no-op) was removed entirely, and the
|
||||||
|
top-level `scsi::reset()` wrappers went with their last callers. The only
|
||||||
|
remaining reset is a Windows-specific device-level helper in
|
||||||
|
[`src/scsi/windows.rs`](../src/scsi/windows.rs), never reached from a read.
|
||||||
|
|
||||||
|
**ISO intermediate, even for single-pass.** The engine's Pass 1 always writes
|
||||||
|
an ISO, and the mux stage reads it back via `FileSectorSource`. For a
|
||||||
|
no-retry rip this costs a few minutes but buys resumability across crashes,
|
||||||
re-muxability without re-ripping, and a persistent forensic artifact. Callers
|
re-muxability without re-ripping, and a persistent forensic artifact. Callers
|
||||||
who need pure speed can bypass and use `DiscStream::new(Box::new(drive), …)`
|
who need pure speed can bypass it with `DiscStream::new(Box::new(drive), …)` —
|
||||||
directly — the lib doesn't forbid it, and layer 3 (adaptive batch halving)
|
nothing forbids it, and layer 3 still applies there.
|
||||||
still applies there.
|
|
||||||
|
|
||||||
**Mapfile in ddrescue format.** Plain text so users can `less` it, `diff` it,
|
|
||||||
or feed it to ddrescue's own tooling. Crash-safe (flush-per-record). Entries
|
|
||||||
coalesce on adjacent same-status ranges so files stay small.
|
|
||||||
|
|
||||||
**Patches target `-`, `*`, `/`, and `?` alike.** The status state machine is
|
|
||||||
ddrescue's but `patch` collapses the distinction — it just tries every
|
|
||||||
non-finished range with the long timeout. Future work can specialize (trim vs.
|
|
||||||
scrape vs. retry with direction reversal) if there's measured benefit.
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [ddrescue manual, Algorithm chapter](https://www.gnu.org/software/ddrescue/manual/ddrescue_manual.html)
|
- [ddrescue manual, Algorithm chapter](https://www.gnu.org/software/ddrescue/manual/ddrescue_manual.html)
|
||||||
- [ddrescue optical media notes](https://www.electric-spoon.com/doc/gddrescue/html/Optical-media.html)
|
- [ddrescue optical media notes](https://www.electric-spoon.com/doc/gddrescue/html/Optical-media.html)
|
||||||
- Source: [`src/disc/mapfile.rs`](../src/disc/mapfile.rs), [`src/disc/mod.rs`](../src/disc/mod.rs) (`Disc::sweep`), [`src/disc/patch.rs`](../src/disc/patch.rs) (`Disc::patch`), [`src/drive/mod.rs`](../src/drive/mod.rs) (`Drive::read`), [`src/mux/disc.rs`](../src/mux/disc.rs) (`DiscStream::fill_extents`).
|
- Recovery strategy and mapfile: `freemkv-engine/src/recovery/`
|
||||||
|
- In this crate: [`src/drive/mod.rs`](../src/drive/mod.rs) (`Drive::read`),
|
||||||
|
[`src/scsi/mod.rs`](../src/scsi/mod.rs) (`SenseFamily`),
|
||||||
|
[`src/mux/disc.rs`](../src/mux/disc.rs) (`DiscStream::fill_extents`),
|
||||||
|
[`src/event.rs`](../src/event.rs) (progress events).
|
||||||
|
|||||||
+4
-4
@@ -715,19 +715,19 @@ impl Drive {
|
|||||||
/// SCSI reset.
|
/// SCSI reset.
|
||||||
///
|
///
|
||||||
/// `recovery=true` uses [`crate::scsi::READ_RECOVERY_TIMEOUT_MS`] (60 s,
|
/// `recovery=true` uses [`crate::scsi::READ_RECOVERY_TIMEOUT_MS`] (60 s,
|
||||||
/// matches sg_dd) for the `Disc::patch` pass; `recovery=false` uses
|
/// matches sg_dd) for the `freemkv_engine::recovery::patch` pass; `recovery=false` uses
|
||||||
/// [`crate::scsi::READ_TIMEOUT_MS`] (10 s) for `Disc::copy`'s fast
|
/// [`crate::scsi::READ_TIMEOUT_MS`] (10 s) for `freemkv_engine::recovery::copy`'s fast
|
||||||
/// skip-forward sweep. Both budgets are generous enough that the drive
|
/// 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
|
/// 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
|
/// 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
|
/// out and escalate while we waited anyway. On any failure returns
|
||||||
/// `Err(DiscRead)` immediately; orchestration (`Disc::patch` multi-pass,
|
/// `Err(DiscRead)` immediately; orchestration (`freemkv_engine::recovery::patch` multi-pass,
|
||||||
/// `DiscStream` adaptive batch halving) handles retry policy.
|
/// `DiscStream` adaptive batch halving) handles retry policy.
|
||||||
///
|
///
|
||||||
/// Inline retry phases (5× gentle + reset+reopen + 5× more) were
|
/// Inline retry phases (5× gentle + reset+reopen + 5× more) were
|
||||||
/// removed in 0.13.6: on some USB-SATA bridges the inline reset wedged
|
/// removed in 0.13.6: on some USB-SATA bridges the inline reset wedged
|
||||||
/// drive firmware without ever recovering a sector. The remaining
|
/// drive firmware without ever recovering a sector. The remaining
|
||||||
/// recovery layers (Disc::patch multi-pass, DiscStream batch halving)
|
/// recovery layers (freemkv_engine::recovery::patch multi-pass, DiscStream batch halving)
|
||||||
/// do not touch the wedge-prone reset path.
|
/// do not touch the wedge-prone reset path.
|
||||||
pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8], recovery: bool) -> Result<usize> {
|
pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8], recovery: bool) -> Result<usize> {
|
||||||
// Bulk path: FUA off (the drive cache IS the streaming throughput).
|
// Bulk path: FUA off (the drive cache IS the streaming throughput).
|
||||||
|
|||||||
+1
-1
@@ -1032,7 +1032,7 @@ impl Error {
|
|||||||
/// REQUEST, BLANK CHECK, kernel `IoError`, and any non-SCSI variant.
|
/// REQUEST, BLANK CHECK, kernel `IoError`, and any non-SCSI variant.
|
||||||
/// Caller-agnostic predicate — describes a property of the *error*,
|
/// Caller-agnostic predicate — describes a property of the *error*,
|
||||||
/// not what one specific call site should do with it. Used by
|
/// not what one specific call site should do with it. Used by
|
||||||
/// `Disc::copy`'s hysteresis dispatch.
|
/// `freemkv_engine::recovery::copy`'s hysteresis dispatch.
|
||||||
pub fn is_marginal_read(&self) -> bool {
|
pub fn is_marginal_read(&self) -> bool {
|
||||||
self.scsi_sense()
|
self.scsi_sense()
|
||||||
.map(crate::scsi::ScsiSense::is_marginal)
|
.map(crate::scsi::ScsiSense::is_marginal)
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
//! One stream type for all disc sources. The source is a SectorSource —
|
//! One stream type for all disc sources. The source is a SectorSource —
|
||||||
//! Drive (hardware) or FileSectorSource (file). DiscStream doesn't care.
|
//! Drive (hardware) or FileSectorSource (file). DiscStream doesn't care.
|
||||||
//!
|
//!
|
||||||
//! Read-only. For disc→ISO (raw sector copy), use `Disc::copy()`.
|
//! Read-only. For disc→ISO (raw sector copy), use `freemkv_engine::recovery::copy`.
|
||||||
|
|
||||||
use crate::disc::{DiscTitle, Extent};
|
use crate::disc::{DiscTitle, Extent};
|
||||||
use crate::drive::extract_scsi_context;
|
use crate::drive::extract_scsi_context;
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
//! output.finish()?;
|
//! output.finish()?;
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! For disc→ISO (raw sector copy), use `Disc::copy()` instead.
|
//! For disc→ISO (raw sector copy), use `freemkv_engine::recovery::copy` instead.
|
||||||
|
|
||||||
// Public modules — types here are intentionally part of the consumable API.
|
// Public modules — types here are intentionally part of the consumable API.
|
||||||
pub mod disc;
|
pub mod disc;
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
//! | fvi:// | -- | Yes | file path (required) — per-picture video index |
|
//! | fvi:// | -- | Yes | file path (required) — per-picture video index |
|
||||||
//!
|
//!
|
||||||
//! Bare paths without a scheme are rejected.
|
//! Bare paths without a scheme are rejected.
|
||||||
//! For disc→ISO (raw sector copy), use `Disc::copy()` instead.
|
//! For disc→ISO (raw sector copy), use `freemkv_engine::recovery::copy` instead.
|
||||||
//!
|
//!
|
||||||
//! Note: `disc://` cannot be opened through [`input`]; it returns
|
//! Note: `disc://` cannot be opened through [`input`]; it returns
|
||||||
//! [`crate::error::Error::DiscUrlNotDirect`]. Live-disc input must go
|
//! [`crate::error::Error::DiscUrlNotDirect`]. Live-disc input must go
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
//! Pipeline-progress reporting for the rip pipeline.
|
//! Pipeline-progress reporting for the rip pipeline.
|
||||||
//!
|
//!
|
||||||
//! Architecture rule: ONE progress signal type. Every long-running
|
//! Architecture rule: ONE progress signal type. Every long-running
|
||||||
//! pipeline operation (`Disc::copy`, `Disc::patch`, `verify_title`) emits the
|
//! pipeline operation (`freemkv_engine::recovery::{copy, patch}`) emits the
|
||||||
//! same [`PassProgress`] shape via the [`Progress`] trait. Consumers (autorip)
|
//! same [`PassProgress`] shape via the [`Progress`] trait. Consumers (autorip)
|
||||||
//! compute their own single derived view from these fields and never reach
|
//! compute their own single derived view from these fields and never reach
|
||||||
//! into per-pass internals.
|
//! into per-pass internals.
|
||||||
@@ -18,12 +18,12 @@
|
|||||||
/// (reverse)", "Scrape", "Mux") or just use a generic "Pass N" label.
|
/// (reverse)", "Scrape", "Mux") or just use a generic "Pass N" label.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum PassKind {
|
pub enum PassKind {
|
||||||
/// `Disc::copy` — initial sweep across the entire disc.
|
/// `freemkv_engine::recovery::copy` — initial sweep across the entire disc.
|
||||||
Sweep,
|
Sweep,
|
||||||
/// `Disc::patch` retry pass with `block_sectors >= 2`. `reverse=true`
|
/// `freemkv_engine::recovery::patch` retry pass with `block_sectors >= 2`. `reverse=true`
|
||||||
/// means walking bad ranges from highest to lowest LBA.
|
/// means walking bad ranges from highest to lowest LBA.
|
||||||
Trim { reverse: bool },
|
Trim { reverse: bool },
|
||||||
/// `Disc::patch` final pass at 1 sector per block.
|
/// `freemkv_engine::recovery::patch` final pass at 1 sector per block.
|
||||||
Scrape { reverse: bool },
|
Scrape { reverse: bool },
|
||||||
/// Demux ISO → output (MKV / M2TS / network). Single phase that runs
|
/// Demux ISO → output (MKV / M2TS / network). Single phase that runs
|
||||||
/// after all rip passes complete. The library's mux pipeline does not
|
/// after all rip passes complete. The library's mux pipeline does not
|
||||||
|
|||||||
+6
-6
@@ -44,7 +44,7 @@ pub const AACS_KEY_CLASS: u8 = 0x02;
|
|||||||
pub(crate) const TUR_TIMEOUT_MS: u32 = 5_000;
|
pub(crate) const TUR_TIMEOUT_MS: u32 = 5_000;
|
||||||
|
|
||||||
/// Timeout for content READ commands (READ_10 / READ_12) on the fast
|
/// Timeout for content READ commands (READ_10 / READ_12) on the fast
|
||||||
/// path — the [`disc::Disc::copy`] sweep that bisects-on-failure.
|
/// path — the `freemkv_engine::recovery::copy` sweep that bisects-on-failure.
|
||||||
///
|
///
|
||||||
/// 10 s is calibrated from live empirical data on an LG BU40N + Initio
|
/// 10 s is calibrated from live empirical data on an LG BU40N + Initio
|
||||||
/// 1618L bridge ripping a UHD with marginal sectors:
|
/// 1618L bridge ripping a UHD with marginal sectors:
|
||||||
@@ -67,7 +67,7 @@ pub(crate) const TUR_TIMEOUT_MS: u32 = 5_000;
|
|||||||
pub(crate) const READ_TIMEOUT_MS: u32 = 10_000;
|
pub(crate) const READ_TIMEOUT_MS: u32 = 10_000;
|
||||||
|
|
||||||
/// Timeout for content READ commands on the recovery path —
|
/// Timeout for content READ commands on the recovery path —
|
||||||
/// [`disc::Disc::patch`]'s targeted retries on bad ranges. Matches
|
/// `freemkv_engine::recovery::patch`'s targeted retries on bad ranges. Matches
|
||||||
/// `sg_dd`'s 60 s ceiling: long enough that any sector the drive can
|
/// `sg_dd`'s 60 s ceiling: long enough that any sector the drive can
|
||||||
/// recover at all gets the time to do so, short enough that an
|
/// recover at all gets the time to do so, short enough that an
|
||||||
/// unresponsive bus is detected before the per-range watchdog fires.
|
/// unresponsive bus is detected before the per-range watchdog fires.
|
||||||
@@ -77,7 +77,7 @@ pub(crate) const READ_TIMEOUT_MS: u32 = 10_000;
|
|||||||
/// safety ceiling, not a steady-state cost.
|
/// safety ceiling, not a steady-state cost.
|
||||||
///
|
///
|
||||||
/// Historical note (2026-05-08): briefly lowered to 2 s with a 5×
|
/// Historical note (2026-05-08): briefly lowered to 2 s with a 5×
|
||||||
/// inline retry loop in `Disc::patch` to mimic the kernel `sr_mod`
|
/// inline retry loop in `freemkv_engine::recovery::patch` to mimic the kernel `sr_mod`
|
||||||
/// driver's auto-retry pattern. The synthetic logic worked but on the
|
/// driver's auto-retry pattern. The synthetic logic worked but on the
|
||||||
/// live drive each "2 s" read paid ~1.5 s of kernel SCSI mid-layer
|
/// live drive each "2 s" read paid ~1.5 s of kernel SCSI mid-layer
|
||||||
/// error escalation on top, so 5× retries took ~17 s per LBA and
|
/// error escalation on top, so 5× retries took ~17 s per LBA and
|
||||||
@@ -237,7 +237,7 @@ impl ScsiSense {
|
|||||||
///
|
///
|
||||||
/// `false` for HARDWARE ERROR, DATA PROTECT, UNIT ATTENTION,
|
/// `false` for HARDWARE ERROR, DATA PROTECT, UNIT ATTENTION,
|
||||||
/// ILLEGAL REQUEST, BLANK CHECK, and any unknown key. Used
|
/// ILLEGAL REQUEST, BLANK CHECK, and any unknown key. Used
|
||||||
/// by [`Error::is_marginal_read`] / `Disc::copy`'s hysteresis
|
/// by [`Error::is_marginal_read`] / `freemkv_engine::recovery::copy`'s hysteresis
|
||||||
/// dispatch.
|
/// dispatch.
|
||||||
pub fn is_marginal(&self) -> bool {
|
pub fn is_marginal(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
@@ -1022,8 +1022,8 @@ mod parse_sense_tests {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod scsi_sense_predicate_tests {
|
mod scsi_sense_predicate_tests {
|
||||||
//! Classification of [`ScsiSense`] predicate methods against SPC-4
|
//! Classification of [`ScsiSense`] predicate methods against SPC-4
|
||||||
//! §4.5.6 Table 28 sense keys. These drive `Disc::copy` hysteresis
|
//! §4.5.6 Table 28 sense keys. These drive `freemkv_engine::recovery::copy` hysteresis
|
||||||
//! and `Disc::patch` routing; a misclassification here silently
|
//! and `freemkv_engine::recovery::patch` routing; a misclassification here silently
|
||||||
//! changes which sectors get retried vs. marked unreadable.
|
//! changes which sectors get retried vs. marked unreadable.
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user