Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

installer: Download and install bitcoind from GUI installer #630

Merged
merged 4 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading