devimint/
util.rs

1use std::collections::BTreeMap;
2use std::ffi::OsStr;
3use std::future::Future;
4use std::ops::ControlFlow;
5use std::path::PathBuf;
6use std::process::Stdio;
7use std::sync::Arc;
8use std::time::Duration;
9use std::{env, unreachable};
10
11use anyhow::{Context, Result, anyhow, bail, format_err};
12use fedimint_core::PeerId;
13use fedimint_core::admin_client::SetupStatus;
14use fedimint_core::envs::{FM_ENABLE_MODULE_LNV1_ENV, FM_ENABLE_MODULE_LNV2_ENV, is_env_var_set};
15use fedimint_core::module::ApiAuth;
16use fedimint_core::task::{self, block_in_place, block_on};
17use fedimint_core::time::now;
18use fedimint_core::util::FmtCompactAnyhow as _;
19use fedimint_core::util::backoff_util::custom_backoff;
20use fedimint_logging::LOG_DEVIMINT;
21use semver::Version;
22use serde::de::DeserializeOwned;
23use tokio::fs::OpenOptions;
24use tokio::process::Child;
25use tokio::sync::Mutex;
26use tracing::{debug, warn};
27
28use crate::envs::{
29    FM_BACKWARDS_COMPATIBILITY_TEST_ENV, FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV,
30    FM_BITCOIND_BASE_EXECUTABLE_ENV, FM_BTC_CLIENT_ENV, FM_CLIENT_DIR_ENV,
31    FM_DEVIMINT_CMD_INHERIT_STDERR_ENV, FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV,
32    FM_ESPLORA_BASE_EXECUTABLE_ENV, FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
33    FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV, FM_FEDIMINTD_BASE_EXECUTABLE_ENV,
34    FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV, FM_GATEWAYD_BASE_EXECUTABLE_ENV, FM_GWCLI_LDK_ENV,
35    FM_GWCLI_LND_ENV, FM_LNCLI_BASE_EXECUTABLE_ENV, FM_LNCLI_ENV, FM_LND_BASE_EXECUTABLE_ENV,
36    FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV, FM_LOGS_DIR_ENV, FM_MINT_CLIENT_ENV,
37    FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV, FM_RECURRINGD_BASE_EXECUTABLE_ENV,
38};
39
40// If a binary doesn't provide a clap version, default to the first stable
41// release (v0.2.1)
42const DEFAULT_VERSION: Version = Version::new(0, 2, 1);
43
44pub fn parse_map(s: &str) -> Result<BTreeMap<String, String>> {
45    let mut map = BTreeMap::new();
46
47    if s.is_empty() {
48        return Ok(map);
49    }
50
51    for pair in s.split(',') {
52        let parts: Vec<&str> = pair.split('=').collect();
53        if parts.len() == 2 {
54            map.insert(parts[0].to_string(), parts[1].to_string());
55        } else {
56            return Err(format_err!("Invalid pair in map: {}", pair));
57        }
58    }
59    Ok(map)
60}
61
62fn send_sigterm(child: &Child) {
63    send_signal(child, nix::sys::signal::Signal::SIGTERM);
64}
65
66fn send_sigkill(child: &Child) {
67    send_signal(child, nix::sys::signal::Signal::SIGKILL);
68}
69
70fn send_signal(child: &Child, signal: nix::sys::signal::Signal) {
71    let _ = nix::sys::signal::kill(
72        nix::unistd::Pid::from_raw(child.id().expect("pid should be present") as _),
73        signal,
74    );
75}
76
77/// Kills process when all references to ProcessHandle are dropped.
78///
79/// NOTE: drop order is significant make sure fields in struct are declared in
80/// correct order it is generally clients, process handle, deps
81#[derive(Debug, Clone)]
82pub struct ProcessHandle(Arc<Mutex<ProcessHandleInner>>);
83
84impl ProcessHandle {
85    pub async fn terminate(&self) -> Result<()> {
86        let mut inner = self.0.lock().await;
87        inner.terminate().await?;
88        Ok(())
89    }
90
91    pub async fn await_terminated(&self) -> Result<()> {
92        let mut inner = self.0.lock().await;
93        inner.await_terminated().await?;
94        Ok(())
95    }
96
97    pub async fn is_running(&self) -> bool {
98        self.0.lock().await.child.is_some()
99    }
100}
101
102#[derive(Debug)]
103pub struct ProcessHandleInner {
104    name: String,
105    child: Option<Child>,
106}
107
108impl ProcessHandleInner {
109    async fn terminate(&mut self) -> anyhow::Result<()> {
110        if let Some(child) = self.child.as_mut() {
111            debug!(
112                target: LOG_DEVIMINT,
113                name=%self.name,
114                signal="SIGTERM",
115                "sending signal to terminate child process"
116            );
117
118            send_sigterm(child);
119
120            if (fedimint_core::runtime::timeout(Duration::from_secs(2), child.wait()).await)
121                .is_err()
122            {
123                debug!(
124                    target: LOG_DEVIMINT,
125                    name=%self.name,
126                    signal="SIGKILL",
127                    "sending signal to terminate child process"
128                );
129
130                send_sigkill(child);
131
132                match fedimint_core::runtime::timeout(Duration::from_secs(5), child.wait()).await {
133                    Ok(Ok(_)) => {}
134                    Ok(Err(err)) => {
135                        bail!("Failed to terminate child process {}: {}", self.name, err);
136                    }
137                    Err(_) => {
138                        bail!("Failed to terminate child process {}: timeout", self.name);
139                    }
140                }
141            }
142        }
143        // only drop the child handle if succeeded to terminate
144        self.child.take();
145        Ok(())
146    }
147
148    async fn await_terminated(&mut self) -> anyhow::Result<()> {
149        match self
150            .child
151            .as_mut()
152            .expect("Process not running")
153            .wait()
154            .await
155        {
156            Ok(_status) => {
157                debug!(
158                    target: LOG_DEVIMINT,
159                    name=%self.name,
160                    "child process terminated"
161                );
162            }
163            Err(err) => {
164                bail!("Failed to wait for child process {}: {}", self.name, err);
165            }
166        }
167
168        // only drop the child handle if succeeded to terminate
169        self.child.take();
170        Ok(())
171    }
172}
173
174impl Drop for ProcessHandleInner {
175    fn drop(&mut self) {
176        if self.child.is_none() {
177            return;
178        }
179
180        if std::thread::panicking() {
181            // Doing block_in_place + block on trickery
182            // breaks down during panics, so let's just
183            // try to kill it and move on
184            if let Some(mut child) = self.child.take() {
185                send_sigterm(&child);
186                let _ = child.try_wait();
187            }
188            return;
189        }
190
191        block_in_place(|| {
192            if let Err(err) = block_on(self.terminate()) {
193                warn!(
194                    target: LOG_DEVIMINT,
195                    name=%self.name,
196                    err = %err.fmt_compact_anyhow(),
197                    "Error terminating process on drop"
198                );
199            }
200        });
201    }
202}
203
204#[derive(Clone)]
205pub struct ProcessManager {
206    pub globals: super::vars::Global,
207}
208
209impl ProcessManager {
210    pub fn new(globals: super::vars::Global) -> Self {
211        Self { globals }
212    }
213
214    /// Logs to $FM_LOGS_DIR/{name}.{out,err}
215    pub async fn spawn_daemon(&self, name: &str, mut cmd: Command) -> Result<ProcessHandle> {
216        debug!(target: LOG_DEVIMINT, %name, "Spawning daemon");
217        let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
218        let path = format!("{logs_dir}/{name}.log");
219        let log = OpenOptions::new()
220            .append(true)
221            .create(true)
222            .open(path)
223            .await?
224            .into_std()
225            .await;
226        cmd.cmd.kill_on_drop(false); // we handle killing ourself
227        cmd.cmd.stdout(log.try_clone()?);
228        cmd.cmd.stderr(log);
229        let child = cmd
230            .cmd
231            .spawn()
232            .with_context(|| format!("Could not spawn: {name}"))?;
233        let handle = ProcessHandle(Arc::new(Mutex::new(ProcessHandleInner {
234            name: name.to_owned(),
235            child: Some(child),
236        })));
237        Ok(handle)
238    }
239}
240
241pub struct Command {
242    pub cmd: tokio::process::Command,
243    pub args_debug: Vec<String>,
244}
245
246impl Command {
247    pub fn arg<T: ToString>(mut self, arg: &T) -> Self {
248        let string = arg.to_string();
249        self.cmd.arg(string.clone());
250        self.args_debug.push(string);
251        self
252    }
253
254    pub fn args<T: ToString>(mut self, args: impl IntoIterator<Item = T>) -> Self {
255        for arg in args {
256            self = self.arg(&arg);
257        }
258        self
259    }
260
261    pub fn env<K, V>(mut self, key: K, val: V) -> Self
262    where
263        K: AsRef<OsStr>,
264        V: AsRef<OsStr>,
265    {
266        self.cmd.env(key, val);
267        self
268    }
269
270    pub fn envs<I, K, V>(mut self, env: I) -> Self
271    where
272        I: IntoIterator<Item = (K, V)>,
273        K: AsRef<OsStr>,
274        V: AsRef<OsStr>,
275    {
276        self.cmd.envs(env);
277        self
278    }
279
280    pub fn kill_on_drop(mut self, kill: bool) -> Self {
281        self.cmd.kill_on_drop(kill);
282        self
283    }
284
285    /// Run the command and get its output as json.
286    pub async fn out_json(&mut self) -> Result<serde_json::Value> {
287        Ok(serde_json::from_str(&self.out_string().await?)?)
288    }
289
290    fn command_debug(&self) -> String {
291        self.args_debug
292            .iter()
293            .map(|x| x.replace(' ', "␣"))
294            .collect::<Vec<_>>()
295            .join(" ")
296    }
297
298    /// Run the command and get its output as string.
299    pub async fn out_string(&mut self) -> Result<String> {
300        let output = self
301            .run_inner(true)
302            .await
303            .with_context(|| format!("command: {}", self.command_debug()))?;
304        let output = String::from_utf8(output.stdout)?;
305        Ok(output.trim().to_owned())
306    }
307
308    /// Returns the json error if the command has a non-zero exit code.
309    pub async fn expect_err_json(&mut self) -> Result<serde_json::Value> {
310        let output = self
311            .run_inner(false)
312            .await
313            .with_context(|| format!("command: {}", self.command_debug()))?;
314        let output = String::from_utf8(output.stdout)?;
315        Ok(serde_json::from_str(output.trim())?)
316    }
317
318    /// Run the command expecting an error, which is parsed using a closure.
319    /// Returns an Err if the closure returns false.
320    pub async fn assert_error(
321        &mut self,
322        predicate: impl Fn(serde_json::Value) -> bool,
323    ) -> Result<()> {
324        let parsed_error = self.expect_err_json().await?;
325        anyhow::ensure!(predicate(parsed_error));
326        Ok(())
327    }
328
329    /// Returns an Err if the command doesn't return an error containing the
330    /// provided error string.
331    pub async fn assert_error_contains(&mut self, error: &str) -> Result<()> {
332        self.assert_error(|err_json| {
333            let error_string = err_json
334                .get("error")
335                .expect("json error contains error field")
336                .as_str()
337                .expect("not a string")
338                .to_owned();
339
340            error_string.contains(error)
341        })
342        .await
343    }
344
345    pub async fn run_inner(&mut self, expect_success: bool) -> Result<std::process::Output> {
346        debug!(target: LOG_DEVIMINT, "> {}", self.command_debug());
347        let output = self
348            .cmd
349            .stdout(Stdio::piped())
350            .stderr(if is_env_var_set(FM_DEVIMINT_CMD_INHERIT_STDERR_ENV) {
351                Stdio::inherit()
352            } else {
353                Stdio::piped()
354            })
355            .spawn()?
356            .wait_with_output()
357            .await?;
358
359        if output.status.success() != expect_success {
360            bail!(
361                "{}\nstdout:\n{}\nstderr:\n{}\n",
362                output.status,
363                String::from_utf8_lossy(&output.stdout),
364                String::from_utf8_lossy(&output.stderr),
365            );
366        }
367        Ok(output)
368    }
369
370    /// Run the command ignoring its output.
371    pub async fn run(&mut self) -> Result<()> {
372        let _ = self
373            .run_inner(true)
374            .await
375            .with_context(|| format!("command: {}", self.command_debug()))?;
376        Ok(())
377    }
378
379    /// Run the command logging the output and error
380    pub async fn run_with_logging(&mut self, name: String) -> Result<()> {
381        let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
382        let path = format!("{logs_dir}/{name}.log");
383        let log = OpenOptions::new()
384            .append(true)
385            .create(true)
386            .open(&path)
387            .await
388            .with_context(|| format!("path: {path} cmd: {name}"))?
389            .into_std()
390            .await;
391        self.cmd.stdout(log.try_clone()?);
392        self.cmd.stderr(log);
393        let status = self
394            .cmd
395            .spawn()
396            .with_context(|| format!("cmd: {name}"))?
397            .wait()
398            .await?;
399        if !status.success() {
400            bail!("{}", status);
401        }
402        Ok(())
403    }
404}
405
406/// easy syntax to create a Command
407///
408/// `(A1, A2, A3)` expands to
409/// ```ignore
410/// A1.cmd().await?
411///     .arg(A2)
412///     .arg(A3)
413///     .kill_on_drop(true)
414/// ```
415///
416/// If `An` is a string literal, it is replaced with `format!(a)`
417#[macro_export]
418macro_rules! cmd {
419    ($(@head ($($head:tt)* ))? $curr:literal $(, $($tail:tt)*)?) => {
420        cmd! {
421            @head ($($($head)*)? format!($curr),)
422            $($($tail)*)?
423        }
424    };
425    ($(@head ($($head:tt)* ))? $curr:expr_2021 $(, $($tail:tt)*)?) => {
426        cmd! {
427            @head ($($($head)*)? $curr,)
428            $($($tail)*)?
429        }
430    };
431    (@head ($($head:tt)* )) => {
432        cmd! {
433            @last
434            $($head)*
435        }
436    };
437    // last matcher
438    (@last $this:expr_2021, $($arg:expr_2021),* $(,)?) => {
439        {
440            #[allow(unused)]
441            use $crate::util::ToCmdExt;
442            $this.cmd()
443                $(.arg(&$arg))*
444                .kill_on_drop(true)
445                .env("RUST_BACKTRACE", "1")
446                .env("RUST_LIB_BACKTRACE", "0")
447        }
448    };
449}
450
451#[macro_export]
452macro_rules! poll_eq {
453    ($left:expr_2021, $right:expr_2021) => {
454        match ($left, $right) {
455            (left, right) => {
456                if left == right {
457                    Ok(())
458                } else {
459                    Err(std::ops::ControlFlow::Continue(anyhow::anyhow!(
460                        "assertion failed, left: {left:?} right: {right:?}"
461                    )))
462                }
463            }
464        }
465    };
466}
467
468#[macro_export]
469macro_rules! poll_almost_equal {
470    ($left:expr_2021, $right:expr_2021) => {
471        match ($left, $right) {
472            (left, right) => $crate::util::almost_equal(left, right, 10_000)
473                .map_err(|e| std::ops::ControlFlow::Continue(anyhow::anyhow!(e))),
474        }
475    };
476}
477
478pub fn almost_equal(a: u64, b: u64, max: u64) -> Result<(), String> {
479    if a.abs_diff(b) <= max {
480        Ok(())
481    } else {
482        Err(format!(
483            "Expected difference is {max} but we found {}",
484            a.abs_diff(b)
485        ))
486    }
487}
488
489// Allow macro to be used within the crate. See https://stackoverflow.com/a/31749071.
490pub(crate) use cmd;
491
492/// Retry until `f` succeeds or timeout is reached
493///
494/// - if `f` return Ok(val), this returns with Ok(val).
495/// - if `f` return Err(Control::Break(err)), this returns Err(err)
496/// - if `f` return Err(ControlFlow::Continue(err)), retries until timeout
497///   reached
498pub async fn poll_with_timeout<Fut, R>(
499    name: &str,
500    timeout: Duration,
501    f: impl Fn() -> Fut,
502) -> Result<R>
503where
504    Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
505{
506    const MIN_BACKOFF: Duration = Duration::from_millis(50);
507    const MAX_BACKOFF: Duration = Duration::from_secs(1);
508
509    let mut backoff = custom_backoff(MIN_BACKOFF, MAX_BACKOFF, None);
510    let start = now();
511    for attempt in 0u64.. {
512        let attempt_start = now();
513        match f().await {
514            Ok(value) => return Ok(value),
515            Err(ControlFlow::Break(err)) => {
516                return Err(err).with_context(|| format!("polling {name}"));
517            }
518            Err(ControlFlow::Continue(err))
519                if attempt_start
520                    .duration_since(start)
521                    .expect("time goes forward")
522                    < timeout =>
523            {
524                debug!(target: LOG_DEVIMINT, %attempt, err = %err.fmt_compact_anyhow(), "Polling {name} failed, will retry...");
525                task::sleep(backoff.next().unwrap_or(MAX_BACKOFF)).await;
526            }
527            Err(ControlFlow::Continue(err)) => {
528                return Err(err).with_context(|| {
529                    format!(
530                        "Polling {name} failed after {attempt} retries (timeout: {}s)",
531                        timeout.as_secs()
532                    )
533                });
534            }
535        }
536    }
537
538    unreachable!();
539}
540
541const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_secs(60);
542const EXTRA_LONG_POLL_TIMEOUT: Duration = Duration::from_secs(90);
543
544/// Retry until `f` succeeds or default timeout is reached
545///
546/// - if `f` return Ok(val), this returns with Ok(val).
547/// - if `f` return Err(Control::Break(err)), this returns Err(err)
548/// - if `f` return Err(ControlFlow::Continue(err)), retries until timeout
549///   reached
550pub async fn poll<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
551where
552    Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
553{
554    poll_with_timeout(
555        name,
556        if is_env_var_set("FM_EXTRA_LONG_POLL") {
557            EXTRA_LONG_POLL_TIMEOUT
558        } else {
559            DEFAULT_POLL_TIMEOUT
560        },
561        f,
562    )
563    .await
564}
565
566pub async fn poll_simple<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
567where
568    Fut: Future<Output = Result<R, anyhow::Error>>,
569{
570    poll(name, || async { f().await.map_err(ControlFlow::Continue) }).await
571}
572
573// used to add `cmd` method.
574pub trait ToCmdExt {
575    fn cmd(self) -> Command;
576}
577
578// a command that uses self as program name
579impl ToCmdExt for &'_ str {
580    fn cmd(self) -> Command {
581        Command {
582            cmd: tokio::process::Command::new(self),
583            args_debug: vec![self.to_owned()],
584        }
585    }
586}
587
588impl ToCmdExt for Vec<String> {
589    fn cmd(self) -> Command {
590        to_command(self)
591    }
592}
593
594pub trait JsonValueExt {
595    fn to_typed<T: DeserializeOwned>(self) -> Result<T>;
596}
597
598impl JsonValueExt for serde_json::Value {
599    fn to_typed<T: DeserializeOwned>(self) -> Result<T> {
600        Ok(serde_json::from_value(self)?)
601    }
602}
603
604const GATEWAYD_FALLBACK: &str = "gatewayd";
605
606const FEDIMINTD_FALLBACK: &str = "fedimintd";
607
608const FEDIMINT_CLI_FALLBACK: &str = "fedimint-cli";
609
610pub fn get_fedimint_cli_path() -> Vec<String> {
611    get_command_str_for_alias(
612        &[FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV],
613        &[FEDIMINT_CLI_FALLBACK],
614    )
615}
616
617const GATEWAY_CLI_FALLBACK: &str = "gateway-cli";
618
619pub fn get_gateway_cli_path() -> Vec<String> {
620    get_command_str_for_alias(
621        &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
622        &[GATEWAY_CLI_FALLBACK],
623    )
624}
625
626const LOAD_TEST_TOOL_FALLBACK: &str = "fedimint-load-test-tool";
627
628const LNCLI_FALLBACK: &str = "lncli";
629
630pub fn get_lncli_path() -> Vec<String> {
631    get_command_str_for_alias(&[FM_LNCLI_BASE_EXECUTABLE_ENV], &[LNCLI_FALLBACK])
632}
633
634const BITCOIN_CLI_FALLBACK: &str = "bitcoin-cli";
635
636pub fn get_bitcoin_cli_path() -> Vec<String> {
637    get_command_str_for_alias(
638        &[FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV],
639        &[BITCOIN_CLI_FALLBACK],
640    )
641}
642
643const BITCOIND_FALLBACK: &str = "bitcoind";
644
645const LND_FALLBACK: &str = "lnd";
646
647const ESPLORA_FALLBACK: &str = "esplora";
648
649const RECOVERYTOOL_FALLBACK: &str = "fedimint-recoverytool";
650
651const DEVIMINT_FAUCET_FALLBACK: &str = "devimint";
652
653const FEDIMINT_DBTOOL_FALLBACK: &str = "fedimint-dbtool";
654
655pub fn get_fedimint_dbtool_cli_path() -> Vec<String> {
656    get_command_str_for_alias(
657        &[FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV],
658        &[FEDIMINT_DBTOOL_FALLBACK],
659    )
660}
661
662/// Maps a version hash to a release version
663fn version_hash_to_version(version_hash: &str) -> Result<Version> {
664    match version_hash {
665        "a8422b84102ab5fc768307215d5b20d807143f27" => Ok(Version::new(0, 2, 1)),
666        "a849377f6466b26bf9b2747242ff01fd4d4a031b" => Ok(Version::new(0, 2, 2)),
667        _ => Err(anyhow!("no version known for version hash: {version_hash}")),
668    }
669}
670
671pub struct FedimintdCmd;
672impl FedimintdCmd {
673    pub fn cmd(self) -> Command {
674        to_command(get_command_str_for_alias(
675            &[FM_FEDIMINTD_BASE_EXECUTABLE_ENV],
676            &[FEDIMINTD_FALLBACK],
677        ))
678    }
679
680    /// Returns the fedimintd version from clap or default min version
681    pub async fn version_or_default() -> Version {
682        match cmd!(FedimintdCmd, "--version").out_string().await {
683            Ok(version) => parse_clap_version(&version),
684            Err(_) => cmd!(FedimintdCmd, "version-hash")
685                .out_string()
686                .await
687                .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
688                .unwrap_or(DEFAULT_VERSION),
689        }
690    }
691}
692
693pub struct Gatewayd;
694impl Gatewayd {
695    pub fn cmd(self) -> Command {
696        to_command(get_command_str_for_alias(
697            &[FM_GATEWAYD_BASE_EXECUTABLE_ENV],
698            &[GATEWAYD_FALLBACK],
699        ))
700    }
701
702    /// Returns the gatewayd version from clap or default min version
703    pub async fn version_or_default() -> Version {
704        match cmd!(Gatewayd, "--version").out_string().await {
705            Ok(version) => parse_clap_version(&version),
706            Err(_) => cmd!(Gatewayd, "version-hash")
707                .out_string()
708                .await
709                .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
710                .unwrap_or(DEFAULT_VERSION),
711        }
712    }
713}
714
715pub struct FedimintCli;
716impl FedimintCli {
717    pub fn cmd(self) -> Command {
718        to_command(get_command_str_for_alias(
719            &[FM_MINT_CLIENT_ENV],
720            &get_fedimint_cli_path()
721                .iter()
722                .map(String::as_str)
723                .collect::<Vec<_>>(),
724        ))
725    }
726
727    /// Returns the fedimint-cli version from clap or default min version
728    pub async fn version_or_default() -> Version {
729        match cmd!(FedimintCli, "--version").out_string().await {
730            Ok(version) => parse_clap_version(&version),
731            Err(_) => DEFAULT_VERSION,
732        }
733    }
734
735    pub async fn set_password(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
736        cmd!(
737            self,
738            "--password",
739            &auth.0,
740            "admin",
741            "dkg",
742            "--ws",
743            endpoint,
744            "set-password",
745        )
746        .run()
747        .await
748    }
749
750    pub async fn set_local_params_leader(
751        self,
752        peer: &PeerId,
753        auth: &ApiAuth,
754        endpoint: &str,
755    ) -> Result<String> {
756        let json = cmd!(
757            self,
758            "--password",
759            &auth.0,
760            "admin",
761            "setup",
762            endpoint,
763            "set-local-params",
764            format!("Devimint Guardian {peer}"),
765            "--federation-name",
766            "Devimint Federation"
767        )
768        .out_json()
769        .await?;
770
771        Ok(serde_json::from_value(json)?)
772    }
773
774    pub async fn set_local_params_follower(
775        self,
776        peer: &PeerId,
777        auth: &ApiAuth,
778        endpoint: &str,
779    ) -> Result<String> {
780        let json = cmd!(
781            self,
782            "--password",
783            &auth.0,
784            "admin",
785            "setup",
786            endpoint,
787            "set-local-params",
788            format!("Devimint Guardian {peer}")
789        )
790        .out_json()
791        .await?;
792
793        Ok(serde_json::from_value(json)?)
794    }
795
796    pub async fn add_peer(self, params: &str, auth: &ApiAuth, endpoint: &str) -> Result<()> {
797        cmd!(
798            self,
799            "--password",
800            &auth.0,
801            "admin",
802            "setup",
803            endpoint,
804            "add-peer",
805            params
806        )
807        .run()
808        .await
809    }
810
811    pub async fn setup_status(self, auth: &ApiAuth, endpoint: &str) -> Result<SetupStatus> {
812        let json = cmd!(
813            self,
814            "--password",
815            &auth.0,
816            "admin",
817            "setup",
818            endpoint,
819            "status",
820        )
821        .out_json()
822        .await?;
823
824        Ok(serde_json::from_value(json)?)
825    }
826
827    pub async fn start_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
828        cmd!(
829            self,
830            "--password",
831            &auth.0,
832            "admin",
833            "setup",
834            endpoint,
835            "start-dkg"
836        )
837        .run()
838        .await
839    }
840
841    pub async fn shutdown(self, auth: &ApiAuth, our_id: u64, session_count: u64) -> Result<()> {
842        cmd!(
843            self,
844            "--password",
845            &auth.0,
846            "--our-id",
847            our_id,
848            "admin",
849            "shutdown",
850            session_count,
851        )
852        .run()
853        .await
854    }
855
856    pub async fn status(self, auth: &ApiAuth, our_id: u64) -> Result<()> {
857        cmd!(
858            self,
859            "--password",
860            &auth.0,
861            "--our-id",
862            our_id,
863            "admin",
864            "status",
865        )
866        .run()
867        .await
868    }
869}
870
871pub struct LoadTestTool;
872impl LoadTestTool {
873    pub fn cmd(self) -> Command {
874        to_command(get_command_str_for_alias(
875            &[FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV],
876            &[LOAD_TEST_TOOL_FALLBACK],
877        ))
878    }
879}
880
881pub struct GatewayCli;
882impl GatewayCli {
883    pub fn cmd(self) -> Command {
884        to_command(get_command_str_for_alias(
885            &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
886            &get_gateway_cli_path()
887                .iter()
888                .map(String::as_str)
889                .collect::<Vec<_>>(),
890        ))
891    }
892
893    /// Returns the gateway-cli version from clap or default min version
894    pub async fn version_or_default() -> Version {
895        match cmd!(GatewayCli, "--version").out_string().await {
896            Ok(version) => parse_clap_version(&version),
897            Err(_) => DEFAULT_VERSION,
898        }
899    }
900}
901
902pub struct GatewayLndCli;
903impl GatewayLndCli {
904    pub fn cmd(self) -> Command {
905        to_command(get_command_str_for_alias(
906            &[FM_GWCLI_LND_ENV],
907            &["gateway-lnd"],
908        ))
909    }
910}
911
912pub struct GatewayLdkCli;
913impl GatewayLdkCli {
914    pub fn cmd(self) -> Command {
915        to_command(get_command_str_for_alias(
916            &[FM_GWCLI_LDK_ENV],
917            &["gateway-ldk"],
918        ))
919    }
920}
921
922pub struct LnCli;
923impl LnCli {
924    pub fn cmd(self) -> Command {
925        to_command(get_command_str_for_alias(
926            &[FM_LNCLI_ENV],
927            &get_lncli_path()
928                .iter()
929                .map(String::as_str)
930                .collect::<Vec<_>>(),
931        ))
932    }
933}
934
935pub struct BitcoinCli;
936impl BitcoinCli {
937    pub fn cmd(self) -> Command {
938        to_command(get_command_str_for_alias(
939            &[FM_BTC_CLIENT_ENV],
940            &get_bitcoin_cli_path()
941                .iter()
942                .map(String::as_str)
943                .collect::<Vec<_>>(),
944        ))
945    }
946}
947
948pub struct Bitcoind;
949impl Bitcoind {
950    pub fn cmd(self) -> Command {
951        to_command(get_command_str_for_alias(
952            &[FM_BITCOIND_BASE_EXECUTABLE_ENV],
953            &[BITCOIND_FALLBACK],
954        ))
955    }
956}
957
958pub struct Lnd;
959impl Lnd {
960    pub fn cmd(self) -> Command {
961        to_command(get_command_str_for_alias(
962            &[FM_LND_BASE_EXECUTABLE_ENV],
963            &[LND_FALLBACK],
964        ))
965    }
966}
967
968pub struct Esplora;
969impl Esplora {
970    pub fn cmd(self) -> Command {
971        to_command(get_command_str_for_alias(
972            &[FM_ESPLORA_BASE_EXECUTABLE_ENV],
973            &[ESPLORA_FALLBACK],
974        ))
975    }
976}
977
978pub struct Recoverytool;
979impl Recoverytool {
980    pub fn cmd(self) -> Command {
981        to_command(get_command_str_for_alias(
982            &[FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV],
983            &[RECOVERYTOOL_FALLBACK],
984        ))
985    }
986}
987
988pub struct DevimintFaucet;
989impl DevimintFaucet {
990    pub fn cmd(self) -> Command {
991        to_command(get_command_str_for_alias(
992            &[FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV],
993            &[DEVIMINT_FAUCET_FALLBACK],
994        ))
995    }
996}
997
998pub struct Recurringd;
999impl Recurringd {
1000    pub fn cmd(self) -> Command {
1001        to_command(get_command_str_for_alias(
1002            &[FM_RECURRINGD_BASE_EXECUTABLE_ENV],
1003            &["fedimint-recurringd"],
1004        ))
1005    }
1006}
1007
1008fn get_command_str_for_alias(aliases: &[&str], default: &[&str]) -> Vec<String> {
1009    // try to use one of the aliases if set
1010    for alias in aliases {
1011        if let Ok(cmd) = std::env::var(alias) {
1012            return cmd.split_whitespace().map(ToOwned::to_owned).collect();
1013        }
1014    }
1015    // otherwise return the default value
1016    default.iter().map(ToString::to_string).collect()
1017}
1018
1019fn to_command(cli: Vec<String>) -> Command {
1020    let mut cmd = tokio::process::Command::new(&cli[0]);
1021    cmd.args(&cli[1..]);
1022    Command {
1023        cmd,
1024        args_debug: cli,
1025    }
1026}
1027
1028pub fn supports_lnv1() -> bool {
1029    std::env::var_os(FM_ENABLE_MODULE_LNV1_ENV).is_none()
1030        || is_env_var_set(FM_ENABLE_MODULE_LNV1_ENV)
1031}
1032
1033pub fn supports_lnv2() -> bool {
1034    std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
1035        || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV)
1036}
1037
1038/// Returns true if running backwards-compatibility tests
1039pub fn is_backwards_compatibility_test() -> bool {
1040    is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1041}
1042
1043/// Sets the fedimint-cli binary to match the fedimintd's version, which is
1044/// needed for running DKG. Returns the original fedimint-cli path and mint
1045/// client alias so the caller can reset the fedimint-cli version after DKG
1046pub async fn use_matching_fedimint_cli_for_dkg() -> Result<(String, String)> {
1047    let pkg_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
1048    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1049    let original_fedimint_cli_path = crate::util::get_fedimint_cli_path().join(" ");
1050
1051    if pkg_version == fedimintd_version {
1052        // we're on the current version if the fedimintd version is the same as the
1053        // package version. to use the current version of `fedimint-cli` built by cargo,
1054        // we need to unset FM_FEDIMINT_CLI_BASE_EXECUTABLE
1055        // TODO: Audit that the environment access only happens in single-threaded code.
1056        unsafe { std::env::remove_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV) };
1057    } else {
1058        let parsed_fedimintd_version = fedimintd_version.to_string().replace(['-', '.'], "_");
1059
1060        // matches format defined by nix_binary_version_var_name in scripts/_common.sh
1061        let fedimint_cli_path_var = format!("fm_bin_fedimint_cli_v{parsed_fedimintd_version}");
1062        let fedimint_cli_path = std::env::var(fedimint_cli_path_var)?;
1063        // TODO: Audit that the environment access only happens in single-threaded code.
1064        unsafe { std::env::set_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, fedimint_cli_path) };
1065    }
1066
1067    let original_fm_mint_client = std::env::var(FM_MINT_CLIENT_ENV)?;
1068    let fm_client_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
1069    let fm_client_dir_path_buf: PathBuf = PathBuf::from(fm_client_dir);
1070
1071    let fm_mint_client: String = format!(
1072        "{fedimint_cli} --data-dir {datadir}",
1073        fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
1074        datadir = crate::vars::utf8(&fm_client_dir_path_buf)
1075    );
1076    // TODO: Audit that the environment access only happens in single-threaded code.
1077    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, fm_mint_client) };
1078
1079    Ok((original_fedimint_cli_path, original_fm_mint_client))
1080}
1081
1082/// Sets the fedimint-cli and mint client alias
1083pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1084    // TODO: Audit that the environment access only happens in single-threaded code.
1085    unsafe {
1086        std::env::set_var(
1087            FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1088            original_fedimint_cli_path,
1089        );
1090    };
1091
1092    // TODO: Audit that the environment access only happens in single-threaded code.
1093    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1094}
1095
1096/// Parses a version string returned from clap
1097/// ex: fedimintd 0.3.0-alpha -> 0.3.0-alpha
1098fn parse_clap_version(res: &str) -> Version {
1099    match res.split(' ').collect::<Vec<&str>>().as_slice() {
1100        [_binary, version] => Version::parse(version).unwrap_or(DEFAULT_VERSION),
1101        _ => DEFAULT_VERSION,
1102    }
1103}
1104
1105#[test]
1106fn test_parse_clap_version() -> Result<()> {
1107    let version_str = "fedimintd 0.3.0-alpha";
1108    let expected_version = Version::parse("0.3.0-alpha")?;
1109    assert_eq!(expected_version, parse_clap_version(version_str));
1110
1111    let version_str = "fedimintd 0.3.12";
1112    let expected_version = Version::parse("0.3.12")?;
1113    assert_eq!(expected_version, parse_clap_version(version_str));
1114
1115    let version_str = "fedimint-cli 2.12.2-rc22";
1116    let expected_version = Version::parse("2.12.2-rc22")?;
1117    assert_eq!(expected_version, parse_clap_version(version_str));
1118
1119    let version_str = "bad version";
1120    let expected_version = DEFAULT_VERSION;
1121    assert_eq!(expected_version, parse_clap_version(version_str));
1122
1123    Ok(())
1124}