diff --git a/crates/brk_cli/Cargo.toml b/crates/brk_cli/Cargo.toml index dbf742d15..417c11dbd 100644 --- a/crates/brk_cli/Cargo.toml +++ b/crates/brk_cli/Cargo.toml @@ -17,7 +17,9 @@ brk_server = { workspace = true } clap = { workspace = true, features = ["string"] } color-eyre = { workspace = true } log = { workspace = true } +serde = { workspace = true } tabled = { workspace = true } +toml = "0.8.20" [[bin]] name = "brk" diff --git a/crates/brk_cli/src/main.rs b/crates/brk_cli/src/main.rs index 0e6bcac67..a7a944c65 100644 --- a/crates/brk_cli/src/main.rs +++ b/crates/brk_cli/src/main.rs @@ -1,7 +1,8 @@ -use std::path::Path; +use std::{ + fs, + path::{Path, PathBuf}, +}; -use brk_computer::Computer; -use brk_indexer::Indexer; use brk_query::Params as QueryArgs; use clap::{Parser, Subcommand}; use query::query; @@ -29,18 +30,23 @@ enum Commands { fn main() -> color_eyre::Result<()> { color_eyre::install()?; - brk_logger::init(Some(Path::new(".log"))); + fs::create_dir_all(path_dot_brk())?; + + brk_logger::init(Some(&path_log())); let cli = Cli::parse(); - let outputs_dir = Path::new("../../_outputs"); - - let indexer = Indexer::import(&outputs_dir.join("indexed"))?; - - let computer = Computer::import(&outputs_dir.join("computed"))?; - - match &cli.command { - Commands::Run(args) => run(indexer, computer, args), - Commands::Query(args) => query(indexer, computer, args), + match cli.command { + Commands::Run(args) => run(args), + Commands::Query(args) => query(args), } } + +pub fn path_dot_brk() -> PathBuf { + let home = std::env::var("HOME").unwrap(); + Path::new(&home).join(".brk") +} + +pub fn path_log() -> PathBuf { + path_dot_brk().join("log") +} diff --git a/crates/brk_cli/src/query.rs b/crates/brk_cli/src/query.rs index 7eb41a112..1a39386e9 100644 --- a/crates/brk_cli/src/query.rs +++ b/crates/brk_cli/src/query.rs @@ -3,7 +3,17 @@ use brk_indexer::Indexer; use brk_query::{Index, Output, Params as QueryParams, Query, Tabled, Value}; use tabled::settings::Style; -pub fn query(indexer: Indexer, computer: Computer, params: &QueryParams) -> color_eyre::Result<()> { +use crate::run::RunConfig; + +pub fn query(params: QueryParams) -> color_eyre::Result<()> { + let config = RunConfig::import(None)?; + + let mut indexer = Indexer::new(&config.indexeddir())?; + indexer.import_vecs()?; + + let mut computer = Computer::new(&config.computeddir()); + computer.import_vecs()?; + let query = Query::build(&indexer, &computer); let ids = params.values.iter().flat_map(|v| v.split(",")).collect::>(); diff --git a/crates/brk_cli/src/run.rs b/crates/brk_cli/src/run.rs index 6eee402b0..9c2b44062 100644 --- a/crates/brk_cli/src/run.rs +++ b/crates/brk_cli/src/run.rs @@ -1,29 +1,47 @@ -use std::{path::Path, thread::sleep, time::Duration}; +use std::{ + fs, + path::{Path, PathBuf}, + thread::sleep, + time::Duration, +}; use brk_computer::Computer; use brk_exit::Exit; use brk_indexer::Indexer; -use brk_parser::rpc::{self, RpcApi}; +use brk_parser::rpc::{self, Auth, RpcApi}; use brk_server::tokio; -use clap::Parser; +use clap::{Parser, ValueEnum}; +use color_eyre::eyre::eyre; use log::info; +use serde::{Deserialize, Serialize}; -#[derive(Parser, Debug)] -pub struct RunConfig { - name: Option, -} +use crate::path_dot_brk; -pub fn run(mut indexer: Indexer, mut computer: Computer, config: &RunConfig) -> color_eyre::Result<()> { - let data_dir = Path::new("../../../bitcoin"); +pub fn run(config: RunConfig) -> color_eyre::Result<()> { + let config = RunConfig::import(Some(config))?; + + let bitcoin_dir = config.bitcoindir(); let rpc = Box::leak(Box::new(rpc::Client::new( - "http://localhost:8332", - rpc::Auth::CookieFile(Path::new(data_dir).join(".cookie")), + &format!( + "http://{}:{}", + config.rpcconnect().unwrap_or(&"localhost".to_string()), + config.rpcport().unwrap_or(8332) + ), + config.to_rpc_auth().unwrap(), )?)); let exit = Exit::new(); - let parser = brk_parser::Parser::new(data_dir, rpc); + let parser = brk_parser::Parser::new(bitcoin_dir.as_path(), rpc); + + let mut indexer = Indexer::new(&config.indexeddir())?; + indexer.import_stores()?; + indexer.import_vecs()?; + + let mut computer = Computer::new(&config.computeddir()); + computer.import_stores()?; + computer.import_vecs()?; tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -32,27 +50,288 @@ pub fn run(mut indexer: Indexer, mut computer: Computer, config: &RunConfig) -> let served_indexer = indexer.clone(); let served_computer = computer.clone(); - tokio::spawn(async move { - brk_server::main(served_indexer, served_computer).await.unwrap(); - }); + let handle = if config.serve() { + Some(tokio::spawn(async move { + brk_server::main(served_indexer, served_computer).await.unwrap(); + })) + } else { + None + }; - loop { - let block_count = rpc.get_block_count()?; + if config.process() { + loop { + let block_count = rpc.get_block_count()?; - info!("{block_count} blocks found."); + info!("{block_count} blocks found."); - let starting_indexes = indexer.index(&parser, rpc, &exit)?; + let starting_indexes = indexer.index(&parser, rpc, &exit)?; - computer.compute(&mut indexer, starting_indexes, &exit)?; + computer.compute(&mut indexer, starting_indexes, &exit)?; - info!("Waiting for new blocks..."); + if let Some(delay) = config.delay() { + sleep(Duration::from_secs(delay)) + } - while block_count == rpc.get_block_count()? { - sleep(Duration::from_secs(1)) + info!("Waiting for new blocks..."); + + while block_count == rpc.get_block_count()? { + sleep(Duration::from_secs(1)) + } } } - #[allow(unreachable_code)] + if let Some(handle) = handle { + handle.await.unwrap(); + } Ok(()) }) } + +#[derive(Parser, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +pub struct RunConfig { + /// Bitcoin data directory path, saved + #[arg(short, long, value_name = "PATH")] + bitcoindir: Option, + + /// Bitcoin Research Kit outputs directory path, saved + #[arg(short, long, value_name = "PATH")] + brkdir: Option, + + /// Executed by the runner, default: all, saved + #[arg(short, long)] + mode: Option, + + /// Bitcoin RPC ip, default: localhost, saved + #[arg(long, value_name = "IP")] + rpcconnect: Option, + + /// Bitcoin RPC port, default: 8332, saved + #[arg(long, value_name = "PORT")] + rpcport: Option, + + /// Bitcoin RPC cookie file, default: --bitcoindir/.cookie, saved + #[arg(long, value_name = "PATH")] + rpccookiefile: Option, + + /// Bitcoin RPC username, saved + #[arg(long, value_name = "USERNAME")] + rpcuser: Option, + + /// Bitcoin RPC password, saved + #[arg(long, value_name = "PASSWORD")] + rpcpassword: Option, + + /// Delay between runs, default: 0, saved + #[arg(long, value_name = "SECONDS")] + delay: Option, +} + +impl RunConfig { + pub fn import(config_args: Option) -> color_eyre::Result { + let path = path_dot_brk(); + + let _ = fs::create_dir_all(&path); + + let path = path.join("config.toml"); + + let mut config_saved = Self::read(&path); + + if let Some(mut config_args) = config_args { + if let Some(bitcoindir) = config_args.bitcoindir.take() { + config_saved.bitcoindir = Some(bitcoindir); + } + + if let Some(brkdir) = config_args.brkdir.take() { + config_saved.brkdir = Some(brkdir); + } + + if let Some(mode) = config_args.mode.take() { + config_saved.mode = Some(mode); + } + + if let Some(rpcconnect) = config_args.rpcconnect.take() { + config_saved.rpcconnect = Some(rpcconnect); + } + + if let Some(rpcport) = config_args.rpcport.take() { + config_saved.rpcport = Some(rpcport); + } + + if let Some(rpccookiefile) = config_args.rpccookiefile.take() { + config_saved.rpccookiefile = Some(rpccookiefile); + } + + if let Some(rpcuser) = config_args.rpcuser.take() { + config_saved.rpcuser = Some(rpcuser); + } + + if let Some(rpcpassword) = config_args.rpcpassword.take() { + config_saved.rpcpassword = Some(rpcpassword); + } + + if let Some(delay) = config_args.delay.take() { + config_saved.delay = Some(delay); + } + + if config_args != RunConfig::default() { + dbg!(config_args); + panic!("Didn't consume the full config") + } + } + + let config = config_saved; + + config.check(); + + config.write(&path)?; + + // info!("Configuration {{"); + // info!(" bitcoindir: {:?}", config.bitcoindir); + // info!(" brkdir: {:?}", config.brkdir); + // info!(" mode: {:?}", config.mode); + // info!(" rpcconnect: {:?}", config.rpcconnect); + // info!(" rpcport: {:?}", config.rpcport); + // info!(" rpccookiefile: {:?}", config.rpccookiefile); + // info!(" rpcuser: {:?}", config.rpcuser); + // info!(" rpcpassword: {:?}", config.rpcpassword); + // info!(" delay: {:?}", config.delay); + // info!("}}"); + + Ok(config) + } + + fn check(&self) { + if self.bitcoindir.is_none() { + println!( + "You need to set the --bitcoindir parameter at least once to run the parser.\nRun the program with '-h' for help." + ); + std::process::exit(1); + } else if !self.bitcoindir().is_dir() { + println!( + "Given --bitcoindir parameter doesn't seem to be a valid directory path.\nRun the program with '-h' for help." + ); + std::process::exit(1); + } + + if self.brkdir.is_none() { + println!( + "You need to set the --brkdir parameter at least once to run the parser.\nRun the program with '-h' for help." + ); + std::process::exit(1); + } else if !self.brkdir().is_dir() { + println!( + "Given --brkdir parameter doesn't seem to be a valid directory path.\nRun the program with '-h' for help." + ); + std::process::exit(1); + } + + let path = self.bitcoindir(); + if !path.is_dir() { + println!("Expect path '{:#?}' to be a directory.", path); + std::process::exit(1); + } + + if self.to_rpc_auth().is_err() { + println!( + "No way found to authenticate the RPC client, please either set --rpccookiefile or --rpcuser and --rpcpassword.\nRun the program with '-h' for help." + ); + std::process::exit(1); + } + } + + fn read(path: &Path) -> Self { + fs::read_to_string(path).map_or(RunConfig::default(), |contents| { + toml::from_str(&contents).unwrap_or_default() + }) + } + + fn write(&self, path: &Path) -> std::io::Result<()> { + fs::write(path, toml::to_string(self).unwrap()) + } + + pub fn to_rpc_auth(&self) -> color_eyre::Result { + let cookie = self.path_cookiefile(); + + if cookie.is_file() { + Ok(Auth::CookieFile(cookie)) + } else if self.rpcuser.is_some() && self.rpcpassword.is_some() { + Ok(Auth::UserPass( + self.rpcuser.clone().unwrap(), + self.rpcpassword.clone().unwrap(), + )) + } else { + Err(eyre!("Failed to find correct auth")) + } + } + + pub fn rpcconnect(&self) -> Option<&String> { + self.rpcconnect.as_ref() + } + + pub fn rpcport(&self) -> Option { + self.rpcport + } + + pub fn delay(&self) -> Option { + self.delay + } + + pub fn bitcoindir(&self) -> PathBuf { + Self::fix_user_path(self.bitcoindir.as_ref().unwrap().as_ref()) + } + + pub fn brkdir(&self) -> PathBuf { + Self::fix_user_path(self.brkdir.as_ref().unwrap().as_ref()) + } + + fn outputsdir(&self) -> PathBuf { + self.brkdir().join("outputs") + } + + pub fn indexeddir(&self) -> PathBuf { + self.outputsdir().join("indexed") + } + + pub fn computeddir(&self) -> PathBuf { + self.outputsdir().join("computed") + } + + pub fn process(&self) -> bool { + self.mode.is_none_or(|m| m == Mode::All || m == Mode::Processor) + } + + pub fn serve(&self) -> bool { + self.mode.is_none_or(|m| m == Mode::All || m == Mode::Server) + } + + fn path_cookiefile(&self) -> PathBuf { + self.rpccookiefile.as_ref().map_or_else( + || self.bitcoindir().join(".cookie"), + |p| Self::fix_user_path(p.as_str()), + ) + } + + fn fix_user_path(path: &str) -> PathBuf { + let fix = move |pattern: &str| { + if path.starts_with(pattern) { + let path = &path.replace(&format!("{pattern}/"), "").replace(pattern, ""); + + let home = std::env::var("HOME").unwrap(); + + Some(Path::new(&home).join(path)) + } else { + None + } + }; + + fix("~").unwrap_or_else(|| fix("$HOME").unwrap_or_else(|| PathBuf::from(&path))) + } +} + +#[derive(Default, Debug, Clone, Copy, Parser, ValueEnum, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum Mode { + #[default] + All, + Processor, + Server, +}