mod error;
mod ext;
mod versions;
pub extern crate bitcoind;
pub extern crate corepc_client;
pub extern crate electrum_client;
use std::env;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use bitcoind::anyhow::Context;
use bitcoind::serde_json::Value;
use bitcoind::tempfile::TempDir;
use bitcoind::{anyhow, get_available_port, BitcoinD};
use electrum_client::raw_client::{ElectrumPlaintextStream, RawClient};
use log::{debug, error, warn};
#[rustfmt::skip] pub use error::Error;
const IS_ALL_FEATURES_BUILD: bool = cfg!(feature = "all_features");
#[derive(Debug, PartialEq, Eq, Clone)]
#[non_exhaustive]
pub struct Conf<'a> {
pub args: Vec<&'a str>,
pub view_stderr: bool,
pub http_enabled: bool,
pub network: &'a str,
pub tmpdir: Option<PathBuf>,
pub staticdir: Option<PathBuf>,
attempts: u8,
}
impl Default for Conf<'_> {
fn default() -> Self {
#[allow(unused_mut)]
let mut args = vec![];
let should_version_use_verbose_flag =
cfg!(any(feature = "electrs_0_9_1", feature = "electrs_0_8_10", feature = "legacy"));
if !IS_ALL_FEATURES_BUILD && should_version_use_verbose_flag {
args.push("-vvv");
}
Conf {
args,
view_stderr: false,
http_enabled: false,
network: "regtest",
tmpdir: None,
staticdir: None,
attempts: 3,
}
}
}
pub struct ElectrsD {
process: Child,
pub client: RawClient<ElectrumPlaintextStream>,
work_dir: DataDir,
pub electrum_url: String,
pub esplora_url: Option<String>,
}
pub enum DataDir {
Persistent(PathBuf),
Temporary(TempDir),
}
impl DataDir {
fn path(&self) -> PathBuf {
match self {
Self::Persistent(path) => path.to_owned(),
Self::Temporary(tmp_dir) => tmp_dir.path().to_path_buf(),
}
}
}
impl ElectrsD {
pub fn new<S: AsRef<OsStr>>(exe: S, bitcoind: &BitcoinD) -> anyhow::Result<ElectrsD> {
ElectrsD::with_conf(exe, bitcoind, &Conf::default())
}
pub fn with_conf<S: AsRef<OsStr>>(
exe: S,
bitcoind: &BitcoinD,
conf: &Conf,
) -> anyhow::Result<ElectrsD> {
let response = bitcoind.client.call::<Value>("getblockchaininfo", &[])?;
if response.get("initialblockdownload").and_then(|v| v.as_bool()).unwrap_or(false) {
let node_address = bitcoind.client.call::<Value>("getnewaddress", &[])?;
bitcoind.client.call::<Value>("generatetoaddress", &[1.into(), node_address]).unwrap();
}
let mut args = conf.args.clone();
let work_dir = match (&conf.tmpdir, &conf.staticdir) {
(Some(_), Some(_)) => return Err(Error::BothDirsSpecified.into()),
(Some(tmpdir), None) => DataDir::Temporary(TempDir::new_in(tmpdir)?),
(None, Some(workdir)) => {
std::fs::create_dir_all(workdir)?;
DataDir::Persistent(workdir.to_owned())
}
(None, None) => match env::var("TEMPDIR_ROOT").map(PathBuf::from) {
Ok(path) => DataDir::Temporary(TempDir::new_in(path)?),
Err(_) => DataDir::Temporary(TempDir::new()?),
},
};
let db_dir = format!("{}", work_dir.path().display());
args.push("--db-dir");
args.push(&db_dir);
args.push("--network");
args.push(conf.network);
let cookie_flag;
let cookie_val = if !IS_ALL_FEATURES_BUILD && cfg!(feature = "legacy") {
cookie_flag = "--cookie";
std::fs::read_to_string(&bitcoind.params.cookie_file)?
} else {
cookie_flag = "--cookie-file";
bitcoind.params.cookie_file.display().to_string()
};
args.push(cookie_flag);
args.push(&cookie_val);
args.push("--daemon-rpc-addr");
let rpc_socket = bitcoind.params.rpc_socket.to_string();
args.push(&rpc_socket);
let p2p_socket;
let should_version_use_jsonrpc_import =
cfg!(any(feature = "electrs_0_8_10", feature = "esplora_a33e97e1", feature = "legacy"));
if !IS_ALL_FEATURES_BUILD && should_version_use_jsonrpc_import {
args.push("--jsonrpc-import");
} else {
args.push("--daemon-p2p-addr");
p2p_socket = bitcoind
.params
.p2p_socket
.expect("electrs needs bitcoind with p2p port open")
.to_string();
args.push(&p2p_socket);
}
let electrum_url = format!("0.0.0.0:{}", get_available_port()?);
args.push("--electrum-rpc-addr");
args.push(&electrum_url);
let monitoring = format!("0.0.0.0:{}", get_available_port()?);
args.push("--monitoring-addr");
args.push(&monitoring);
let esplora_url_string;
let esplora_url = if conf.http_enabled {
esplora_url_string = format!("0.0.0.0:{}", get_available_port()?);
args.push("--http-addr");
args.push(&esplora_url_string);
#[allow(clippy::redundant_clone)]
Some(esplora_url_string.clone())
} else {
None
};
let view_stderr = if conf.view_stderr { Stdio::inherit() } else { Stdio::null() };
debug!("args: {:?}", args);
let mut process = Command::new(&exe)
.args(args)
.stderr(view_stderr)
.spawn()
.with_context(|| format!("Error while executing {:?}", exe.as_ref()))?;
let client = loop {
if let Some(status) = process.try_wait()? {
if conf.attempts > 0 {
warn!("early exit with: {:?}. Trying to launch again ({} attempts remaining), maybe some other process used our available port", status, conf.attempts);
let mut conf = conf.clone();
conf.attempts -= 1;
return Self::with_conf(exe, bitcoind, &conf)
.with_context(|| format!("Remaining attempts {}", conf.attempts));
} else {
error!("early exit with: {:?}", status);
return Err(Error::EarlyExit(status).into());
}
}
match RawClient::new(&electrum_url, None) {
Ok(client) => break client,
Err(_) => std::thread::sleep(Duration::from_millis(500)),
}
};
Ok(ElectrsD { process, client, work_dir, electrum_url, esplora_url })
}
#[cfg(not(target_os = "windows"))]
pub fn trigger(&self) -> anyhow::Result<()> {
Ok(nix::sys::signal::kill(
nix::unistd::Pid::from_raw(self.process.id() as i32),
nix::sys::signal::SIGUSR1,
)?)
}
#[cfg(target_os = "windows")]
pub fn trigger(&self) -> anyhow::Result<()> { Ok(()) }
pub fn workdir(&self) -> PathBuf { self.work_dir.path() }
pub fn kill(&mut self) -> anyhow::Result<()> {
match self.work_dir {
DataDir::Persistent(_) => {
self.inner_kill()?;
match self.process.wait() {
Ok(_) => Ok(()),
Err(e) => Err(e.into()),
}
}
DataDir::Temporary(_) => Ok(self.process.kill()?),
}
}
#[cfg(not(target_os = "windows"))]
fn inner_kill(&mut self) -> anyhow::Result<()> {
Ok(nix::sys::signal::kill(
nix::unistd::Pid::from_raw(self.process.id() as i32),
nix::sys::signal::SIGINT,
)?)
}
#[cfg(target_os = "windows")]
fn inner_kill(&mut self) -> anyhow::Result<()> { Ok(self.process.kill()?) }
}
impl Drop for ElectrsD {
fn drop(&mut self) { let _ = self.kill(); }
}
pub fn downloaded_exe_path() -> Option<String> {
if std::env::var_os("ELECTRSD_SKIP_DOWNLOAD").is_none() {
Some(format!("{}/electrs/{}/electrs", env!("OUT_DIR"), versions::electrs_name(),))
} else {
None
}
}
pub fn exe_path() -> anyhow::Result<String> {
if let (Ok(_), Ok(_)) = (std::env::var("ELECTRS_EXEC"), std::env::var("ELECTRS_EXE")) {
return Err(error::Error::BothEnvVars.into());
}
if let Ok(path) = std::env::var("ELECTRS_EXEC") {
return Ok(path);
}
if let Ok(path) = std::env::var("ELECTRS_EXE") {
return Ok(path);
}
if let Some(path) = downloaded_exe_path() {
return Ok(path);
}
let path_var = env::var("PATH").map_err(|_| Error::NoElectrsExecutableFound)?;
#[cfg(target_os = "windows")]
let path_separator = ';';
#[cfg(not(target_os = "windows"))]
let path_separator = ':';
for path_dir in path_var.split(path_separator) {
let mut candidate = PathBuf::from(path_dir);
candidate.push("electrs");
#[cfg(target_os = "windows")]
{
candidate.set_extension("exe");
}
if candidate.is_file() {
#[cfg(not(target_os = "windows"))]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = std::fs::metadata(&candidate) {
let permissions = metadata.permissions();
if permissions.mode() & 0o111 != 0 {
return Ok(candidate.display().to_string());
}
}
}
#[cfg(target_os = "windows")]
{
return Ok(candidate.display().to_string());
}
}
}
Err(Error::NoElectrsExecutableFound.into())
}
#[cfg(test)]
mod test {
use std::env;
use bitcoind::P2P;
use electrum_client::ElectrumApi;
use log::{debug, log_enabled, Level};
use crate::{exe_path, ElectrsD, IS_ALL_FEATURES_BUILD};
#[test]
#[ignore] fn test_both_env_vars() {
env::set_var("ELECTRS_EXEC", "placeholder");
env::set_var("ELECTRS_EXE", "placeholder");
assert!(exe_path().is_err());
env::remove_var("ELECTRS_EXEC");
env::remove_var("ELECTRS_EXE");
}
#[test]
fn test_electrsd() {
let (electrs_exe, bitcoind, electrsd) = setup_nodes();
let header = electrsd.client.block_headers_subscribe().unwrap();
assert_eq!(header.height, 1);
let address = bitcoind.client.new_address().unwrap();
bitcoind.client.generate_to_address(100, &address).unwrap();
electrsd.trigger().unwrap();
let header = loop {
std::thread::sleep(std::time::Duration::from_millis(100));
let header = electrsd.client.block_headers_subscribe().unwrap();
if header.height > 100 {
break header;
}
};
assert_eq!(header.height, 101);
let electrsd = ElectrsD::new(&electrs_exe, &bitcoind).unwrap();
let header = electrsd.client.block_headers_subscribe().unwrap();
assert_eq!(header.height, 101);
}
#[test]
fn test_kill() {
let (_, bitcoind, mut electrsd) = setup_nodes();
let _ = bitcoind.client.get_network_info().unwrap(); electrsd.client.ping().unwrap();
assert!(electrsd.client.ping().is_ok());
electrsd.kill().unwrap();
assert!(electrsd.client.ping().is_err());
}
pub(crate) fn setup_nodes() -> (String, bitcoind::BitcoinD, ElectrsD) {
let (bitcoind_exe, electrs_exe) = init();
debug!("bitcoind: {}", &bitcoind_exe);
debug!("electrs: {}", &electrs_exe);
let mut conf = bitcoind::Conf::default();
conf.view_stdout = log_enabled!(Level::Debug);
let should_version_use_p2p =
!cfg!(any(feature = "electrs_0_8_10", feature = "esplora_a33e97e1"));
if IS_ALL_FEATURES_BUILD || should_version_use_p2p {
conf.p2p = P2P::Yes;
}
let bitcoind = bitcoind::BitcoinD::with_conf(&bitcoind_exe, &conf).unwrap();
let electrs_conf =
crate::Conf { view_stderr: log_enabled!(Level::Debug), ..Default::default() };
let electrsd = ElectrsD::with_conf(&electrs_exe, &bitcoind, &electrs_conf).unwrap();
(electrs_exe, bitcoind, electrsd)
}
fn init() -> (String, String) {
let _ = env_logger::try_init();
let bitcoind_exe_path = bitcoind::exe_path().unwrap();
let electrs_exe_path = exe_path().unwrap();
(bitcoind_exe_path, electrs_exe_path)
}
}