Merge #1018: Gui handle poller error

a7f2bccb9a443c78a9788eedce06e2d753f7d11e gui: handle daemon stop error (edouardparis)
924df8e1d5440159306504c0ddbbee6f962cd7c5 bump liana:master (edouardparis)

Pull request description:

  based on #986

ACKs for top commit:
  darosior:
    ACK a7f2bccb9a443c78a9788eedce06e2d753f7d11e

Tree-SHA512: 24c48948f10eed7f04dcab0779b75d89c5fd40441c04dffb6f4e5b3e682ca2ca36de51cdc69e7679ed262c4e92691d789c29822972f5c99290c54d13c7dc3472
This commit is contained in:
Antoine Poinsot 2024-03-22 15:20:21 +01:00
commit 3b31871514
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
7 changed files with 116 additions and 59 deletions

2
gui/Cargo.lock generated
View File

@ -2620,7 +2620,7 @@ dependencies = [
[[package]] [[package]]
name = "liana" name = "liana"
version = "4.0.0" version = "4.0.0"
source = "git+https://github.com/wizardsardine/liana?branch=master#dd29578c500ea7644154a5923bea9fa1db9b68e5" source = "git+https://github.com/wizardsardine/liana?branch=master#2aa8874456bd8e43085c1496dedde68d3ce9d49a"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bdk_coin_select", "bdk_coin_select",

View File

@ -16,7 +16,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use iced::{clipboard, time, Command, Subscription}; use iced::{clipboard, time, Command, Subscription};
use tracing::{info, warn}; use tracing::{error, info, warn};
pub use liana::{config::Config as DaemonConfig, miniscript::bitcoin}; pub use liana::{config::Config as DaemonConfig, miniscript::bitcoin};
use liana_ui::widget::Element; use liana_ui::widget::Element;
@ -217,8 +217,11 @@ impl App {
pub fn stop(&mut self) { pub fn stop(&mut self) {
info!("Close requested"); info!("Close requested");
if !self.daemon.is_external() { if !self.daemon.is_external() {
self.daemon.stop(); if let Err(e) = self.daemon.stop() {
error!("{}", e);
} else {
info!("Internal daemon stopped"); info!("Internal daemon stopped");
}
if let Some(bitcoind) = &self.internal_bitcoind { if let Some(bitcoind) = &self.internal_bitcoind {
bitcoind.stop(); bitcoind.stop();
} }
@ -232,6 +235,9 @@ impl App {
let datadir_path = self.cache.datadir_path.clone(); let datadir_path = self.cache.datadir_path.clone();
Command::perform( Command::perform(
async move { async move {
// we check every 10 second if the daemon poller is alive
daemon.is_alive()?;
let info = daemon.get_info()?; let info = daemon.get_info()?;
// todo: filter coins to only have current coins. // todo: filter coins to only have current coins.
let coins = daemon.list_coins()?; let coins = daemon.list_coins()?;
@ -278,7 +284,7 @@ impl App {
daemon_config_path: &PathBuf, daemon_config_path: &PathBuf,
cfg: DaemonConfig, cfg: DaemonConfig,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.daemon.stop(); self.daemon.stop()?;
let daemon = EmbeddedDaemon::start(cfg)?; let daemon = EmbeddedDaemon::start(cfg)?;
self.daemon = Arc::new(daemon); self.daemon = Arc::new(daemon);

View File

@ -61,7 +61,11 @@ impl<C: Client + Debug> Daemon for Lianad<C> {
None None
} }
fn stop(&self) { fn is_alive(&self) -> Result<(), DaemonError> {
Ok(())
}
fn stop(&self) -> Result<(), DaemonError> {
unreachable!("GUI should not ask external client to stop") unreachable!("GUI should not ask external client to stop")
} }

View File

@ -1,4 +1,5 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use super::{model::*, Daemon, DaemonError}; use super::{model::*, Daemon, DaemonError};
use liana::{ use liana::{
@ -10,24 +11,35 @@ use liana::{
pub struct EmbeddedDaemon { pub struct EmbeddedDaemon {
config: Config, config: Config,
handle: DaemonHandle, handle: Mutex<Option<DaemonHandle>>,
} }
impl EmbeddedDaemon { impl EmbeddedDaemon {
pub fn start(config: Config) -> Result<EmbeddedDaemon, DaemonError> { pub fn start(config: Config) -> Result<EmbeddedDaemon, DaemonError> {
let handle = DaemonHandle::start_default(config.clone()).map_err(DaemonError::Start)?; let handle = DaemonHandle::start_default(config.clone()).map_err(DaemonError::Start)?;
Ok(Self { handle, config }) Ok(Self {
handle: Mutex::new(Some(handle)),
config,
})
} }
fn control(&self) -> Result<&DaemonControl, DaemonError> { pub fn command<T, F>(&self, method: F) -> Result<T, DaemonError>
if self.handle.shutdown_complete() { where
Err(DaemonError::DaemonStopped) F: FnOnce(&DaemonControl) -> Result<T, DaemonError>,
} else { {
Ok(&self.handle.control) match self.handle.lock()?.as_ref() {
Some(DaemonHandle::Controller { control, .. }) => method(control),
None => Err(DaemonError::DaemonStopped),
} }
} }
} }
impl<T> From<std::sync::PoisonError<T>> for DaemonError {
fn from(value: std::sync::PoisonError<T>) -> Self {
DaemonError::Unexpected(format!("Daemon panic: {}", value))
}
}
impl std::fmt::Debug for EmbeddedDaemon { impl std::fmt::Debug for EmbeddedDaemon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DaemonHandle").finish() f.debug_struct("DaemonHandle").finish()
@ -43,30 +55,48 @@ impl Daemon for EmbeddedDaemon {
Some(&self.config) Some(&self.config)
} }
fn stop(&self) { fn is_alive(&self) -> Result<(), DaemonError> {
self.handle.trigger_shutdown(); let mut handle = self.handle.lock()?;
while !self.handle.shutdown_complete() { if let Some(h) = handle.as_ref() {
tracing::debug!("Waiting daemon to shutdown"); if h.is_alive() {
std::thread::sleep(std::time::Duration::from_millis(500)); return Ok(());
} }
} }
// if the daemon poller is not alive, we try to terminate it to fetch the error.
if let Some(h) = handle.take() {
h.stop()
.map_err(|e| DaemonError::Unexpected(e.to_string()))?;
}
Ok(())
}
fn stop(&self) -> Result<(), DaemonError> {
let mut handle = self.handle.lock()?;
if let Some(h) = handle.take() {
h.stop()
.map_err(|e| DaemonError::Unexpected(e.to_string()))?;
}
Ok(())
}
fn get_info(&self) -> Result<GetInfoResult, DaemonError> { fn get_info(&self) -> Result<GetInfoResult, DaemonError> {
Ok(self.control()?.get_info()) self.command(|daemon| Ok(daemon.get_info()))
} }
fn get_new_address(&self) -> Result<GetAddressResult, DaemonError> { fn get_new_address(&self) -> Result<GetAddressResult, DaemonError> {
Ok(self.control()?.get_new_address()) self.command(|daemon| Ok(daemon.get_new_address()))
} }
fn list_coins(&self) -> Result<ListCoinsResult, DaemonError> { fn list_coins(&self) -> Result<ListCoinsResult, DaemonError> {
Ok(self.control()?.list_coins(&[], &[])) self.command(|daemon| Ok(daemon.list_coins(&[], &[])))
} }
fn list_spend_txs(&self) -> Result<ListSpendResult, DaemonError> { fn list_spend_txs(&self) -> Result<ListSpendResult, DaemonError> {
self.control()? self.command(|daemon| {
daemon
.list_spend(None) .list_spend(None)
.map_err(|e| DaemonError::Unexpected(e.to_string())) .map_err(|e| DaemonError::Unexpected(e.to_string()))
})
} }
fn list_confirmed_txs( fn list_confirmed_txs(
@ -75,13 +105,11 @@ impl Daemon for EmbeddedDaemon {
end: u32, end: u32,
limit: u64, limit: u64,
) -> Result<ListTransactionsResult, DaemonError> { ) -> Result<ListTransactionsResult, DaemonError> {
Ok(self self.command(|daemon| Ok(daemon.list_confirmed_transactions(start, end, limit)))
.control()?
.list_confirmed_transactions(start, end, limit))
} }
fn list_txs(&self, txids: &[Txid]) -> Result<ListTransactionsResult, DaemonError> { fn list_txs(&self, txids: &[Txid]) -> Result<ListTransactionsResult, DaemonError> {
Ok(self.control()?.list_transactions(txids)) self.command(|daemon| Ok(daemon.list_transactions(txids)))
} }
fn create_spend_tx( fn create_spend_tx(
@ -91,9 +119,11 @@ impl Daemon for EmbeddedDaemon {
feerate_vb: u64, feerate_vb: u64,
change_address: Option<Address<address::NetworkUnchecked>>, change_address: Option<Address<address::NetworkUnchecked>>,
) -> Result<CreateSpendResult, DaemonError> { ) -> Result<CreateSpendResult, DaemonError> {
self.control()? self.command(|daemon| {
daemon
.create_spend(destinations, coins_outpoints, feerate_vb, change_address) .create_spend(destinations, coins_outpoints, feerate_vb, change_address)
.map_err(|e| DaemonError::Unexpected(e.to_string())) .map_err(|e| DaemonError::Unexpected(e.to_string()))
})
} }
fn rbf_psbt( fn rbf_psbt(
@ -102,32 +132,42 @@ impl Daemon for EmbeddedDaemon {
is_cancel: bool, is_cancel: bool,
feerate_vb: Option<u64>, feerate_vb: Option<u64>,
) -> Result<CreateSpendResult, DaemonError> { ) -> Result<CreateSpendResult, DaemonError> {
self.control()? self.command(|daemon| {
daemon
.rbf_psbt(txid, is_cancel, feerate_vb) .rbf_psbt(txid, is_cancel, feerate_vb)
.map_err(|e| DaemonError::Unexpected(e.to_string())) .map_err(|e| DaemonError::Unexpected(e.to_string()))
})
} }
fn update_spend_tx(&self, psbt: &Psbt) -> Result<(), DaemonError> { fn update_spend_tx(&self, psbt: &Psbt) -> Result<(), DaemonError> {
self.control()? self.command(|daemon| {
daemon
.update_spend(psbt.clone()) .update_spend(psbt.clone())
.map_err(|e| DaemonError::Unexpected(e.to_string())) .map_err(|e| DaemonError::Unexpected(e.to_string()))
})
} }
fn delete_spend_tx(&self, txid: &Txid) -> Result<(), DaemonError> { fn delete_spend_tx(&self, txid: &Txid) -> Result<(), DaemonError> {
self.control()?.delete_spend(txid); self.command(|daemon| {
daemon.delete_spend(txid);
Ok(()) Ok(())
})
} }
fn broadcast_spend_tx(&self, txid: &Txid) -> Result<(), DaemonError> { fn broadcast_spend_tx(&self, txid: &Txid) -> Result<(), DaemonError> {
self.control()? self.command(|daemon| {
daemon
.broadcast_spend(txid) .broadcast_spend(txid)
.map_err(|e| DaemonError::Unexpected(e.to_string())) .map_err(|e| DaemonError::Unexpected(e.to_string()))
})
} }
fn start_rescan(&self, t: u32) -> Result<(), DaemonError> { fn start_rescan(&self, t: u32) -> Result<(), DaemonError> {
self.control()? self.command(|daemon| {
daemon
.start_rescan(t) .start_rescan(t)
.map_err(|e| DaemonError::Unexpected(e.to_string())) .map_err(|e| DaemonError::Unexpected(e.to_string()))
})
} }
fn create_recovery( fn create_recovery(
@ -136,21 +176,25 @@ impl Daemon for EmbeddedDaemon {
feerate_vb: u64, feerate_vb: u64,
sequence: Option<u16>, sequence: Option<u16>,
) -> Result<Psbt, DaemonError> { ) -> Result<Psbt, DaemonError> {
self.control()? self.command(|daemon| {
daemon
.create_recovery(address, feerate_vb, sequence) .create_recovery(address, feerate_vb, sequence)
.map_err(|e| DaemonError::Unexpected(e.to_string()))
.map(|res| res.psbt) .map(|res| res.psbt)
.map_err(|e| DaemonError::Unexpected(e.to_string()))
})
} }
fn get_labels( fn get_labels(
&self, &self,
items: &HashSet<LabelItem>, items: &HashSet<LabelItem>,
) -> Result<HashMap<String, String>, DaemonError> { ) -> Result<HashMap<String, String>, DaemonError> {
Ok(self.handle.control.get_labels(items).labels) self.command(|daemon| Ok(daemon.get_labels(items).labels))
} }
fn update_labels(&self, items: &HashMap<LabelItem, Option<String>>) -> Result<(), DaemonError> { fn update_labels(&self, items: &HashMap<LabelItem, Option<String>>) -> Result<(), DaemonError> {
self.handle.control.update_labels(items); self.command(|daemon| {
daemon.update_labels(items);
Ok(()) Ok(())
})
} }
} }

View File

@ -52,7 +52,8 @@ impl std::fmt::Display for DaemonError {
pub trait Daemon: Debug { pub trait Daemon: Debug {
fn is_external(&self) -> bool; fn is_external(&self) -> bool;
fn config(&self) -> Option<&Config>; fn config(&self) -> Option<&Config>;
fn stop(&self); fn is_alive(&self) -> Result<(), DaemonError>;
fn stop(&self) -> Result<(), DaemonError>;
fn get_info(&self) -> Result<model::GetInfoResult, DaemonError>; fn get_info(&self) -> Result<model::GetInfoResult, DaemonError>;
fn get_new_address(&self) -> Result<model::GetAddressResult, DaemonError>; fn get_new_address(&self) -> Result<model::GetAddressResult, DaemonError>;
fn list_coins(&self) -> Result<model::ListCoinsResult, DaemonError>; fn list_coins(&self) -> Result<model::ListCoinsResult, DaemonError>;

View File

@ -260,10 +260,9 @@ impl Installer {
pub fn daemon_check(cfg: liana::config::Config) -> Result<(), Error> { pub fn daemon_check(cfg: liana::config::Config) -> Result<(), Error> {
// Start Daemon to check correctness of installation // Start Daemon to check correctness of installation
match liana::DaemonHandle::start_default(cfg) { match liana::DaemonHandle::start_default(cfg) {
Ok(daemon) => { Ok(daemon) => daemon
daemon.shutdown(); .stop()
Ok(()) .map_err(|e| Error::Unexpected(format!("Failed to stop Liana daemon: {}", e))),
}
Err(e) => Err(Error::Unexpected(format!( Err(e) => Err(Error::Unexpected(format!(
"Failed to start Liana daemon: {}", "Failed to start Liana daemon: {}",
e e

View File

@ -228,10 +228,13 @@ impl Loader {
if let Step::Syncing { daemon, .. } = &mut self.step { if let Step::Syncing { daemon, .. } = &mut self.step {
if !daemon.is_external() { if !daemon.is_external() {
info!("Stopping internal daemon..."); info!("Stopping internal daemon...");
daemon.stop(); if let Err(e) = daemon.stop() {
warn!("Internal daemon failed to stop: {}", e);
} else {
info!("Internal daemon stopped"); info!("Internal daemon stopped");
} }
} }
}
// NOTE: we take() the internal_bitcoind here to make sure the debug.log reader // NOTE: we take() the internal_bitcoind here to make sure the debug.log reader
// subscription is dropped. // subscription is dropped.