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
40const 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#[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 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 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 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 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); 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 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 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 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 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 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 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 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#[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 $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
489pub(crate) use cmd;
491
492pub 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
544pub 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
573pub trait ToCmdExt {
575 fn cmd(self) -> Command;
576}
577
578impl 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
662fn 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 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 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 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 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 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 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
1038pub fn is_backwards_compatibility_test() -> bool {
1040 is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1041}
1042
1043pub 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 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 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 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 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
1082pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1084 unsafe {
1086 std::env::set_var(
1087 FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1088 original_fedimint_cli_path,
1089 );
1090 };
1091
1092 unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1094}
1095
1096fn 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}