From a71dad735eb9f9e6a722d1552951ff0d125fa496 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Mon, 20 Jul 2026 00:16:49 +0400 Subject: [PATCH] refactor: block windows app update if the browser is running --- .github/workflows/release.yml | 2 + .github/workflows/rolling-release.yml | 29 +++--- src-tauri/installer-hooks.nsh | 15 +++ src-tauri/src/app_auto_updater.rs | 115 ++++++++++++++++++++- src-tauri/src/bin/proxy_server.rs | 1 + src-tauri/src/browser_runner.rs | 4 +- src-tauri/src/proxy_manager.rs | 4 + src-tauri/src/proxy_runner.rs | 104 +++++++++++++++++++ src-tauri/src/vpn_worker_runner.rs | 2 + src-tauri/tauri.conf.json | 5 +- src-tauri/tests/donut_proxy_integration.rs | 31 +++++- src/app/page.tsx | 2 +- src/hooks/use-app-update-notifications.tsx | 2 +- src/i18n/locales/en.json | 5 +- src/i18n/locales/es.json | 5 +- src/i18n/locales/fr.json | 5 +- src/i18n/locales/ja.json | 5 +- src/i18n/locales/ko.json | 5 +- src/i18n/locales/pt.json | 5 +- src/i18n/locales/ru.json | 5 +- src/i18n/locales/tr.json | 5 +- src/i18n/locales/vi.json | 5 +- src/i18n/locales/zh.json | 5 +- src/lib/backend-errors.ts | 9 ++ 24 files changed, 345 insertions(+), 30 deletions(-) create mode 100644 src-tauri/installer-hooks.nsh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index faee0be..38df28c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -160,6 +160,8 @@ jobs: - name: Build sidecar binaries shell: bash working-directory: ./src-tauri + env: + GITHUB_REF_NAME: ${{ github.ref_name }} run: | cargo build --bin donut-proxy --target ${{ matrix.target }} --release diff --git a/.github/workflows/rolling-release.yml b/.github/workflows/rolling-release.yml index e81111b..01e1bc9 100644 --- a/.github/workflows/rolling-release.yml +++ b/.github/workflows/rolling-release.yml @@ -164,9 +164,25 @@ jobs: echo "Checking from src-tauri perspective:" ls -la src-tauri/../dist || echo "Warning: dist not accessible from src-tauri" + - name: Generate nightly timestamp + id: timestamp + shell: bash + run: | + # Committer date, not wall clock: every job in this run (including + # update-nightly-release, which runs much later) must derive the + # exact same tag, or a run straddling midnight UTC splits the + # release from its checksums. + TIMESTAMP=$(git show -s --format=%cs HEAD) + COMMIT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-7) + echo "timestamp=${TIMESTAMP}-${COMMIT_HASH}" >> $GITHUB_OUTPUT + echo "Generated timestamp: ${TIMESTAMP}-${COMMIT_HASH}" + - name: Build sidecar binaries shell: bash working-directory: ./src-tauri + env: + BUILD_TAG: "nightly-${{ steps.timestamp.outputs.timestamp }}" + GITHUB_REF_NAME: "nightly-${{ steps.timestamp.outputs.timestamp }}" run: | cargo build --bin donut-proxy --target ${{ matrix.target }} --release @@ -214,19 +230,6 @@ jobs: rm -f $CERT_PATH $KEY_PATH $PEM_PATH $P12_PATH - - name: Generate nightly timestamp - id: timestamp - shell: bash - run: | - # Committer date, not wall clock: every job in this run (including - # update-nightly-release, which runs much later) must derive the - # exact same tag, or a run straddling midnight UTC splits the - # release from its checksums. - TIMESTAMP=$(git show -s --format=%cs HEAD) - COMMIT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-7) - echo "timestamp=${TIMESTAMP}-${COMMIT_HASH}" >> $GITHUB_OUTPUT - echo "Generated timestamp: ${TIMESTAMP}-${COMMIT_HASH}" - - name: Build Tauri app uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f #v1.0.0 env: diff --git a/src-tauri/installer-hooks.nsh b/src-tauri/installer-hooks.nsh new file mode 100644 index 0000000..447bd8a --- /dev/null +++ b/src-tauri/installer-hooks.nsh @@ -0,0 +1,15 @@ +!macro NSIS_HOOK_PREINSTALL + IfFileExists "$INSTDIR\donut-proxy.exe" 0 donut_proxy_preinstall_done + + DetailPrint "Stopping Donut proxy workers before replacing application files" + nsExec::ExecToStack '"$SYSDIR\taskkill.exe" /F /T /IM "donut-proxy.exe"' + Pop $0 + Pop $1 + Sleep 1000 + + ; Removing the old sidecar first prevents NSIS from retaining a same-version + ; or previously locked executable while updating the main application. + Delete "$INSTDIR\donut-proxy.exe" + + donut_proxy_preinstall_done: +!macroend diff --git a/src-tauri/src/app_auto_updater.rs b/src-tauri/src/app_auto_updater.rs index 3aacda2..0869612 100644 --- a/src-tauri/src/app_auto_updater.rs +++ b/src-tauri/src/app_auto_updater.rs @@ -1698,6 +1698,95 @@ impl AppAutoUpdater { } } + #[cfg(any(target_os = "windows", test))] + async fn prepare_windows_installer() -> Result<(), Box> { + let profiles = match crate::profile::ProfileManager::instance().list_profiles() { + Ok(profiles) => profiles, + Err(e) => { + log::error!("Failed to inspect running profiles before app update: {e}"); + return Err( + serde_json::json!({ + "code": "UPDATE_PREPARATION_FAILED" + }) + .to_string() + .into(), + ); + } + }; + + let has_running_profiles = profiles.into_iter().any(|profile| { + profile + .process_id + .is_some_and(|pid| pid != 0 && crate::proxy_storage::is_process_running(pid)) + }); + if has_running_profiles { + return Err( + serde_json::json!({ + "code": "UPDATE_PROFILES_RUNNING" + }) + .to_string() + .into(), + ); + } + + let proxy_configs = crate::proxy_storage::list_proxy_configs(); + let vpn_configs = crate::vpn_worker_storage::list_vpn_worker_configs(); + let mut worker_pids: Vec = proxy_configs + .iter() + .filter_map(|config| config.pid) + .chain(vpn_configs.iter().filter_map(|config| config.pid)) + .collect(); + worker_pids.sort_unstable(); + worker_pids.dedup(); + + let proxy_ids: Vec = proxy_configs.into_iter().map(|config| config.id).collect(); + let vpn_ids: Vec = vpn_configs.into_iter().map(|config| config.id).collect(); + + let stop_proxies = futures_util::future::join_all( + proxy_ids + .iter() + .map(|id| crate::proxy_runner::stop_proxy_process(id)), + ); + let stop_vpns = futures_util::future::join_all( + vpn_ids + .iter() + .map(|id| crate::vpn_worker_runner::stop_vpn_worker(id)), + ); + let (proxy_results, vpn_results) = tokio::join!(stop_proxies, stop_vpns); + + for result in proxy_results.into_iter().chain(vpn_results) { + if let Err(e) = result { + log::warn!("Failed to stop a network worker before app update: {e}"); + } + } + + for _ in 0..20 { + if worker_pids + .iter() + .all(|pid| !crate::proxy_storage::is_process_running(*pid)) + { + return Ok(()); + } + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + + let remaining: Vec = worker_pids + .into_iter() + .filter(|pid| crate::proxy_storage::is_process_running(*pid)) + .collect(); + log::error!( + "App update aborted because donut-proxy worker PIDs are still running: {:?}", + remaining + ); + Err( + serde_json::json!({ + "code": "UPDATE_PREPARATION_FAILED" + }) + .to_string() + .into(), + ) + } + /// Restart the application async fn restart_application(&self) -> Result<(), Box> { #[cfg(target_os = "macos")] @@ -1764,6 +1853,11 @@ rm "{}" let pending = PENDING_INSTALLER_PATH.lock().unwrap().take(); if let Some(installer_path) = pending { + if let Err(e) = Self::prepare_windows_installer().await { + *PENDING_INSTALLER_PATH.lock().unwrap() = Some(installer_path); + return Err(e); + } + // Use ShellExecuteW to run the installer directly — no batch script, // no cmd.exe console window. The NSIS/MSI installer handles killing the // old process and restarting the app natively (via /UPDATE and @@ -1991,7 +2085,7 @@ pub async fn restart_application() -> Result<(), String> { updater .restart_application() .await - .map_err(|e| format!("Failed to restart application: {e}")) + .map_err(|e| crate::wrap_backend_error(e, "Failed to restart application")) } #[tauri::command] @@ -2197,6 +2291,25 @@ not-a-hash Donut_0.29.0_amd64.deb assert_eq!(with.digest.as_deref(), Some("sha256:ab12")); } + #[test] + fn test_windows_installer_hook_protects_sidecar_replacement() { + let _ = AppAutoUpdater::prepare_windows_installer; + + let config: serde_json::Value = + serde_json::from_str(include_str!("../tauri.conf.json")).unwrap(); + assert_eq!( + config["bundle"]["windows"]["nsis"]["installerHooks"].as_str(), + Some("installer-hooks.nsh") + ); + + let hooks = include_str!("../installer-hooks.nsh"); + assert!(hooks.contains("NSIS_HOOK_PREINSTALL")); + assert!(hooks.contains("IfFileExists \"$INSTDIR\\donut-proxy.exe\"")); + assert!(hooks.contains("taskkill.exe")); + assert!(hooks.contains("donut-proxy.exe")); + assert!(hooks.contains("Delete \"$INSTDIR\\donut-proxy.exe\"")); + } + #[test] fn test_platform_specific_download_urls() { let updater = AppAutoUpdater::instance(); diff --git a/src-tauri/src/bin/proxy_server.rs b/src-tauri/src/bin/proxy_server.rs index 28e00fd..b7649f6 100644 --- a/src-tauri/src/bin/proxy_server.rs +++ b/src-tauri/src/bin/proxy_server.rs @@ -110,6 +110,7 @@ async fn main() { })); let matches = Command::new("donut-proxy") + .version(env!("BUILD_VERSION")) .subcommand( Command::new("proxy") .about("Manage proxy servers") diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 1a5e6f0..c069228 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -274,7 +274,7 @@ impl BrowserRunner { ) .await .map_err(|e| { - let error_msg = format!("Failed to start local proxy for Wayfern: {e}"); + let error_msg = crate::wrap_backend_error(e, "Failed to start local proxy for Wayfern"); log::error!("{}", error_msg); error_msg })?; @@ -1297,7 +1297,7 @@ pub async fn launch_browser_profile_impl( return format!("Failed to launch browser: Executable format error. This browser version is not compatible with your system architecture ({}). Please try a different browser or version that supports your platform.", std::env::consts::ARCH); } } - format!("Failed to launch browser or open URL: {e}") + crate::wrap_backend_error(e, "Failed to launch browser or open URL") })?; log::info!( diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index dc562a4..8c5abae 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -1614,6 +1614,10 @@ impl ProxyManager { } } + crate::proxy_runner::ensure_sidecar_version() + .await + .map_err(|e| e.to_string())?; + // Start a new proxy using the donut-proxy binary with the correct CLI interface let mut proxy_cmd = app_handle .shell() diff --git a/src-tauri/src/proxy_runner.rs b/src-tauri/src/proxy_runner.rs index 0acf102..4e25192 100644 --- a/src-tauri/src/proxy_runner.rs +++ b/src-tauri/src/proxy_runner.rs @@ -4,11 +4,14 @@ use crate::proxy_storage::{ }; use std::path::{Path, PathBuf}; use std::process::Stdio; +use std::sync::atomic::{AtomicBool, Ordering}; lazy_static::lazy_static! { static ref PROXY_PROCESSES: std::sync::Mutex> = std::sync::Mutex::new(std::collections::HashMap::new()); } +static SIDECAR_VERSION_VERIFIED: AtomicBool = AtomicBool::new(false); + fn target_binary_name(base_name: &str) -> Option { let target = std::env::var("TARGET").ok()?; @@ -156,6 +159,77 @@ pub(crate) fn find_sidecar_executable( ) } +fn parse_sidecar_version(stdout: &[u8]) -> Option { + let output = std::str::from_utf8(stdout).ok()?.trim(); + output + .strip_prefix("donut-proxy ") + .map(str::trim) + .filter(|version| !version.is_empty() && !version.contains(char::is_whitespace)) + .map(str::to_string) +} + +fn sidecar_version_mismatch_error() -> Box { + serde_json::json!({ + "code": "PROXY_SIDECAR_VERSION_MISMATCH" + }) + .to_string() + .into() +} + +/// Verify that the installed sidecar was built for the same release as the +/// main app. Windows can otherwise retain an executing, locked sidecar while +/// NSIS replaces the app, leaving an incompatible mixed-version installation. +pub(crate) async fn ensure_sidecar_version() -> Result<(), Box> { + if SIDECAR_VERSION_VERIFIED.load(Ordering::Acquire) { + return Ok(()); + } + + let executable = match find_sidecar_executable("donut-proxy") { + Ok(executable) => executable, + Err(e) => { + log::error!("Failed to locate donut-proxy for version verification: {e}"); + return Err(sidecar_version_mismatch_error()); + } + }; + let mut command = std::process::Command::new(&executable); + command.arg("--version"); + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + command.creation_flags(CREATE_NO_WINDOW); + } + + let output = match command.output() { + Ok(output) => output, + Err(e) => { + log::error!( + "Failed to run {} for version verification: {e}", + executable.display() + ); + return Err(sidecar_version_mismatch_error()); + } + }; + let actual_version = parse_sidecar_version(&output.stdout); + let expected_version = env!("BUILD_VERSION"); + + if output.status.success() && actual_version.as_deref() == Some(expected_version) { + SIDECAR_VERSION_VERIFIED.store(true, Ordering::Release); + return Ok(()); + } + + log::error!( + "donut-proxy version mismatch: expected {}, got {:?}; status={}, stdout={:?}, stderr={:?}", + expected_version, + actual_version, + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + Err(sidecar_version_mismatch_error()) +} + pub async fn start_proxy_process( upstream_url: Option, port: Option, @@ -173,6 +247,8 @@ pub async fn start_proxy_process_with_profile( dns_allowlist_mode: bool, local_protocol: Option, ) -> Result> { + ensure_sidecar_version().await?; + let id = generate_proxy_id(); let upstream = upstream_url.unwrap_or_else(|| "DIRECT".to_string()); @@ -442,3 +518,31 @@ pub async fn stop_all_proxy_processes() -> Result<(), Box } Ok(()) } + +#[cfg(test)] +mod tests { + use super::parse_sidecar_version; + + #[test] + fn parses_exact_sidecar_version_output() { + assert_eq!( + parse_sidecar_version(b"donut-proxy v0.28.2\n").as_deref(), + Some("v0.28.2") + ); + assert_eq!( + parse_sidecar_version(b"donut-proxy nightly-2026-07-19-a4ed5c8\r\n").as_deref(), + Some("nightly-2026-07-19-a4ed5c8") + ); + } + + #[test] + fn rejects_missing_or_ambiguous_sidecar_version_output() { + assert_eq!(parse_sidecar_version(b""), None); + assert_eq!(parse_sidecar_version(b"donut-proxy"), None); + assert_eq!(parse_sidecar_version(b"other-proxy v0.28.2"), None); + assert_eq!( + parse_sidecar_version(b"donut-proxy v0.28.2\nunexpected"), + None + ); + } +} diff --git a/src-tauri/src/vpn_worker_runner.rs b/src-tauri/src/vpn_worker_runner.rs index ce7f8db..e30f880 100644 --- a/src-tauri/src/vpn_worker_runner.rs +++ b/src-tauri/src/vpn_worker_runner.rs @@ -100,6 +100,8 @@ async fn wait_for_vpn_worker_ready( } pub async fn start_vpn_worker(vpn_id: &str) -> Result> { + crate::proxy_runner::ensure_sidecar_version().await?; + for config in list_vpn_worker_configs() { if let Some(pid) = config.pid { if !is_process_running(pid) { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c2e7159..e5af1cd 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -57,7 +57,10 @@ "windows": { "certificateThumbprint": null, "digestAlgorithm": "sha256", - "timestampUrl": "" + "timestampUrl": "", + "nsis": { + "installerHooks": "installer-hooks.nsh" + } } }, "plugins": { diff --git a/src-tauri/tests/donut_proxy_integration.rs b/src-tauri/tests/donut_proxy_integration.rs index ff06dc1..f4aee7f 100644 --- a/src-tauri/tests/donut_proxy_integration.rs +++ b/src-tauri/tests/donut_proxy_integration.rs @@ -66,7 +66,17 @@ async fn setup_test() -> Result Result<(), Box> +{ + let binary_path = setup_test().await?; + let output = TestUtils::execute_command(&binary_path, &["--version"]).await?; + + assert!( + output.status.success(), + "donut-proxy --version failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + String::from_utf8(output.stdout)?.trim(), + format!("donut-proxy {}", env!("BUILD_VERSION")) + ); + Ok(()) +} + /// Test starting a local proxy without upstream proxy (DIRECT) #[tokio::test] #[serial] diff --git a/src/app/page.tsx b/src/app/page.tsx index 02bd57b..cce5b7c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -950,7 +950,7 @@ export default function Home() { } } catch (err: unknown) { console.error("Failed to launch browser:", err); - const errorMessage = err instanceof Error ? err.message : String(err); + const errorMessage = translateBackendError(t, err); showErrorToast( t("errors.launchBrowserFailed", { error: errorMessage }), ); diff --git a/src/hooks/use-app-update-notifications.tsx b/src/hooks/use-app-update-notifications.tsx index a379dbc..22e13c4 100644 --- a/src/hooks/use-app-update-notifications.tsx +++ b/src/hooks/use-app-update-notifications.tsx @@ -106,7 +106,7 @@ export function useAppUpdateNotifications() { showToast({ type: "error", title: t("appUpdate.toast.restartFailed"), - description: String(error), + description: translateBackendError(t, error), duration: 6000, }); } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 4d43f2e..c58cec0 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1870,7 +1870,10 @@ "unsupportedDnsRulesFormat": "Unsupported rules format: {{format}}", "dnsRulesSaveFailed": "Failed to save the DNS rules.", "dnsRulesExportFailed": "Failed to export the DNS rules.", - "fingerprintMatchFailed": "Couldn't match the fingerprint to the proxy." + "fingerprintMatchFailed": "Couldn't match the fingerprint to the proxy.", + "proxySidecarVersionMismatch": "Some Donut Browser files are from different versions. Reinstall the latest update; your profiles will stay safe.", + "updateProfilesRunning": "Stop all running profiles before installing the update.", + "updatePreparationFailed": "Donut Browser could not safely stop a background network process. Restart your computer, then try the update again." }, "rail": { "profiles": "Profiles", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index eaf6176..17fc290 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1870,7 +1870,10 @@ "unsupportedDnsRulesFormat": "Formato de reglas no compatible: {{format}}", "dnsRulesSaveFailed": "No se pudieron guardar las reglas DNS.", "dnsRulesExportFailed": "No se pudieron exportar las reglas DNS.", - "fingerprintMatchFailed": "No se pudo ajustar la huella al proxy." + "fingerprintMatchFailed": "No se pudo ajustar la huella al proxy.", + "proxySidecarVersionMismatch": "Algunos archivos de Donut Browser pertenecen a versiones diferentes. Reinstala la última actualización; tus perfiles permanecerán seguros.", + "updateProfilesRunning": "Detén todos los perfiles en ejecución antes de instalar la actualización.", + "updatePreparationFailed": "Donut Browser no pudo detener de forma segura un proceso de red en segundo plano. Reinicia el equipo y vuelve a intentar la actualización." }, "rail": { "profiles": "Perfiles", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index ba55a42..8a4587f 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -1870,7 +1870,10 @@ "unsupportedDnsRulesFormat": "Format de règles non pris en charge : {{format}}", "dnsRulesSaveFailed": "Échec de l'enregistrement des règles DNS.", "dnsRulesExportFailed": "Échec de l'exportation des règles DNS.", - "fingerprintMatchFailed": "Impossible d'aligner l'empreinte sur le proxy." + "fingerprintMatchFailed": "Impossible d'aligner l'empreinte sur le proxy.", + "proxySidecarVersionMismatch": "Certains fichiers de Donut Browser proviennent de versions différentes. Réinstallez la dernière mise à jour ; vos profils resteront intacts.", + "updateProfilesRunning": "Arrêtez tous les profils en cours d’exécution avant d’installer la mise à jour.", + "updatePreparationFailed": "Donut Browser n’a pas pu arrêter en toute sécurité un processus réseau en arrière-plan. Redémarrez l’ordinateur, puis réessayez la mise à jour." }, "rail": { "profiles": "Profils", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 4d4cf96..b22dae6 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1870,7 +1870,10 @@ "unsupportedDnsRulesFormat": "サポートされていないルール形式: {{format}}", "dnsRulesSaveFailed": "DNS ルールを保存できませんでした。", "dnsRulesExportFailed": "DNS ルールをエクスポートできませんでした。", - "fingerprintMatchFailed": "フィンガープリントをプロキシに合わせられませんでした。" + "fingerprintMatchFailed": "フィンガープリントをプロキシに合わせられませんでした。", + "proxySidecarVersionMismatch": "Donut Browser のファイルに異なるバージョンが混在しています。最新のアップデートを再インストールしてください。プロファイルはそのまま保持されます。", + "updateProfilesRunning": "アップデートをインストールする前に、実行中のプロファイルをすべて停止してください。", + "updatePreparationFailed": "バックグラウンドのネットワークプロセスを安全に停止できませんでした。コンピューターを再起動してから、もう一度アップデートしてください。" }, "rail": { "profiles": "プロファイル", diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index 5479e0f..9c23739 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -1870,7 +1870,10 @@ "unsupportedDnsRulesFormat": "지원되지 않는 규칙 형식: {{format}}", "dnsRulesSaveFailed": "DNS 규칙을 저장하지 못했습니다.", "dnsRulesExportFailed": "DNS 규칙을 내보내지 못했습니다.", - "fingerprintMatchFailed": "지문을 프록시에 맞추지 못했습니다." + "fingerprintMatchFailed": "지문을 프록시에 맞추지 못했습니다.", + "proxySidecarVersionMismatch": "Donut Browser 파일에 서로 다른 버전이 섞여 있습니다. 최신 업데이트를 다시 설치해 주세요. 프로필은 안전하게 유지됩니다.", + "updateProfilesRunning": "업데이트를 설치하기 전에 실행 중인 모든 프로필을 중지하세요.", + "updatePreparationFailed": "Donut Browser가 백그라운드 네트워크 프로세스를 안전하게 중지하지 못했습니다. 컴퓨터를 다시 시작한 후 업데이트를 다시 시도하세요." }, "rail": { "profiles": "프로필", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index b03c7bb..89017f5 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -1870,7 +1870,10 @@ "unsupportedDnsRulesFormat": "Formato de regras não suportado: {{format}}", "dnsRulesSaveFailed": "Falha ao salvar as regras DNS.", "dnsRulesExportFailed": "Falha ao exportar as regras DNS.", - "fingerprintMatchFailed": "Não foi possível ajustar a impressão digital ao proxy." + "fingerprintMatchFailed": "Não foi possível ajustar a impressão digital ao proxy.", + "proxySidecarVersionMismatch": "Alguns arquivos do Donut Browser são de versões diferentes. Reinstale a atualização mais recente; seus perfis permanecerão seguros.", + "updateProfilesRunning": "Pare todos os perfis em execução antes de instalar a atualização.", + "updatePreparationFailed": "O Donut Browser não conseguiu encerrar com segurança um processo de rede em segundo plano. Reinicie o computador e tente atualizar novamente." }, "rail": { "profiles": "Perfis", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index de60a32..5b6ffc8 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -1870,7 +1870,10 @@ "unsupportedDnsRulesFormat": "Неподдерживаемый формат правил: {{format}}", "dnsRulesSaveFailed": "Не удалось сохранить правила DNS.", "dnsRulesExportFailed": "Не удалось экспортировать правила DNS.", - "fingerprintMatchFailed": "Не удалось подогнать отпечаток под прокси." + "fingerprintMatchFailed": "Не удалось подогнать отпечаток под прокси.", + "proxySidecarVersionMismatch": "Некоторые файлы Donut Browser относятся к разным версиям. Переустановите последнее обновление — ваши профили останутся в безопасности.", + "updateProfilesRunning": "Остановите все запущенные профили перед установкой обновления.", + "updatePreparationFailed": "Donut Browser не удалось безопасно остановить фоновый сетевой процесс. Перезагрузите компьютер и повторите обновление." }, "rail": { "profiles": "Профили", diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json index e93bb2c..3f94729 100644 --- a/src/i18n/locales/tr.json +++ b/src/i18n/locales/tr.json @@ -1870,7 +1870,10 @@ "unsupportedDnsRulesFormat": "Desteklenmeyen kural biçimi: {{format}}", "dnsRulesSaveFailed": "DNS kuralları kaydedilemedi.", "dnsRulesExportFailed": "DNS kuralları dışa aktarılamadı.", - "fingerprintMatchFailed": "Parmak izi proxy'ye eşlenemedi." + "fingerprintMatchFailed": "Parmak izi proxy'ye eşlenemedi.", + "proxySidecarVersionMismatch": "Bazı Donut Browser dosyaları farklı sürümlere ait. En son güncellemeyi yeniden yükleyin; profilleriniz güvende kalır.", + "updateProfilesRunning": "Güncellemeyi yüklemeden önce çalışan tüm profilleri durdurun.", + "updatePreparationFailed": "Donut Browser arka plandaki bir ağ işlemini güvenli şekilde durduramadı. Bilgisayarınızı yeniden başlatıp güncellemeyi tekrar deneyin." }, "rail": { "profiles": "Profiller", diff --git a/src/i18n/locales/vi.json b/src/i18n/locales/vi.json index dfcb13a..83e6440 100644 --- a/src/i18n/locales/vi.json +++ b/src/i18n/locales/vi.json @@ -1870,7 +1870,10 @@ "unsupportedDnsRulesFormat": "Định dạng quy tắc không được hỗ trợ: {{format}}", "dnsRulesSaveFailed": "Không thể lưu quy tắc DNS.", "dnsRulesExportFailed": "Không thể xuất quy tắc DNS.", - "fingerprintMatchFailed": "Không thể khớp vân tay với proxy." + "fingerprintMatchFailed": "Không thể khớp vân tay với proxy.", + "proxySidecarVersionMismatch": "Một số tệp Donut Browser thuộc các phiên bản khác nhau. Hãy cài đặt lại bản cập nhật mới nhất; hồ sơ của bạn vẫn được giữ an toàn.", + "updateProfilesRunning": "Hãy dừng tất cả hồ sơ đang chạy trước khi cài đặt bản cập nhật.", + "updatePreparationFailed": "Donut Browser không thể dừng an toàn một tiến trình mạng chạy nền. Hãy khởi động lại máy tính rồi thử cập nhật lại." }, "rail": { "profiles": "Profile", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index ee14865..c9216f6 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1870,7 +1870,10 @@ "unsupportedDnsRulesFormat": "不支持的规则格式:{{format}}", "dnsRulesSaveFailed": "保存 DNS 规则失败。", "dnsRulesExportFailed": "导出 DNS 规则失败。", - "fingerprintMatchFailed": "无法将指纹匹配到代理。" + "fingerprintMatchFailed": "无法将指纹匹配到代理。", + "proxySidecarVersionMismatch": "部分 Donut Browser 文件来自不同版本。请重新安装最新更新;你的配置文件将保持安全。", + "updateProfilesRunning": "安装更新前,请停止所有正在运行的配置文件。", + "updatePreparationFailed": "Donut Browser 无法安全停止后台网络进程。请重启电脑,然后再次尝试更新。" }, "rail": { "profiles": "配置文件", diff --git a/src/lib/backend-errors.ts b/src/lib/backend-errors.ts index ec0872b..bd5cfb2 100644 --- a/src/lib/backend-errors.ts +++ b/src/lib/backend-errors.ts @@ -36,8 +36,11 @@ export type BackendErrorCode = | "PROXY_PAYMENT_REQUIRED" | "VPN_NOT_WORKING" | "CAMOUFOX_IMPORT_DEPRECATED" + | "PROXY_SIDECAR_VERSION_MISMATCH" | "UPDATE_CHECKSUMS_UNAVAILABLE" | "UPDATE_CHECKSUM_MISMATCH" + | "UPDATE_PROFILES_RUNNING" + | "UPDATE_PREPARATION_FAILED" | "PROFILE_NAME_EXISTS" | "IMPORT_SOURCE_NOT_FOUND" | "IMPORT_NO_ITEMS" @@ -162,6 +165,8 @@ export function translateBackendError(t: TFunction, err: unknown): string { return t("backendErrors.vpnNotWorking"); case "CAMOUFOX_IMPORT_DEPRECATED": return t("backendErrors.camoufoxImportDeprecated"); + case "PROXY_SIDECAR_VERSION_MISMATCH": + return t("backendErrors.proxySidecarVersionMismatch"); case "UPDATE_CHECKSUMS_UNAVAILABLE": return t("backendErrors.updateChecksumsUnavailable", { version: parsed.params?.version ?? "", @@ -170,6 +175,10 @@ export function translateBackendError(t: TFunction, err: unknown): string { return t("backendErrors.updateChecksumMismatch", { file: parsed.params?.file ?? "", }); + case "UPDATE_PROFILES_RUNNING": + return t("backendErrors.updateProfilesRunning"); + case "UPDATE_PREPARATION_FAILED": + return t("backendErrors.updatePreparationFailed"); case "PROFILE_NAME_EXISTS": return t("backendErrors.profileNameExists", { name: parsed.params?.name ?? "",