From 8ca5a417ac124e5462bf0bca09c9f04686208f6d Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:53:01 +0400 Subject: [PATCH] fix: no launch while pulling --- src-tauri/src/api_server.rs | 4 +++- src-tauri/src/sync/engine.rs | 36 ++++++++++++++++++++++++++++++++---- src-tauri/src/sync/lease.rs | 6 +++++- src/i18n/locales/de.json | 3 ++- src/i18n/locales/en.json | 3 ++- src/i18n/locales/es.json | 3 ++- src/i18n/locales/fr.json | 3 ++- src/i18n/locales/ja.json | 3 ++- src/i18n/locales/ko.json | 3 ++- src/i18n/locales/pt.json | 3 ++- src/i18n/locales/ru.json | 3 ++- src/i18n/locales/tr.json | 3 ++- src/i18n/locales/vi.json | 3 ++- src/i18n/locales/zh.json | 3 ++- src/lib/backend-errors.ts | 4 ++++ 15 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs index 99e5b33d..9434f466 100644 --- a/src-tauri/src/api_server.rs +++ b/src-tauri/src/api_server.rs @@ -1532,9 +1532,10 @@ pub async fn get_api_server_status() -> Result, String> { /// apart from a validation failure, cannot drift from the guards that produce /// them. `PROFILE_REMOTE_SYNC_PENDING` in particular is temporary by nature: the /// pull that clears it is already running. -const LAUNCH_CONFLICT_CODES: [&str; 6] = [ +const LAUNCH_CONFLICT_CODES: [&str; 7] = [ "PROFILE_RUNNING", "PROFILE_LEASE_HELD", + "PROFILE_STILL_SYNCING", "PROFILE_RUNNING_REMOTELY", "PROFILE_REMOTE_SYNC_PENDING", "PROFILE_LOCKED_BY_MEMBER", @@ -7123,6 +7124,7 @@ mod tests { "PROFILE_LOCKED_BY_MEMBER", "PROFILE_LOCKED_ELSEWHERE", "PROFILE_LEASE_HELD", + "PROFILE_STILL_SYNCING", ] { let (status, body) = manager_error_response(serde_json::json!({ "code": code }).to_string()); assert_eq!(status, StatusCode::CONFLICT, "{code} must be a 409"); diff --git a/src-tauri/src/sync/engine.rs b/src-tauri/src/sync/engine.rs index 7c41d6e5..1717ba42 100644 --- a/src-tauri/src/sync/engine.rs +++ b/src-tauri/src/sync/engine.rs @@ -558,15 +558,20 @@ const LAUNCH_PULL_STOP: std::time::Duration = std::time::Duration::from_secs(30) /// Brings the profile up to date before its browser starts, so a device that /// starts after another one never opens an older cookie jar: the site would /// revoke the session on both. A normal sync with the download guard. A -/// failure or a timeout launches with what this device has. -pub(super) async fn pull_before_launch(app_handle: &tauri::AppHandle, profile: &BrowserProfile) { +/// failure or a timeout launches with what this device has, unless the +/// timed-out download is still writing into the profile: then the launch is +/// refused (`launch_after_pull`). +pub(super) async fn pull_before_launch( + app_handle: &tauri::AppHandle, + profile: &BrowserProfile, +) -> Result<(), String> { let started = Instant::now(); let profile_id = profile.id.to_string(); let engine = match SyncEngine::create_from_settings(app_handle).await { Ok(engine) => engine, Err(e) => { log::warn!("Profile not pulled before launch profile={profile_id} err=\"{e}\""); - return; + return Ok(()); } }; let app = app_handle.clone(); @@ -590,12 +595,26 @@ pub(super) async fn pull_before_launch(app_handle: &tauri::AppHandle, profile: & Err(_) => { request_sync_cancel(&profile_id); let stopped = tokio::time::timeout(LAUNCH_PULL_STOP, task).await.is_ok(); + let decision = launch_after_pull(stopped); log::warn!( - "Profile pull before launch timed out, launching profile={profile_id} stopped={stopped} elapsed_ms={}", + "Profile pull before launch timed out profile={profile_id} stopped={stopped} launch={} elapsed_ms={}", + decision.is_ok(), started.elapsed().as_millis() ); + return decision; } } + Ok(()) +} + +/// Whether a launch may go on after its pull timed out: never while the +/// download can still write into the profile under the browser. +fn launch_after_pull(stopped: bool) -> Result<(), String> { + if stopped { + Ok(()) + } else { + Err(crate::backend_error("PROFILE_STILL_SYNCING")) + } } /// Notes the copy both sides hold now, and ends a taken-over session's hold @@ -5003,6 +5022,15 @@ pub async fn rollover_encryption_for_all_entities( #[cfg(test)] mod tests { + #[test] + fn a_launch_waits_for_a_pull_that_is_still_writing() { + assert!(super::launch_after_pull(true).is_ok()); + assert_eq!( + super::launch_after_pull(false).unwrap_err(), + r#"{"code":"PROFILE_STILL_SYNCING"}"# + ); + } + #[test] fn a_resumed_download_skips_a_file_only_at_the_same_content() { let entry = |hash: &str| super::super::manifest::ManifestFileEntry { diff --git a/src-tauri/src/sync/lease.rs b/src-tauri/src/sync/lease.rs index 97c61cff..fafce4b8 100644 --- a/src-tauri/src/sync/lease.rs +++ b/src-tauri/src/sync/lease.rs @@ -196,7 +196,11 @@ pub(crate) async fn acquire(profile: &BrowserProfile, force: bool) -> Result