reth_ethereum_cli/
app.rs

1use crate::{interface::Commands, Cli};
2use eyre::{eyre, Result};
3use reth_chainspec::{ChainSpec, EthChainSpec, Hardforks};
4use reth_cli::chainspec::ChainSpecParser;
5use reth_cli_commands::{
6    common::{CliComponentsBuilder, CliHeader, CliNodeTypes},
7    launcher::{FnLauncher, Launcher},
8};
9use reth_cli_runner::CliRunner;
10use reth_db::DatabaseEnv;
11use reth_node_api::NodePrimitives;
12use reth_node_builder::{NodeBuilder, WithLaunchContext};
13use reth_node_ethereum::{consensus::EthBeaconConsensus, EthEvmConfig, EthereumNode};
14use reth_node_metrics::recorder::install_prometheus_recorder;
15use reth_rpc_server_types::RpcModuleValidator;
16use reth_tracing::{FileWorkerGuard, Layers};
17use std::{fmt, sync::Arc};
18use tracing::info;
19
20/// A wrapper around a parsed CLI that handles command execution.
21#[derive(Debug)]
22pub struct CliApp<Spec: ChainSpecParser, Ext: clap::Args + fmt::Debug, Rpc: RpcModuleValidator> {
23    cli: Cli<Spec, Ext, Rpc>,
24    runner: Option<CliRunner>,
25    layers: Option<Layers>,
26    guard: Option<FileWorkerGuard>,
27}
28
29impl<C, Ext, Rpc> CliApp<C, Ext, Rpc>
30where
31    C: ChainSpecParser,
32    Ext: clap::Args + fmt::Debug,
33    Rpc: RpcModuleValidator,
34{
35    pub(crate) fn new(cli: Cli<C, Ext, Rpc>) -> Self {
36        Self { cli, runner: None, layers: Some(Layers::new()), guard: None }
37    }
38
39    /// Sets the runner for the CLI commander.
40    ///
41    /// This replaces any existing runner with the provided one.
42    pub fn set_runner(&mut self, runner: CliRunner) {
43        self.runner = Some(runner);
44    }
45
46    /// Access to tracing layers.
47    ///
48    /// Returns a mutable reference to the tracing layers, or error
49    /// if tracing initialized and layers have detached already.
50    pub fn access_tracing_layers(&mut self) -> Result<&mut Layers> {
51        self.layers.as_mut().ok_or_else(|| eyre!("Tracing already initialized"))
52    }
53
54    /// Execute the configured cli command.
55    ///
56    /// This accepts a closure that is used to launch the node via the
57    /// [`NodeCommand`](reth_cli_commands::node::NodeCommand).
58    pub fn run(self, launcher: impl Launcher<C, Ext>) -> Result<()>
59    where
60        C: ChainSpecParser<ChainSpec = ChainSpec>,
61    {
62        let components = |spec: Arc<ChainSpec>| {
63            (EthEvmConfig::ethereum(spec.clone()), Arc::new(EthBeaconConsensus::new(spec)))
64        };
65
66        self.run_with_components::<EthereumNode>(components, |builder, ext| async move {
67            launcher.entrypoint(builder, ext).await
68        })
69    }
70
71    /// Execute the configured cli command with the provided [`CliComponentsBuilder`].
72    ///
73    /// This accepts a closure that is used to launch the node via the
74    /// [`NodeCommand`](reth_cli_commands::node::NodeCommand) and allows providing custom
75    /// components.
76    pub fn run_with_components<N>(
77        mut self,
78        components: impl CliComponentsBuilder<N>,
79        launcher: impl AsyncFnOnce(
80            WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
81            Ext,
82        ) -> Result<()>,
83    ) -> Result<()>
84    where
85        N: CliNodeTypes<
86            Primitives: NodePrimitives<BlockHeader: CliHeader>,
87            ChainSpec: Hardforks + EthChainSpec,
88        >,
89        C: ChainSpecParser<ChainSpec = N::ChainSpec>,
90    {
91        let runner = match self.runner.take() {
92            Some(runner) => runner,
93            None => CliRunner::try_default_runtime()?,
94        };
95
96        // Add network name if available to the logs dir
97        if let Some(chain_spec) = self.cli.command.chain_spec() {
98            self.cli.logs.log_file_directory =
99                self.cli.logs.log_file_directory.join(chain_spec.chain().to_string());
100        }
101
102        self.init_tracing()?;
103        // Install the prometheus recorder to be sure to record all metrics
104        let _ = install_prometheus_recorder();
105
106        run_commands_with::<C, Ext, Rpc, N>(self.cli, runner, components, launcher)
107    }
108
109    /// Initializes tracing with the configured options.
110    ///
111    /// If file logging is enabled, this function stores guard to the struct.
112    pub fn init_tracing(&mut self) -> Result<()> {
113        if self.guard.is_none() {
114            let layers = self.layers.take().unwrap_or_default();
115            self.guard = self.cli.logs.init_tracing_with_layers(layers)?;
116            info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.cli.logs.log_file_directory);
117        }
118        Ok(())
119    }
120}
121
122/// Run CLI commands with the provided runner, components and launcher.
123/// This is the shared implementation used by both `CliApp` and Cli methods.
124pub(crate) fn run_commands_with<C, Ext, Rpc, N>(
125    cli: Cli<C, Ext, Rpc>,
126    runner: CliRunner,
127    components: impl CliComponentsBuilder<N>,
128    launcher: impl AsyncFnOnce(
129        WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
130        Ext,
131    ) -> Result<()>,
132) -> Result<()>
133where
134    C: ChainSpecParser<ChainSpec = N::ChainSpec>,
135    Ext: clap::Args + fmt::Debug,
136    Rpc: RpcModuleValidator,
137    N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: CliHeader>, ChainSpec: Hardforks>,
138{
139    match cli.command {
140        Commands::Node(command) => {
141            // Validate RPC modules using the configured validator
142            if let Some(http_api) = &command.rpc.http_api {
143                Rpc::validate_selection(http_api, "http.api").map_err(|e| eyre!("{e}"))?;
144            }
145            if let Some(ws_api) = &command.rpc.ws_api {
146                Rpc::validate_selection(ws_api, "ws.api").map_err(|e| eyre!("{e}"))?;
147            }
148
149            runner.run_command_until_exit(|ctx| {
150                command.execute(ctx, FnLauncher::new::<C, Ext>(launcher))
151            })
152        }
153        Commands::Init(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
154        Commands::InitState(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
155        Commands::Import(command) => {
156            runner.run_blocking_until_ctrl_c(command.execute::<N, _>(components))
157        }
158        Commands::ImportEra(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
159        Commands::ExportEra(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
160        Commands::DumpGenesis(command) => runner.run_blocking_until_ctrl_c(command.execute()),
161        Commands::Db(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
162        Commands::Download(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
163        Commands::Stage(command) => {
164            runner.run_command_until_exit(|ctx| command.execute::<N, _>(ctx, components))
165        }
166        Commands::P2P(command) => runner.run_until_ctrl_c(command.execute::<N>()),
167        Commands::Config(command) => runner.run_until_ctrl_c(command.execute()),
168        Commands::Recover(command) => {
169            runner.run_command_until_exit(|ctx| command.execute::<N>(ctx))
170        }
171        Commands::Prune(command) => runner.run_until_ctrl_c(command.execute::<N>()),
172        #[cfg(feature = "dev")]
173        Commands::TestVectors(command) => runner.run_until_ctrl_c(command.execute()),
174        Commands::ReExecute(command) => runner.run_until_ctrl_c(command.execute::<N>(components)),
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::chainspec::EthereumChainSpecParser;
182    use clap::Parser;
183    use reth_cli_commands::node::NoArgs;
184
185    #[test]
186    fn test_cli_app_creation() {
187        let args = vec!["reth", "config"];
188        let cli = Cli::<EthereumChainSpecParser, NoArgs>::try_parse_from(args).unwrap();
189        let app = cli.configure();
190
191        // Verify app is created correctly
192        assert!(app.runner.is_none());
193        assert!(app.layers.is_some());
194        assert!(app.guard.is_none());
195    }
196
197    #[test]
198    fn test_set_runner() {
199        let args = vec!["reth", "config"];
200        let cli = Cli::<EthereumChainSpecParser, NoArgs>::try_parse_from(args).unwrap();
201        let mut app = cli.configure();
202
203        // Create and set a runner
204        if let Ok(runner) = CliRunner::try_default_runtime() {
205            app.set_runner(runner);
206            assert!(app.runner.is_some());
207        }
208    }
209
210    #[test]
211    fn test_access_tracing_layers() {
212        let args = vec!["reth", "config"];
213        let cli = Cli::<EthereumChainSpecParser, NoArgs>::try_parse_from(args).unwrap();
214        let mut app = cli.configure();
215
216        // Should be able to access layers before initialization
217        assert!(app.access_tracing_layers().is_ok());
218
219        // After taking layers (simulating initialization), access should error
220        app.layers = None;
221        assert!(app.access_tracing_layers().is_err());
222    }
223}