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::Prune(command) => runner.run_until_ctrl_c(command.execute::<N>()),
169        #[cfg(feature = "dev")]
170        Commands::TestVectors(command) => runner.run_until_ctrl_c(command.execute()),
171        Commands::ReExecute(command) => runner.run_until_ctrl_c(command.execute::<N>(components)),
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::chainspec::EthereumChainSpecParser;
179    use clap::Parser;
180    use reth_cli_commands::node::NoArgs;
181
182    #[test]
183    fn test_cli_app_creation() {
184        let args = vec!["reth", "config"];
185        let cli = Cli::<EthereumChainSpecParser, NoArgs>::try_parse_from(args).unwrap();
186        let app = cli.configure();
187
188        // Verify app is created correctly
189        assert!(app.runner.is_none());
190        assert!(app.layers.is_some());
191        assert!(app.guard.is_none());
192    }
193
194    #[test]
195    fn test_set_runner() {
196        let args = vec!["reth", "config"];
197        let cli = Cli::<EthereumChainSpecParser, NoArgs>::try_parse_from(args).unwrap();
198        let mut app = cli.configure();
199
200        // Create and set a runner
201        if let Ok(runner) = CliRunner::try_default_runtime() {
202            app.set_runner(runner);
203            assert!(app.runner.is_some());
204        }
205    }
206
207    #[test]
208    fn test_access_tracing_layers() {
209        let args = vec!["reth", "config"];
210        let cli = Cli::<EthereumChainSpecParser, NoArgs>::try_parse_from(args).unwrap();
211        let mut app = cli.configure();
212
213        // Should be able to access layers before initialization
214        assert!(app.access_tracing_layers().is_ok());
215
216        // After taking layers (simulating initialization), access should error
217        app.layers = None;
218        assert!(app.access_tracing_layers().is_err());
219    }
220}