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}