Skip to content

Commit

Permalink
Merge #630: installer: Download and install bitcoind from GUI installer
Browse files Browse the repository at this point in the history
bf67f94 installer: download and install bitcoind (jp1ac4)
d507d57 gui: add subscription to Step trait (jp1ac4)
ae23fbc gui: add download module (jp1ac4)
b3bc943 gui: add dependencies to download and install bitcoind (jp1ac4)

Pull request description:

  This is a follow-up PR to #592 as part of #570 to download and install bitcoind.

  I'm creating this draft PR now to facilitate discussion. Once #592 has been merged, I'll rebase on master.

ACKs for top commit:
  darosior:
    ACK bf67f94 -- modulo a few changes we'll address in follow-ups. I've not reviewed the code but significantly tested it on both Windows and Linux.
  edouardparis:
    ACK bf67f94

Tree-SHA512: bda4b8bfbb3a59917d9ea60c074c2b0021229213240bebc4bd176f9909c62ab323d0a8d4becffbac192f29fad92adb81b2da0bc9b37c8eea1654c26ec6699077
  • Loading branch information
edouardparis committed Aug 30, 2023
2 parents ddb5303 + bf67f94 commit 3704995
Show file tree
Hide file tree
Showing 11 changed files with 1,067 additions and 108 deletions.
455 changes: 422 additions & 33 deletions gui/Cargo.lock

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,17 @@ toml = "0.5"

chrono = "0.4"

# Used for managing internal bitcoind
bitcoin_hashes = "0.12"
reqwest = { version = "0.11", default-features=false, features = ["rustls-tls"] }
rust-ini = "0.19.0"
which = "4.4.0"

[target.'cfg(windows)'.dependencies]
zip = { version = "0.6", default-features=false, features = ["bzip2", "deflate"] }

[target.'cfg(unix)'.dependencies]
tar = { version = "0.4", default-features=false }
flate2 = { version = "1.0", default-features=false }

[dev-dependencies]
tokio = {version = "1.9.0", features = ["rt", "macros"]}
Expand Down
41 changes: 30 additions & 11 deletions gui/src/bitcoind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ use tracing::{info, warn};

use crate::app::config::InternalBitcoindExeConfig;

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;

/// Possible errors when starting bitcoind.
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum StartInternalBitcoindError {
CommandError(String),
CouldNotCanonicalizeExePath(String),
CouldNotCanonicalizeDataDir(String),
CouldNotCanonicalizeCookiePath(String),
CookieFileNotFound(String),
Expand All @@ -20,6 +27,9 @@ impl std::fmt::Display for StartInternalBitcoindError {
Self::CommandError(e) => {
write!(f, "Command to start bitcoind returned an error: {}", e)
}
Self::CouldNotCanonicalizeExePath(e) => {
write!(f, "Failed to canonicalize executable path: {}", e)
}
Self::CouldNotCanonicalizeDataDir(e) => {
write!(f, "Failed to canonicalize datadir: {}", e)
}
Expand All @@ -43,22 +53,31 @@ pub fn start_internal_bitcoind(
network: &bitcoin::Network,
exe_config: InternalBitcoindExeConfig,
) -> Result<std::process::Child, StartInternalBitcoindError> {
let datadir_path_str = exe_config
.data_dir
.canonicalize()
.map_err(|e| StartInternalBitcoindError::CouldNotCanonicalizeDataDir(e.to_string()))?
.to_str()
.ok_or_else(|| {
StartInternalBitcoindError::CouldNotCanonicalizeDataDir(
"Couldn't convert path to str.".to_string(),
)
})?
.to_string();
#[cfg(target_os = "windows")]
// See https://github.com/rust-lang/rust/issues/42869.
let datadir_path_str = datadir_path_str.replace("\\\\?\\", "").replace("\\\\?", "");
let args = vec![
format!("-chain={}", network.to_core_arg()),
format!(
"-datadir={}",
exe_config
.data_dir
.canonicalize()
.map_err(|e| StartInternalBitcoindError::CouldNotCanonicalizeDataDir(
e.to_string()
))?
.to_string_lossy()
),
format!("-datadir={}", datadir_path_str),
];
std::process::Command::new(exe_config.exe_path)
let mut command = std::process::Command::new(exe_config.exe_path);
#[cfg(target_os = "windows")]
let command = command.creation_flags(CREATE_NO_WINDOW);
command
.args(&args)
.stdout(std::process::Stdio::null()) // We still get bitcoind's logs in debug.log.
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| StartInternalBitcoindError::CommandError(e.to_string()))
}
Expand Down
135 changes: 135 additions & 0 deletions gui/src/download.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// This is based on https://github.com/iced-rs/iced/blob/master/examples/download_progress/src/download.rs
// with some modifications to store the downloaded bytes in `Progress::Finished` and `State::Downloading`
// and to keep track of any download errors.
use iced::subscription;

use std::hash::Hash;

// Just a little utility function
pub fn file<I: 'static + Hash + Copy + Send + Sync, T: ToString>(
id: I,
url: T,
) -> iced::Subscription<(I, Progress)> {
subscription::unfold(id, State::Ready(url.to_string()), move |state| {
download(id, state)
})
}

