diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..c25218b553fd9301210afd1f1b7beef4ba7b201d --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "duniter-core" +version = "0.1.0" +authors = ["librelois <elois@ifee.fr>"] +description = "Duniter-rs core." +license = "AGPL-3.0" + +[lib] +path = "lib.rs" + +[dependencies] +clap = {version = "2.31.2", features = ["yaml"]} +duniter-blockchain = { path = "../blockchain" } +duniter-conf = { path = "../conf" } +duniter-crypto = { path = "../crypto" } +duniter-message = { path = "../message" } +duniter-module = { path = "../module" } +lazy_static = "1.0.0" +log = "0.4.1" +rand = "0.4.2" +regex = "0.2.6" +rust-crypto = "0.2.36" +serde = "1.0.24" +serde_derive = "1.0.24" +serde_json = "1.0.9" +simplelog = "0.5.1" +sqlite = "0.23.9" +threadpool = "1.7.1" \ No newline at end of file diff --git a/core/cli/en.yml b/core/cli/en.yml new file mode 100644 index 0000000000000000000000000000000000000000..6c0c983b81520e9ba1a7287c7566371db856a989 --- /dev/null +++ b/core/cli/en.yml @@ -0,0 +1,64 @@ +name: durs +version: "0.1.0" +author: Elois L. <elois@duniter.org> +about: Rust implementation of Duniter +args: + - profile: + short: p + long: profile + value_name: CUSTOM_PROFILE + help: Set a custom datas folder + takes_value: true + - logs: + short: l + long : logs + value_name: LOGS_LEVEL + takes_value: true + possible_values: ["e", "w", "i", "d", "t", "error", "warn", "info", "debug", "trace"] + help: Set the level of logs verbosity + long_help: "Set the level of logs verbosity :\n + error : print serious errors\n + warn : print hazardous situations\n + info : default level\n + debug : print a lot of debug informations\n + trace : print all traces (highly verbose)" +subcommands: + - start: + about: start duniter server + version: "0.1.0" + author: Elois L. <elois@duniter.org> + - sync_ts: + about: synchronization via a duniter-ts database + version: "0.1.0" + author: Elois L. <elois@duniter.org> + args: + - TS_PROFILE: + help: Set the ts profile to use + index: 1 + - cautious: + short: c + long: cautious + help: cautious mode (check all protocol rules, very slow) + - msync_ts: + about: synchronization in memory mode via a duniter-ts database + version: "0.1.0" + author: Elois L. <elois@duniter.org> + args: + - TS_PROFILE: + help: Set the ts profile to use + index: 1 + - cautious: + short: c + long: cautious + help: cautious mode (check all protocol rules, very slow) + - reset: + about: reset data or conf or all + version: "0.1.0" + author: Elois L. <elois@duniter.org> + args: + - DATAS_TYPE: + help : choose type datas to reset + index: 1 + possible_values: ["data","conf","all"] + required: true + \ No newline at end of file diff --git a/core/lib.rs b/core/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..1d4bd89f6f3837dcd667f7e0d504900045066858 --- /dev/null +++ b/core/lib.rs @@ -0,0 +1,440 @@ +// Copyright (C) 2018 The Duniter Project Developers. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Crate containing Duniter-rust core. + +#![cfg_attr(feature = "strict", deny(warnings))] +#![deny( + missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, + trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, + unused_qualifications +)] + +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate duniter_blockchain; +extern crate duniter_conf; +extern crate duniter_crypto; +extern crate duniter_message; +extern crate duniter_module; +extern crate serde_json; +extern crate simplelog; +extern crate sqlite; +extern crate threadpool; + +use self::threadpool::ThreadPool; +use clap::{App, ArgMatches}; +use duniter_blockchain::BlockchainModule; +use duniter_conf::DuniterKeyPairs; +use duniter_crypto::keys::ed25519; +use duniter_message::DuniterMessage; +use duniter_module::*; +use log::Level; +use simplelog::*; +use std::env; +use std::fs; +use std::fs::{File, OpenOptions}; +use std::sync::mpsc; +use std::thread; +use std::time::Duration; + +#[derive(Debug)] +/// Duniter Core Datas +pub struct DuniterCore { + /// Does the entered command require to launch server ? + pub start: bool, + /// Software name + pub soft_name: &'static str, + /// Soft version + pub soft_version: &'static str, + /// Keypairs + pub keypairs: DuniterKeyPairs, + /// Duniter configuration + pub conf: DuniterConf, + /// Run duration. Zero = infinite duration. + pub run_duration_in_secs: u64, + /// Sender channel of rooter thread + pub rooter_sender: mpsc::Sender<RooterThreadMessage<DuniterMessage>>, + /// Count the number of plugged modules + pub modules_count: usize, + /// ThreadPool that execute plugged modules + pub thread_pool: ThreadPool, +} + +impl DuniterCore { + /// Instantiate Duniter classic node + pub fn new(soft_name: &'static str, soft_version: &'static str) -> Option<DuniterCore> { + DuniterCore::new_specialized_node(soft_name, soft_version, 0, vec![], vec![], None) + } + /// Instantiate Duniter specialize node + pub fn new_specialized_node<'a, 'b>( + soft_name: &'static str, + soft_version: &'static str, + run_duration_in_secs: u64, + external_followers: Vec<mpsc::Sender<DuniterMessage>>, + sup_apps: Vec<App<'a, 'b>>, + sup_apps_fn: Option<&Fn(&str, &ArgMatches) -> ()>, + ) -> Option<DuniterCore> { + // Get cli conf + let yaml = load_yaml!("./cli/en.yml"); + let cli_conf = App::from_yaml(yaml); + + // Math command line arguments + let cli_args = if !sup_apps.is_empty() { + cli_conf.subcommands(sup_apps).get_matches() + } else { + cli_conf.get_matches() + }; + + // Get datas profile name + let profile = match_profile(&cli_args); + + // Init logger + init_logger(profile.as_str(), soft_name, &cli_args); + + // Load global conf + let (conf, keypairs) = duniter_conf::load_conf(profile.as_str()); + info!("Success to load global conf."); + + if let Some(_matches) = cli_args.subcommand_matches("start") { + Some(start( + soft_name, + soft_version, + keypairs, + conf, + run_duration_in_secs, + external_followers, + )) + } else if let Some(matches) = cli_args.subcommand_matches("sync_ts") { + let ts_profile = matches.value_of("TS_PROFILE").unwrap_or("duniter_default"); + sync_ts(conf, ts_profile, matches.is_present("cautious")); + None + } else if let Some(matches) = cli_args.subcommand_matches("reset") { + let mut profile_path = match env::home_dir() { + Some(path) => path, + None => panic!("Impossible to get your home dir !"), + }; + profile_path.push(".config"); + profile_path.push(duniter_conf::get_user_datas_folder()); + profile_path.push(profile.clone()); + if !profile_path.as_path().exists() { + panic!(format!("Error : {} profile don't exist !", profile)); + } + match matches.value_of("DATAS_TYPE").unwrap() { + "data" => { + let mut currency_datas_path = profile_path.clone(); + currency_datas_path.push("g1"); + fs::remove_dir_all(currency_datas_path.as_path()) + .expect("Fail to remove all currency datas !"); + } + "conf" => { + let mut conf_file_path = profile_path.clone(); + conf_file_path.push("conf.json"); + fs::remove_file(conf_file_path.as_path()).expect("Fail to remove conf file !"); + let mut conf_keys_path = profile_path.clone(); + conf_keys_path.push("keypairs.json"); + fs::remove_file(conf_keys_path.as_path()) + .expect("Fail to remove keypairs file !"); + } + "all" => { + fs::remove_dir_all(profile_path.as_path()) + .expect("Fail to remove all profile datas !"); + } + _ => {} + } + None + } else if let Some(sup_apps_fn) = sup_apps_fn { + sup_apps_fn(profile.as_str(), &cli_args); + None + } else { + panic!("unknow sub-command !") + } + } + /// Start blockchain module + pub fn start_blockchain(&self) { + if self.start { + thread::sleep(Duration::from_secs(2)); + // Create blockchain module channel + let (blockchain_sender, blockchain_receiver): ( + mpsc::Sender<DuniterMessage>, + mpsc::Receiver<DuniterMessage>, + ) = mpsc::channel(); + + // Send blockchain sender to rooter thread + self.rooter_sender + .send(RooterThreadMessage::ModuleSender(blockchain_sender)) + .expect("Fatal error: fail to send blockchain sender to rooter thread !"); + + // Send modules_count to rooter thread + self.rooter_sender + .send(RooterThreadMessage::ModulesCount(self.modules_count + 1)) + .expect("Fatal error: fail to send modules count to rooter thread !"); + + // Instantiate blockchain module and load is conf + let mut blockchain_module = BlockchainModule::load_blockchain_conf( + &self.conf, + RequiredKeysContent::MemberKeyPair(None), + false, + ); + info!("Success to load Blockchain module."); + + // Start blockchain module in main thread + blockchain_module.start_blockchain(blockchain_receiver); + } + } + /// Plug a module + pub fn plug<M: DuniterModule<ed25519::PublicKey, ed25519::KeyPair, DuniterMessage>>(&mut self) { + if self.start { + // Start module in a new thread + let soft_name_clone = self.soft_name.clone(); + let soft_version_clone = self.soft_version.clone(); + let required_keys = DuniterKeyPairs::get_required_keys_content( + M::ask_required_keys(), + self.keypairs.clone(), + ); + let module_conf = if let Some(module_conf_) = self + .conf + .clone() + .modules() + .get(&M::id().to_string().as_str()) + { + module_conf_.clone() + } else { + M::default_conf() + }; + let rooter_sender_clone = self.rooter_sender.clone(); + let conf_clone = self.conf.clone(); + self.thread_pool.execute(move || { + M::start( + soft_name_clone, + soft_version_clone, + required_keys, + &conf_clone, + &module_conf, + rooter_sender_clone, + false, + ).expect(&format!( + "Fatal error : fail to load {} Module !", + M::id().to_string() + )); + }); + self.modules_count += 1; + info!("Success to load {} module.", M::id().to_string()); + } + } +} + +/// Match cli option --profile +pub fn match_profile(cli_args: &ArgMatches) -> String { + String::from(cli_args.value_of("profile").unwrap_or("default")) +} + +/// Launch duniter server +pub fn start( + soft_name: &'static str, + soft_version: &'static str, + keypairs: DuniterKeyPairs, + conf: DuniterConf, + run_duration_in_secs: u64, + external_followers: Vec<mpsc::Sender<DuniterMessage>>, +) -> DuniterCore { + info!("Starting Duniter-rs..."); + + // Create senders channel + let (rooter_sender, main_receiver): ( + mpsc::Sender<RooterThreadMessage<DuniterMessage>>, + mpsc::Receiver<RooterThreadMessage<DuniterMessage>>, + ) = mpsc::channel(); + + // Create rooter thread + thread::spawn(move || { + // Wait to receiver modules senders + let mut modules_senders: Vec<mpsc::Sender<DuniterMessage>> = Vec::new(); + let mut modules_count_expected = None; + while modules_count_expected.is_none() + || modules_senders.len() < modules_count_expected.unwrap() + 1 + { + match main_receiver.recv_timeout(Duration::from_secs(20)) { + Ok(mess) => { + match mess { + RooterThreadMessage::ModuleSender(module_sender) => { + // Subscribe this module to all others modules + for other_module in modules_senders.clone() { + if other_module + .send(DuniterMessage::Followers(vec![module_sender.clone()])) + .is_err() + { + panic!("Fatal error : fail to send all modules senders to all modules !"); + } + } + // Subcribe this module to all external_followers + for external_follower in external_followers.clone() { + if external_follower + .send(DuniterMessage::Followers(vec![module_sender.clone()])) + .is_err() + { + panic!("Fatal error : fail to send all modules senders to all external_followers !"); + } + } + // Subscribe all other modules to this module + if module_sender + .send(DuniterMessage::Followers(modules_senders.clone())) + .is_err() + { + panic!("Fatal error : fail to send all modules senders to all modules !"); + } + // Subcribe all external_followers to this module + if module_sender + .send(DuniterMessage::Followers(external_followers.clone())) + .is_err() + { + panic!("Fatal error : fail to send all external_followers to all modules !"); + } + // Push this module to modules_senders list + modules_senders.push(module_sender); + // Log the number of modules_senders received + info!( + "Rooter thread receive {} module senders", + modules_senders.len() + ); + } + RooterThreadMessage::ModulesCount(modules_count) => { + info!("Rooter thread receive ModulesCount({})", modules_count); + if modules_senders.len() == modules_count { + break; + } else if modules_senders.len() < modules_count { + modules_count_expected = Some(modules_count); + } else { + panic!("Fatal error : Receive more modules_sender than expected !") + } + } + } + } + Err(e) => match e { + mpsc::RecvTimeoutError::Timeout => { + panic!("Fatal error : not receive all modules_senders after 20 secs !") + } + mpsc::RecvTimeoutError::Disconnected => { + panic!("Fatal error : rooter thread disconnnected !") + } + }, + } + } + info!("Receive all modules senders."); + if run_duration_in_secs > 0 { + thread::sleep(Duration::from_secs(run_duration_in_secs)); + // Send DuniterMessage::Stop() to all modules + for sender in modules_senders { + if sender.send(DuniterMessage::Stop()).is_err() { + panic!("Fail to send Stop() message to one module !") + } + } + thread::sleep(Duration::from_secs(2)); + } + }); + + // Instanciate DuniterCore + DuniterCore { + start: true, + soft_name, + soft_version, + keypairs, + conf, + run_duration_in_secs, + rooter_sender, + modules_count: 0, + thread_pool: ThreadPool::new(2), + } +} + +/// Launch synchronisation from a duniter-ts database +pub fn sync_ts(conf: DuniterConf, ts_profile: &str, cautious: bool) { + // Launch sync-ts + BlockchainModule::sync_ts(&conf, ts_profile, cautious); +} + +/// Initialize logger +pub fn init_logger(profile: &str, soft_name: &'static str, cli_args: &ArgMatches) { + // Get datas folder path + let mut log_file_path = match env::home_dir() { + Some(path) => path, + None => panic!("Fatal error : Impossible to get your home dir!"), + }; + log_file_path.push(".config"); + if !log_file_path.as_path().exists() { + fs::create_dir(log_file_path.as_path()).expect("Impossible to create ~/.config dir !"); + } + log_file_path.push(duniter_conf::get_user_datas_folder()); + if !log_file_path.as_path().exists() { + fs::create_dir(log_file_path.as_path()).expect("Impossible to create ~/.config/durs dir !"); + } + log_file_path.push(profile); + // Create datas folder if not exist + if !log_file_path.as_path().exists() { + fs::create_dir(log_file_path.as_path()).expect("Impossible to create your profile dir !"); + } + + // Get log_file_path + log_file_path.push(format!("{}.log", soft_name)); + + // Get log level + let log_level = match cli_args.value_of("logs").unwrap_or("i") { + "e" | "error" => Level::Error, + "w" | "warn" => Level::Warn, + "i" | "info" => Level::Info, + "d" | "debug" => Level::Debug, + "t" | "trace" => Level::Trace, + _ => panic!("Fatal error : unknow log level !"), + }; + + // Config logger + let logger_config = Config { + time: Some(Level::Error), + level: Some(Level::Error), + target: Some(Level::Debug), + location: Some(Level::Debug), + time_format: Some("%Y-%m-%d %H:%M:%S%:z"), + }; + + // Create log file if not exist + if !log_file_path.as_path().exists() { + File::create( + log_file_path + .to_str() + .expect("Fatal error : fail to get log file path !"), + ).expect("Fatal error : fail to create log file path !"); + } + + CombinedLogger::init(vec![ + TermLogger::new(LevelFilter::Error, logger_config).unwrap(), + WriteLogger::new( + log_level.to_level_filter(), + logger_config, + OpenOptions::new() + .write(true) + .append(true) + .open( + log_file_path + .to_str() + .expect("Fatal error : fail to get log file path !"), + ) + .expect("Fatal error : fail to open log file !"), + ), + ]).expect("Fatal error : fail to init logger !"); +}