diff --git a/Cargo.toml b/Cargo.toml index 0012894f..d69c2e7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,3 @@ -[package] -name = "cuprate" -version = "0.0.1" -edition = "2021" -rust-version = "1.68.0" -description = "An upcoming experimental, modern & secure monero node" -readme = "readme.md" -license = "AGPL-3.0-only" -repository = "https://github.com/SyntheticBird45/cuprate" - - -# All Contributors on github -authors=[ - "SyntheticBird45", - "Boog900" - ] [workspace] @@ -36,20 +20,3 @@ tracing-subscriber = "*" # As suggested by /u/danda : thiserror = "*" - -[profile.release] -opt-level = 3 -debug = 0 -strip = "symbols" -lto = "thin" -panic = "abort" - -[build] -linker="clang" -rustflags=[ - "-Clink-arg=-fuse-ld=mold", - "-Zcf-protection=full", - "-Zsanitizer=cfi", - "-Crelocation-model=pie", - "-Cstack-protector=all", -] \ No newline at end of file diff --git a/cuprate/Cargo.toml b/cuprate/Cargo.toml index 4379627e..9da30887 100644 --- a/cuprate/Cargo.toml +++ b/cuprate/Cargo.toml @@ -1,12 +1,21 @@ [package] -name = "cuprate-bin" +name = "cuprate" +authors = [] version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -clap = { version = "*", features = [] } -clap_complete = "*" -tracing = { workspace = true } -tracing-subscriber = { workspace = true } \ No newline at end of file +clap = "4" +serde = { version = "1", features = ["serde_derive"] } +thiserror = "1" + +[dependencies.abscissa_core] +version = "0.7.0" +# optional: use `gimli` to capture backtraces +# see https://github.com/rust-lang/backtrace-rs/issues/189 +# features = ["gimli-backtrace"] + +[dev-dependencies] +abscissa_core = { version = "0.7.0", features = ["testing"] } +once_cell = "1.2" + diff --git a/cuprate/README.md b/cuprate/README.md new file mode 100644 index 00000000..8ffe6a93 --- /dev/null +++ b/cuprate/README.md @@ -0,0 +1,9 @@ + +This application is authored using [Abscissa], a Rust application framework. + +For more information, see: + +[Documentation] + +[Abscissa]: https://github.com/iqlusioninc/abscissa +[Documentation]: https://docs.rs/abscissa_core/ diff --git a/cuprate/src/application.rs b/cuprate/src/application.rs new file mode 100644 index 00000000..f4eb96b4 --- /dev/null +++ b/cuprate/src/application.rs @@ -0,0 +1,88 @@ +//! Cuprate Abscissa Application + +use crate::{commands::EntryPoint, config::CuprateConfig}; +use abscissa_core::{ + application::{self, AppCell}, + config::{self, CfgCell}, + trace, Application, FrameworkError, StandardPaths, +}; + +/// Application state +pub static APP: AppCell = AppCell::new(); + +/// Cuprate Application +#[derive(Debug)] +pub struct CuprateApp { + /// Application configuration. + config: CfgCell, + + /// Application state. + state: application::State, +} + +/// Initialize a new application instance. +/// +/// By default no configuration is loaded, and the framework state is +/// initialized to a default, empty state (no components, threads, etc). +impl Default for CuprateApp { + fn default() -> Self { + Self { + config: CfgCell::default(), + state: application::State::default(), + } + } +} + +impl Application for CuprateApp { + /// Entrypoint command for this application. + type Cmd = EntryPoint; + + /// Application configuration. + type Cfg = CuprateConfig; + + /// Paths to resources within the application. + type Paths = StandardPaths; + + /// Accessor for application configuration. + fn config(&self) -> config::Reader { + self.config.read() + } + + /// Borrow the application state immutably. + fn state(&self) -> &application::State { + &self.state + } + + /// Register all components used by this application. + /// + /// If you would like to add additional components to your application + /// beyond the default ones provided by the framework, this is the place + /// to do so. + fn register_components(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> { + let framework_components = self.framework_components(command)?; + let mut app_components = self.state.components_mut(); + app_components.register(framework_components) + } + + /// Post-configuration lifecycle callback. + /// + /// Called regardless of whether config is loaded to indicate this is the + /// time in app lifecycle when configuration would be loaded if + /// possible. + fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError> { + // Configure components + let mut components = self.state.components_mut(); + components.after_config(&config)?; + self.config.set_once(config); + Ok(()) + } + + /// Get tracing configuration from command-line options + fn tracing_config(&self, command: &EntryPoint) -> trace::Config { + if command.verbose { + trace::Config::verbose() + } else { + trace::Config::default() + } + } +} diff --git a/cuprate/src/bin/cuprate/main.rs b/cuprate/src/bin/cuprate/main.rs new file mode 100644 index 00000000..c349c179 --- /dev/null +++ b/cuprate/src/bin/cuprate/main.rs @@ -0,0 +1,11 @@ +//! Main entry point for Cuprate + +#![deny(warnings, missing_docs, trivial_casts, unused_qualifications)] +#![forbid(unsafe_code)] + +use cuprate::application::APP; + +/// Boot Cuprate +fn main() { + abscissa_core::boot(&APP); +} diff --git a/cuprate/src/cli.rs b/cuprate/src/cli.rs deleted file mode 100644 index 9e7c8d7a..00000000 --- a/cuprate/src/cli.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::CUPRATE_VERSION; -use clap::{value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command}; -use tracing::{event, span, Level, Span}; - -/// This function simply contains clap arguments -pub fn args() -> ArgMatches { - Command::new("Cuprate") - .version(CUPRATE_VERSION) - .author("Cuprate's contributors") - .about("An upcoming experimental, modern, and secure monero node") - // Generic Arguments - .arg( - Arg::new("log") - .long("log-level") - .value_name("Level") - .help("Set the log level") - .value_parser(value_parser!(u8)) - .default_value("1") - .long_help("Set the log level. There is 3 log level: <1~INFO, 2~DEBUG >3~TRACE.") - .required(false) - .action(ArgAction::Set), - ) - .get_matches() -} - -/// This function initialize the FmtSubscriber used by tracing to display event in the console. It send back a span used during runtime. -pub fn init(matches: &ArgMatches) -> Span { - // Getting the log level from args - let log_level = matches.get_one::("log").unwrap(); - let level_filter = match log_level { - 2 => Level::DEBUG, - x if x > &2 => Level::TRACE, - _ => Level::INFO, - }; - - // Initializing tracing subscriber and runtime span - let subscriber = tracing_subscriber::FmtSubscriber::builder() - .with_max_level(level_filter) - .with_target(false) - .finish(); - tracing::subscriber::set_global_default(subscriber).expect("Failed to set global subscriber for tracing. We prefer to abort the node since without it you have no output in the console"); - let runtime_span = span!(Level::INFO, "Runtime"); - let _guard = runtime_span.enter(); - - // Notifying log level - event!(Level::INFO, "Log level set to {}", level_filter); - - drop(_guard); - runtime_span -} diff --git a/cuprate/src/commands.rs b/cuprate/src/commands.rs new file mode 100644 index 00000000..78ec2f6b --- /dev/null +++ b/cuprate/src/commands.rs @@ -0,0 +1,87 @@ +//! Cuprate Subcommands +//! +//! This is where you specify the subcommands of your application. +//! +//! The default application comes with two subcommands: +//! +//! - `start`: launches the application +//! - `--version`: print application version +//! +//! See the `impl Configurable` below for how to specify the path to the +//! application's configuration file. + +mod start; + +use self::start::StartCmd; +use crate::config::CuprateConfig; +use abscissa_core::{config::Override, Command, Configurable, FrameworkError, Runnable}; +use std::path::PathBuf; + +/// Cuprate Configuration Filename +pub const CONFIG_FILE: &str = "cuprate.toml"; + +/// Cuprate Subcommands +/// Subcommands need to be listed in an enum. +#[derive(clap::Parser, Command, Debug, Runnable)] +pub enum CuprateCmd { + /// The `start` subcommand + Start(StartCmd), +} + +/// Entry point for the application. It needs to be a struct to allow using subcommands! +#[derive(clap::Parser, Command, Debug)] +#[command(author, about, version)] +pub struct EntryPoint { + #[command(subcommand)] + cmd: CuprateCmd, + + /// Enable verbose logging + #[arg(short, long)] + pub verbose: bool, + + /// Use the specified config file + #[arg(short, long)] + pub config: Option, +} + +impl Runnable for EntryPoint { + fn run(&self) { + self.cmd.run() + } +} + +/// This trait allows you to define how application configuration is loaded. +impl Configurable for EntryPoint { + /// Location of the configuration file + fn config_path(&self) -> Option { + // Check if the config file exists, and if it does not, ignore it. + // If you'd like for a missing configuration file to be a hard error + // instead, always return `Some(CONFIG_FILE)` here. + let filename = self + .config + .as_ref() + .map(PathBuf::from) + .unwrap_or_else(|| CONFIG_FILE.into()); + + if filename.exists() { + Some(filename) + } else { + None + } + } + + /// Apply changes to the config after it's been loaded, e.g. overriding + /// values in a config file using command-line options. + /// + /// This can be safely deleted if you don't want to override config + /// settings from command-line options. + fn process_config(&self, config: CuprateConfig) -> Result { + match &self.cmd { + CuprateCmd::Start(cmd) => cmd.override_config(config), + // + // If you don't need special overrides for some + // subcommands, you can just use a catch all + // _ => Ok(config), + } + } +} diff --git a/cuprate/src/commands/start.rs b/cuprate/src/commands/start.rs new file mode 100644 index 00000000..525e4b07 --- /dev/null +++ b/cuprate/src/commands/start.rs @@ -0,0 +1,42 @@ +//! `start` subcommand - example of how to write a subcommand + +/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` +/// accessors along with logging macros. Customize as you see fit. +use crate::prelude::*; + +use crate::config::CuprateConfig; +use abscissa_core::{config, Command, FrameworkError, Runnable}; + +/// `start` subcommand +/// +/// The `Parser` proc macro generates an option parser based on the struct +/// definition, and is defined in the `clap` crate. See their documentation +/// for a more comprehensive example: +/// +/// +#[derive(clap::Parser, Command, Debug)] +pub struct StartCmd { + /// To whom are we saying hello? + recipient: Vec, +} + +impl Runnable for StartCmd { + /// Start the application. + fn run(&self) { + let config = APP.config(); + println!("Hello, {}!", &config.hello.recipient); + } +} + +impl config::Override for StartCmd { + // Process the given command line options, overriding settings from + // a configuration file using explicit flags taken from command-line + // arguments. + fn override_config(&self, mut config: CuprateConfig) -> Result { + if !self.recipient.is_empty() { + config.hello.recipient = self.recipient.join(" "); + } + + Ok(config) + } +} diff --git a/cuprate/src/config.rs b/cuprate/src/config.rs new file mode 100644 index 00000000..7fad4dac --- /dev/null +++ b/cuprate/src/config.rs @@ -0,0 +1,45 @@ +//! Cuprate Config +//! +//! See instructions in `commands.rs` to specify the path to your +//! application's configuration file and/or command-line options +//! for specifying it. + +use serde::{Deserialize, Serialize}; + +/// Cuprate Configuration +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct CuprateConfig { + /// An example configuration section + pub hello: ExampleSection, +} + +/// Default configuration settings. +/// +/// Note: if your needs are as simple as below, you can +/// use `#[derive(Default)]` on CuprateConfig instead. +impl Default for CuprateConfig { + fn default() -> Self { + Self { + hello: ExampleSection::default(), + } + } +} + +/// Example configuration section. +/// +/// Delete this and replace it with your actual configuration structs. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ExampleSection { + /// Example configuration value + pub recipient: String, +} + +impl Default for ExampleSection { + fn default() -> Self { + Self { + recipient: "world".to_owned(), + } + } +} diff --git a/cuprate/src/error.rs b/cuprate/src/error.rs new file mode 100644 index 00000000..cdc847f7 --- /dev/null +++ b/cuprate/src/error.rs @@ -0,0 +1,70 @@ +//! Error types + +use abscissa_core::error::{BoxError, Context}; +use std::{ + fmt::{self, Display}, + io, + ops::Deref, +}; +use thiserror::Error; + +/// Kinds of errors +#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)] +pub enum ErrorKind { + /// Error in configuration file + #[error("config error")] + Config, + + /// Input/output error + #[error("I/O error")] + Io, +} + +impl ErrorKind { + /// Create an error context from this error + pub fn context(self, source: impl Into) -> Context { + Context::new(self, Some(source.into())) + } +} + +/// Error type +#[derive(Debug)] +pub struct Error(Box>); + +impl Deref for Error { + type Target = Context; + + fn deref(&self) -> &Context { + &self.0 + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.0.source() + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Context::new(kind, None).into() + } +} + +impl From> for Error { + fn from(context: Context) -> Self { + Error(Box::new(context)) + } +} + +impl From for Error { + fn from(err: io::Error) -> Self { + ErrorKind::Io.context(err).into() + } +} diff --git a/cuprate/src/lib.rs b/cuprate/src/lib.rs new file mode 100644 index 00000000..23bfc749 --- /dev/null +++ b/cuprate/src/lib.rs @@ -0,0 +1,22 @@ +//! Cuprate +//! +//! Application based on the [Abscissa] framework. +//! +//! [Abscissa]: https://github.com/iqlusioninc/abscissa + +// Tip: Deny warnings with `RUSTFLAGS="-D warnings"` environment variable in CI + +#![forbid(unsafe_code)] +#![warn( + missing_docs, + rust_2018_idioms, + trivial_casts, + unused_lifetimes, + unused_qualifications +)] + +pub mod application; +pub mod commands; +pub mod config; +pub mod error; +pub mod prelude; diff --git a/cuprate/src/main.rs b/cuprate/src/main.rs deleted file mode 100644 index eb378f34..00000000 --- a/cuprate/src/main.rs +++ /dev/null @@ -1,13 +0,0 @@ -use tracing::{event, info, span, Level}; - -pub mod cli; - -const CUPRATE_VERSION: &str = "0.1.0"; - -fn main() { - // Collecting options - let matches = cli::args(); - - // Initializing tracing subscriber and runtime span - let _runtime_span = cli::init(&matches); -} diff --git a/cuprate/src/prelude.rs b/cuprate/src/prelude.rs new file mode 100644 index 00000000..025040d7 --- /dev/null +++ b/cuprate/src/prelude.rs @@ -0,0 +1,9 @@ +//! Application-local prelude: conveniently import types/functions/macros +//! which are generally useful and should be available in every module with +//! `use crate::prelude::*; + +/// Abscissa core prelude +pub use abscissa_core::prelude::*; + +/// Application state +pub use crate::application::APP; diff --git a/cuprate/tests/acceptance.rs b/cuprate/tests/acceptance.rs new file mode 100644 index 00000000..edfb1e0a --- /dev/null +++ b/cuprate/tests/acceptance.rs @@ -0,0 +1,91 @@ +//! Acceptance test: runs the application as a subprocess and asserts its +//! output for given argument combinations matches what is expected. +//! +//! Modify and/or delete these as you see fit to test the specific needs of +//! your application. +//! +//! For more information, see: +//! + +// Tip: Deny warnings with `RUSTFLAGS="-D warnings"` environment variable in CI + +#![forbid(unsafe_code)] +#![warn( + missing_docs, + rust_2018_idioms, + trivial_casts, + unused_lifetimes, + unused_qualifications +)] + +use abscissa_core::testing::prelude::*; +use cuprate::config::CuprateConfig; +use once_cell::sync::Lazy; + +/// Executes your application binary via `cargo run`. +/// +/// Storing this value as a [`Lazy`] static ensures that all instances of +/// the runner acquire a mutex when executing commands and inspecting +/// exit statuses, serializing what would otherwise be multithreaded +/// invocations as `cargo test` executes tests in parallel by default. +pub static RUNNER: Lazy = Lazy::new(|| CmdRunner::default()); + +/// Use `CuprateConfig::default()` value if no config or args +#[test] +fn start_no_args() { + let mut runner = RUNNER.clone(); + let mut cmd = runner.arg("start").capture_stdout().run(); + cmd.stdout().expect_line("Hello, world!"); + cmd.wait().unwrap().expect_success(); +} + +/// Use command-line argument value +#[test] +fn start_with_args() { + let mut runner = RUNNER.clone(); + let mut cmd = runner + .args(&["start", "acceptance", "test"]) + .capture_stdout() + .run(); + + cmd.stdout().expect_line("Hello, acceptance test!"); + cmd.wait().unwrap().expect_success(); +} + +/// Use configured value +#[test] +fn start_with_config_no_args() { + let mut config = CuprateConfig::default(); + config.hello.recipient = "configured recipient".to_owned(); + let expected_line = format!("Hello, {}!", &config.hello.recipient); + + let mut runner = RUNNER.clone(); + let mut cmd = runner.config(&config).arg("start").capture_stdout().run(); + cmd.stdout().expect_line(&expected_line); + cmd.wait().unwrap().expect_success(); +} + +/// Override configured value with command-line argument +#[test] +fn start_with_config_and_args() { + let mut config = CuprateConfig::default(); + config.hello.recipient = "configured recipient".to_owned(); + + let mut runner = RUNNER.clone(); + let mut cmd = runner + .config(&config) + .args(&["start", "acceptance", "test"]) + .capture_stdout() + .run(); + + cmd.stdout().expect_line("Hello, acceptance test!"); + cmd.wait().unwrap().expect_success(); +} + +/// Example of a test which matches a regular expression +#[test] +fn version_no_args() { + let mut runner = RUNNER.clone(); + let mut cmd = runner.arg("--version").capture_stdout().run(); + cmd.stdout().expect_regex(r"\A\w+ [\d\.\-]+\z"); +}