1#![doc(
4 html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5 html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6 issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
7)]
8#![cfg_attr(not(test), warn(unused_crate_dependencies))]
9#![cfg_attr(docsrs, feature(doc_cfg))]
10
11use reth_tasks::{PanickedTaskError, TaskExecutor};
14use std::{
15 future::Future,
16 pin::{pin, Pin},
17 sync::mpsc,
18 time::Duration,
19};
20use tokio::task::JoinHandle;
21use tracing::{debug, error, info};
22
23#[derive(Debug)]
27pub struct CliRunner {
28 config: CliRunnerConfig,
29 runtime: reth_tasks::Runtime,
30}
31
32impl CliRunner {
33 pub fn try_default_runtime() -> Result<Self, reth_tasks::RuntimeBuildError> {
38 Self::try_with_runtime_config(reth_tasks::RuntimeConfig::default())
39 }
40
41 pub fn try_with_runtime_config(
43 config: reth_tasks::RuntimeConfig,
44 ) -> Result<Self, reth_tasks::RuntimeBuildError> {
45 let runtime = reth_tasks::RuntimeBuilder::new(config).build()?;
46 Ok(Self { config: CliRunnerConfig::default(), runtime })
47 }
48
49 pub const fn with_config(mut self, config: CliRunnerConfig) -> Self {
51 self.config = config;
52 self
53 }
54
55 pub fn runtime(&self) -> reth_tasks::Runtime {
57 self.runtime.clone()
58 }
59
60 pub fn block_on<F, T>(&self, fut: F) -> T
62 where
63 F: Future<Output = T>,
64 {
65 self.runtime.handle().block_on(Box::pin(fut) as Pin<Box<dyn Future<Output = T> + '_>>)
66 }
67
68 pub fn run_command_until_exit<F, E>(
74 self,
75 command: impl FnOnce(CliContext) -> F,
76 ) -> Result<(), E>
77 where
78 F: Future<Output = Result<(), E>>,
79 E: Send
80 + Sync
81 + std::fmt::Display
82 + From<std::io::Error>
83 + From<reth_tasks::PanickedTaskError>
84 + 'static,
85 {
86 let (context, task_manager_handle) = cli_context(&self.runtime);
87 self.run_until_exit(task_manager_handle, Box::pin(command(context)))
88 }
89
90 fn run_until_exit<E>(
92 self,
93 task_manager_handle: JoinHandle<Result<(), PanickedTaskError>>,
94 command: Pin<Box<dyn Future<Output = Result<(), E>> + '_>>,
95 ) -> Result<(), E>
96 where
97 E: Send
98 + Sync
99 + std::fmt::Display
100 + From<std::io::Error>
101 + From<reth_tasks::PanickedTaskError>
102 + 'static,
103 {
104 let command_res = self
106 .runtime
107 .handle()
108 .block_on(run_to_completion_or_panic(task_manager_handle, run_until_ctrl_c(command)));
109
110 if let Err(err) = &command_res {
111 error!(target: "reth::cli", %err, "shutting down due to error");
112 } else {
113 debug!(target: "reth::cli", "shutting down gracefully");
114 self.runtime.graceful_shutdown_with_timeout(self.config.graceful_shutdown_timeout);
118 }
119
120 runtime_shutdown(self.runtime, true);
121
122 command_res
123 }
124
125 pub fn run_blocking_command_until_exit<F, E>(
129 self,
130 command: impl FnOnce(CliContext) -> F + Send + 'static,
131 ) -> Result<(), E>
132 where
133 F: Future<Output = Result<(), E>> + Send + 'static,
134 E: Send
135 + Sync
136 + std::fmt::Display
137 + From<std::io::Error>
138 + From<reth_tasks::PanickedTaskError>
139 + 'static,
140 {
141 self.run_blocking_until_exit(Box::new(move |context| Box::pin(command(context))))
142 }
143
144 fn run_blocking_until_exit<E>(
147 self,
148 command: Box<dyn FnOnce(CliContext) -> BoxedCommand<E> + Send>,
149 ) -> Result<(), E>
150 where
151 E: Send
152 + Sync
153 + std::fmt::Display
154 + From<std::io::Error>
155 + From<reth_tasks::PanickedTaskError>
156 + 'static,
157 {
158 let (context, task_manager_handle) = cli_context(&self.runtime);
159
160 let handle = self.runtime.handle().clone();
162 let command_handle = self.runtime.spawn_blocking(move || handle.block_on(command(context)));
163
164 let command_res = self.runtime.handle().block_on(run_to_completion_or_panic(
166 task_manager_handle,
167 run_until_ctrl_c(
168 async move { command_handle.await.expect("Failed to join blocking task") },
169 ),
170 ));
171
172 if let Err(err) = &command_res {
173 error!(target: "reth::cli", %err, "shutting down due to error");
174 } else {
175 debug!(target: "reth::cli", "shutting down gracefully");
176 self.runtime.graceful_shutdown_with_timeout(self.config.graceful_shutdown_timeout);
177 }
178
179 runtime_shutdown(self.runtime, true);
180
181 command_res
182 }
183
184 pub fn run_until_ctrl_c<F, E>(self, fut: F) -> Result<(), E>
186 where
187 F: Future<Output = Result<(), E>>,
188 E: Send + Sync + From<std::io::Error> + 'static,
189 {
190 self.runtime.handle().block_on(run_until_ctrl_c(
191 Box::pin(fut) as Pin<Box<dyn Future<Output = Result<(), E>> + '_>>
192 ))?;
193 Ok(())
194 }
195
196 pub fn run_blocking_until_ctrl_c<F, E>(self, fut: F) -> Result<(), E>
201 where
202 F: Future<Output = Result<(), E>> + Send + 'static,
203 E: Send + Sync + From<std::io::Error> + 'static,
204 {
205 self.run_blocking_until_ctrl_c_boxed(Box::pin(fut))
206 }
207
208 fn run_blocking_until_ctrl_c_boxed<E>(self, fut: BoxedCommand<E>) -> Result<(), E>
209 where
210 E: Send + Sync + From<std::io::Error> + 'static,
211 {
212 let handle = self.runtime.handle().clone();
213 let fut = self.runtime.spawn_blocking(move || handle.block_on(fut));
214 self.runtime
215 .handle()
216 .block_on(run_until_ctrl_c(async move { fut.await.expect("Failed to join task") }))?;
217
218 runtime_shutdown(self.runtime, false);
219
220 Ok(())
221 }
222}
223
224type BoxedCommand<E> = Pin<Box<dyn Future<Output = Result<(), E>> + Send>>;
226
227fn cli_context(
229 runtime: &reth_tasks::Runtime,
230) -> (CliContext, JoinHandle<Result<(), PanickedTaskError>>) {
231 let handle =
232 runtime.take_task_manager_handle().expect("Runtime must contain a TaskManager handle");
233 let context = CliContext { task_executor: runtime.clone() };
234 (context, handle)
235}
236
237#[derive(Debug)]
239pub struct CliContext {
240 pub task_executor: TaskExecutor,
242}
243
244const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
246
247#[derive(Debug, Clone)]
249pub struct CliRunnerConfig {
250 pub graceful_shutdown_timeout: Duration,
255}
256
257impl Default for CliRunnerConfig {
258 fn default() -> Self {
259 Self::new()
260 }
261}
262
263impl CliRunnerConfig {
264 pub const fn new() -> Self {
266 Self { graceful_shutdown_timeout: DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT }
267 }
268
269 pub const fn with_graceful_shutdown_timeout(mut self, timeout: Duration) -> Self {
271 self.graceful_shutdown_timeout = timeout;
272 self
273 }
274}
275
276async fn run_to_completion_or_panic<F, E>(
280 task_manager_handle: JoinHandle<Result<(), PanickedTaskError>>,
281 fut: F,
282) -> Result<(), E>
283where
284 F: Future<Output = Result<(), E>>,
285 E: Send + Sync + From<reth_tasks::PanickedTaskError> + 'static,
286{
287 let fut = pin!(fut);
288 tokio::select! {
289 task_manager_result = task_manager_handle => {
290 if let Ok(Err(panicked_error)) = task_manager_result {
291 return Err(panicked_error.into());
292 }
293 },
294 res = fut => res?,
295 }
296 Ok(())
297}
298
299async fn run_until_ctrl_c<F, E>(fut: F) -> Result<(), E>
303where
304 F: Future<Output = Result<(), E>>,
305 E: Send + Sync + 'static + From<std::io::Error>,
306{
307 let ctrl_c = tokio::signal::ctrl_c();
308
309 #[cfg(unix)]
310 {
311 let mut stream = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
312 let sigterm = stream.recv();
313 let sigterm = pin!(sigterm);
314 let ctrl_c = pin!(ctrl_c);
315 let fut = pin!(fut);
316
317 tokio::select! {
318 _ = ctrl_c => {
319 info!(target: "reth::cli", "Received ctrl-c");
320 },
321 _ = sigterm => {
322 info!(target: "reth::cli", "Received SIGTERM");
323 },
324 res = fut => res?,
325 }
326 }
327
328 #[cfg(not(unix))]
329 {
330 let ctrl_c = pin!(ctrl_c);
331 let fut = pin!(fut);
332
333 tokio::select! {
334 _ = ctrl_c => {
335 info!(target: "reth::cli", "Received ctrl-c");
336 },
337 res = fut => res?,
338 }
339 }
340
341 Ok(())
342}
343
344const DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
346
347fn runtime_shutdown(rt: reth_tasks::Runtime, wait: bool) {
352 let (tx, rx) = mpsc::channel();
353 std::thread::Builder::new()
354 .name("rt-shutdown".to_string())
355 .spawn(move || {
356 rt.shutdown_timeout(DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT);
357 let _ = tx.send(());
358 })
359 .unwrap();
360
361 if wait {
362 let _ = rx.recv_timeout(DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT).inspect_err(|err| {
363 tracing::warn!(target: "reth::cli", %err, "runtime shutdown timed out");
364 });
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn runtime_shutdown_stops_tasks_with_live_runtime_clones() {
374 let runtime = reth_tasks::Runtime::test();
375 let retained = runtime.clone();
376 let task_runtime = runtime.clone();
377 let (started, ready) = mpsc::channel();
378 let task = runtime.handle().spawn(async move {
379 let _runtime = task_runtime;
380 started.send(()).unwrap();
381 std::future::pending::<()>().await;
382 });
383 ready.recv_timeout(Duration::from_secs(5)).unwrap();
384
385 runtime_shutdown(runtime, true);
386
387 assert!(task.is_finished(), "shutdown returned while a task still owned the runtime");
388 assert!(retained.handle().block_on(task).unwrap_err().is_cancelled());
389 }
390}