fix(peer): repair update lifecycle regressions
FINDINGS.md identified three merge blockers in the post-plan install/update flow. Updates now use FetchLatestFromPeers so the Tauri update command bypasses local manifest serving and asks peers that advertise the latest version for fresh file metadata. PeerGameDB now aggregates and validates file descriptions from latest-version peers, keeping stale cached metadata for older versions from poisoning chunk planning when filenames stay the same but sizes change. Download-to-install handoff now performs explicit async state transitions. The download task mutates Downloading to Installing or Updating under the active-operation write lock, clears the cancellation token, and then runs the install transaction. OperationGuard remains armed only as crash or abort cleanup and is disarmed after normal explicit cleanup, so final refreshes no longer race a deferred Drop cleanup. Local library index writers now serialize the load/mutate/save window with one async mutex. The index fingerprint also includes the root version.ini contents so a same-length version rewrite in the same mtime second still updates the reported local version. The tradeoff is that local index mutations are serialized in-process instead of moved into a dedicated actor. That keeps the fix small and scoped to the merge blockers while preserving the existing scanner API. Test Plan: - just fmt - just test - just clippy - just build - git diff --check Refs: - FINDINGS.md
This commit is contained in:
@@ -124,6 +124,7 @@ pub(crate) struct OperationGuard {
|
||||
active_operations: Arc<RwLock<HashMap<String, OperationKind>>>,
|
||||
active_downloads: Arc<RwLock<HashMap<String, CancellationToken>>>,
|
||||
clears_download: bool,
|
||||
armed: bool,
|
||||
}
|
||||
|
||||
impl OperationGuard {
|
||||
@@ -136,6 +137,7 @@ impl OperationGuard {
|
||||
active_operations,
|
||||
active_downloads: Arc::new(RwLock::new(HashMap::new())),
|
||||
clears_download: false,
|
||||
armed: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,13 +151,26 @@ impl OperationGuard {
|
||||
active_operations,
|
||||
active_downloads,
|
||||
clears_download: true,
|
||||
armed: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn disarm(mut self) {
|
||||
self.armed = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OperationGuard {
|
||||
fn drop(&mut self) {
|
||||
if !self.armed {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = self.id.clone();
|
||||
log::error!(
|
||||
"Operation guard is cleaning up {id}; operation ended without explicit state cleanup"
|
||||
);
|
||||
|
||||
if let Ok(mut guard) = self.active_operations.try_write() {
|
||||
guard.remove(&id);
|
||||
} else if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
@@ -238,7 +253,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn operation_guard_clears_tracking_on_completion() {
|
||||
async fn operation_guard_cleans_tracking_when_not_disarmed() {
|
||||
let id = "game-complete";
|
||||
let (active_operations, active_downloads, _) = tracked_download_state(id);
|
||||
|
||||
@@ -252,7 +267,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn operation_guard_clears_tracking_after_cancellation() {
|
||||
async fn operation_guard_cleans_tracking_after_cancellation() {
|
||||
let id = "game-cancelled";
|
||||
let (active_operations, active_downloads, cancel) = tracked_download_state(id);
|
||||
cancel.cancel();
|
||||
@@ -267,7 +282,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn operation_guard_clears_tracking_when_task_is_dropped() {
|
||||
async fn disarmed_operation_guard_does_not_clean_tracking() {
|
||||
let id = "game-finished";
|
||||
let (active_operations, active_downloads, _) = tracked_download_state(id);
|
||||
|
||||
OperationGuard::download(
|
||||
id.to_string(),
|
||||
active_operations.clone(),
|
||||
active_downloads.clone(),
|
||||
)
|
||||
.disarm();
|
||||
|
||||
assert!(active_operations.read().await.contains_key(id));
|
||||
assert!(active_downloads.read().await.contains_key(id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn operation_guard_cleans_tracking_when_task_is_dropped() {
|
||||
let id = "game-aborted";
|
||||
let (active_operations, active_downloads, _) = tracked_download_state(id);
|
||||
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
Reference in New Issue
Block a user