Commit Graph
940 Commits
Author SHA1 Message Date
MattJackson 92dc145864 v0.13.11: revert SgIoTransport timeout path — keep transport alive
v0.13.10's 'fd=-1 on first poll timeout' was too aggressive: a single
transient killed the entire transport, Pass 1 finished in 45ms with
0 GB good on Dune 2.

Revert to spawn-close + main-thread-reopen (the v0.13.5/8 pattern).
Per-timeout cost is up to ~60s while the kernel completes the
abandoned command, but the v0.13.9 Disc::copy stall guard caps
catastrophic stalls at 120s of bytes_good non-advance. Pass 1 bails
cleanly with NonTrimmed ranges; Pass 2 has a working Drive for
retries with recovery=true + 30s timeouts.
2026-04-25 08:47:10 -07:00
MattJackson 36b65f526a v0.13.10: version sync 2026-04-25 08:25:08 -07:00
MattJackson ca8ebf418f v0.13.9: Disc::copy stall guard + SgIoTransport no-reopen-on-timeout
Fixes the silent Pass 1 hang observed on Dune 2 with v0.13.8 (drive
grinding through bad sectors at 0 KB/s, errs=0, no error surfaced).

Root cause: SgIoTransport::execute's reopen-after-poll-timeout opened
a fresh /dev/sg* fd on the main thread, which serialized against the
spawned close() of the old fd via the kernel's per-device state lock.
The userspace 1.5s timeout still fired, but the abandon-and-reopen
recovery itself blocked the main thread for as long as close() took.
Net: reads returned slowly, skip-forward fired on every iteration,
bytes_good never advanced.

- SgIoTransport::execute: on poll timeout, spawn close, set fd=-1,
  return Err. No reopen on the main thread. The transport is now
  invalidated until the consumer creates a fresh Drive.
- Disc::copy: add stall guard. CopyOptions.stall_secs (default 120s).
  If bytes_good doesn't advance for the threshold, break 'outer
  cleanly with complete=false, bytes_pending > 0 so Pass 2 retries
  pick up the NonTrimmed ranges with recovery=true 30s timeouts.
- New regression test: test_disc_copy_stall_detection_triggers_
  skip_forward in tests/integration_progress_and_halt.rs.
2026-04-25 08:12:01 -07:00
MattJackson 34182d956a v0.13.8: version sync 2026-04-25 07:11:47 -07:00
MattJackson 0f967a2084 v0.13.7: version sync 2026-04-25 06:58:43 -07:00
MattJackson 0341995c0d fix: allow dead_code on Drive::emit (DiscStream owns BytesRead emission post-0.13.6 strip) 2026-04-24 21:43:30 -07:00
MattJackson fe0ec0c5cf fix: drop unused INQUIRY_* constants (clippy -D warnings) 2026-04-24 21:42:30 -07:00
MattJackson 022657d481 docs: complete 0.13.6 docs sweep (architecture, api-design, disc-to-rip, README)
Follow-up to the rip-recovery + drive-access updates: aligns the
remaining docs with the v0.13.6 single-shot read model and the
three-layer recovery architecture.

- architecture.md: module map says single-shot read; new paragraph on
  layered recovery with postmortem pointer.
- api-design.md: EventKind enum example expanded; emission notes
  document that BytesRead now fires from DiscStream::fill_extents and
  Retry/SectorRecovered are no longer emitted in 0.13.6+.
- disc-to-rip.md: Step 10 of the pipeline diagram + module table
  reflect single-shot read.
- docs/README.md: added rip-recovery.md to the TOC.
2026-04-24 21:36:49 -07:00
MattJackson fc8eca44e1 docs: rewrite rip-recovery + drive-access for 0.13.6 single-shot model
Updates docs/ to reflect the recovery-loop strip:
- rip-recovery.md: drops Phase 1/2/3 description, replaces with three-layer
  model (Disc::patch multi-pass / DiscStream batch halving / Drive::read
  single-shot). Notes that no SCSI resets fire from any retry path.
