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