reth_ethereum_cli/
app.rs

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