From 70eca3525a5c87a638a138611fe233936dcd6708 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Thu, 30 Apr 2026 21:50:09 +0800 Subject: [PATCH] Serialize credentials file updates Credentials writes use a load-modify-save JSON path, so concurrent Settings debounce saves could read the same stale root and overwrite each other. Guard the CredentialsVault file accessors with one process-wide mutex to keep read-modify-write operations ordered. Constraint: Issue #61 asks for minimal protection around the existing JSON vault path Rejected: Atomic compare-and-swap rewrite | broader than needed for current single-process Tauri writes Confidence: high Scope-risk: narrow Tested: cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml Tested: git diff --check --- openless-all/app/src-tauri/src/persistence.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openless-all/app/src-tauri/src/persistence.rs b/openless-all/app/src-tauri/src/persistence.rs index a9ec2e99..3d772ace 100644 --- a/openless-all/app/src-tauri/src/persistence.rs +++ b/openless-all/app/src-tauri/src/persistence.rs @@ -14,6 +14,7 @@ use std::fs; use std::path::{Path, PathBuf}; +use std::sync::OnceLock; use anyhow::{anyhow, Context, Result}; use chrono::Utc; @@ -36,6 +37,12 @@ const VOCAB_FILE: &str = "dictionary.json"; const LEGACY_CREDS_DIR: &str = ".openless"; const LEGACY_CREDS_FILE: &str = "credentials.json"; +static CREDENTIALS_LOCK: OnceLock> = OnceLock::new(); + +fn credentials_lock() -> &'static Mutex<()> { + CREDENTIALS_LOCK.get_or_init(|| Mutex::new(())) +} + // ───────────────────────── path helpers ───────────────────────── fn data_dir() -> Result { @@ -657,10 +664,12 @@ impl CredentialsVault { pub const SERVICE_NAME: &'static str = "com.openless.app"; pub fn get(account: CredentialAccount) -> Result> { + let _guard = credentials_lock().lock(); Ok(lookup_account(&load_credentials(), account)) } pub fn set(account: CredentialAccount, value: &str) -> Result<()> { + let _guard = credentials_lock().lock(); let mut root = load_credentials(); let v = if value.is_empty() { None @@ -672,28 +681,33 @@ impl CredentialsVault { } pub fn remove(account: CredentialAccount) -> Result<()> { + let _guard = credentials_lock().lock(); let mut root = load_credentials(); write_account(&mut root, account, None); save_credentials(&root) } pub fn get_active_asr() -> String { + let _guard = credentials_lock().lock(); load_credentials().active.asr } pub fn set_active_asr_provider(id: &str) -> Result<()> { + let _guard = credentials_lock().lock(); let mut root = load_credentials(); root.active.asr = id.to_string(); save_credentials(&root) } pub fn set_active_llm_provider(id: &str) -> Result<()> { + let _guard = credentials_lock().lock(); let mut root = load_credentials(); root.active.llm = id.to_string(); save_credentials(&root) } pub fn snapshot() -> CredentialsSnapshot { + let _guard = credentials_lock().lock(); let root = load_credentials(); CredentialsSnapshot { volcengine_app_key: lookup_account(&root, CredentialAccount::VolcengineAppKey),