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