- drive-access.md: removes SG_SCSI_RESET + STOP/START UNIT escalation
  references; SgIoTransport::reset is now kernel SG_IO flush + ALLOW
  MEDIUM REMOVAL only.
- src/mux/disc.rs + tests/: cargo fmt cleanup.
2026-04-24 21:35:33 -07:00
MattJackson 43836865be v0.13.6: strip Drive::read inline recovery + reset escalation; emit BytesRead
Drive::read is now single-shot. Phase 1/2/3 retries + scsi::reset+reopen
removed (~80 lines). recovery=true bumps timeout to 30s; recovery=false
stays at 1.5s. On any failure returns Err(DiscRead) immediately — caller
(Disc::patch outer loop, DiscStream batch halver) handles retries.

Inline reset+reopen WAS the wedge primitive on the LG BU40N. Per prior
post-mortem, every USB/SCSI reset path tested fails to recover the
wedged Initio bridge — the inline retry was pure cost.

SgIoTransport::reset (Linux) trimmed to kernel SG_IO state flush +
ALLOW MEDIUM REMOVAL. SG_SCSI_RESET ioctl + STOP/START UNIT escalation
removed. macOS reset removed (no-op). scsi::reset() top-level family
removed (no callers).

EventKind::BytesRead { bytes, total } now actually emitted from
DiscStream::fill_extents after each successful sector read. Was
declared in 0.13.0, never fired. Drives autorip per-device progress
in direct mode.

EventKind::Retry / SectorRecovered no longer emitted (variants kept
for forward compat). SpeedChange still emitted via Drive::set_speed
public path.

Tests: new tests/integration_progress_and_halt.rs (5 tests). 233 unit
tests + 5 integration green.
2026-04-24 21:32:45 -07:00
MattJackson 5e22199441 v0.13.5: version sync (autorip-side fixes) 2026-04-24 20:13:07 -07:00
MattJackson 761b77bbd9 v0.13.4: roll back wedge recovery + add sysfs identity fallback
USB/SCSI recovery escalation in drive_has_disc (0.13.1-0.13.3) tested
on LG BU40N USB BD-RE: USBDEVFS_RESET, authorized toggle, driver
unbind/rebind, SCSI host rescan — all succeed at the USB transport
layer but the drive firmware below the bridge stays locked. Only
physical unplug-replug clears it. Rolled back so consumers can
surface the real failure to the user.

New: list_drives falls back to sysfs-cached vendor/model/rev from
/sys/class/scsi_generic/sgN/device/ when live INQUIRY returns empty,
so wedged drives still show their identity in UIs.

Removed: scsi::usb_reset, usb_reset_with_timeout, per-platform
usb_reset methods, recover_then_probe, is_wedge_signature. Breadcrumb
comment in scsi/linux.rs::drive_has_disc points at v0.13.3 tag for
the full implementation if future hardware needs it back.

Linux/macOS/Windows pass-through symmetric; 233 tests passing.
2026-04-24 19:53:44 -07:00
MattJackson cc05ef2a3a v0.13.3: broaden is_wedge_signature — fix dead-code wedge recovery
0.13.2's is_wedge_signature gated on opcode=SCSI_INQUIRY (0x12), but
drive_has_disc issues TEST UNIT READY (0x00). Production wedge errors
(E4000: 0x00/0xff/0x00) never matched → SCSI reset + USB reset
escalation never fired.

Drop the opcode gate. Status byte 0xFF is synthesised by our own
execute() path on poll() timeout — it's the ground-truth wedge
marker for any opcode.

Linux-only; macOS/Windows use sense-key-based wedge detection.
2026-04-24 19:26:11 -07:00
MattJackson e27b82ce5b v0.13.2: list_drives + drive_has_disc; SCSI primitives pub(crate)
Architectural cleanup. autorip + freemkv CLI were reimplementing drive
discovery (sysfs walking, type-5 filtering, sg-path construction) and
calling SCSI reset primitives directly. All of that hardware-aware code
moves into libfreemkv with two cheap public probes:

