fix: no launch while pulling

This commit is contained in:
zhom committed 2026-11-04 02:53:01 +04:00
1 parent a36b447360
commit 8ca5a417ac
15 files changed
+66 -17

No files matched your search

+3 -1
View File
@@ -1532,9 +1532,10 @@ pub async fn get_api_server_status() -> Result<Option<u16>, 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");
+32 -4
View File
@@ -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 {
+5 -1
View File
@@ -196,7 +196,11 @@ pub(crate) async fn acquire(profile: &BrowserProfile, force: bool) -> Result<boo
// followed by a launch, which pulls then.
if taken && !force {
if let Some(app) = APP.get() {
super::engine::pull_before_launch(app, profile).await;
if let Err(refusal) = super::engine::pull_before_launch(app, profile).await {
// No browser starts, so the lease is not needed.
release(&profile.id.to_string(), "pull_unfinished").await;
return Err(refusal);
}
}
}
Ok(taken)
+2 -1
View File
@@ -2978,7 +2978,8 @@
"siteStorageUnsafePath": "Diese Websitedaten verweisen außerhalb des Profilordners, daher hat Donut sie nicht geändert.",
"siteStorageIoFailed": "Donut konnte die Websitedaten nicht ändern: {{detail}}",
"agentChatNotFound": "Dieser Chat existiert nicht mehr.",
"profileLeaseHeld": "Dieses Profil ist auf {{device}} geöffnet. Es wird um {{time}} frei oder wenn dieses Gerät es schließt."
"profileLeaseHeld": "Dieses Profil ist auf {{device}} geöffnet. Es wird um {{time}} frei oder wenn dieses Gerät es schließt.",
"profileStillSyncing": "Dieses Profil wird noch synchronisiert. Versuchen Sie es gleich erneut."
},
"rail": {
"profiles": "Profile",
+2 -1
View File
@@ -2978,7 +2978,8 @@
"siteStorageUnsafePath": "This site data links outside the profile folder, so Donut did not change it.",
"siteStorageIoFailed": "Donut could not change the site data: {{detail}}",
"agentChatNotFound": "This chat no longer exists.",
"profileLeaseHeld": "This profile is open on {{device}}. It becomes free at {{time}}, or when that device closes it."
"profileLeaseHeld": "This profile is open on {{device}}. It becomes free at {{time}}, or when that device closes it.",
"profileStillSyncing": "This profile is still syncing. Try again shortly."
},
"rail": {
"profiles": "Profiles",
+2 -1
View File
@@ -2998,7 +2998,8 @@
"siteStorageUnsafePath": "Estos datos del sitio apuntan fuera de la carpeta del perfil, así que Donut no los cambió.",
"siteStorageIoFailed": "Donut no pudo cambiar los datos del sitio: {{detail}}",
"agentChatNotFound": "Este chat ya no existe.",
"profileLeaseHeld": "Este perfil está abierto en {{device}}. Quedará libre a las {{time}} o cuando ese dispositivo lo cierre."
"profileLeaseHeld": "Este perfil está abierto en {{device}}. Quedará libre a las {{time}} o cuando ese dispositivo lo cierre.",
"profileStillSyncing": "Este perfil aún se está sincronizando. Inténtalo de nuevo en breve."
},
"rail": {
"profiles": "Perfiles",
+2 -1
View File
@@ -2998,7 +2998,8 @@
"siteStorageUnsafePath": "Ces données de site pointent hors du dossier du profil, Donut ne les a donc pas modifiées.",
"siteStorageIoFailed": "Donut n'a pas pu modifier les données du site : {{detail}}",
"agentChatNotFound": "Ce chat n’existe plus.",
"profileLeaseHeld": "Ce profil est ouvert sur {{device}}. Il sera libre à {{time}}, ou quand cet appareil le fermera."
"profileLeaseHeld": "Ce profil est ouvert sur {{device}}. Il sera libre à {{time}}, ou quand cet appareil le fermera.",
"profileStillSyncing": "Ce profil est encore en cours de synchronisation. Réessayez dans un instant."
},
"rail": {
"profiles": "Profils",
+2 -1
View File
@@ -2978,7 +2978,8 @@
"siteStorageUnsafePath": "このサイトデータはプロファイルフォルダーの外を指しているため、Donut は変更しませんでした。",
"siteStorageIoFailed": "Donut はサイトデータを変更できませんでした: {{detail}}",
"agentChatNotFound": "このチャットはもう存在しません。",
"profileLeaseHeld": "このプロファイルは {{device}} で開かれています。{{time}} またはそのデバイスが閉じると使用できます。"
"profileLeaseHeld": "このプロファイルは {{device}} で開かれています。{{time}} またはそのデバイスが閉じると使用できます。",
"profileStillSyncing": "このプロファイルはまだ同期中です。しばらくしてからもう一度お試しください。"
},
"rail": {
"profiles": "プロファイル",
+2 -1
View File
@@ -2978,7 +2978,8 @@
"siteStorageUnsafePath": "이 사이트 데이터가 프로필 폴더 밖을 가리켜 Donut이 변경하지 않았습니다.",
"siteStorageIoFailed": "Donut이 사이트 데이터를 변경하지 못했습니다: {{detail}}",
"agentChatNotFound": "이 채팅은 더 이상 없습니다.",
"profileLeaseHeld": "이 프로필은 {{device}}에서 열려 있습니다. {{time}}에 또는 해당 기기가 닫으면 사용할 수 있습니다."
"profileLeaseHeld": "이 프로필은 {{device}}에서 열려 있습니다. {{time}}에 또는 해당 기기가 닫으면 사용할 수 있습니다.",
"profileStillSyncing": "이 프로필은 아직 동기화 중입니다. 잠시 후 다시 시도하세요."
},
"rail": {
"profiles": "프로필",
+2 -1
View File
@@ -2998,7 +2998,8 @@
"siteStorageUnsafePath": "Estes dados do site apontam para fora da pasta do perfil, então o Donut não os alterou.",
"siteStorageIoFailed": "O Donut não conseguiu alterar os dados do site: {{detail}}",
"agentChatNotFound": "Este chat não existe mais.",
"profileLeaseHeld": "Este perfil está aberto em {{device}}. Fica livre às {{time}} ou quando esse dispositivo o fechar."
"profileLeaseHeld": "Este perfil está aberto em {{device}}. Fica livre às {{time}} ou quando esse dispositivo o fechar.",
"profileStillSyncing": "Este perfil ainda está a sincronizar. Tente novamente daqui a pouco."
},
"rail": {
"profiles": "Perfis",
+2 -1
View File
@@ -3018,7 +3018,8 @@
"siteStorageUnsafePath": "Эти данные сайта указывают за пределы папки профиля, поэтому Donut их не изменил.",
"siteStorageIoFailed": "Donut не удалось изменить данные сайта: {{detail}}",
"agentChatNotFound": "Этого чата больше нет.",
"profileLeaseHeld": "Этот профиль открыт на {{device}}. Он освободится в {{time}} или когда это устройство его закроет."
"profileLeaseHeld": "Этот профиль открыт на {{device}}. Он освободится в {{time}} или когда это устройство его закроет.",
"profileStillSyncing": "Этот профиль ещё синхронизируется. Повторите попытку чуть позже."
},
"rail": {
"profiles": "Профили",
+2 -1
View File
@@ -2978,7 +2978,8 @@
"siteStorageUnsafePath": "Bu site verisi profil klasörünün dışını gösteriyor, bu yüzden Donut onu değiştirmedi.",
"siteStorageIoFailed": "Donut site verisini değiştiremedi: {{detail}}",
"agentChatNotFound": "Bu sohbet artık yok.",
"profileLeaseHeld": "Bu profil {{device}} üzerinde açık. {{time}} saatinde veya o cihaz kapattığında serbest kalır."
"profileLeaseHeld": "Bu profil {{device}} üzerinde açık. {{time}} saatinde veya o cihaz kapattığında serbest kalır.",
"profileStillSyncing": "Bu profil hâlâ eşitleniyor. Birazdan yeniden deneyin."
},
"rail": {
"profiles": "Profiller",
+2 -1
View File
@@ -2978,7 +2978,8 @@
"siteStorageUnsafePath": "Dữ liệu trang này trỏ ra ngoài thư mục hồ sơ, nên Donut không thay đổi nó.",
"siteStorageIoFailed": "Donut không thể thay đổi dữ liệu trang: {{detail}}",
"agentChatNotFound": "Cuộc trò chuyện này không còn nữa.",
"profileLeaseHeld": "Hồ sơ này đang mở trên {{device}}. Hồ sơ sẽ trống lúc {{time}} hoặc khi thiết bị đó đóng nó."
"profileLeaseHeld": "Hồ sơ này đang mở trên {{device}}. Hồ sơ sẽ trống lúc {{time}} hoặc khi thiết bị đó đóng nó.",
"profileStillSyncing": "Hồ sơ này vẫn đang đồng bộ. Hãy thử lại sau ít phút."
},
"rail": {
"profiles": "Profile",
+2 -1
View File
@@ -2978,7 +2978,8 @@
"siteStorageUnsafePath": "此网站数据指向配置文件夹之外,因此 Donut 未作更改。",
"siteStorageIoFailed": "Donut 无法更改网站数据:{{detail}}",
"agentChatNotFound": "此对话已不存在。",
"profileLeaseHeld": "此配置文件已在 {{device}} 上打开。它将在 {{time}} 或该设备关闭它时释放。"
"profileLeaseHeld": "此配置文件已在 {{device}} 上打开。它将在 {{time}} 或该设备关闭它时释放。",
"profileStillSyncing": "此配置文件仍在同步。请稍后再试。"
},
"rail": {
"profiles": "配置文件",
+4
View File
@@ -244,6 +244,8 @@ export type BackendErrorCode =
// `params.device` names it, `params.expiresAt` is when its lease ends,
// `params.canTakeOver` is "true" for this account's own device.
| "PROFILE_LEASE_HELD"
// The pull before a launch timed out and its download had not stopped.
| "PROFILE_STILL_SYNCING"
| "NOT_TEAM_MEMBER"
| "COOKIE_BOT_NOT_ENTITLED"
| "COOKIE_BOT_NOT_ENROLLED"
@@ -1030,6 +1032,8 @@ export function translateBackendError(t: TFunction, err: unknown): string {
device: parsed.params?.device ?? "",
time: leaseExpiryTime(parsed.params?.expiresAt),
});
case "PROFILE_STILL_SYNCING":
return t("backendErrors.profileStillSyncing");
case "PROFILE_LOCK_UNAVAILABLE":
return t("backendErrors.profileLockUnavailable");
case "NOT_TEAM_MEMBER":