From 9b65cb8fa13afc3ddc2e069caaae7b0b886339dc Mon Sep 17 00:00:00 2001 From: Matthew Jackson Date: Fri, 24 Apr 2026 07:41:13 -0700 Subject: [PATCH] =?UTF-8?q?v0.11.18:=20DiscStream=20halt=20flag=20?= =?UTF-8?q?=E2=80=94=20Stop=20works=20in=20dense=20bad-sector=20regions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) — 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. --- CHANGELOG.md | 12 ++++++++++++ Cargo.toml | 2 +- src/mux/disc.rs | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 748cd2c..d11e60e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.11.18 (2026-04-24) + +### DiscStream halt flag — Stop works during dense bad-sector regions + +`DiscStream::fill_extents` loops internally when the demuxer hasn't accumulated enough data to emit a PES frame — during a dense bad-sector run, that loop can spend many minutes shrinking batch sizes and zero-filling sectors without ever returning to the outer read() call. Without an internal halt check, the caller's Stop request goes unserviced until the demuxer eventually emits a frame, which may be very far away. + +- **`DiscStream::set_halt(Arc)`** — share a halt flag with the stream. Typically wired to `Drive::halt_flag()` so Stop propagates across both the drive's recovery phases and the stream's sector processing. +- **`fill_extents()` checks the halt flag** at the top of every retry iteration (before each attempt at every size level). Raising the flag aborts within one read round-trip — at most the current SCSI command's timeout. +- Returns `Err(Error::Halted)` (E6010) so the outer rip pipeline terminates cleanly. + +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 where the stream was stuck in a 12+ hour bad-sector grind. + ## 0.11.17 (2026-04-23) ### Adaptive batch sizer in DiscStream — no more per-sector descent diff --git a/Cargo.toml b/Cargo.toml index dc2b9f1..76cfadc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfreemkv" -version = "0.11.17" +version = "0.11.18" edition = "2021" rust-version = "1.86" license = "AGPL-3.0-only" diff --git a/src/mux/disc.rs b/src/mux/disc.rs index a3f3897..89a6d3f 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -9,6 +9,8 @@ use crate::disc::{Disc, DiscTitle, Extent}; use crate::event::{BatchSizeReason, Event, EventKind}; use crate::sector::SectorReader; use std::io; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; /// Ramp back up to the preferred batch size after this many sectors /// of clean reading at the current (reduced) size. 100 MiB = 51,200 sectors. @@ -125,6 +127,11 @@ pub struct DiscStream { adaptive: AdaptiveBatch, pub errors: u64, pub skip_errors: bool, + /// When set and the flag is raised, fill_extents returns Err(Halted) at the + /// next retry boundary. Unlike skip_errors, this propagates the error up so + /// the rip terminates cleanly. Share the Arc with Drive::halt_flag() to get + /// unified Stop behavior across drive reads and sector processing. + halt: Option>, event_fn: Option>, eof: bool, @@ -192,6 +199,7 @@ impl DiscStream { adaptive: AdaptiveBatch::new(batch_sectors), errors: 0, skip_errors: false, + halt: None, event_fn: None, eof: false, ts_demuxer, @@ -207,6 +215,22 @@ impl DiscStream { self.event_fn = Some(Box::new(f)); } + /// Share a halt flag — typically from `Drive::halt_flag()`. When raised, + /// the next read-retry boundary inside fill_extents returns Err(Halted) + /// instead of continuing. Required for Stop to work during dense bad-sector + /// regions (where read() loops internally waiting for enough clean data to + /// emit a frame and would otherwise never check an external stop signal). + pub fn set_halt(&mut self, flag: Arc) { + self.halt = Some(flag); + } + + fn is_halted(&self) -> bool { + self.halt + .as_ref() + .map(|h| h.load(Ordering::Relaxed)) + .unwrap_or(false) + } + fn emit(&self, kind: EventKind) { if let Some(ref f) = self.event_fn { f(Event { kind }); @@ -242,7 +266,15 @@ impl DiscStream { // Adaptive sizer: start at current (preferred until a failure), shrink // on failure, advance on success. One 5s read attempt per try — no // retry loops, no sleeps. On size-1 failure, skip or error. + // + // Halt is checked at the top of every iteration — in a dense bad zone + // this loop can spend minutes shrinking and skipping sectors; without + // the check, Stop wouldn't take effect until the outer PES read() loop + // finally emits a frame, which may never happen. loop { + if self.is_halted() { + return Err(crate::error::Error::Halted.into()); + } let mut sectors = remaining.min(self.adaptive.current() as u32) as u16; // Align to 3-sector AACS units when possible. Partial units at // extent boundaries are safely handled by decrypt_sectors().