1use std::fmt::Write;
2use std::ops::ControlFlow;
3use std::path::{Path, PathBuf};
4use std::time::Duration;
5use std::{env, ffi};
6
7use anyhow::{Context, Result, anyhow, ensure};
8use clap::builder::BoolishValueParser;
9use clap::{Parser, Subcommand};
10use fedimint_core::task::TaskGroup;
11use fedimint_core::util::{FmtCompact as _, FmtCompactAnyhow as _, write_overwrite_async};
12use fedimint_logging::LOG_DEVIMINT;
13use rand::Rng as _;
14use rand::distributions::Alphanumeric;
15use tokio::fs;
16use tokio::time::Instant;
17use tracing::{debug, error, info, trace, warn};
18
19use crate::devfed::DevJitFed;
20use crate::envs::{
21 FM_DEVIMINT_STATIC_DATA_DIR_ENV, FM_FED_SIZE_ENV, FM_FEDERATIONS_BASE_PORT_ENV,
22 FM_GATEWAY_BASE_PORT_ENV, FM_INVITE_CODE_ENV, FM_LINK_TEST_DIR_ENV, FM_NUM_FEDS_ENV,
23 FM_OFFLINE_NODES_ENV, FM_PRE_DKG_ENV, FM_TEST_DIR_ENV,
24};
25use crate::util::{ProcessManager, poll};
26use crate::vars::mkdir;
27use crate::{external_daemons, vars};
28
29fn random_test_dir_suffix() -> String {
30 rand::thread_rng()
31 .sample_iter(&Alphanumeric)
32 .filter(u8::is_ascii_digit)
33 .take(3)
34 .map(char::from)
35 .collect::<String>()
36}
37
38#[derive(Parser, Clone, Default)]
39pub struct CommonArgs {
40 #[clap(short = 'd', long, env = FM_TEST_DIR_ENV)]
41 pub test_dir: Option<PathBuf>,
42
43 #[arg(long, env = "FM_SKIP_SETUP", value_parser = BoolishValueParser::new())]
46 skip_setup: bool,
47
48 #[arg(long, env = FM_PRE_DKG_ENV, value_parser = BoolishValueParser::new())]
50 pre_dkg: bool,
51
52 #[clap(short = 'n', long, env = FM_FED_SIZE_ENV, default_value = "4")]
54 pub fed_size: usize,
55
56 #[clap(long, env = FM_NUM_FEDS_ENV, default_value = "1")]
58 pub num_feds: usize,
59
60 #[clap(long, env = FM_LINK_TEST_DIR_ENV)]
61 pub link_test_dir: Option<PathBuf>,
63
64 #[clap(long, default_value_t = random_test_dir_suffix())]
65 pub link_test_dir_suffix: String,
66
67 #[clap(long, env = FM_OFFLINE_NODES_ENV, default_value = "0")]
69 pub offline_nodes: usize,
70
71 #[clap(long, env = FM_FEDERATIONS_BASE_PORT_ENV)]
73 pub federations_base_port: Option<u16>,
74
75 #[clap(long, env = FM_GATEWAY_BASE_PORT_ENV)]
77 pub gateway_base_port: Option<u16>,
78}
79
80impl CommonArgs {
81 pub fn mk_test_dir(&self) -> Result<PathBuf> {
82 if self.skip_setup {
83 ensure!(
84 self.test_dir.is_some(),
85 "When using `--skip-setup`, `--test-dir` must be set"
86 );
87 }
88 let path = self.test_dir();
89
90 std::fs::create_dir_all(&path)
91 .with_context(|| format!("Creating tmp directory {}", path.display()))?;
92
93 Ok(path)
94 }
95
96 pub fn test_dir(&self) -> PathBuf {
97 self.test_dir.clone().unwrap_or_else(|| {
98 std::env::temp_dir().join(format!(
99 "devimint-{}-{}",
100 std::process::id(),
101 self.link_test_dir_suffix
102 ))
103 })
104 }
105}
106
107#[derive(Subcommand)]
108pub enum Cmd {
109 ExternalDaemons {
111 #[arg(long, trailing_var_arg = true, allow_hyphen_values = true, num_args=1..)]
112 exec: Option<Vec<ffi::OsString>>,
113 },
114 DevFed {
119 #[arg(long, trailing_var_arg = true, allow_hyphen_values = true, num_args=1..)]
120 exec: Option<Vec<ffi::OsString>>,
121 },
122 DevFedPreRestore {
125 #[arg(long, trailing_var_arg = true, allow_hyphen_values = true, num_args=1..)]
126 exec: Option<Vec<ffi::OsString>>,
127 },
128 #[clap(flatten)]
131 Rpc(RpcCmd),
132}
133
134#[derive(Subcommand)]
135pub enum RpcCmd {
136 Wait,
137 Env,
138}
139
140pub async fn setup(arg: CommonArgs) -> Result<(ProcessManager, TaskGroup)> {
141 let test_dir = &arg.mk_test_dir()?;
142 mkdir(test_dir.clone()).await?;
143 let logs_dir: PathBuf = test_dir.join("logs");
144 mkdir(logs_dir.clone()).await?;
145
146 let log_file = fs::OpenOptions::new()
147 .write(true)
148 .create(true)
149 .append(true)
150 .open(logs_dir.join("devimint.log"))
151 .await?
152 .into_std()
153 .await;
154
155 fedimint_logging::TracingSetup::default()
156 .with_file(Some(log_file))
157 .with_directive("jsonrpsee-client=off")
159 .init()?;
160
161 let globals = vars::Global::new(
162 test_dir,
163 arg.num_feds,
164 arg.fed_size,
165 arg.offline_nodes,
166 arg.federations_base_port,
167 arg.gateway_base_port,
168 )
169 .await?;
170
171 if let Some(link_test_dir) = arg.link_test_dir.as_ref() {
172 update_test_dir_link(link_test_dir, &arg.test_dir()).await?;
173 }
174 info!(target: LOG_DEVIMINT, path = %globals.FM_DATA_DIR.display(), "Devimint data dir");
175
176 let mut env_string = String::new();
177 for (var, value) in globals.vars() {
178 debug!(var, value, "Env variable set");
179 writeln!(env_string, r#"export {var}="{value}""#)?; unsafe { std::env::set_var(var, value) };
182 }
183 write_overwrite_async(globals.FM_TEST_DIR.join("env"), env_string).await?;
184 let process_mgr = ProcessManager::new(globals);
185 let task_group = TaskGroup::new();
186 task_group.install_kill_handler();
187 Ok((process_mgr, task_group))
188}
189
190pub async fn update_test_dir_link(
191 link_test_dir: &Path,
192 test_dir: &Path,
193) -> Result<(), anyhow::Error> {
194 let make_link = match fs::read_link(link_test_dir).await {
195 Ok(existing) => {
196 if existing == test_dir {
197 false
198 } else {
199 debug!(
200 old = %existing.display(),
201 new = %test_dir.display(),
202 link = %link_test_dir.display(),
203 "Updating exinst test dir link"
204 );
205
206 fs::remove_file(link_test_dir).await?;
207 true
208 }
209 }
210 _ => true,
211 };
212 if make_link {
213 debug!(src = %test_dir.display(), dst = %link_test_dir.display(), "Linking test dir");
214 fs::symlink(&test_dir, link_test_dir).await?;
215 }
216 Ok(())
217}
218
219pub async fn cleanup_on_exit(
225 main_process: impl futures::Future<Output = Result<()>>,
226 task_group: TaskGroup,
227) -> Result<()> {
228 let result = match task_group
229 .make_handle()
230 .cancel_on_shutdown(main_process)
231 .await
232 {
233 Err(_) => {
234 info!(
235 target: LOG_DEVIMINT,
236 "Received shutdown signal before finishing main process, exiting early"
237 );
238 Ok(())
239 }
240 Ok(Ok(())) => {
241 debug!(
242 target: LOG_DEVIMINT,
243 "Main process finished successfully, shutting down task group"
244 );
245 Ok(())
246 }
247 Ok(Err(err)) => {
248 warn!(
249 target: LOG_DEVIMINT,
250 err = %err.fmt_compact_anyhow(),
251 "Main process failed, will shutdown"
252 );
253 Err(err)
254 }
255 };
256
257 let joined = task_group.shutdown_join_all(Duration::from_secs(30)).await;
258
259 match (result, joined) {
260 (Ok(()), joined) => joined.map_err(anyhow::Error::from),
261 (Err(err), Ok(())) => Err(err),
262 (Err(err), Err(join_err)) => {
263 warn!(
265 target: LOG_DEVIMINT,
266 err = %join_err.fmt_compact(),
267 "Task group did not shut down cleanly"
268 );
269 Err(err)
270 }
271 }
272}
273
274pub async fn exec_or_wait_for_shutdown(
277 exec: Option<Vec<ffi::OsString>>,
278 task_group: &TaskGroup,
279) -> Result<()> {
280 if let Some(exec) = exec {
281 debug!(target: LOG_DEVIMINT, "Starting exec command");
282 exec_user_command(exec).await
283 } else {
284 debug!(target: LOG_DEVIMINT, "Waiting for group task shutdown");
285 task_group.make_handle().make_shutdown_rx().await;
286 Ok(())
287 }
288}
289
290pub async fn write_ready_file<T>(global: &vars::Global, result: Result<T>) -> Result<T> {
291 let ready_file = &global.FM_READY_FILE;
292 match result {
293 Ok(_) => write_overwrite_async(ready_file, "READY").await?,
294 Err(_) => write_overwrite_async(ready_file, "ERROR").await?,
295 }
296 result
297}
298
299pub async fn handle_command(cmd: Cmd, common_args: CommonArgs) -> Result<()> {
300 match cmd {
301 Cmd::ExternalDaemons { exec } => {
302 let (process_mgr, task_group) = setup(common_args).await?;
303 let _daemons =
304 write_ready_file(&process_mgr.globals, external_daemons(&process_mgr).await)
305 .await?;
306 if let Some(exec) = exec {
307 exec_user_command(exec).await?;
308 task_group.shutdown();
309 }
310 task_group.make_handle().make_shutdown_rx().await;
311 }
312 Cmd::DevFed { exec } => handle_dev_fed_command(common_args, exec, false).await?,
313 Cmd::DevFedPreRestore { exec } => handle_dev_fed_command(common_args, exec, true).await?,
314 Cmd::Rpc(rpc_cmd) => rpc_command(rpc_cmd, common_args).await?,
315 }
316 Ok(())
317}
318
319async fn handle_dev_fed_command(
320 common_args: CommonArgs,
321 exec: Option<Vec<ffi::OsString>>,
322 pre_restore: bool,
323) -> Result<()> {
324 trace!(target: LOG_DEVIMINT, "Starting dev fed");
325 let start_time = Instant::now();
326 let skip_setup = common_args.skip_setup;
327 let pre_dkg = common_args.pre_dkg;
328 ensure!(
329 !pre_restore || (!skip_setup && !pre_dkg),
330 "dev-fed-pre-restore cannot be combined with skip-setup or pre-dkg"
331 );
332 let (process_mgr, task_group) = setup(common_args).await?;
333 let dev_fed = DevJitFed::new_with_pre_restore(&process_mgr, skip_setup, pre_dkg, pre_restore)?;
334 let main = {
335 let task_group = task_group.clone();
336 let dev_fed = dev_fed.clone();
337 async move {
338 let pegin_start_time = Instant::now();
339 debug!(target: LOG_DEVIMINT, "Peging in client and gateways");
340
341 if !skip_setup && !pre_dkg && !pre_restore {
342 const GW_PEGIN_AMOUNT: u64 = 1_000_000;
343 const CLIENT_PEGIN_AMOUNT: u64 = 1_000_000;
344
345 let (handle, (), ()) = tokio::try_join!(
346 async {
347 let (address, handle) =
348 dev_fed.internal_client().await?.get_deposit_addr().await?;
349 debug!(
350 target: LOG_DEVIMINT,
351 %address,
352 %handle,
353 "Sending funds to client deposit addr"
354 );
355 dev_fed
356 .bitcoind()
357 .await?
358 .send_to(address, CLIENT_PEGIN_AMOUNT)
359 .await?;
360 Ok(handle)
361 },
362 async {
363 let address = dev_fed
364 .gw_lnd_registered()
365 .await?
366 .client()
367 .get_pegin_addr(&dev_fed.fed().await?.calculate_federation_id())
368 .await?;
369 debug!(
370 target: LOG_DEVIMINT,
371 %address,
372 "Sending funds to LND deposit addr"
373 );
374 dev_fed
375 .bitcoind()
376 .await?
377 .send_to(address, GW_PEGIN_AMOUNT)
378 .await
379 .map(|_| ())
380 },
381 async {
382 if crate::util::supports_lnv2() {
383 let gw_ldk = dev_fed.gw_ldk_connected().await?;
384 let address = gw_ldk
385 .client()
386 .get_pegin_addr(&dev_fed.fed().await?.calculate_federation_id())
387 .await?;
388 debug!(
389 target: LOG_DEVIMINT,
390 %address,
391 "Sending funds to LDK deposit addr"
392 );
393 dev_fed
394 .bitcoind()
395 .await?
396 .send_to(address, GW_PEGIN_AMOUNT)
397 .await
398 .map(|_| ())
399 } else {
400 Ok(())
401 }
402 },
403 )?;
404
405 dev_fed.bitcoind().await?.mine_blocks_no_wait(11).await?;
406 if crate::util::supports_wallet_v2() {
407 if crate::util::FedimintCli::version_or_default().await
408 >= *crate::version_constants::VERSION_0_12_0_ALPHA
409 {
410 dev_fed
412 .internal_client()
413 .await?
414 .await_receive(&handle)
415 .await?;
416 } else {
417 dev_fed
420 .internal_client()
421 .await?
422 .await_balance(CLIENT_PEGIN_AMOUNT * 1000 * 9 / 10)
423 .await?;
424 }
425 } else {
426 dev_fed
427 .internal_client()
428 .await?
429 .await_deposit(&handle)
430 .await?;
431 }
432
433 info!(
434 target: LOG_DEVIMINT,
435 elapsed_ms = %pegin_start_time.elapsed().as_millis(),
436 "Pegins completed"
437 );
438 }
439
440 if !pre_dkg && !pre_restore {
441 unsafe {
444 std::env::set_var(FM_INVITE_CODE_ENV, dev_fed.fed().await?.invite_code()?);
445 };
446 }
447
448 if pre_restore {
449 let _ = dev_fed.fed().await?;
450 } else {
451 dev_fed.finalize(&process_mgr).await?;
452 }
453
454 write_ready_file(&process_mgr.globals, Ok(())).await?;
455
456 info!(
457 target: LOG_DEVIMINT,
458 elapsed_ms = %start_time.elapsed().as_millis(),
459 path = %process_mgr.globals.FM_DATA_DIR.display(),
460 "Devfed ready"
461 );
462 exec_or_wait_for_shutdown(exec, &task_group).await
463 }
464 };
465 let result = cleanup_on_exit(main, task_group).await;
466 dev_fed.fast_terminate().await;
469 result
470}
471
472pub async fn exec_user_command(path: Vec<ffi::OsString>) -> Result<(), anyhow::Error> {
473 let cmd_str = path
474 .join(ffi::OsStr::new(" "))
475 .to_string_lossy()
476 .to_string();
477
478 let path_with_aliases = if let Some(existing_path) = env::var_os("PATH") {
479 let mut path = devimint_static_data_dir();
480 path.push("/aliases:");
481 path.push(existing_path);
482 path
483 } else {
484 let mut path = devimint_static_data_dir();
485 path.push("/aliases");
486 path
487 };
488 debug!(target: LOG_DEVIMINT, cmd = %cmd_str, "Executing user command");
489 if !tokio::process::Command::new(&path[0])
490 .args(&path[1..])
491 .env("PATH", path_with_aliases)
492 .kill_on_drop(true)
493 .status()
494 .await
495 .with_context(|| format!("Executing user command failed: {cmd_str}"))?
496 .success()
497 {
498 error!(cmd = %cmd_str, "User command failed");
499 return Err(anyhow!("User command failed: {cmd_str}"));
500 }
501 Ok(())
502}
503
504fn devimint_static_data_dir() -> ffi::OsString {
505 env::var_os(FM_DEVIMINT_STATIC_DATA_DIR_ENV).unwrap_or(
507 env!(
508 "FM_DEVIMINT_STATIC_DATA_DIR"
510 )
511 .into(),
512 )
513}
514
515pub async fn rpc_command(rpc: RpcCmd, common: CommonArgs) -> Result<()> {
516 fedimint_logging::TracingSetup::default().init()?;
517 match rpc {
518 RpcCmd::Env => {
519 let env_file = common.test_dir().join("env");
520 poll("env file", || async {
521 if fs::try_exists(&env_file)
522 .await
523 .context("env file")
524 .map_err(ControlFlow::Continue)?
525 {
526 Ok(())
527 } else {
528 Err(ControlFlow::Continue(anyhow!("env file not found")))
529 }
530 })
531 .await?;
532 let env = fs::read_to_string(&env_file).await?;
533 print!("{env}");
534 Ok(())
535 }
536 RpcCmd::Wait => {
537 let ready_file = common.test_dir().join("ready");
538 poll("ready file", || async {
539 if fs::try_exists(&ready_file)
540 .await
541 .context("ready file")
542 .map_err(ControlFlow::Continue)?
543 {
544 Ok(())
545 } else {
546 Err(ControlFlow::Continue(anyhow!("ready file not found")))
547 }
548 })
549 .await?;
550 let env = fs::read_to_string(&ready_file).await?;
551 print!("{env}");
552
553 let test_dir = &common.test_dir();
555 let env_file = test_dir.join("env");
556 let invite_file = test_dir.join("cfg/invite-code");
557 if fs::try_exists(&env_file).await.ok().unwrap_or(false)
558 && fs::try_exists(&invite_file).await.ok().unwrap_or(false)
559 {
560 let invite = fs::read_to_string(&invite_file).await?;
561 let mut env_string = fs::read_to_string(&env_file).await?;
562 writeln!(env_string, r#"export FM_INVITE_CODE="{invite}""#)?;
563 unsafe { std::env::set_var(FM_INVITE_CODE_ENV, invite) };
565 write_overwrite_async(env_file, env_string).await?;
566 }
567
568 Ok(())
569 }
570 }
571}