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
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 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 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); 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 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 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 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 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 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 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 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#[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 $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
475pub(crate) use cmd;
477
478pub 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
530pub 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
559pub trait ToCmdExt {
561 fn cmd(self) -> Command;
562}
563
564impl 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
648fn 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 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 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 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 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 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 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
1019pub fn is_backwards_compatibility_test() -> bool {
1021 is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1022}
1023
1024pub 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 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 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 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 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
1063pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1065 unsafe {
1067 std::env::set_var(
1068 FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1069 original_fedimint_cli_path,
1070 );
1071 };
1072
1073 unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1075}
1076
1077fn 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}