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