feat: asynchronous management of internal bitcoind

This commit is contained in:
edouardparis 2025-05-02 09:10:42 +02:00
parent 13709aa302
commit 0488ed37a3
12 changed files with 177 additions and 82 deletions

1
Cargo.lock generated
View File

@ -3025,6 +3025,7 @@ dependencies = [
"liana",
"liana-ui",
"lianad",
"libc",
"log",
"reqwest",
"rfd",

View File

@ -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"] }

View File

@ -11,6 +11,7 @@ pub struct Config {
/// Use iced debug feature if true.
pub debug: Option<bool>,
/// Start internal bitcoind executable.
/// Legacy field, replaced by settings.json start_internal_bitcoind field
#[serde(default)]
pub start_internal_bitcoind: bool,
}

View File

@ -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();
}
}

View File

@ -96,6 +96,9 @@ pub struct WalletSetting {
#[serde(default)]
pub hardware_wallets: Vec<HardwareWalletConfig>,
pub remote_backend_auth: Option<AuthConfig>,
/// Start internal bitcoind executable.
/// if None, the app must refer to the gui.toml start_internal_bitcoind field.
pub start_internal_bitcoind: Option<bool>,
}
impl WalletSetting {

View File

@ -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,
}],
};

View File

@ -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()),
}],
}
}

View File

@ -60,7 +60,7 @@ pub trait Step {
true
}
fn revert(&self, _ctx: &mut Context) {}
fn stop(&self) {}
fn stop(&mut self) {}
}
pub struct Final {

View File

@ -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();
}
}

View File

@ -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<Bitcoind>,
pub waiting_daemon_bitcoind: bool,
pub backup: Option<Backup>,
pub wallet_setting: Option<WalletSetting>,
step: Step,
}
@ -119,6 +119,7 @@ impl Loader {
network: bitcoin::Network,
internal_bitcoind: Option<Bitcoind>,
backup: Option<Backup>,
wallet_setting: Option<WalletSetting>,
) -> (Self, Task<Message>) {
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<dyn Daemon + Sync + Send>,
@ -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<Bitcoind> = 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");

View File

@ -208,10 +208,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 +218,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 +280,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 +307,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)))

View File

@ -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; 6] = ["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<std::process::Child>,
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<Self, StartInternalBitcoindError> {
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,22 +526,92 @@ 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<Self, Box<dyn std::error::Error>> {
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<bool, Box<dyn std::error::Error>> {
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)
}
}
}