mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-09 10:49:29 +00:00
fix(properties): harden progress and torrent diagnostics
This commit is contained in:
+48
-17
@@ -8494,6 +8494,8 @@ async fn verify_torrent_data(
|
||||
let metadata = crate::torrent::parse_torrent_bytes(&bytes)?;
|
||||
crate::torrent::validate_info_hash(item.torrent_info_hash.as_deref(), &metadata.info_hash)?;
|
||||
let restore_status = item.status.as_str().to_string();
|
||||
let restore_status_value = item.status;
|
||||
let restore_has_been_dispatched = item.has_been_dispatched;
|
||||
let destination = verification_destination(&app_handle, &item)?;
|
||||
if matches!(item.status, crate::ipc::DownloadStatus::Paused) {
|
||||
detach_download_for_reconfigure_locked(
|
||||
@@ -8508,6 +8510,11 @@ async fn verify_torrent_data(
|
||||
{
|
||||
return Err("Torrent lifecycle changed; pause it before verifying its data".to_string());
|
||||
}
|
||||
// Allocate the internally-created generation after detaching any retained
|
||||
// paused lifecycle, so the value includes every cancellation watermark
|
||||
// observed during that transition. Reservation still remains the atomic
|
||||
// acceptance point immediately before ownership registration.
|
||||
let verification_generation = state.queue_manager.next_enqueue_generation(&id).await?;
|
||||
let enqueue_item = queue::EnqueueItem {
|
||||
id: item.id.clone(),
|
||||
queue_id: item
|
||||
@@ -8559,7 +8566,7 @@ async fn verify_torrent_data(
|
||||
torrent_file_allocation: item.torrent_file_allocation.clone(),
|
||||
torrent_verify_only: Some(true),
|
||||
torrent_verify_restore_status: Some(restore_status.clone()),
|
||||
lifecycle_generation: None,
|
||||
lifecycle_generation: Some(verification_generation.to_string()),
|
||||
};
|
||||
|
||||
{
|
||||
@@ -8589,9 +8596,10 @@ async fn verify_torrent_data(
|
||||
enqueue_download_locked(&app_handle, state.inner(), enqueue_item, &control_guard).await
|
||||
{
|
||||
// Roll back only this verification marker, and only while the row is
|
||||
// still the queued verification lifecycle. Never restore the stale
|
||||
// full array captured before enqueue: frontend persistence or another
|
||||
// command may have changed unrelated rows in the meantime.
|
||||
// still this verification lifecycle (queued or already restored by a
|
||||
// renderer snapshot). Never restore the stale full array captured
|
||||
// before enqueue: frontend persistence or another command may have
|
||||
// changed unrelated rows in the meantime.
|
||||
let rollback_result = (|| {
|
||||
let mut connection = database.lock()?;
|
||||
crate::db::mutate_download(
|
||||
@@ -8599,18 +8607,20 @@ async fn verify_torrent_data(
|
||||
&id,
|
||||
database.is_portable(),
|
||||
|object| {
|
||||
let is_verification_marker = object
|
||||
.get("status")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some("queued")
|
||||
&& object
|
||||
.get("torrentVerifyOnly")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
== Some(true)
|
||||
let has_verification_marker = object
|
||||
.get("torrentVerifyOnly")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
== Some(true)
|
||||
&& object
|
||||
.get("torrentVerifyRestoreStatus")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some(restore_status.as_str());
|
||||
let current_status = object
|
||||
.get("status")
|
||||
.and_then(serde_json::Value::as_str);
|
||||
let is_verification_marker = has_verification_marker
|
||||
&& (current_status == Some("queued")
|
||||
|| current_status == Some(restore_status.as_str()));
|
||||
// The native marker is an additional persistence fence,
|
||||
// not a prerequisite for rollback. A renderer snapshot
|
||||
// may have already acknowledged and removed that native
|
||||
@@ -8621,18 +8631,39 @@ async fn verify_torrent_data(
|
||||
"status".to_string(),
|
||||
serde_json::json!(restore_status),
|
||||
);
|
||||
match restore_has_been_dispatched {
|
||||
Some(value) => {
|
||||
object.insert(
|
||||
"hasBeenDispatched".to_string(),
|
||||
serde_json::json!(value),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
object.remove("hasBeenDispatched");
|
||||
}
|
||||
}
|
||||
object.remove("torrentVerifyOnly");
|
||||
object.remove("torrentVerifyRestoreStatus");
|
||||
object.remove("torrentVerifyNative");
|
||||
}
|
||||
Ok(())
|
||||
Ok(is_verification_marker)
|
||||
},
|
||||
)
|
||||
})();
|
||||
if let Err(rollback_error) = rollback_result {
|
||||
return Err(format!(
|
||||
"{error}; failed to roll back Torrent verification state: {rollback_error}"
|
||||
));
|
||||
let rollback_applied = match rollback_result {
|
||||
Ok(applied) => applied,
|
||||
Err(rollback_error) => {
|
||||
return Err(format!(
|
||||
"{error}; failed to roll back Torrent verification state: {rollback_error}"
|
||||
));
|
||||
}
|
||||
};
|
||||
if rollback_applied {
|
||||
use tauri::Emitter;
|
||||
let _ = app_handle.emit(
|
||||
"download-state",
|
||||
crate::ipc::DownloadStateEvent::new(&id, restore_status_value),
|
||||
);
|
||||
}
|
||||
return Err(error.to_string());
|
||||
}
|
||||
|
||||
@@ -1615,6 +1615,21 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
.or_insert(generation);
|
||||
}
|
||||
|
||||
/// Return a generation newer than every observed or cancelled enqueue for
|
||||
/// an internally-created lifecycle. The caller still passes this value to
|
||||
/// `reserve_enqueue_generation`, which performs the atomic ownership and
|
||||
/// cancellation checks before the enqueue is committed.
|
||||
pub async fn next_enqueue_generation(&self, id: &str) -> Result<u64, String> {
|
||||
let cancellations = self.enqueue_cancellations.lock().await;
|
||||
let generations = self.enqueue_generations.lock().await;
|
||||
let previous_generation = generations.get(id).copied().unwrap_or_default();
|
||||
let cancelled_generation = cancellations.get(id).copied().unwrap_or_default();
|
||||
previous_generation
|
||||
.max(cancelled_generation)
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| "Download lifecycle generation exhausted".to_string())
|
||||
}
|
||||
|
||||
/// Atomically reserve an ID after rejecting cancelled or replayed generations.
|
||||
/// The returned watermark must be passed to `rollback_enqueue_reservation`
|
||||
/// if ownership registration fails before the task is committed.
|
||||
@@ -8184,6 +8199,27 @@ mod tests {
|
||||
assert!(item.into_task().payload.torrent_remove_unselected_file);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn internally_created_enqueue_advances_past_cancelled_generation() {
|
||||
let app = tauri::test::mock_builder()
|
||||
.build(tauri::test::mock_context(tauri::test::noop_assets()))
|
||||
.expect("mock app");
|
||||
let manager = QueueManager::test_new(app.handle().clone(), 1, Arc::new(TestSpawner));
|
||||
|
||||
manager.cancel_enqueue_generation("torrent", 4).await;
|
||||
let generation = manager
|
||||
.next_enqueue_generation("torrent")
|
||||
.await
|
||||
.expect("a fresh generation should be available");
|
||||
|
||||
assert_eq!(generation, 5);
|
||||
manager
|
||||
.reserve_enqueue_generation("torrent", generation)
|
||||
.await
|
||||
.expect("the fresh generation should not be rejected as stale");
|
||||
assert_eq!(manager.registered_lifecycle_generation("torrent").await, Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torrent_options_reject_invalid_seed_values() {
|
||||
let mut options = serde_json::Map::new();
|
||||
|
||||
Reference in New Issue
Block a user