From 13652e3c33f1844440831761d01faa7bfc499d0e Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 24 Jun 2025 05:56:53 +0200 Subject: [PATCH] settings: add a lock to read/write global settings --- Cargo.lock | 11 +++ liana-gui/Cargo.toml | 1 + liana-gui/src/app/settings.rs | 132 ++++++++++++++++++++++++---------- liana-gui/src/gui/mod.rs | 36 +++++----- liana-gui/src/main.rs | 6 +- 5 files changed, 124 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44b58e48..b6f24a6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1827,6 +1827,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "futures" version = "0.3.31" @@ -3053,6 +3063,7 @@ dependencies = [ "dirs 3.0.2", "email_address", "flate2", + "fs2", "hex", "iced", "iced_aw", diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index fe969d87..4affc27c 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -54,6 +54,7 @@ bitcoin_hashes = "0.12" reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream"] } rust-ini = "0.19.0" rfd = "0.15.1" +fs2 = "0.4.3" [target.'cfg(windows)'.dependencies] diff --git a/liana-gui/src/app/settings.rs b/liana-gui/src/app/settings.rs index e2602f4b..626b50ff 100644 --- a/liana-gui/src/app/settings.rs +++ b/liana-gui/src/app/settings.rs @@ -380,13 +380,15 @@ impl std::fmt::Display for SettingsError { pub mod global { use crate::dir::LianaDirectory; use async_hwi::bitbox::{ConfigError, NoiseConfig, NoiseConfigData}; + use fs2::FileExt; use serde::{Deserialize, Serialize}; - use std::io::{Read, Write}; + use std::fs::OpenOptions; + use std::io::{Read, Seek, SeekFrom, Write}; use std::path::PathBuf; pub const DEFAULT_FILE_NAME: &str = "global_settings.json"; - #[derive(Debug, Deserialize, Serialize)] + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct WindowConfig { pub width: f32, pub height: f32, @@ -402,40 +404,87 @@ pub mod global { pub fn path(global_datadir: &LianaDirectory) -> PathBuf { global_datadir.path().join(DEFAULT_FILE_NAME) } - pub fn load(path: &PathBuf) -> Result, String> { - if !path.exists() { - return Ok(None); - } - - let mut file = std::fs::File::open(path).map_err(|e| e.to_string())?; - - let mut contents = String::new(); - file.read_to_string(&mut contents) - .map_err(|e| e.to_string())?; - - let settings = - serde_json::from_str::(&contents).map_err(|e| e.to_string())?; - Ok(Some(settings)) - } pub fn load_window_config(path: &PathBuf) -> Option { - Self::load(path) - .ok() - .flatten() - .and_then(|s| s.window_config) + let mut ret = None; + if let Err(e) = Self::update(path, |s| ret = s.window_config.clone(), false) { + tracing::error!("Failed to load window config: {e}"); + } + ret } - pub fn to_file(&self, path: &PathBuf) -> Result<(), String> { - let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?; + pub fn update_window_config( + path: &PathBuf, + window_config: &WindowConfig, + ) -> Result<(), String> { + Self::update( + path, + |s| s.window_config = Some(window_config.clone()), + true, + ) + } + + pub fn load_bitbox_settings(path: &PathBuf) -> Result, String> { + let mut ret = None; + Self::update(path, |s| ret = s.bitbox.clone(), false)?; + Ok(ret) + } + + pub fn update_bitbox_settings( + path: &PathBuf, + bitbox: &BitboxSettings, + ) -> Result<(), String> { + Self::update(path, |s| s.bitbox = Some(bitbox.clone()), true) + } + + pub fn update(path: &PathBuf, mut update: F, write: bool) -> Result<(), String> + where + F: FnMut(&mut GlobalSettings), + { + let exists = path.is_file(); + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(path) + .map_err(|e| format!("Opening file: {e}"))?; + + file.lock_exclusive() + .map_err(|e| format!("Locking file: {e}"))?; + + let mut global_settings = if exists { + let mut content = String::new(); + file.read_to_string(&mut content) + .map_err(|e| format!("Reading file: {e}"))?; + + serde_json::from_str::(&content).map_err(|e| e.to_string())? + } else { + GlobalSettings::default() + }; + + update(&mut global_settings); + + if write { + let content = serde_json::to_vec_pretty(&global_settings) + .map_err(|e| format!("Failed to serialize GlobalSettings: {e}"))?; + + file.seek(SeekFrom::Start(0)) + .map_err(|e| format!("Failed to seek file: {e}"))?; + + file.write_all(&content) + .map_err(|e| format!("Failed to write file: {e}"))?; + file.set_len(content.len() as u64) + .map_err(|e| format!("Failed to truncate file: {e}"))?; + } + + file.unlock().map_err(|e| format!("Unlocking file: {e}"))?; - let contents = serde_json::to_string_pretty(&self).map_err(|e| e.to_string())?; - file.write_all(contents.as_bytes()) - .map_err(|e| e.to_string())?; Ok(()) } } - #[derive(Debug, Deserialize, Serialize)] + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct BitboxSettings { pub noise_config: NoiseConfigData, } @@ -458,23 +507,28 @@ pub mod global { impl NoiseConfig for PersistedBitboxNoiseConfig { fn read_config(&self) -> Result { - let settings = GlobalSettings::load(&self.file_path) + let res = GlobalSettings::load_bitbox_settings(&self.file_path) .map_err(ConfigError)? - .unwrap_or_default(); - Ok(settings - .bitbox .map(|s| s.noise_config) - .unwrap_or_else(NoiseConfigData::default)) + .unwrap_or_else(NoiseConfigData::default); + Ok(res) } fn store_config(&self, conf: &NoiseConfigData) -> Result<(), ConfigError> { - let mut cfg = GlobalSettings::load(&self.file_path) - .map_err(ConfigError)? - .unwrap_or_default(); - cfg.bitbox = Some(BitboxSettings { - noise_config: conf.clone(), - }); - cfg.to_file(&self.file_path).map_err(ConfigError) + GlobalSettings::update( + &self.file_path, + |s| { + if let Some(bitbox) = s.bitbox.as_mut() { + bitbox.noise_config = conf.clone(); + } else { + s.bitbox = Some(BitboxSettings { + noise_config: conf.clone(), + }); + } + }, + true, + ) + .map_err(ConfigError) } } } diff --git a/liana-gui/src/gui/mod.rs b/liana-gui/src/gui/mod.rs index bfbd244f..b1091067 100644 --- a/liana-gui/src/gui/mod.rs +++ b/liana-gui/src/gui/mod.rs @@ -146,15 +146,15 @@ impl GUI { iced::window::Settings::default().size }; let path = GlobalSettings::path(&self.config.liana_directory); - let mut settings = GlobalSettings::load(&path) - .ok() - .flatten() - .unwrap_or_default(); - settings.window_config = Some(WindowConfig { - width: new_size.width, - height: new_size.height, - }); - settings.to_file(&path).unwrap(); + if let Err(e) = GlobalSettings::update_window_config( + &path, + &WindowConfig { + width: new_size.width, + height: new_size.height, + }, + ) { + tracing::error!("Failed to update the window config: {e}"); + } Task::batch(batch) } // we already have a record of the last window size and we update it @@ -163,16 +163,14 @@ impl GUI { *width = monitor_size.width; *height = monitor_size.height; let path = GlobalSettings::path(&self.config.liana_directory); - let mut settings = GlobalSettings::load(&path) - .ok() - .flatten() - .unwrap_or_default(); - settings.window_config = Some(WindowConfig { - width: *width, - height: *height, - }); - if let Err(e) = settings.to_file(&path) { - tracing::error!("Fail to write window config to file: {e}"); + if let Err(e) = GlobalSettings::update_window_config( + &path, + &WindowConfig { + width: *width, + height: *height, + }, + ) { + tracing::error!("Failed to update the window config: {e}"); } } Task::none() diff --git a/liana-gui/src/main.rs b/liana-gui/src/main.rs index 8607e707..f95c1e58 100644 --- a/liana-gui/src/main.rs +++ b/liana-gui/src/main.rs @@ -108,10 +108,8 @@ fn main() -> Result<(), Box> { }; let global_config_path = GlobalSettings::path(&config.liana_directory); - let initial_size = if let Ok(Some(GlobalSettings { - window_config: Some(WindowConfig { width, height }), - .. - })) = GlobalSettings::load(&global_config_path) + let initial_size = if let Some(WindowConfig { width, height }) = + GlobalSettings::load_window_config(&global_config_path) { Size { width, height } } else {