darkfi/util/
cli.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{
20    fs,
21    io::Write,
22    path::Path,
23    str,
24    sync::{Arc, Mutex},
25    time::Instant,
26};
27
28use crate::Result;
29
30/*
31#[derive(Clone, Default)]
32pub struct Config<T> {
33    config: PhantomData<T>,
34}
35
36impl<T: Serialize + DeserializeOwned> Config<T> {
37    pub fn load(path: PathBuf) -> Result<T> {
38        if Path::new(&path).exists() {
39            let toml = fs::read(&path)?;
40            let str_buff = str::from_utf8(&toml)?;
41            let config: T = toml::from_str(str_buff)?;
42            Ok(config)
43        } else {
44            let path = path.to_str();
45            if path.is_some() {
46                println!("Could not find/parse configuration file in: {}", path.unwrap());
47            } else {
48                println!("Could not find/parse configuration file");
49            }
50            println!("Please follow the instructions in the README");
51            Err(Error::ConfigNotFound)
52        }
53    }
54}
55*/
56
57pub fn spawn_config(path: &Path, contents: &[u8]) -> Result<()> {
58    if !path.exists() {
59        if let Some(parent) = path.parent() {
60            fs::create_dir_all(parent)?;
61        }
62
63        let mut file = fs::File::create(path)?;
64        file.write_all(contents)?;
65        println!("Config file created in {path:?}. Please review it and try again.");
66        std::process::exit(2);
67    }
68
69    Ok(())
70}
71
72/// This macro is used for a standard way of daemonizing darkfi binaries
73/// with TOML config file configuration, and argument parsing.
74///
75/// It also spawns a multithreaded async executor and passes it into the
76/// given function.
77///
78/// The Cargo.toml dependencies needed for this are:
79/// ```text
80/// darkfi = { path = "../../", features = ["util"] }
81/// easy-parallel = "3.2.0"
82/// signal-hook-async-std = "0.2.2"
83/// signal-hook = "0.3.15"
84/// tracing-subscriber = "0.3.19"
85/// tracing-appender = "0.2.3"
86/// smol = "1.2.5"
87///
88/// # Argument parsing
89/// serde = {version = "1.0.135", features = ["derive"]}
90/// structopt = "0.3.26"
91/// structopt-toml = "0.5.1"
92/// ```
93///
94/// Example usage:
95/// ```
96/// use darkfi::{async_daemonize, cli_desc, Result};
97/// use smol::stream::StreamExt;
98/// use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
99///
100/// const CONFIG_FILE: &str = "daemond_config.toml";
101/// const CONFIG_FILE_CONTENTS: &str = include_str!("../daemond_config.toml");
102///
103/// #[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
104/// #[serde(default)]
105/// #[structopt(name = "daemond", about = cli_desc!())]
106/// struct Args {
107///     #[structopt(short, long)]
108///     /// Configuration file to use
109///     config: Option<String>,
110///
111///     #[structopt(short, long)]
112///     /// Set log file to ouput into
113///     log: Option<String>,
114///
115///     #[structopt(short, parse(from_occurrences))]
116///     /// Increase verbosity (-vvv supported)
117///     verbose: u8,
118/// }
119///
120/// async_daemonize!(realmain);
121/// async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
122///     println!("Hello, world!");
123///     Ok(())
124/// }
125/// ```
126#[cfg(feature = "async-daemonize")]
127#[macro_export]
128macro_rules! async_daemonize {
129    ($realmain:ident) => {
130        fn main() -> Result<()> {
131            let args = match Args::from_args_with_toml("") {
132                Ok(v) => v,
133                Err(e) => {
134                    eprintln!("Unable to get args: {e}");
135                    return Err(Error::ConfigInvalid)
136                }
137            };
138            let cfg_path =
139                match darkfi::util::path::get_config_path(args.config.clone(), CONFIG_FILE) {
140                    Ok(v) => v,
141                    Err(e) => {
142                        eprintln!("Unable to get config path `{:?}`: {e}", args.config);
143                        return Err(e)
144                    }
145                };
146            if let Err(e) =
147                darkfi::util::cli::spawn_config(&cfg_path, CONFIG_FILE_CONTENTS.as_bytes())
148            {
149                eprintln!("Spawn config failed `{cfg_path:?}`: {e}");
150                return Err(e)
151            }
152            let cfg_text = match std::fs::read_to_string(&cfg_path) {
153                Ok(c) => c,
154                Err(e) => {
155                    eprintln!("Read config failed `{cfg_path:?}`: {e}");
156                    return Err(e.into())
157                }
158            };
159            let args = match Args::from_args_with_toml(&cfg_text) {
160                Ok(v) => v,
161                Err(e) => {
162                    eprintln!("Parsing config failed `{cfg_path:?}`: {e}");
163                    return Err(Error::ConfigInvalid)
164                }
165            };
166
167            // If a log file has been configured, create a terminal and file logger.
168            // Otherwise, output to terminal logger only.
169            let (non_blocking, file_guard) = match args.log {
170                Some(ref log_path) => {
171                    let log_path = match darkfi::util::path::expand_path(log_path) {
172                        Ok(v) => v,
173                        Err(e) => {
174                            eprintln!("Expanding log path failed `{log_path:?}`: {e}");
175                            return Err(e)
176                        }
177                    };
178                    let log_file = match std::fs::File::create(&log_path) {
179                        Ok(v) => v,
180                        Err(e) => {
181                            eprintln!("Creating log file failed `{log_path:?}`: {e}");
182                            return Err(e.into())
183                        }
184                    };
185
186                    // Hold guard until process stops to ensure buffer logs are flushed to file
187                    let (non_blocking, guard) = tracing_appender::non_blocking(log_file);
188                    (Some(non_blocking), Some(guard))
189                }
190                None => (None, None),
191            };
192            if let Err(e) = darkfi::util::logger::setup_logging(args.verbose, non_blocking) {
193                if args.log.is_some() {
194                    eprintln!("Unable to init logger with term + logfile combo: {e}");
195                } else {
196                    eprintln!("Unable to init term logger: {e}");
197                }
198                return Err(e.into())
199            }
200
201            // https://docs.rs/smol/latest/smol/struct.Executor.html#examples
202            let n_threads = std::thread::available_parallelism().unwrap().get();
203            let ex = std::sync::Arc::new(smol::Executor::new());
204            let (signal, shutdown) = smol::channel::unbounded::<()>();
205            let (_, result) = easy_parallel::Parallel::new()
206                // Run four executor threads
207                .each(0..n_threads, |_| smol::future::block_on(ex.run(shutdown.recv())))
208                // Run the main future on the current thread.
209                .finish(|| {
210                    smol::future::block_on(async {
211                        $realmain(args, ex.clone()).await?;
212                        drop(signal);
213                        Ok::<(), darkfi::Error>(())
214                    })
215                });
216
217            result
218        }
219
220        /// Auxiliary structure used to keep track of signals
221        struct SignalHandler {
222            /// Termination signal channel receiver
223            term_rx: smol::channel::Receiver<()>,
224            /// Signals handle
225            handle: signal_hook_async_std::Handle,
226            /// SIGHUP publisher to retrieve new configuration,
227            sighup_pub: darkfi::system::PublisherPtr<Args>,
228        }
229
230        impl SignalHandler {
231            fn new(
232                ex: std::sync::Arc<smol::Executor<'static>>,
233            ) -> Result<(Self, smol::Task<Result<()>>)> {
234                let (term_tx, term_rx) = smol::channel::bounded::<()>(1);
235                let signals = signal_hook_async_std::Signals::new([
236                    signal_hook::consts::SIGHUP,
237                    signal_hook::consts::SIGTERM,
238                    signal_hook::consts::SIGINT,
239                    signal_hook::consts::SIGQUIT,
240                ])?;
241                let handle = signals.handle();
242                let sighup_pub = darkfi::system::Publisher::new();
243                let signals_task =
244                    ex.spawn(handle_signals(signals, term_tx, sighup_pub.clone(), ex.clone()));
245
246                Ok((Self { term_rx, handle, sighup_pub }, signals_task))
247            }
248
249            /// Handler waits for termination signal
250            async fn wait_termination(&self, signals_task: smol::Task<Result<()>>) -> Result<()> {
251                self.term_rx.recv().await?;
252                print!("\r");
253                self.handle.close();
254                signals_task.await?;
255
256                Ok(())
257            }
258        }
259
260        /// Auxiliary task to handle SIGINT for forceful process abort
261        async fn handle_abort(mut signals: signal_hook_async_std::Signals) {
262            let mut n_sigint = 0;
263            while let Some(signal) = signals.next().await {
264                n_sigint += 1;
265                if n_sigint == 2 {
266                    print!("\r");
267                    info!("Aborting. Good luck.");
268                    std::process::abort();
269                }
270            }
271        }
272
273        /// Auxiliary task to handle SIGHUP, SIGTERM, SIGINT and SIGQUIT signals
274        async fn handle_signals(
275            mut signals: signal_hook_async_std::Signals,
276            term_tx: smol::channel::Sender<()>,
277            publisher: darkfi::system::PublisherPtr<Args>,
278            ex: std::sync::Arc<smol::Executor<'static>>,
279        ) -> Result<()> {
280            while let Some(signal) = signals.next().await {
281                match signal {
282                    signal_hook::consts::SIGHUP => {
283                        let args = Args::from_args_with_toml("").unwrap();
284                        let cfg_path =
285                            darkfi::util::path::get_config_path(args.config, CONFIG_FILE)?;
286                        darkfi::util::cli::spawn_config(
287                            &cfg_path,
288                            CONFIG_FILE_CONTENTS.as_bytes(),
289                        )?;
290                        let args = Args::from_args_with_toml(&std::fs::read_to_string(cfg_path)?);
291                        if args.is_err() {
292                            println!("handle_signals():: Error parsing the config file");
293                            continue
294                        }
295                        publisher.notify(args.unwrap()).await;
296                    }
297                    signal_hook::consts::SIGINT => {
298                        // Spawn a new background task to listen for more SIGINT.
299                        // This lets us forcefully abort the process if necessary.
300                        let signals =
301                            signal_hook_async_std::Signals::new([signal_hook::consts::SIGINT])?;
302                        let handle = signals.handle();
303                        ex.spawn(handle_abort(signals)).detach();
304
305                        term_tx.send(()).await?;
306                    }
307                    signal_hook::consts::SIGTERM | signal_hook::consts::SIGQUIT => {
308                        term_tx.send(()).await?;
309                    }
310
311                    _ => println!("handle_signals():: Unsupported signal"),
312                }
313            }
314            Ok(())
315        }
316    };
317}
318
319pub fn fg_red(message: &str) -> String {
320    format!("\x1b[31m{message}\x1b[0m")
321}
322
323pub fn fg_green(message: &str) -> String {
324    format!("\x1b[32m{message}\x1b[0m")
325}
326
327pub fn fg_reset() -> String {
328    "\x1b[0m".to_string()
329}
330
331pub struct ProgressInc {
332    position: Arc<Mutex<u64>>,
333    timer: Arc<Mutex<Option<Instant>>>,
334}
335
336impl Default for ProgressInc {
337    fn default() -> Self {
338        Self::new()
339    }
340}
341
342impl ProgressInc {
343    pub fn new() -> Self {
344        eprint!("\x1b[?25l");
345        Self { position: Arc::new(Mutex::new(0)), timer: Arc::new(Mutex::new(None)) }
346    }
347
348    pub fn inc(&self, n: u64) {
349        let mut position = self.position.lock().unwrap();
350
351        if *position == 0 {
352            *self.timer.lock().unwrap() = Some(Instant::now());
353        }
354
355        *position += n;
356
357        let binding = self.timer.lock().unwrap();
358        let Some(elapsed) = binding.as_ref() else { return };
359        let elapsed = elapsed.elapsed();
360        let pos = *position;
361
362        eprint!("\r[{elapsed:?}] {pos} attempts");
363    }
364
365    pub fn position(&self) -> u64 {
366        *self.position.lock().unwrap()
367    }
368
369    pub fn finish_and_clear(&self) {
370        *self.timer.lock().unwrap() = None;
371        eprint!("\r\x1b[2K\x1b[?25h");
372    }
373}