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.
This commit is contained in:
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# 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<AtomicBool>)`** — 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)
|
## 0.11.17 (2026-04-23)
|
||||||
|
|
||||||
### Adaptive batch sizer in DiscStream — no more per-sector descent
|
### Adaptive batch sizer in DiscStream — no more per-sector descent
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "libfreemkv"
|
name = "libfreemkv"
|
||||||
version = "0.11.17"
|
version = "0.11.18"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.86"
|
rust-version = "1.86"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ use crate::disc::{Disc, DiscTitle, Extent};
|
|||||||
use crate::event::{BatchSizeReason, Event, EventKind};
|
use crate::event::{BatchSizeReason, Event, EventKind};
|
||||||
use crate::sector::SectorReader;
|
use crate::sector::SectorReader;
|
||||||
use std::io;
|
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
|
/// 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.
|
/// of clean reading at the current (reduced) size. 100 MiB = 51,200 sectors.
|
||||||
@@ -125,6 +127,11 @@ pub struct DiscStream {
|
|||||||
adaptive: AdaptiveBatch,
|
adaptive: AdaptiveBatch,
|
||||||
pub errors: u64,
|
pub errors: u64,
|
||||||
pub skip_errors: bool,
|
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<Arc<AtomicBool>>,
|
||||||
event_fn: Option<Box<dyn Fn(Event) + Send>>,
|
event_fn: Option<Box<dyn Fn(Event) + Send>>,
|
||||||
eof: bool,
|
eof: bool,
|
||||||
|
|
||||||
@@ -192,6 +199,7 @@ impl DiscStream {
|
|||||||
adaptive: AdaptiveBatch::new(batch_sectors),
|
adaptive: AdaptiveBatch::new(batch_sectors),
|
||||||
errors: 0,
|
errors: 0,
|
||||||
skip_errors: false,
|
skip_errors: false,
|
||||||
|
halt: None,
|
||||||
event_fn: None,
|
event_fn: None,
|
||||||
eof: false,
|
eof: false,
|
||||||
ts_demuxer,
|
ts_demuxer,
|
||||||
@@ -207,6 +215,22 @@ impl DiscStream {
|
|||||||
self.event_fn = Some(Box::new(f));
|
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<AtomicBool>) {
|
||||||
|
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) {
|
fn emit(&self, kind: EventKind) {
|
||||||
if let Some(ref f) = self.event_fn {
|
if let Some(ref f) = self.event_fn {
|
||||||
f(Event { kind });
|
f(Event { kind });
|
||||||
@@ -242,7 +266,15 @@ impl DiscStream {
|
|||||||
// Adaptive sizer: start at current (preferred until a failure), shrink
|
// Adaptive sizer: start at current (preferred until a failure), shrink
|
||||||
// on failure, advance on success. One 5s read attempt per try — no
|
// on failure, advance on success. One 5s read attempt per try — no
|
||||||
// retry loops, no sleeps. On size-1 failure, skip or error.
|
// 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 {
|
loop {
|
||||||
|
if self.is_halted() {
|
||||||
|
return Err(crate::error::Error::Halted.into());
|
||||||
|
}
|
||||||
let mut sectors = remaining.min(self.adaptive.current() as u32) as u16;
|
let mut sectors = remaining.min(self.adaptive.current() as u32) as u16;
|
||||||
// Align to 3-sector AACS units when possible. Partial units at
|
// Align to 3-sector AACS units when possible. Partial units at
|
||||||
// extent boundaries are safely handled by decrypt_sectors().
|
// extent boundaries are safely handled by decrypt_sectors().
|
||||||
|
|||||||
Reference in New Issue
Block a user