- DriveInfo + list_drives() — multi-OS enumeration (Linux/macOS/Windows)
  with peripheral-type-5 filtering and INQUIRY identity. Cheap.
- drive_has_disc(path) — single TUR with internal wedge recovery
  escalation (SCSI reset → USB reset → retry) hidden from callers.

USB-layer reset (USBDEVFS_RESET / IOUSBDeviceInterface::ResetDevice /
storport's combined reset) wired across all three platforms.

Visibility tightening — scsi::reset, scsi::usb_reset, and the timeout
constants are now pub(crate). Compile-time guarantee that no consumer
crate can issue SCSI commands directly.

233 lib tests pass; clippy clean.
2026-04-24 17:31:15 -07:00
MattJackson 8af47e4c19 v0.13.1: scsi::reset() bounded by wallclock timeout
Production incident: autorip's poll loop called scsi::reset() on a
wedged BU40N USB drive. The Linux SG_SCSI_RESET ioctl blocked
indefinitely (kernel SCSI subsystem waiting for a bus-wedged device to
ack a reset that will never come). Caller's poll loop hung for 60+
seconds before manual intervention.

scsi::reset() now spawns a detached worker for the platform-specific
reset and bounds the caller's wait via mpsc::recv_timeout. Default
30 s (DEFAULT_RESET_TIMEOUT_SECS); reset_with_timeout(device, dur)
exposes the bound for callers that want a different value. Returns
DeviceResetFailed on timeout. Worker thread keeps running until the
kernel eventually unblocks — leaks one OS thread per hard wedge, but
the daemon stays responsive instead of hanging forever.

Follow-up flagged for 0.13.2: USB-attached drives wedge at the USB
Mass Storage layer below SCSI; SG_SCSI_RESET doesn't help. A
scsi::usb_reset(path) using USBDEVFS_RESET is the proper escalation.
2026-04-24 16:58:13 -07:00
MattJackson d1f09439a5 v0.13.0: zero English in library + API hygiene + dead-code sweep
Audit pass against the project docs "no English text in library code" rule.
Found 9 call sites that violated the contract by stuffing English into
io::Error::new(kind, "…") or by abusing Error::DeviceNotFound { path }
as a free-form description field. Each is now a typed Error variant.

New variants and codes: ScsiInterfaceUnavailable (E1004), DeviceLocked
(E1005), IoKitPluginFailed (E1006), UnsupportedPlatform (E2003),
PlatformNotImplemented (E2004), MapfileInvalid (E6011), DiscUrlNotDirect
(E9009).

labels::apply() previously pushed Commentary/Descriptive/Score/IME and
" (Secondary)" English literals into AudioStream.label, leaking into
MKV titles + autorip UI. AudioStream now exposes structured `purpose:
LabelPurpose`, SubtitleStream `qualifier: LabelQualifier`. Callers
translate to localized text. label keeps codec-formatting only.

API hygiene: 11 mux/* modules dropped from `pub` to `pub(crate)` —
their *types* are still re-exported from lib.rs, but the modules were
leaking low-level EBML/TS/network primitives. Stream trait gets a real
rustdoc explaining read-vs-write split. lib.rs grouped re-exports into
documented sections. ScanOptions::with_keydb() removed (one-method-per-
action rule); use struct literal.

Dead-code sweep: removed lookahead.rs (orphan, never declared as mod),
tsreader.rs (TsDemuxReader unused), ebml::{write_int,read_vint,SEEK_*},
ts::{scan_first/last_pts,scan_duration,SCAN_HEAD/TAIL_SIZE,take/set_
remainder}, MkvMuxer codec_private_slots/filled fields and
fill_codec_private method (deferred-codecPrivate path never used since
the v0.10 PES rewrite). cargo clippy --all-targets -D warnings clean.

Tests: new error::tests for variant codes + Display "no English" guard +
io::ErrorKind mapping. 233 lib tests, all green (was 230).

Breaking: ScanOptions::with_keydb removed; mux/* modules pub(crate);
AudioStream and SubtitleStream gained required fields; UnsupportedDrive
{ product_revision: "Renesas not yet implemented" } no longer produced
(use PlatformNotImplemented).
2026-04-24 16:41:02 -07:00
MattJackson 37e721ee7e v0.12.2: hide halt behind checked_sleep / checked_exec primitives
Drive::read is now halt-check-free in its body. Previously, the halt
flag was checked in 4 places and the sleep logic was scattered across
4 "if halt_aware_sleep { return Halted }" call sites — correct, but
the ugliness invited drift: a new sleep added by someone unfamiliar
with the pattern would silently swallow Stop requests.

Two private primitives now own halt awareness:

  checked_sleep(Duration) -> Result<()>
  checked_exec(cdb, dir, buf, timeout_ms) -> Result<ScsiResult>

Both return Err(Halted) instead of a bool. The ? operator in read()
then propagates halts for free. The recovery path reads top-to-bottom
with no halt vocabulary.

sleep_until_halted lives as a free function so it's unit-testable
without a live Drive. 4 new tests: completes normally, bails on
pre-set flag within one slice, wakes mid-sleep, zero-duration no-op.

Public API unchanged — halt_flag/halt/clear_halt still exposed, the
refactor is entirely internal.
2026-04-24 13:12:24 -07:00
MattJackson 481b47d90d v0.12.1: halt-aware sleeps in Drive::read recovery
Stop was waiting up to 30 s to register when the drive hit L-EC
recovery mid-read — the recovery phase does 30 s sleeps between
retries and the halt flag was only checked at the start of each sleep.
UI feels broken ("stop isnt working") even though the halt was set.

halt_aware_sleep breaks each wait into 100 ms slices and returns
early on halt. Applied to all 4 sleeps in the recovery path (both
30 s retry delays, both 5 s reset-phase delays).
2026-04-24 12:59:36 -07:00
MattJackson dd7cbd70a9 v0.12.1: cut non-recovery read timeout 5s → 1500ms
Disc::copy fast pass (skip_on_error=true, recovery=false) was giving
the drive 5 s per 64 KB block. On structure-protected / marginal UHD
sectors the drive grinds L-EC for nearly the full budget per block,
pinning throughput at ~13 KB/s even though skip_forward would happily
skip past the region.

1500ms bounds the floor at ~43 KB/s/block. Recoverable sectors that
would have succeeded at 3-5 s get picked up on Disc::patch (pass 2+)
where recovery=true and the per-read budget is 30 s.
2026-04-24 12:57:53 -07:00
MattJackson 3dea679dac style: cargo fmt 2026-04-24 12:23:42 -07:00
MattJackson 64ecca0d12 fix(windows): Rust 2024 requires unsafe extern blocks
Missed in the prior 2024 edition sweep because local builds don't
exercise the cfg(target_os = "windows") path. check-windows CI caught it.
2026-04-24 12:21:43 -07:00
MattJackson 84f6baba38 v0.12.0: Rust 2024 edition migration
- edition = "2024" bump.
- FFI block in src/scsi/macos.rs wrapped in `unsafe extern "C" { }`.
- vtable_fn body gets an explicit unsafe block (unsafe_op_in_unsafe_fn).
- Match-ergonomics cleanup in mux/meta.rs, mkvstream.rs, network.rs,
  stdio.rs — removed redundant `ref` / `ref mut` bindings.

MSRV unchanged at 1.86. 226 tests pass. No behavior change.
2026-04-24 12:07:05 -07:00
MattJackson bd5b7795bd docs: multi-pass recovery — README snippet + new rip-recovery.md
- README quick-start gains a multi-pass example using the new
  Disc::copy + Disc::patch primitives.
- New docs/rip-recovery.md documents the two-stage rip model: mapfile
  format (ddrescue-compatible), CopyOptions/PatchOptions surface, the
  pass-1/pass-2 algorithm, and the design decisions (why no MODE
  SELECT, why ISO intermediate, why ddrescue mapfile).

No code change.
2026-04-24 11:53:28 -07:00
MattJackson a57506c1ce v0.11.22: version sync — no functional changes
autorip 0.11.22 ships the full multi-pass UI (live mapfile stats,
bad-range viz, Recovery settings). libfreemkv API is unchanged from
0.11.21. Part of the 0.11.22 ecosystem sync.
2026-04-24 11:43:32 -07:00
MattJackson c33f3e9557 v0.11.21: multi-pass rip — Disc::copy + Disc::patch + mapfile module
New primitives for two-stage rip workflows: fast forward pass with
zero-fill on failures, then targeted retries of bad ranges via a
ddrescue-compatible mapfile.

- Disc::copy now takes &CopyOptions (breaking change from positional
  args). Always writes a sidecar .mapfile. Opt-in skip_on_error +
  skip_forward give ddrescue-style fast sweep: 64 KB blocks,
  exponential skip-forward on failure, zero-fill bad blocks. Defaults
  preserve pre-0.11.21 behavior (recovery reads, abort on bad sector).

- Disc::patch is new and idempotent. Reads the mapfile, re-reads every
  non-finished range with full drive recovery, patches good bytes back
  into the ISO at exact offsets. Call N times for N retry attempts.

- disc::mapfile is a new module. ddrescue text format, crash-safe
  (flushed on every record()), greppable, human-editable, tool-compatible.
  Status chars match ddrescue: ? / * / / / - / +.

- Re-exports FileSectorReader from the crate root.

- freemkv CLI caller (pipe.rs) updated to the new Disc::copy signature
  in lockstep — shipped in the 0.11.21 freemkv CLI release.

Part of the 0.11.21 ecosystem sync (libfreemkv + freemkv + bdemu +
autorip all on 0.11.21).
2026-04-24 09:24:32 -07:00
MattJackson 166fc4bf8c v0.11.18: DiscStream halt flag — Stop works in dense bad-sector regions
DiscStream::fill_extents loops internally while the demuxer waits for
enough clean data to emit a PES frame. In a dense bad zone that loop
can run for minutes without returning to the outer read() call, so
the caller's Stop signal never gets serviced until a frame is finally
emitted — which may be very far away.

Add DiscStream::set_halt(Arc<AtomicBool>) — typically wired to
Drive::halt_flag() for unified Stop across drive recovery phases and
stream sector processing. fill_extents checks the flag at the top of
every retry iteration; raising it returns Err(Error::Halted) within
one SCSI round-trip.

No behavior change for callers that don't call set_halt. Unblocks the
architectural fix for the "Stop doesn't stop" bug observed on a
damaged UHD disc.
2026-04-24 07:41:13 -07:00
MattJackson 9aaddaa9b8 v0.11.17: adaptive batch sizer — no per-sector descent
Replace read_with_binary_search + 3×5s light recovery with an adaptive
sizer that shrinks on failure (halve, 3-aligned ≥6) and probes back up
after 100 MiB (51,200 sectors) of clean reads. Descent cost is paid
once per bad region, not once per bad sector.

Emit BatchSizeChanged { new_size, reason } on shrink and probe-up.
Remove BinarySearch event — no longer produced.

Side fix: scsi/macos.rs one-liner for manual_c_str_literals clippy
lint that surfaced on a newer toolchain.
2026-04-23 20:24:40 -07:00
Matt Jackson 40ec1e1eba v0.11.16: API cleanup — one method per action 2026-04-21 19:17:50 +00:00
Matt Jackson 8843833ea7 v0.11.15: lint cleanup — fmt + clippy clean 2026-04-21 18:52:55 +00:00
Matt Jackson afae37c8c3 fix: iso_dump example missing recovery arg 2026-04-21 18:41:41 +00:00
Matt Jackson d8089ec491 v0.11.14: audit fixes — trailing sectors, verify stop, SCSI sense, O_CLOEXEC
Fix trailing sectors dropped at extent boundaries when sector_count % 3 != 0.
Add verify_title stop support via progress callback returning bool.
Add O_CLOEXEC on all SCSI fd opens to prevent leak to child processes.
Fix SCSI sense descriptor format detection (0x72/0x73 vs 0x70/0x71).
2026-04-21 18:40:04 +00:00
Matt Jackson ce8132d181 Update docs: async sg transport, Drive::read recovery phases 2026-04-21 18:01:48 +00:00
Matt Jackson 71e1f57364 Async SG_IO: enforceable timeouts via write/poll/read
Replace blocking ioctl(SG_IO) with the sg driver's async interface.
Commands are submitted via write(), waited on via poll() with a hard
wall-clock timeout, and completed via read(). If poll() times out,
the fd is abandoned and a fresh one opened — the kernel can no longer
hold us hostage during USB error recovery.

- write() submits command, returns immediately
- poll() enforces exact timeout (EINTR-safe with deadline tracking)
- read() retrieves result + copies data to caller's buffer
- On timeout: old fd closed in background thread, new fd opened
- No SG_FLAG_DIRECT_IO — kernel buffers for safe timeout abandonment
- Store device_path for fd reopen after timeout
- Drop guards fd=-1 (abandoned fd)
2026-04-21 17:57:58 +00:00
Matt Jackson d8092ee093 Clean API: merge read() and read_fast() into read(recovery: bool) 2026-04-21 03:27:17 +00:00
Matt Jackson dd49d55b11 v0.11.13: all rip reads use fast timeout, no full recovery in read path 2026-04-21 02:16:49 +00:00
Matt Jackson 6ba958fe8a Fix: initial batch read uses fast read, not full recovery 2026-04-21 02:02:13 +00:00
Matt Jackson f30b958408 v0.11.12: halt, events, light recovery 2026-04-21 00:25:44 +00:00
Matt Jackson 6971f00be2 Drive halt flag, sector events, binary search light recovery (3x5s) 2026-04-21 00:18:46 +00:00
Matt Jackson ad86f92d85 v0.11.11: binary search error recovery in fill_extents 2026-04-20 21:51:46 +00:00
Matt Jackson cce1be29b2 v0.11.10: skip_errors, read_sectors_recover API 2026-04-20 15:27:56 +00:00
Matt Jackson 081ad4f619 DiscStream skip_errors, read_sectors_recover(recovery) API 2026-04-20 03:38:32 +00:00
Matt Jackson 43ecad2d43 Clean API: read_sectors_recover(recovery: bool) replaces read_sectors_fast 2026-04-20 01:12:59 +00:00
Matt Jackson 8870efa77b v0.11.9: fast verify reads — 5s timeout, no recovery loop 2026-04-20 01:01:44 +00:00
Matt Jackson a693ef965b v0.11.8: disc verify module 2026-04-20 00:03:26 +00:00
Matt Jackson 0596307c19 Add verify module: sector-by-sector disc health check 2026-04-20 00:01:02 +00:00
Matt Jackson 18c6365e08 Improve label generator: proper video labels, all resolutions, no false Atmos claims 2026-04-19 17:50:17 +00:00
Matt Jackson 981f30b1b0 Move label generation to labels system — fill_defaults() for all stream types 2026-04-19 17:23:50 +00:00
Matt Jackson 04086d73a0 Generate audio track labels when disc doesn't provide them 2026-04-19 16:34:35 +00:00
Matt Jackson 20e404e59a README: fix dep version 0.10 → 0.11, update share command 2026-04-19 04:57:55 +00:00
Matt Jackson 1bea8eb650 v0.11.7: TrueHD parser rewrite — 12-bit length, AC-3 skip, cross-PES buffering 2026-04-19 01:36:56 +00:00