Skip to main content

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