1use crate::{chainspec::EthereumChainSpecParser, debug_cmd};
4use clap::{Parser, Subcommand};
5use reth_chainspec::ChainSpec;
6use reth_cli::chainspec::ChainSpecParser;
7use reth_cli_commands::{
8 config_cmd, db, download, dump_genesis, import, import_era, init_cmd, init_state,
9 launcher::FnLauncher,
10 node::{self, NoArgs},
11 p2p, prune, recover, stage,
12};
13use reth_cli_runner::CliRunner;
14use reth_db::DatabaseEnv;
15use reth_network::EthNetworkPrimitives;
16use reth_node_builder::{NodeBuilder, WithLaunchContext};
17use reth_node_core::{
18 args::LogArgs,
19 version::{LONG_VERSION, SHORT_VERSION},
20};
21use reth_node_ethereum::{consensus::EthBeaconConsensus, EthExecutorProvider, EthereumNode};
22use reth_node_metrics::recorder::install_prometheus_recorder;
23use reth_tracing::FileWorkerGuard;
24use std::{ffi::OsString, fmt, future::Future, sync::Arc};
25use tracing::info;
26
27#[derive(Debug, Parser)]
31#[command(author, version = SHORT_VERSION, long_version = LONG_VERSION, about = "Reth", long_about = None)]
32pub struct Cli<C: ChainSpecParser = EthereumChainSpecParser, Ext: clap::Args + fmt::Debug = NoArgs>
33{
34 #[command(subcommand)]
36 pub command: Commands<C, Ext>,
37
38 #[command(flatten)]
40 pub logs: LogArgs,
41}
42
43impl Cli {
44 pub fn parse_args() -> Self {
46 Self::parse()
47 }
48
49 pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
51 where
52 I: IntoIterator<Item = T>,
53 T: Into<OsString> + Clone,
54 {
55 Self::try_parse_from(itr)
56 }
57}
58
59impl<C: ChainSpecParser<ChainSpec = ChainSpec>, Ext: clap::Args + fmt::Debug> Cli<C, Ext> {
60 pub fn run<L, Fut>(self, launcher: L) -> eyre::Result<()>
103 where
104 L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
105 Fut: Future<Output = eyre::Result<()>>,
106 {
107 self.with_runner(CliRunner::try_default_runtime()?, launcher)
108 }
109
110 pub fn with_runner<L, Fut>(mut self, runner: CliRunner, launcher: L) -> eyre::Result<()>
136 where
137 L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
138 Fut: Future<Output = eyre::Result<()>>,
139 {
140 if let Some(chain_spec) = self.command.chain_spec() {
142 self.logs.log_file_directory =
143 self.logs.log_file_directory.join(chain_spec.chain.to_string());
144 }
145 let _guard = self.init_tracing()?;
146 info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory);
147
148 let _ = install_prometheus_recorder();
150
151 let components = |spec: Arc<C::ChainSpec>| {
152 (EthExecutorProvider::ethereum(spec.clone()), EthBeaconConsensus::new(spec))
153 };
154 match self.command {
155 Commands::Node(command) => runner.run_command_until_exit(|ctx| {
156 command.execute(ctx, FnLauncher::new::<C, Ext>(launcher))
157 }),
158 Commands::Init(command) => {
159 runner.run_blocking_until_ctrl_c(command.execute::<EthereumNode>())
160 }
161 Commands::InitState(command) => {
162 runner.run_blocking_until_ctrl_c(command.execute::<EthereumNode>())
163 }
164 Commands::Import(command) => {
165 runner.run_blocking_until_ctrl_c(command.execute::<EthereumNode, _, _>(components))
166 }
167 Commands::ImportEra(command) => {
168 runner.run_blocking_until_ctrl_c(command.execute::<EthereumNode>())
169 }
170 Commands::DumpGenesis(command) => runner.run_blocking_until_ctrl_c(command.execute()),
171 Commands::Db(command) => {
172 runner.run_blocking_until_ctrl_c(command.execute::<EthereumNode>())
173 }
174 Commands::Download(command) => {
175 runner.run_blocking_until_ctrl_c(command.execute::<EthereumNode>())
176 }
177 Commands::Stage(command) => runner.run_command_until_exit(|ctx| {
178 command.execute::<EthereumNode, _, _, EthNetworkPrimitives>(ctx, components)
179 }),
180 Commands::P2P(command) => {
181 runner.run_until_ctrl_c(command.execute::<EthNetworkPrimitives>())
182 }
183 #[cfg(feature = "dev")]
184 Commands::TestVectors(command) => runner.run_until_ctrl_c(command.execute()),
185 Commands::Config(command) => runner.run_until_ctrl_c(command.execute()),
186 Commands::Debug(command) => {
187 runner.run_command_until_exit(|ctx| command.execute::<EthereumNode>(ctx))
188 }
189 Commands::Recover(command) => {
190 runner.run_command_until_exit(|ctx| command.execute::<EthereumNode>(ctx))
191 }
192 Commands::Prune(command) => runner.run_until_ctrl_c(command.execute::<EthereumNode>()),
193 }
194 }
195
196 pub fn init_tracing(&self) -> eyre::Result<Option<FileWorkerGuard>> {
201 let guard = self.logs.init_tracing()?;
202 Ok(guard)
203 }
204}
205
206#[derive(Debug, Subcommand)]
208#[expect(clippy::large_enum_variant)]
209pub enum Commands<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> {
210 #[command(name = "node")]
212 Node(Box<node::NodeCommand<C, Ext>>),
213 #[command(name = "init")]
215 Init(init_cmd::InitCommand<C>),
216 #[command(name = "init-state")]
218 InitState(init_state::InitStateCommand<C>),
219 #[command(name = "import")]
221 Import(import::ImportCommand<C>),
222 #[command(name = "import-era")]
224 ImportEra(import_era::ImportEraCommand<C>),
225 DumpGenesis(dump_genesis::DumpGenesisCommand<C>),
227 #[command(name = "db")]
229 Db(db::Command<C>),
230 #[command(name = "download")]
232 Download(download::DownloadCommand<C>),
233 #[command(name = "stage")]
235 Stage(stage::Command<C>),
236 #[command(name = "p2p")]
238 P2P(p2p::Command<C>),
239 #[cfg(feature = "dev")]
241 #[command(name = "test-vectors")]
242 TestVectors(reth_cli_commands::test_vectors::Command),
243 #[command(name = "config")]
245 Config(config_cmd::Command),
246 #[command(name = "debug")]
248 Debug(Box<debug_cmd::Command<C>>),
249 #[command(name = "recover")]
251 Recover(recover::Command<C>),
252 #[command(name = "prune")]
254 Prune(prune::PruneCommand<C>),
255}
256
257impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> Commands<C, Ext> {
258 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
260 match self {
261 Self::Node(cmd) => cmd.chain_spec(),
262 Self::Init(cmd) => cmd.chain_spec(),
263 Self::InitState(cmd) => cmd.chain_spec(),
264 Self::Import(cmd) => cmd.chain_spec(),
265 Self::ImportEra(cmd) => cmd.chain_spec(),
266 Self::DumpGenesis(cmd) => cmd.chain_spec(),
267 Self::Db(cmd) => cmd.chain_spec(),
268 Self::Download(cmd) => cmd.chain_spec(),
269 Self::Stage(cmd) => cmd.chain_spec(),
270 Self::P2P(cmd) => cmd.chain_spec(),
271 #[cfg(feature = "dev")]
272 Self::TestVectors(_) => None,
273 Self::Config(_) => None,
274 Self::Debug(cmd) => cmd.chain_spec(),
275 Self::Recover(cmd) => cmd.chain_spec(),
276 Self::Prune(cmd) => cmd.chain_spec(),
277 }
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::chainspec::SUPPORTED_CHAINS;
285 use clap::CommandFactory;
286 use reth_node_core::args::ColorMode;
287
288 #[test]
289 fn parse_color_mode() {
290 let reth = Cli::try_parse_args_from(["reth", "node", "--color", "always"]).unwrap();
291 assert_eq!(reth.logs.color, ColorMode::Always);
292 }
293
294 #[test]
298 fn test_parse_help_all_subcommands() {
299 let reth = Cli::<EthereumChainSpecParser, NoArgs>::command();
300 for sub_command in reth.get_subcommands() {
301 let err = Cli::try_parse_args_from(["reth", sub_command.get_name(), "--help"])
302 .err()
303 .unwrap_or_else(|| {
304 panic!("Failed to parse help message {}", sub_command.get_name())
305 });
306
307 assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
310 }
311 }
312
313 #[test]
316 fn parse_logs_path_node() {
317 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
318 if let Some(chain_spec) = reth.command.chain_spec() {
319 reth.logs.log_file_directory =
320 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
321 }
322 let log_dir = reth.logs.log_file_directory;
323 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
324 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
325
326 let mut iter = SUPPORTED_CHAINS.iter();
327 iter.next();
328 for chain in iter {
329 let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
330 let chain =
331 reth.command.chain_spec().map(|c| c.chain.to_string()).unwrap_or(String::new());
332 reth.logs.log_file_directory = reth.logs.log_file_directory.join(chain.clone());
333 let log_dir = reth.logs.log_file_directory;
334 let end = format!("reth/logs/{chain}");
335 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
336 }
337 }
338
339 #[test]
342 fn parse_logs_path_init() {
343 let mut reth = Cli::try_parse_args_from(["reth", "init"]).unwrap();
344 if let Some(chain_spec) = reth.command.chain_spec() {
345 reth.logs.log_file_directory =
346 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
347 }
348 let log_dir = reth.logs.log_file_directory;
349 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
350 println!("{log_dir:?}");
351 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
352 }
353
354 #[test]
356 fn parse_empty_logs_path() {
357 let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
358 if let Some(chain_spec) = reth.command.chain_spec() {
359 reth.logs.log_file_directory =
360 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
361 }
362 let log_dir = reth.logs.log_file_directory;
363 let end = "reth/logs".to_string();
364 println!("{log_dir:?}");
365 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
366 }
367
368 #[test]
369 fn parse_env_filter_directives() {
370 let temp_dir = tempfile::tempdir().unwrap();
371
372 unsafe { std::env::set_var("RUST_LOG", "info,evm=debug") };
373 let reth = Cli::try_parse_args_from([
374 "reth",
375 "init",
376 "--datadir",
377 temp_dir.path().to_str().unwrap(),
378 "--log.file.filter",
379 "debug,net=trace",
380 ])
381 .unwrap();
382 assert!(reth.run(async move |_, _| Ok(())).is_ok());
383 }
384}