#[derive(Debug, Hash, Clone)]
pub struct Download<I> {
id: I,
url: String,
}

/// Possible errors with download.
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum DownloadError {
UnknownContentLength,
RequestError(String),
}

impl std::fmt::Display for DownloadError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::UnknownContentLength => {
write!(f, "Response has unknown content length.")
}
Self::RequestError(e) => {
write!(f, "Request error: '{}'.", e)
}
}
}
}

async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) {
match state {
State::Ready(url) => {
let response = reqwest::get(&url).await;

match response {
Ok(response) => {
if let Some(total) = response.content_length() {
(
(id, Progress::Started),
State::Downloading {
response,
total,
downloaded: 0,
bytes: Vec::new(),
},
)
} else {
(
(id, Progress::Errored(DownloadError::UnknownContentLength)),
State::Finished,
)
}
}
Err(e) => (
(
id,
Progress::Errored(DownloadError::RequestError(e.to_string())),
),
State::Finished,
),
}
}
State::Downloading {
mut response,
total,
downloaded,
mut bytes,
} => match response.chunk().await {
Ok(Some(chunk)) => {
let downloaded = downloaded + chunk.len() as u64;

let percentage = (downloaded as f32 / total as f32) * 100.0;

bytes.append(&mut chunk.to_vec());

(
(id, Progress::Advanced(percentage)),
State::Downloading {
response,
total,
downloaded,
bytes,
},
)
}
Ok(None) => ((id, Progress::Finished(bytes)), State::Finished),
Err(e) => (
(
id,
Progress::Errored(DownloadError::RequestError(e.to_string())),
),
State::Finished,
),
},
State::Finished => {
// We do not let the stream die, as it would start a
// new download repeatedly if the user is not careful
// in case of errors.
iced::futures::future::pending().await
}
}
}

#[derive(Debug, Clone)]
pub enum Progress {
Started,
Advanced(f32),
Finished(Vec<u8>),
Errored(DownloadError),
}

pub enum State {
Ready(String),
Downloading {
response: reqwest::Response,
total: u64,
downloaded: u64,
bytes: Vec<u8>,
},
Finished,
}
5 changes: 4 additions & 1 deletion gui/src/installer/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use liana::miniscript::{
use std::path::PathBuf;

use super::Error;
use crate::hw::HardwareWallet;
use crate::{download::Progress, hw::HardwareWallet};
use async_hwi::DeviceKind;

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -55,6 +55,9 @@ pub enum InternalBitcoindMsg {
Previous,
Reload,
DefineConfig,
Download,
DownloadProgressed(Progress),
Install,
Start,
}

Expand Down
5 changes: 4 additions & 1 deletion gui/src/installer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ impl Installer {
}

pub fn subscription(&self) -> Subscription<Message> {
Subscription::none()
self.steps
.get(self.current)
.expect("There is always a step")
.subscription()
}

pub fn stop(&mut self) {
Expand Down
1 change: 0 additions & 1 deletion gui/src/installer/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str =
pub const REGISTER_DESCRIPTOR_HELP: &str = "To be used with the wallet, a signing device needs the descriptor. If the descriptor contains one or more keys imported from an external signing device, the descriptor must be registered on it. Registration confirms that the device is able to handle the policy. Registration on a device is not a substitute for backing up the descriptor.";
pub const MNEMONIC_HELP: &str = "A hot key generated on this computer was used for creating this wallet. It needs to be backed up. \n Keep it in a safe place. Never share it with anyone.";
pub const RECOVER_MNEMONIC_HELP: &str = "If you were using a hot key (a key stored on the computer) in your wallet, you will need to recover it from mnemonics to be able to sign transactions again. Otherwise you can directly go the next step.";
pub const SELECT_BITCOIND_TYPE: &str = "Liana requires a Bitcoin node to be running. You can either use your own node that you manage yourself or you can let Liana install and manage a pruned Bitcoin node for use while running Liana.";
Loading

0 comments on commit 3704995

Please sign in to comment.