mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-22 20:31:03 +02:00
refactor: block windows app update if the browser is running
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -57,7 +57,10 @@
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
"timestampUrl": "",
|
||||
"nsis": {
|
||||
"installerHooks": "installer-hooks.nsh"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
|
||||
@@ -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
@@ -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 }),
|
||||
);
|
||||
|
||||
@@ -106,7 +106,7 @@ export function useAppUpdateNotifications() {
|
||||
showToast({
|
||||
type: "error",
|
||||
title: t("appUpdate.toast.restartFailed"),
|
||||
description: String(error),
|
||||
description: translateBackendError(t, error),
|
||||
duration: 6000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1870,7 +1870,10 @@
|
||||
"unsupportedDnsRulesFormat": "サポートされていないルール形式: {{format}}",
|
||||
"dnsRulesSaveFailed": "DNS ルールを保存できませんでした。",
|
||||
"dnsRulesExportFailed": "DNS ルールをエクスポートできませんでした。",
|
||||
"fingerprintMatchFailed": "フィンガープリントをプロキシに合わせられませんでした。"
|
||||
"fingerprintMatchFailed": "フィンガープリントをプロキシに合わせられませんでした。",
|
||||
"proxySidecarVersionMismatch": "Donut Browser のファイルに異なるバージョンが混在しています。最新のアップデートを再インストールしてください。プロファイルはそのまま保持されます。",
|
||||
"updateProfilesRunning": "アップデートをインストールする前に、実行中のプロファイルをすべて停止してください。",
|
||||
"updatePreparationFailed": "バックグラウンドのネットワークプロセスを安全に停止できませんでした。コンピューターを再起動してから、もう一度アップデートしてください。"
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "プロファイル",
|
||||
|
||||
@@ -1870,7 +1870,10 @@
|
||||
"unsupportedDnsRulesFormat": "지원되지 않는 규칙 형식: {{format}}",
|
||||
"dnsRulesSaveFailed": "DNS 규칙을 저장하지 못했습니다.",
|
||||
"dnsRulesExportFailed": "DNS 규칙을 내보내지 못했습니다.",
|
||||
"fingerprintMatchFailed": "지문을 프록시에 맞추지 못했습니다."
|
||||
"fingerprintMatchFailed": "지문을 프록시에 맞추지 못했습니다.",
|
||||
"proxySidecarVersionMismatch": "Donut Browser 파일에 서로 다른 버전이 섞여 있습니다. 최신 업데이트를 다시 설치해 주세요. 프로필은 안전하게 유지됩니다.",
|
||||
"updateProfilesRunning": "업데이트를 설치하기 전에 실행 중인 모든 프로필을 중지하세요.",
|
||||
"updatePreparationFailed": "Donut Browser가 백그라운드 네트워크 프로세스를 안전하게 중지하지 못했습니다. 컴퓨터를 다시 시작한 후 업데이트를 다시 시도하세요."
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "프로필",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1870,7 +1870,10 @@
|
||||
"unsupportedDnsRulesFormat": "Неподдерживаемый формат правил: {{format}}",
|
||||
"dnsRulesSaveFailed": "Не удалось сохранить правила DNS.",
|
||||
"dnsRulesExportFailed": "Не удалось экспортировать правила DNS.",
|
||||
"fingerprintMatchFailed": "Не удалось подогнать отпечаток под прокси."
|
||||
"fingerprintMatchFailed": "Не удалось подогнать отпечаток под прокси.",
|
||||
"proxySidecarVersionMismatch": "Некоторые файлы Donut Browser относятся к разным версиям. Переустановите последнее обновление — ваши профили останутся в безопасности.",
|
||||
"updateProfilesRunning": "Остановите все запущенные профили перед установкой обновления.",
|
||||
"updatePreparationFailed": "Donut Browser не удалось безопасно остановить фоновый сетевой процесс. Перезагрузите компьютер и повторите обновление."
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "Профили",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1870,7 +1870,10 @@
|
||||
"unsupportedDnsRulesFormat": "不支持的规则格式:{{format}}",
|
||||
"dnsRulesSaveFailed": "保存 DNS 规则失败。",
|
||||
"dnsRulesExportFailed": "导出 DNS 规则失败。",
|
||||
"fingerprintMatchFailed": "无法将指纹匹配到代理。"
|
||||
"fingerprintMatchFailed": "无法将指纹匹配到代理。",
|
||||
"proxySidecarVersionMismatch": "部分 Donut Browser 文件来自不同版本。请重新安装最新更新;你的配置文件将保持安全。",
|
||||
"updateProfilesRunning": "安装更新前,请停止所有正在运行的配置文件。",
|
||||
"updatePreparationFailed": "Donut Browser 无法安全停止后台网络进程。请重启电脑,然后再次尝试更新。"
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "配置文件",
|
||||
|
||||
@@ -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 ?? "",
|
||||
|
||||
Reference in New Issue
Block a user