feat: asynchronous management of internal bitcoind
This commit is contained in:
parent
13709aa302
commit
0488ed37a3
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3025,6 +3025,7 @@ dependencies = [
|
||||
"liana",
|
||||
"liana-ui",
|
||||
"lianad",
|
||||
"libc",
|
||||
"log",
|
||||
"reqwest",
|
||||
"rfd",
|
||||
|
||||
@ -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"] }
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}],
|
||||
};
|
||||
|
||||
|
||||
@ -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()),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ pub trait Step {
|
||||
true
|
||||
}
|
||||
fn revert(&self, _ctx: &mut Context) {}
|
||||
fn stop(&self) {}
|
||||
fn stop(&mut self) {}
|
||||
}
|
||||
|
||||
pub struct Final {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user