refactor: block windows app update if the browser is running

This commit is contained in:
zhom
2026-07-20 00:16:49 +04:00
parent a4ed5c855a
commit a71dad735e
24 changed files with 345 additions and 30 deletions
+2
View File
@@ -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
+16 -13
View File
@@ -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:
+15
View File
@@ -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
+114 -1
View File
@@ -1698,6 +1698,95 @@ impl AppAutoUpdater {
}
}
#[cfg(any(target_os = "windows", test))]
async fn prepare_windows_installer() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<u32> = 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<String> = proxy_configs.into_iter().map(|config| config.id).collect();
let vpn_ids: Vec<String> = 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<u32> = 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<dyn std::error::Error + Send + Sync>> {
#[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();
+1
View File
@@ -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")
+2 -2
View File
@@ -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!(
+4
View File
@@ -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()
+104
View File
@@ -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::collections::HashMap<String, u32>> =
std::sync::Mutex::new(std::collections::HashMap::new());
}
static SIDECAR_VERSION_VERIFIED: AtomicBool = AtomicBool::new(false);
fn target_binary_name(base_name: &str) -> Option<String> {
let target = std::env::var("TARGET").ok()?;
@@ -156,6 +159,77 @@ pub(crate) fn find_sidecar_executable(
)
}
fn parse_sidecar_version(stdout: &[u8]) -> Option<String> {
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<dyn std::error::Error> {
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<dyn std::error::Error>> {
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<String>,
port: Option<u16>,
@@ -173,6 +247,8 @@ pub async fn start_proxy_process_with_profile(
dns_allowlist_mode: bool,
local_protocol: Option<String>,
) -> Result<ProxyConfig, Box<dyn std::error::Error>> {
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<dyn std::error::Error>
}
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
);
}
}
+2
View File
@@ -100,6 +100,8 @@ async fn wait_for_vpn_worker_ready(
}
pub async fn start_vpn_worker(vpn_id: &str) -> Result<VpnWorkerConfig, Box<dyn std::error::Error>> {
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) {
+4 -1
View File
@@ -57,7 +57,10 @@
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
"timestampUrl": "",
"nsis": {
"installerHooks": "installer-hooks.nsh"
}
}
},
"plugins": {
+30 -1
View File
@@ -66,7 +66,17 @@ async fn setup_test() -> Result<std::path::PathBuf, Box<dyn std::error::Error +
.join("debug")
.join(proxy_binary_name);
if !proxy_binary.exists() {
let binary_is_current = proxy_binary.exists()
&& std::process::Command::new(&proxy_binary)
.arg("--version")
.output()
.is_ok_and(|output| {
output.status.success()
&& String::from_utf8_lossy(&output.stdout).trim()
== format!("donut-proxy {}", env!("BUILD_VERSION"))
});
if !binary_is_current {
println!("Building donut-proxy binary for integration tests...");
let build_status = std::process::Command::new("cargo")
.args(["build", "--bin", "donut-proxy"])
@@ -127,6 +137,25 @@ impl Drop for ProxyTestTracker {
}
}
#[tokio::test]
#[serial]
async fn test_sidecar_reports_build_version() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
{
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]
+1 -1
View File
@@ -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 }),
);
+1 -1
View File
@@ -106,7 +106,7 @@ export function useAppUpdateNotifications() {
showToast({
type: "error",
title: t("appUpdate.toast.restartFailed"),
description: String(error),
description: translateBackendError(t, error),
duration: 6000,
});
}
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -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 dexécution avant dinstaller la mise à jour.",
"updatePreparationFailed": "Donut Browser na pas pu arrêter en toute sécurité un processus réseau en arrière-plan. Redémarrez lordinateur, puis réessayez la mise à jour."
},
"rail": {
"profiles": "Profils",
+4 -1
View File
@@ -1870,7 +1870,10 @@
"unsupportedDnsRulesFormat": "サポートされていないルール形式: {{format}}",
"dnsRulesSaveFailed": "DNS ルールを保存できませんでした。",
"dnsRulesExportFailed": "DNS ルールをエクスポートできませんでした。",
"fingerprintMatchFailed": "フィンガープリントをプロキシに合わせられませんでした。"
"fingerprintMatchFailed": "フィンガープリントをプロキシに合わせられませんでした。",
"proxySidecarVersionMismatch": "Donut Browser のファイルに異なるバージョンが混在しています。最新のアップデートを再インストールしてください。プロファイルはそのまま保持されます。",
"updateProfilesRunning": "アップデートをインストールする前に、実行中のプロファイルをすべて停止してください。",
"updatePreparationFailed": "バックグラウンドのネットワークプロセスを安全に停止できませんでした。コンピューターを再起動してから、もう一度アップデートしてください。"
},
"rail": {
"profiles": "プロファイル",
+4 -1
View File
@@ -1870,7 +1870,10 @@
"unsupportedDnsRulesFormat": "지원되지 않는 규칙 형식: {{format}}",
"dnsRulesSaveFailed": "DNS 규칙을 저장하지 못했습니다.",
"dnsRulesExportFailed": "DNS 규칙을 내보내지 못했습니다.",
"fingerprintMatchFailed": "지문을 프록시에 맞추지 못했습니다."
"fingerprintMatchFailed": "지문을 프록시에 맞추지 못했습니다.",
"proxySidecarVersionMismatch": "Donut Browser 파일에 서로 다른 버전이 섞여 있습니다. 최신 업데이트를 다시 설치해 주세요. 프로필은 안전하게 유지됩니다.",
"updateProfilesRunning": "업데이트를 설치하기 전에 실행 중인 모든 프로필을 중지하세요.",
"updatePreparationFailed": "Donut Browser가 백그라운드 네트워크 프로세스를 안전하게 중지하지 못했습니다. 컴퓨터를 다시 시작한 후 업데이트를 다시 시도하세요."
},
"rail": {
"profiles": "프로필",
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -1870,7 +1870,10 @@
"unsupportedDnsRulesFormat": "Неподдерживаемый формат правил: {{format}}",
"dnsRulesSaveFailed": "Не удалось сохранить правила DNS.",
"dnsRulesExportFailed": "Не удалось экспортировать правила DNS.",
"fingerprintMatchFailed": "Не удалось подогнать отпечаток под прокси."
"fingerprintMatchFailed": "Не удалось подогнать отпечаток под прокси.",
"proxySidecarVersionMismatch": "Некоторые файлы Donut Browser относятся к разным версиям. Переустановите последнее обновление — ваши профили останутся в безопасности.",
"updateProfilesRunning": "Остановите все запущенные профили перед установкой обновления.",
"updatePreparationFailed": "Donut Browser не удалось безопасно остановить фоновый сетевой процесс. Перезагрузите компьютер и повторите обновление."
},
"rail": {
"profiles": "Профили",
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -1870,7 +1870,10 @@
"unsupportedDnsRulesFormat": "不支持的规则格式:{{format}}",
"dnsRulesSaveFailed": "保存 DNS 规则失败。",
"dnsRulesExportFailed": "导出 DNS 规则失败。",
"fingerprintMatchFailed": "无法将指纹匹配到代理。"
"fingerprintMatchFailed": "无法将指纹匹配到代理。",
"proxySidecarVersionMismatch": "部分 Donut Browser 文件来自不同版本。请重新安装最新更新;你的配置文件将保持安全。",
"updateProfilesRunning": "安装更新前,请停止所有正在运行的配置文件。",
"updatePreparationFailed": "Donut Browser 无法安全停止后台网络进程。请重启电脑,然后再次尝试更新。"
},
"rail": {
"profiles": "配置文件",
+9
View File
@@ -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 ?? "",