mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-21 06:51:24 +02:00
fix: launch waits time out
This commit is contained in:
1 parent
62d6a37d34
commit
c446e986e5
15 files changed
+260
-20
No files matched your search
+121
-11
@@ -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<i32>,
|
||||
stdout: Vec<u8>,
|
||||
stderr: Vec<u8>,
|
||||
}
|
||||
|
||||
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<SidecarOutput, String> {
|
||||
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<tauri_plugin_shell::process::CommandEvent>,
|
||||
limit: std::time::Duration,
|
||||
) -> Option<SidecarOutput> {
|
||||
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;
|
||||
|
||||
|
||||
@@ -197,6 +197,17 @@ fn parse_sidecar_version(stdout: &[u8]) -> Option<String> {
|
||||
.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<dyn std::error::Error> {
|
||||
serde_json::json!({
|
||||
"code": "PROXY_SIDECAR_VERSION_MISMATCH"
|
||||
@@ -221,25 +232,37 @@ pub(crate) async fn ensure_sidecar_version() -> Result<(), Box<dyn std::error::E
|
||||
}
|
||||
};
|
||||
let checked = std::time::Instant::now();
|
||||
let mut command = std::process::Command::new(&executable);
|
||||
command.arg("--version");
|
||||
let mut command = tokio::process::Command::new(&executable);
|
||||
command
|
||||
.arg("--version")
|
||||
.stdin(std::process::Stdio::null())
|
||||
.kill_on_drop(true);
|
||||
|
||||
#[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) => {
|
||||
// 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");
|
||||
|
||||
@@ -1403,6 +1403,11 @@ pub struct WayfernLaunchResult {
|
||||
pub stored_profile: Option<BrowserProfile>,
|
||||
}
|
||||
|
||||
/// 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<u32>,
|
||||
profile_path: Option<String>,
|
||||
@@ -2072,14 +2077,30 @@ impl WayfernManager {
|
||||
port: u16,
|
||||
child: &mut tokio::process::Child,
|
||||
log: &BrowserLogTap,
|
||||
) -> Result<Vec<CdpTarget>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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<Vec<CdpTarget>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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<String> = 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<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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の利用規約に同意してください。",
|
||||
|
||||
@@ -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 이용 약관에 동의하세요.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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。",
|
||||
|
||||
@@ -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 ?? "",
|
||||
|
||||
Reference in new issue
Block a user