diff --git a/Cargo.lock b/Cargo.lock index bc90d070..2c38c790 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3025,6 +3025,7 @@ dependencies = [ "liana", "liana-ui", "lianad", + "libc", "log", "reqwest", "rfd", diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index bf96d225..bc076839 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -47,6 +47,7 @@ toml = "0.5" chrono = "0.4.38" # Used for managing internal bitcoind +libc = "0.2" base64 = "0.21" bitcoin_hashes = "0.12" reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream"] } diff --git a/liana-gui/src/app/config.rs b/liana-gui/src/app/config.rs index def173e8..b48bf06e 100644 --- a/liana-gui/src/app/config.rs +++ b/liana-gui/src/app/config.rs @@ -11,6 +11,7 @@ pub struct Config { /// Use iced debug feature if true. pub debug: Option, /// Start internal bitcoind executable. + /// Legacy field, replaced by settings.json start_internal_bitcoind field #[serde(default)] pub start_internal_bitcoind: bool, } diff --git a/liana-gui/src/app/mod.rs b/liana-gui/src/app/mod.rs index 894d5e42..7a1d2f12 100644 --- a/liana-gui/src/app/mod.rs +++ b/liana-gui/src/app/mod.rs @@ -300,7 +300,7 @@ impl App { } else { info!("Internal daemon stopped"); } - if let Some(bitcoind) = &self.internal_bitcoind { + if let Some(bitcoind) = self.internal_bitcoind.take() { bitcoind.stop(); } } diff --git a/liana-gui/src/app/settings.rs b/liana-gui/src/app/settings.rs index fc741685..261c983c 100644 --- a/liana-gui/src/app/settings.rs +++ b/liana-gui/src/app/settings.rs @@ -96,6 +96,9 @@ pub struct WalletSetting { #[serde(default)] pub hardware_wallets: Vec, pub remote_backend_auth: Option, + /// Start internal bitcoind executable. + /// if None, the app must refer to the gui.toml start_internal_bitcoind field. + pub start_internal_bitcoind: Option, } impl WalletSetting { diff --git a/liana-gui/src/app/wallet.rs b/liana-gui/src/app/wallet.rs index c1f0fb72..30064663 100644 --- a/liana-gui/src/app/wallet.rs +++ b/liana-gui/src/app/wallet.rs @@ -132,6 +132,7 @@ impl Wallet { // Only local wallet from previous version of Liana GUI may not have a // settings.json file remote_backend_auth: None, + start_internal_bitcoind: None, }], }; diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index 31239d13..768d2402 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -188,10 +188,9 @@ impl Installer { .expect("There is always a step") .stop(); // Now use context to determine what to stop. - if let Some(bitcoind) = &self.context.internal_bitcoind { + if let Some(bitcoind) = self.context.internal_bitcoind.take() { bitcoind.stop(); } - self.context.internal_bitcoind = None; } fn skip_steps(&mut self) { @@ -675,6 +674,7 @@ pub async fn extract_remote_gui_settings(ctx: &Context, backend: &BackendWalletC backend.user_email().to_string(), backend.wallet_id(), )), + start_internal_bitcoind: None, }], } } @@ -708,6 +708,7 @@ pub fn extract_local_gui_settings(ctx: &Context) -> Settings { keys: ctx.keys.values().cloned().collect(), hardware_wallets, remote_backend_auth: None, + start_internal_bitcoind: Some(ctx.internal_bitcoind.is_some()), }], } } diff --git a/liana-gui/src/installer/step/mod.rs b/liana-gui/src/installer/step/mod.rs index 3f4384d1..98fc4568 100644 --- a/liana-gui/src/installer/step/mod.rs +++ b/liana-gui/src/installer/step/mod.rs @@ -60,7 +60,7 @@ pub trait Step { true } fn revert(&self, _ctx: &mut Context) {} - fn stop(&self) {} + fn stop(&mut self) {} } pub struct Final { diff --git a/liana-gui/src/installer/step/node/bitcoind.rs b/liana-gui/src/installer/step/node/bitcoind.rs index 807d7a50..5388d49d 100644 --- a/liana-gui/src/installer/step/node/bitcoind.rs +++ b/liana-gui/src/installer/step/node/bitcoind.rs @@ -554,10 +554,9 @@ impl Step for InternalBitcoindStep { if let Message::InternalBitcoind(msg) = message { match msg { message::InternalBitcoindMsg::Previous => { - if let Some(bitcoind) = &self.internal_bitcoind { + if let Some(bitcoind) = self.internal_bitcoind.take() { bitcoind.stop(); } - self.internal_bitcoind = None; if let Some(download) = self.exe_download.as_ref() { // Clear exe_download if not Finished. if let DownloadState::Finished { .. } = download.state { @@ -707,7 +706,8 @@ impl Step for InternalBitcoindStep { .as_ref() .expect("already added") .clone(); - match Bitcoind::start(&self.network, bitcoind_config, &self.liana_datadir) { + match Bitcoind::maybe_start(self.network, bitcoind_config, &self.liana_datadir) + { Err(e) => { self.started = Some(Err(StartInternalBitcoindError::CommandError(e.to_string()))); @@ -785,9 +785,9 @@ impl Step for InternalBitcoindStep { ) } - fn stop(&self) { + fn stop(&mut self) { // In case the installer is closed before changes written to context, stop bitcoind. - if let Some(bitcoind) = &self.internal_bitcoind { + if let Some(bitcoind) = self.internal_bitcoind.take() { bitcoind.stop(); } } diff --git a/liana-gui/src/loader.rs b/liana-gui/src/loader.rs index 43bf9aea..2631c81e 100644 --- a/liana-gui/src/loader.rs +++ b/liana-gui/src/loader.rs @@ -23,6 +23,7 @@ use lianad::{ }; use crate::app; +use crate::app::settings::WalletSetting; use crate::backup::Backup; use crate::dir::LianaDirectory; use crate::export::RestoreBackupError; @@ -33,9 +34,7 @@ use crate::{ wallet::{Wallet, WalletError}, }, daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError}, - node::bitcoind::{ - internal_bitcoind_debug_log_path, stop_bitcoind, Bitcoind, StartInternalBitcoindError, - }, + node::bitcoind::{internal_bitcoind_debug_log_path, Bitcoind, StartInternalBitcoindError}, }; const SYNCING_PROGRESS_1: &str = "Bitcoin Core is synchronising the blockchain. A full synchronisation typically takes a few days and is resource-intensive. Once the initial synchronisation is done, the next ones will be much faster."; @@ -60,6 +59,7 @@ pub struct Loader { pub internal_bitcoind: Option, pub waiting_daemon_bitcoind: bool, pub backup: Option, + pub wallet_setting: Option, step: Step, } @@ -119,6 +119,7 @@ impl Loader { network: bitcoin::Network, internal_bitcoind: Option, backup: Option, + wallet_setting: Option, ) -> (Self, Task) { let path = socket_path(&datadir_path, network); ( @@ -130,12 +131,27 @@ impl Loader { daemon_started: false, internal_bitcoind, waiting_daemon_bitcoind: false, + wallet_setting, backup, }, Task::perform(connect(path), Message::Loaded), ) } + fn start_bitcoind(&self) -> bool { + if self.internal_bitcoind.is_some() { + false + } else if let Some(start) = self + .wallet_setting + .as_ref() + .and_then(|setting| setting.start_internal_bitcoind) + { + start + } else { + self.gui_config.start_internal_bitcoind + } + } + fn maybe_skip_syncing( &mut self, daemon: Arc, @@ -189,8 +205,7 @@ impl Loader { return Task::perform( start_bitcoind_and_daemon( self.datadir_path.clone(), - self.gui_config.start_internal_bitcoind - && self.internal_bitcoind.is_none(), + self.start_bitcoind(), self.network, ), Message::Started, @@ -281,25 +296,7 @@ impl Loader { // NOTE: we take() the internal_bitcoind here to make sure the debug.log reader // subscription is dropped. if let Some(bitcoind) = self.internal_bitcoind.take() { - log::info!("Stopping managed bitcoind.."); bitcoind.stop(); - log::info!("Managed bitcoind stopped."); - } else if self.waiting_daemon_bitcoind && self.gui_config.start_internal_bitcoind { - let mut daemon_config_path = self - .datadir_path - .network_directory(self.network) - .path() - .to_path_buf(); - daemon_config_path.push("daemon.toml"); - if let Ok(config) = Config::from_file(Some(daemon_config_path)) { - if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = &config.bitcoin_backend { - let mut retry = 0; - while !stop_bitcoind(bitcoind_config) && retry < 10 { - std::thread::sleep(std::time::Duration::from_millis(500)); - retry += 1; - } - } - } } } @@ -312,6 +309,7 @@ impl Loader { self.network, self.internal_bitcoind.clone(), self.backup.clone(), + self.wallet_setting.clone(), ); *self = loader; cmd @@ -562,26 +560,17 @@ pub async fn start_bitcoind_and_daemon( .to_path_buf(); config_path.push("daemon.toml"); let config = Config::from_file(Some(config_path)).map_err(Error::Config)?; - let mut bitcoind: Option = None; - if start_internal_bitcoind { - if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = &config.bitcoin_backend { - // Check if bitcoind is already running before trying to start it. - if lianad::BitcoinD::new(bitcoind_config, "internal_bitcoind_start".to_string()).is_ok() - { - info!("Internal bitcoind is already running"); - } else { - info!("Starting internal bitcoind"); - bitcoind = Some( - Bitcoind::start( - &config.bitcoin_config.network, - bitcoind_config.clone(), - &liana_datadir_path, - ) - .map_err(Error::Bitcoind)?, - ); - } - } - } + let bitcoind = match (start_internal_bitcoind, &config.bitcoin_backend) { + (true, Some(BitcoinBackend::Bitcoind(bitcoind_config))) => Some( + Bitcoind::maybe_start( + config.bitcoin_config.network, + bitcoind_config.clone(), + &liana_datadir_path, + ) + .map_err(Error::Bitcoind)?, + ), + _ => None, + }; debug!("starting liana daemon"); diff --git a/liana-gui/src/main.rs b/liana-gui/src/main.rs index 18d90afa..28397bfb 100644 --- a/liana-gui/src/main.rs +++ b/liana-gui/src/main.rs @@ -30,6 +30,7 @@ use liana_gui::{ launcher::{self, Launcher}, loader::{self, Loader}, logger::Logger, + node::bitcoind::delete_all_bitcoind_locks_for_process, services::connect::{ client::backend::{api, BackendWalletClient}, login, @@ -208,10 +209,9 @@ impl GUI { ); let network_dir = datadir_path.network_directory(network); if let Ok(settings) = app::settings::Settings::from_file(&network_dir) { - if let Some(setting) = settings - .wallets - .into_iter() - .find_map(|w| w.remote_backend_auth) + let setting = settings.wallets.into_iter().next(); + if let Some(setting) = + setting.as_ref().and_then(|w| w.remote_backend_auth.clone()) { let (login, command) = login::LianaLiteLogin::new(datadir_path, network, setting); @@ -219,12 +219,13 @@ impl GUI { command.map(|msg| Message::Login(Box::new(msg))) } else { let (loader, command) = - Loader::new(datadir_path, cfg, network, None, None); + Loader::new(datadir_path, cfg, network, None, None, setting); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) } } else { - let (loader, command) = Loader::new(datadir_path, cfg, network, None, None); + let (loader, command) = + Loader::new(datadir_path, cfg, network, None, None, None); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) } @@ -280,10 +281,9 @@ impl GUI { let network_dir = i.datadir.network_directory(i.network); let settings = app::settings::Settings::from_file(&network_dir) .expect("A settings file was created"); - if let Some(setting) = settings - .wallets - .into_iter() - .find_map(|w| w.remote_backend_auth) + let setting = settings.wallets.into_iter().next(); + if let Some(setting) = + setting.as_ref().and_then(|w| w.remote_backend_auth.clone()) { let (login, command) = login::LianaLiteLogin::new(i.datadir.clone(), i.network, setting); @@ -308,6 +308,7 @@ impl GUI { i.network, internal_bitcoind, i.context.backup.take(), + setting, ); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) @@ -537,7 +538,7 @@ fn main() -> Result<(), Box> { None }; - setup_panic_hook(); + setup_panic_hook(&config.liana_directory); let settings = Settings { id: Some("Liana".to_string()), @@ -584,8 +585,13 @@ fn main() -> Result<(), Box> { } // A panic in any thread should stop the main thread, and print the panic. -fn setup_panic_hook() { +fn setup_panic_hook(liana_directory: &LianaDirectory) { + let bitcoind_dir = liana_directory.bitcoind_directory(); std::panic::set_hook(Box::new(move |panic_info| { + error!("Panic occured"); + if let Err(e) = delete_all_bitcoind_locks_for_process(bitcoind_dir.clone()) { + error!("Failed to delete internal bitcoind locks: {}", e); + } let file = panic_info .location() .map(|l| l.file()) diff --git a/liana-gui/src/node/bitcoind.rs b/liana-gui/src/node/bitcoind.rs index c1b25964..e660857e 100644 --- a/liana-gui/src/node/bitcoind.rs +++ b/liana-gui/src/node/bitcoind.rs @@ -10,20 +10,23 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt; use std::path::{Path, PathBuf}; -use std::sync::Arc; use std::thread; use std::time; +use std::time::{SystemTime, UNIX_EPOCH}; use tracing::{info, warn}; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; -use crate::dir::LianaDirectory; +use crate::dir::{BitcoindDirectory, LianaDirectory}; #[cfg(target_os = "windows")] const CREATE_NO_WINDOW: u32 = 0x08000000; +#[cfg(target_os = "windows")] +const DETACHED_PROCESS: u32 = 0x00000008; + /// Current and previous managed bitcoind versions, in order of descending version. pub const VERSIONS: [&str; 7] = ["29.0", "28.0", "27.1", "26.1", "26.0", "25.1", "25.0"]; @@ -371,6 +374,7 @@ impl InternalBitcoindConfig { /// Possible errors when starting bitcoind. #[derive(PartialEq, Eq, Debug, Clone)] pub enum StartInternalBitcoindError { + Lock(String), CommandError(String), CouldNotCanonicalizeDataDir(String), BitcoinDError(String), @@ -381,6 +385,9 @@ pub enum StartInternalBitcoindError { impl std::fmt::Display for StartInternalBitcoindError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { + Self::Lock(e) => { + write!(f, "lock file error: {}", e) + } Self::CommandError(e) => { write!(f, "Command to start bitcoind returned an error: {}", e) } @@ -397,17 +404,25 @@ impl std::fmt::Display for StartInternalBitcoindError { } #[derive(Debug, Clone)] pub struct Bitcoind { - _process: Arc, pub config: BitcoindConfig, + lock: LockFile, } impl Bitcoind { /// Start internal bitcoind for the given network. - pub fn start( - network: &bitcoin::Network, + pub fn maybe_start( + network: bitcoin::Network, config: BitcoindConfig, liana_datadir: &LianaDirectory, ) -> Result { + if lianad::BitcoinD::new(&config, "internal_bitcoind_start".to_string()).is_ok() { + info!("Internal bitcoind is already running"); + return Ok(Bitcoind { + config, + lock: LockFile::create(liana_datadir.bitcoind_directory(), network) + .map_err(|e| StartInternalBitcoindError::Lock(format!("{:?}", e)))?, + }); + } let bitcoind_datadir = internal_bitcoind_datadir(liana_datadir); // Find most recent bitcoind version available. let bitcoind_exe_path = VERSIONS @@ -448,7 +463,19 @@ impl Bitcoind { let mut command = std::process::Command::new(bitcoind_exe_path); #[cfg(target_os = "windows")] - let command = command.creation_flags(CREATE_NO_WINDOW); + let command = command.creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS); + + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + // Create a new session to detach the child from the main process. + unsafe { + command.pre_exec(|| { + libc::setsid(); + Ok(()) + }); + } + } let mut process = command .args(&args) @@ -474,7 +501,8 @@ impl Bitcoind { log::info!("Bitcoind seems to have successfully started."); return Ok(Self { config, - _process: Arc::new(process), + lock: LockFile::create(liana_datadir.bitcoind_directory(), network) + .map_err(|e| StartInternalBitcoindError::Lock(format!("{:?}", e)))?, }); } Err(lianad::BitcoindError::CookieFile(_)) => { @@ -498,26 +526,127 @@ impl Bitcoind { } /// Stop (internal) bitcoind. - pub fn stop(&self) { - stop_bitcoind(&self.config); + pub fn stop(self) { + match self.lock.delete() { + Err(e) => { + tracing::error!("Failed to release bitcoind lock: {}", e); + } + Ok(false) => { + info!("Other processes are using internal bitcoind. Process lock has been deleted"); + } + Ok(true) => { + match lianad::BitcoinD::new(&self.config, "internal_bitcoind_stop".to_string()) { + Ok(bitcoind) => { + info!("Stopping internal bitcoind..."); + bitcoind.stop(); + info!("Stopped liana managed bitcoind"); + } + Err(e) => { + warn!("Could not create interface to internal bitcoind: '{}'.", e); + } + } + } + } } } -pub fn stop_bitcoind(config: &BitcoindConfig) -> bool { - match lianad::BitcoinD::new(config, "internal_bitcoind_stop".to_string()) { - Ok(bitcoind) => { - info!("Stopping internal bitcoind..."); - bitcoind.stop(); - info!("Stopped liana managed bitcoind"); - true - } - Err(e) => { - warn!("Could not create interface to internal bitcoind: '{}'.", e); - false +const LOCK_DIRECTORY_NAME: &str = "locks"; + +#[derive(Debug, Clone)] +struct LockFile { + path: PathBuf, + directory: BitcoindDirectory, + network: Network, +} + +impl LockFile { + fn create( + directory: BitcoindDirectory, + network: Network, + ) -> Result> { + let mut path = directory.clone().path().to_path_buf(); + path.push(LOCK_DIRECTORY_NAME); + path.push(network.to_string()); + std::fs::create_dir_all(&path)?; + + path.push(format!( + "{}-{}.lock", + std::process::id(), + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + )); + + std::fs::File::create(&path)?; + Ok(Self { + path, + directory, + network, + }) + } + + // returns true if the lock directory is removed because empty. + fn delete(self) -> Result> { + std::fs::remove_file(self.path)?; + if std::fs::read_dir( + self.directory + .path() + .join(LOCK_DIRECTORY_NAME) + .join(self.network.to_string()), + )? + .next() + .is_none() + { + std::fs::remove_dir( + self.directory + .path() + .join(LOCK_DIRECTORY_NAME) + .join(self.network.to_string()), + )?; + + if std::fs::read_dir(self.directory.path().join(LOCK_DIRECTORY_NAME))? + .next() + .is_none() + { + std::fs::remove_dir(self.directory.path().join(LOCK_DIRECTORY_NAME))?; + } + + Ok(true) + } else { + Ok(false) } } } +// In case of panic, we remove all the bitcoind locks created by the process. +pub fn delete_all_bitcoind_locks_for_process( + directory: BitcoindDirectory, +) -> Result<(), Box> { + let locks_directory = directory.path().join(LOCK_DIRECTORY_NAME); + if !locks_directory.exists() { + tracing::debug!("No internal bitcoind locks for the current process"); + return Ok(()); + } + tracing::info!("Deleting all internal bitcoind locks for the current process"); + let process_prefix = format!("{}-", std::process::id()); + for network_dir in std::fs::read_dir(&locks_directory)? { + let dir = network_dir?.path(); + for lock_file in std::fs::read_dir(&dir)? { + let file = lock_file?.path(); + if let Some(name) = file.file_name().and_then(|n| n.to_str()) { + if name.starts_with(&process_prefix) { + std::fs::remove_file(file)?; + } + } + } + if std::fs::read_dir(&dir)?.next().is_none() { + std::fs::remove_dir(dir)?; + } + } + if std::fs::read_dir(&locks_directory)?.next().is_none() { + std::fs::remove_dir(locks_directory)?; + } + Ok(()) +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum RpcAuthType { CookieFile, diff --git a/lianad/src/bin/daemon.rs b/lianad/src/bin/daemon.rs index 7b1ffbaa..dcc756bd 100644 --- a/lianad/src/bin/daemon.rs +++ b/lianad/src/bin/daemon.rs @@ -5,7 +5,7 @@ use std::{ process, thread, time, }; -use lianad::{config::Config, DaemonHandle, VERSION}; +use lianad::{config::Config, setup_panic_hook, DaemonHandle, VERSION}; fn print_help_exit(code: i32) { eprintln!("lianad version {}", VERSION); @@ -80,6 +80,8 @@ fn main() { process::exit(1); }); + setup_panic_hook(); + let handle = DaemonHandle::start_default(config, cfg!(unix)).unwrap_or_else(|e| { log::error!("Error starting Liana daemon: {}", e); process::exit(1); diff --git a/lianad/src/lib.rs b/lianad/src/lib.rs index 7189bfe1..8ff2306b 100644 --- a/lianad/src/lib.rs +++ b/lianad/src/lib.rs @@ -40,7 +40,7 @@ use miniscript::bitcoin::{constants::ChainHash, hashes::Hash, secp256k1, BlockHa use std::panic; // A panic in any thread should stop the main thread, and print the panic. #[cfg(not(test))] -fn setup_panic_hook() { +pub fn setup_panic_hook() { panic::set_hook(Box::new(move |panic_info| { let file = panic_info .location() @@ -396,9 +396,6 @@ impl DaemonHandle { db: Option, with_rpc_server: bool, ) -> Result { - #[cfg(not(test))] - setup_panic_hook(); - let secp = secp256k1::Secp256k1::verification_only(); // First, check the data directory