diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index e260fbb8..7c8e3911 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -231,6 +231,82 @@ pub const LAUNCH_PLACEHOLDER_PID_MIN: u32 = u32::MAX - 0x00FF_FFFF; static NEXT_LAUNCH_PLACEHOLDER_PID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(u32::MAX); +/// What a donut-proxy command printed and how it ended. +struct SidecarOutput { + code: Option, + stdout: Vec, + stderr: Vec, +} + +impl SidecarOutput { + fn success(&self) -> bool { + self.code == Some(0) + } +} + +/// How long output that is still on its way is read after the command exits. +const SIDECAR_DRAIN: std::time::Duration = std::time::Duration::from_secs(1); + +/// Run a short donut-proxy command. A command that does not exit in time is +/// killed, and the launch reports it instead of waiting forever. +async fn run_sidecar( + command: tauri_plugin_shell::process::Command, +) -> Result { + let started = std::time::Instant::now(); + let (events, child) = command + .spawn() + .map_err(|e| format!("Failed to execute donut-proxy: {e}"))?; + match collect_sidecar(events, crate::proxy_runner::SIDECAR_RESPONSE_LIMIT).await { + Some(output) => Ok(output), + None => { + let _ = child.kill(); + log::error!( + "donut-proxy command timed out elapsed_ms={}", + started.elapsed().as_millis() + ); + Err(crate::proxy_runner::sidecar_not_responding_error()) + } + } +} + +/// What a command printed until it exited, or `None` when it did not exit +/// within `limit`. Its exit ends the wait, not the end of its output: on +/// Windows the worker it starts can keep the output pipe open, so a wait for +/// that would last as long as the worker. +async fn collect_sidecar( + mut events: tauri::async_runtime::Receiver, + limit: std::time::Duration, +) -> Option { + use tauri_plugin_shell::process::CommandEvent; + let mut output = SidecarOutput { + code: None, + stdout: Vec::new(), + stderr: Vec::new(), + }; + let mut deadline = tokio::time::Instant::now() + limit; + let mut exited = false; + loop { + match tokio::time::timeout_at(deadline, events.recv()).await { + Ok(Some(CommandEvent::Stdout(line))) => { + output.stdout.extend(line); + output.stdout.push(b'\n'); + } + Ok(Some(CommandEvent::Stderr(line))) => { + output.stderr.extend(line); + output.stderr.push(b'\n'); + } + Ok(Some(CommandEvent::Terminated(payload))) => { + output.code = payload.code; + exited = true; + deadline = deadline.min(tokio::time::Instant::now() + SIDECAR_DRAIN); + } + Ok(Some(_)) => {} + Ok(None) => return exited.then_some(output), + Err(_) => return exited.then_some(output), + } + } +} + /// Allocate a unique `active_proxies` key for a launch in flight, so concurrent /// launches can never overwrite each other's placeholder entry. pub fn next_launch_placeholder_pid() -> u32 { @@ -2226,14 +2302,10 @@ impl ProxyManager { // Tell the worker which protocol to serve the browser (http or socks5) proxy_cmd = proxy_cmd.arg("--local-protocol").arg(local_protocol); - // Execute the command and wait for it to complete - // The donut-proxy binary should start the worker and then exit - let output = proxy_cmd - .output() - .await - .map_err(|e| format!("Failed to execute donut-proxy: {e}"))?; + // The donut-proxy command starts the worker, prints it and exits. + let output = run_sidecar(proxy_cmd).await?; - if !output.status.success() { + if !output.success() { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); return Err(format!( @@ -2382,8 +2454,8 @@ impl ProxyManager { // A failed spawn (sidecar missing, permission denied, fd exhaustion) must // not panic the cleanup task — the proxy is already removed from tracking, // so degrade gracefully like the non-success branch below. - match proxy_cmd.output().await { - Ok(output) if !output.status.success() => { + match run_sidecar(proxy_cmd).await { + Ok(output) if !output.success() => { log::warn!( "Proxy worker stop command failed id={proxy_id} stderr={:?}", String::from_utf8_lossy(&output.stderr).trim() @@ -2453,8 +2525,8 @@ impl ProxyManager { .arg(&proxy_id); // Don't panic if the sidecar can't be spawned — still clear the mapping. - match proxy_cmd.output().await { - Ok(output) if !output.status.success() => { + match run_sidecar(proxy_cmd).await { + Ok(output) if !output.success() => { log::warn!( "Proxy worker stop command failed id={proxy_id} stderr={:?}", String::from_utf8_lossy(&output.stderr).trim() @@ -3388,6 +3460,44 @@ mod route_check_tests { #[cfg(test)] mod tests { use super::*; + + #[tokio::test] + async fn a_sidecar_that_exits_is_read_while_its_worker_holds_the_pipe() { + use tauri_plugin_shell::process::{CommandEvent, TerminatedPayload}; + let (tx, rx) = tauri::async_runtime::channel(8); + tx.send(CommandEvent::Stdout(b"{\"id\":\"p\"}".to_vec())) + .await + .unwrap(); + tx.send(CommandEvent::Terminated(TerminatedPayload { + code: Some(0), + signal: None, + })) + .await + .unwrap(); + // `tx` stays alive, as the pipe does while a worker keeps it. + let output = tokio::time::timeout( + std::time::Duration::from_secs(5), + collect_sidecar(rx, std::time::Duration::from_secs(30)), + ) + .await + .expect("the exit ends the wait") + .expect("an exited command has output"); + assert!(output.success()); + assert_eq!(output.stdout, b"{\"id\":\"p\"}\n"); + drop(tx); + } + + #[tokio::test] + async fn a_sidecar_that_never_exits_is_given_up() { + use tauri_plugin_shell::process::CommandEvent; + let (tx, rx) = tauri::async_runtime::channel(8); + tx.send(CommandEvent::Stdout(b"starting".to_vec())) + .await + .unwrap(); + let output = collect_sidecar(rx, std::time::Duration::from_millis(200)).await; + assert!(output.is_none()); + drop(tx); + } use std::env; use std::path::PathBuf; diff --git a/src-tauri/src/proxy_runner.rs b/src-tauri/src/proxy_runner.rs index bb80d04f..71d08626 100644 --- a/src-tauri/src/proxy_runner.rs +++ b/src-tauri/src/proxy_runner.rs @@ -197,6 +197,17 @@ fn parse_sidecar_version(stdout: &[u8]) -> Option { .map(str::to_string) } +/// The longest Donut waits for one run of its donut-proxy helper. +pub(crate) const SIDECAR_RESPONSE_LIMIT: std::time::Duration = std::time::Duration::from_secs(20); + +pub(crate) fn sidecar_not_responding_error() -> String { + serde_json::json!({ + "code": "PROXY_SIDECAR_NOT_RESPONDING", + "params": { "seconds": SIDECAR_RESPONSE_LIMIT.as_secs().to_string() } + }) + .to_string() +} + fn sidecar_version_mismatch_error() -> Box { serde_json::json!({ "code": "PROXY_SIDECAR_VERSION_MISMATCH" @@ -221,25 +232,37 @@ pub(crate) async fn ensure_sidecar_version() -> Result<(), Box output, - Err(e) => { + // Security software can hold a helper it has not seen before, and a launch + // that waits on it forever shows only a spinner. + let output = match tokio::time::timeout(SIDECAR_RESPONSE_LIMIT, command.output()).await { + Ok(Ok(output)) => output, + Ok(Err(e)) => { log::error!( "donut-proxy version check failed to run path={} err=\"{e}\"", executable.display() ); return Err(sidecar_version_mismatch_error()); } + Err(_) => { + log::error!( + "donut-proxy version check timed out path={} elapsed_ms={}", + executable.display(), + checked.elapsed().as_millis() + ); + return Err(sidecar_not_responding_error().into()); + } }; let actual_version = parse_sidecar_version(&output.stdout); let expected_version = env!("BUILD_VERSION"); diff --git a/src-tauri/src/wayfern_manager.rs b/src-tauri/src/wayfern_manager.rs index f71cf51f..d81ca63d 100644 --- a/src-tauri/src/wayfern_manager.rs +++ b/src-tauri/src/wayfern_manager.rs @@ -1403,6 +1403,11 @@ pub struct WayfernLaunchResult { pub stored_profile: Option, } +/// How long a started browser has to open its DevTools port. +const CDP_READY_LIMIT: Duration = Duration::from_secs(90); +/// How long one DevTools command may take to answer. +const CDP_COMMAND_LIMIT: Duration = Duration::from_secs(60); + struct WayfernInstance { process_id: Option, profile_path: Option, @@ -2072,14 +2077,30 @@ impl WayfernManager { port: u16, child: &mut tokio::process::Child, log: &BrowserLogTap, + ) -> Result, Box> { + self + .wait_for_cdp_ready_within(port, child, log, CDP_READY_LIMIT) + .await + } + + async fn wait_for_cdp_ready_within( + &self, + port: u16, + child: &mut tokio::process::Child, + log: &BrowserLogTap, + limit: Duration, ) -> Result, Box> { let url = format!("http://127.0.0.1:{port}/json/version"); - let max_attempts = 120; let delay = Duration::from_millis(500); + // A refused connection can take seconds on Windows, so the wait is bounded + // by time, not by a count of tries. + let deadline = std::time::Instant::now() + limit; let pid = child.id().unwrap_or_default(); let mut last_error: Option = None; - for attempt in 0..max_attempts { + let mut attempt = 0u32; + while std::time::Instant::now() < deadline { + attempt += 1; if let Ok(Some(status)) = child.try_wait() { tokio::time::sleep(Duration::from_millis(300)).await; let said = log.lines().join("\n"); @@ -2107,7 +2128,20 @@ impl WayfernManager { } let detail = last_error.unwrap_or_else(|| "no attempts completed".to_string()); - Err(format!("CDP not ready after {max_attempts} attempts on port {port}: {detail}").into()) + log::warn!( + "Browser did not answer CDP pid={pid} cdp_port={port} attempts={attempt} err=\"{detail}\"" + ); + Err( + serde_json::json!({ + "code": "WAYFERN_NOT_RESPONDING", + "params": { + "seconds": limit.as_secs().to_string(), + "detail": format!("CDP not ready on port {port}: {detail}"), + } + }) + .to_string() + .into(), + ) } // The control port can open before the first tab. Both launch paths must @@ -2177,6 +2211,24 @@ impl WayfernManager { ws_url: &str, method: &str, params: serde_json::Value, + ) -> Result> { + match tokio::time::timeout(CDP_COMMAND_LIMIT, self.cdp_exchange(ws_url, method, params)).await { + Ok(result) => result, + Err(_) => Err( + format!( + "CDP command {method} got no answer in {} s", + CDP_COMMAND_LIMIT.as_secs() + ) + .into(), + ), + } + } + + async fn cdp_exchange( + &self, + ws_url: &str, + method: &str, + params: serde_json::Value, ) -> Result> { let (mut ws_stream, _) = connect_async(ws_url).await?; @@ -5497,6 +5549,29 @@ mod tests { server.abort(); } + #[tokio::test] + async fn a_browser_that_never_opens_cdp_is_reported_in_time() { + let closed = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let port = closed.local_addr().unwrap().port(); + drop(closed); + let mut child = cdp_test_process(); + let pid = child.id().unwrap(); + let error = tokio::time::timeout( + Duration::from_secs(10), + WayfernManager::new().wait_for_cdp_ready_within( + port, + &mut child, + &BrowserLogTap::default(), + Duration::from_secs(1), + ), + ) + .await + .expect("the wait ends at its limit") + .unwrap_err(); + assert_eq!(code_of(&error.to_string()), "WAYFERN_NOT_RESPONDING"); + force_kill_process(pid); + } + #[tokio::test] async fn cdp_page_wait_stops_when_the_browser_exits() { use std::sync::atomic::Ordering; diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 62934da9..53fec778 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -2658,6 +2658,8 @@ "dnsRulesExportFailed": "Die DNS-Regeln konnten nicht exportiert werden.", "fingerprintMatchFailed": "Der Fingerprint konnte nicht an den Proxy angepasst werden.", "proxySidecarVersionMismatch": "Einige Dateien von Donut Browser stammen aus unterschiedlichen Versionen. Installieren Sie das neueste Update erneut; Ihre Profile bleiben dabei erhalten.", + "proxySidecarNotResponding": "Der Donut-Proxy-Helfer (donut-proxy) wurde nicht innerhalb von {{seconds}} Sekunden fertig. Möglicherweise hält ihn eine Sicherheitssoftware fest: Erlauben Sie donut-proxy in Ihrem Virenschutz und versuchen Sie es erneut.", + "wayfernNotResponding": "Wayfern wurde gestartet, hat aber nicht innerhalb von {{seconds}} Sekunden geantwortet. Möglicherweise hält eine Sicherheitssoftware es fest, oder es konnte sein Fenster nicht öffnen. Erlauben Sie Wayfern in Ihrem Virenschutz und versuchen Sie es erneut.", "updateProfilesRunning": "Stoppen Sie alle laufenden Profile, bevor Sie das Update installieren.", "updatePreparationFailed": "Donut Browser konnte einen Netzwerkprozess im Hintergrund nicht sicher beenden. Starten Sie Ihren Computer neu und versuchen Sie das Update dann erneut.", "wayfernTermsRequired": "Akzeptieren Sie die Nutzungsbedingungen von Wayfern, um Wayfern zu verwenden.", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 335f6211..cf762c1d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -2658,6 +2658,8 @@ "dnsRulesExportFailed": "Failed to export the DNS rules.", "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.", + "proxySidecarNotResponding": "The Donut proxy helper (donut-proxy) did not finish within {{seconds}} seconds. Security software may be holding it: allow donut-proxy in your antivirus, then try again.", + "wayfernNotResponding": "Wayfern started but did not answer within {{seconds}} seconds. Security software may be holding it, or it could not open its window. Allow Wayfern in your antivirus, then try again.", "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.", "wayfernTermsRequired": "Accept the Wayfern Terms and Conditions to use Wayfern.", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 331bc460..62f62d9d 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -2677,6 +2677,8 @@ "dnsRulesExportFailed": "No se pudieron exportar las reglas DNS.", "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.", + "proxySidecarNotResponding": "El asistente de proxy de Donut (donut-proxy) no terminó en {{seconds}} segundos. Puede que un software de seguridad lo esté reteniendo: permite donut-proxy en tu antivirus y vuelve a intentarlo.", + "wayfernNotResponding": "Wayfern se inició, pero no respondió en {{seconds}} segundos. Puede que un software de seguridad lo esté reteniendo o que no haya podido abrir su ventana. Permite Wayfern en tu antivirus y vuelve a intentarlo.", "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.", "wayfernTermsRequired": "Acepta los Términos y condiciones de Wayfern para usar Wayfern.", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index b2acfbfc..89618b75 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -2677,6 +2677,8 @@ "dnsRulesExportFailed": "Échec de l'exportation des règles DNS.", "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.", + "proxySidecarNotResponding": "L'assistant proxy de Donut (donut-proxy) n'a pas terminé en {{seconds}} secondes. Un logiciel de sécurité le bloque peut-être : autorisez donut-proxy dans votre antivirus, puis réessayez.", + "wayfernNotResponding": "Wayfern a démarré mais n'a pas répondu en {{seconds}} secondes. Un logiciel de sécurité le bloque peut-être, ou il n'a pas pu ouvrir sa fenêtre. Autorisez Wayfern dans votre antivirus, puis réessayez.", "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.", "wayfernTermsRequired": "Acceptez les conditions générales de Wayfern pour utiliser Wayfern.", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index e0da5943..f949681c 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -2658,6 +2658,8 @@ "dnsRulesExportFailed": "DNS ルールをエクスポートできませんでした。", "fingerprintMatchFailed": "フィンガープリントをプロキシに合わせられませんでした。", "proxySidecarVersionMismatch": "Donut Browser のファイルに異なるバージョンが混在しています。最新のアップデートを再インストールしてください。プロファイルはそのまま保持されます。", + "proxySidecarNotResponding": "Donut のプロキシ補助プログラム (donut-proxy) が {{seconds}} 秒以内に完了しませんでした。セキュリティソフトが止めている可能性があります。ウイルス対策ソフトで donut-proxy を許可してから、もう一度お試しください。", + "wayfernNotResponding": "Wayfern は起動しましたが、{{seconds}} 秒以内に応答しませんでした。セキュリティソフトが止めているか、ウィンドウを開けなかった可能性があります。ウイルス対策ソフトで Wayfern を許可してから、もう一度お試しください。", "updateProfilesRunning": "アップデートをインストールする前に、実行中のプロファイルをすべて停止してください。", "updatePreparationFailed": "バックグラウンドのネットワークプロセスを安全に停止できませんでした。コンピューターを再起動してから、もう一度アップデートしてください。", "wayfernTermsRequired": "Wayfernを使用するには、Wayfernの利用規約に同意してください。", diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index dd9cbe4a..c75f42b0 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -2658,6 +2658,8 @@ "dnsRulesExportFailed": "DNS 규칙을 내보내지 못했습니다.", "fingerprintMatchFailed": "지문을 프록시에 맞추지 못했습니다.", "proxySidecarVersionMismatch": "Donut Browser 파일에 서로 다른 버전이 섞여 있습니다. 최신 업데이트를 다시 설치해 주세요. 프로필은 안전하게 유지됩니다.", + "proxySidecarNotResponding": "Donut 프록시 도우미(donut-proxy)가 {{seconds}}초 안에 끝나지 않았습니다. 보안 소프트웨어가 막고 있을 수 있습니다. 백신 프로그램에서 donut-proxy를 허용한 뒤 다시 시도하세요.", + "wayfernNotResponding": "Wayfern이 시작되었지만 {{seconds}}초 안에 응답하지 않았습니다. 보안 소프트웨어가 막고 있거나 창을 열지 못했을 수 있습니다. 백신 프로그램에서 Wayfern을 허용한 뒤 다시 시도하세요.", "updateProfilesRunning": "업데이트를 설치하기 전에 실행 중인 모든 프로필을 중지하세요.", "updatePreparationFailed": "Donut Browser가 백그라운드 네트워크 프로세스를 안전하게 중지하지 못했습니다. 컴퓨터를 다시 시작한 후 업데이트를 다시 시도하세요.", "wayfernTermsRequired": "Wayfern을 사용하려면 Wayfern 이용 약관에 동의하세요.", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index db0b9dcf..5ae4f704 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -2677,6 +2677,8 @@ "dnsRulesExportFailed": "Falha ao exportar as regras DNS.", "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.", + "proxySidecarNotResponding": "O assistente de proxy do Donut (donut-proxy) não terminou em {{seconds}} segundos. Um software de segurança pode estar bloqueando-o: permita o donut-proxy no seu antivírus e tente novamente.", + "wayfernNotResponding": "O Wayfern iniciou, mas não respondeu em {{seconds}} segundos. Um software de segurança pode estar bloqueando-o, ou ele não conseguiu abrir a janela. Permita o Wayfern no seu antivírus e tente novamente.", "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.", "wayfernTermsRequired": "Aceite os Termos e Condições da Wayfern para usar o Wayfern.", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index ed5eaabb..0e31b440 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -2696,6 +2696,8 @@ "dnsRulesExportFailed": "Не удалось экспортировать правила DNS.", "fingerprintMatchFailed": "Не удалось подогнать отпечаток под прокси.", "proxySidecarVersionMismatch": "Некоторые файлы Donut Browser относятся к разным версиям. Переустановите последнее обновление — ваши профили останутся в безопасности.", + "proxySidecarNotResponding": "Вспомогательный прокси Donut (donut-proxy) не завершился за {{seconds}} с. Возможно, его удерживает защитное ПО: разрешите donut-proxy в антивирусе и повторите попытку.", + "wayfernNotResponding": "Wayfern запустился, но не ответил за {{seconds}} с. Возможно, его удерживает защитное ПО или он не смог открыть окно. Разрешите Wayfern в антивирусе и повторите попытку.", "updateProfilesRunning": "Остановите все запущенные профили перед установкой обновления.", "updatePreparationFailed": "Donut Browser не удалось безопасно остановить фоновый сетевой процесс. Перезагрузите компьютер и повторите обновление.", "wayfernTermsRequired": "Примите Условия использования Wayfern, чтобы пользоваться Wayfern.", diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json index cb0fe58f..fd08f14b 100644 --- a/src/i18n/locales/tr.json +++ b/src/i18n/locales/tr.json @@ -2658,6 +2658,8 @@ "dnsRulesExportFailed": "DNS kuralları dışa aktarılamadı.", "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.", + "proxySidecarNotResponding": "Donut proxy yardımcısı (donut-proxy) {{seconds}} saniye içinde bitmedi. Bir güvenlik yazılımı onu bekletiyor olabilir: antivirüs programınızda donut-proxy'ye izin verin ve yeniden deneyin.", + "wayfernNotResponding": "Wayfern başladı ancak {{seconds}} saniye içinde yanıt vermedi. Bir güvenlik yazılımı onu bekletiyor olabilir veya penceresini açamadı. Antivirüs programınızda Wayfern'e izin verin ve yeniden deneyin.", "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.", "wayfernTermsRequired": "Wayfern'ü kullanmak için Wayfern Hüküm ve Koşullarını kabul edin.", diff --git a/src/i18n/locales/vi.json b/src/i18n/locales/vi.json index a00ba5c5..7a01e951 100644 --- a/src/i18n/locales/vi.json +++ b/src/i18n/locales/vi.json @@ -2658,6 +2658,8 @@ "dnsRulesExportFailed": "Không thể xuất quy tắc DNS.", "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.", + "proxySidecarNotResponding": "Trình trợ giúp proxy của Donut (donut-proxy) không hoàn tất trong {{seconds}} giây. Có thể phần mềm bảo mật đang giữ nó: hãy cho phép donut-proxy trong phần mềm diệt virus rồi thử lại.", + "wayfernNotResponding": "Wayfern đã khởi động nhưng không phản hồi trong {{seconds}} giây. Có thể phần mềm bảo mật đang giữ nó, hoặc nó không mở được cửa sổ. Hãy cho phép Wayfern trong phần mềm diệt virus rồi thử lại.", "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.", "wayfernTermsRequired": "Hãy chấp nhận Điều khoản và Điều kiện của Wayfern để sử dụng Wayfern.", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 18069ea7..f5244d43 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -2658,6 +2658,8 @@ "dnsRulesExportFailed": "导出 DNS 规则失败。", "fingerprintMatchFailed": "无法将指纹匹配到代理。", "proxySidecarVersionMismatch": "部分 Donut Browser 文件来自不同版本。请重新安装最新更新;你的配置文件将保持安全。", + "proxySidecarNotResponding": "Donut 代理助手 (donut-proxy) 未在 {{seconds}} 秒内完成。可能是安全软件拦截了它:请在杀毒软件中允许 donut-proxy,然后重试。", + "wayfernNotResponding": "Wayfern 已启动,但未在 {{seconds}} 秒内响应。可能是安全软件拦截了它,或它无法打开窗口。请在杀毒软件中允许 Wayfern,然后重试。", "updateProfilesRunning": "安装更新前,请停止所有正在运行的配置文件。", "updatePreparationFailed": "Donut Browser 无法安全停止后台网络进程。请重启电脑,然后再次尝试更新。", "wayfernTermsRequired": "请先接受 Wayfern 条款与条件,再使用 Wayfern。", diff --git a/src/lib/backend-errors.ts b/src/lib/backend-errors.ts index 228f10a3..c8578812 100644 --- a/src/lib/backend-errors.ts +++ b/src/lib/backend-errors.ts @@ -104,6 +104,8 @@ export type BackendErrorCode = | "VPN_NOT_WORKING" | "CAMOUFOX_IMPORT_DEPRECATED" | "PROXY_SIDECAR_VERSION_MISMATCH" + | "PROXY_SIDECAR_NOT_RESPONDING" + | "WAYFERN_NOT_RESPONDING" | "UPDATE_CHECKSUMS_UNAVAILABLE" | "UPDATE_CHECKSUM_MISMATCH" | "BROWSER_CHECKSUM_UNAVAILABLE" @@ -652,6 +654,14 @@ export function translateBackendError(t: TFunction, err: unknown): string { return t("backendErrors.camoufoxImportDeprecated"); case "PROXY_SIDECAR_VERSION_MISMATCH": return t("backendErrors.proxySidecarVersionMismatch"); + case "PROXY_SIDECAR_NOT_RESPONDING": + return t("backendErrors.proxySidecarNotResponding", { + seconds: parsed.params?.seconds ?? "", + }); + case "WAYFERN_NOT_RESPONDING": + return t("backendErrors.wayfernNotResponding", { + seconds: parsed.params?.seconds ?? "", + }); case "UPDATE_CHECKSUMS_UNAVAILABLE": return t("backendErrors.updateChecksumsUnavailable", { version: parsed.params?.version ?? "",