solana-test-validator.rs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. use {
  2. agave_validator::{
  3. admin_rpc_service, cli, commands::FromClapArgMatches, dashboard::Dashboard,
  4. ledger_lockfile, lock_ledger, println_name_value,
  5. },
  6. clap::{crate_name, value_t, value_t_or_exit, values_t_or_exit},
  7. crossbeam_channel::unbounded,
  8. itertools::Itertools,
  9. log::*,
  10. solana_account::AccountSharedData,
  11. solana_accounts_db::accounts_index::{AccountIndex, AccountSecondaryIndexes},
  12. solana_clap_utils::{
  13. input_parsers::{pubkey_of, pubkeys_of, value_of},
  14. input_validators::normalize_to_url_if_moniker,
  15. },
  16. solana_clock::Slot,
  17. solana_core::consensus::tower_storage::FileTowerStorage,
  18. solana_epoch_schedule::EpochSchedule,
  19. solana_faucet::faucet::{run_faucet, Faucet},
  20. solana_inflation::Inflation,
  21. solana_keypair::{read_keypair_file, write_keypair_file, Keypair},
  22. solana_native_token::sol_str_to_lamports,
  23. solana_net_utils::SocketAddrSpace,
  24. solana_pubkey::Pubkey,
  25. solana_rent::Rent,
  26. solana_rpc::{
  27. rpc::{JsonRpcConfig, RpcBigtableConfig},
  28. rpc_pubsub_service::PubSubConfig,
  29. },
  30. solana_rpc_client::rpc_client::RpcClient,
  31. solana_signer::Signer,
  32. solana_system_interface::program as system_program,
  33. solana_test_validator::*,
  34. std::{
  35. collections::{HashMap, HashSet},
  36. fs, io,
  37. net::{IpAddr, Ipv4Addr, SocketAddr},
  38. path::{Path, PathBuf},
  39. process::exit,
  40. sync::{Arc, Mutex, RwLock},
  41. thread,
  42. time::{Duration, SystemTime, UNIX_EPOCH},
  43. },
  44. };
  45. #[derive(PartialEq, Eq)]
  46. enum Output {
  47. None,
  48. Log,
  49. Dashboard,
  50. }
  51. fn main() {
  52. let default_args = cli::DefaultTestArgs::new();
  53. let version = solana_version::version!();
  54. let matches = cli::test_app(version, &default_args).get_matches();
  55. let output = if matches.is_present("quiet") {
  56. Output::None
  57. } else if matches.is_present("log") {
  58. Output::Log
  59. } else {
  60. Output::Dashboard
  61. };
  62. let ledger_path = value_t_or_exit!(matches, "ledger_path", PathBuf);
  63. let reset_ledger = matches.is_present("reset");
  64. let indexes: HashSet<AccountIndex> = matches
  65. .values_of("account_indexes")
  66. .unwrap_or_default()
  67. .map(|value| match value {
  68. "program-id" => AccountIndex::ProgramId,
  69. "spl-token-mint" => AccountIndex::SplTokenMint,
  70. "spl-token-owner" => AccountIndex::SplTokenOwner,
  71. _ => unreachable!(),
  72. })
  73. .collect();
  74. let account_indexes = AccountSecondaryIndexes {
  75. keys: None,
  76. indexes,
  77. };
  78. if !ledger_path.exists() {
  79. fs::create_dir(&ledger_path).unwrap_or_else(|err| {
  80. println!(
  81. "Error: Unable to create directory {}: {}",
  82. ledger_path.display(),
  83. err
  84. );
  85. exit(1);
  86. });
  87. }
  88. let mut ledger_lock = ledger_lockfile(&ledger_path);
  89. let _ledger_write_guard = lock_ledger(&ledger_path, &mut ledger_lock);
  90. if reset_ledger {
  91. remove_directory_contents(&ledger_path).unwrap_or_else(|err| {
  92. println!("Error: Unable to remove {}: {}", ledger_path.display(), err);
  93. exit(1);
  94. })
  95. }
  96. solana_runtime::snapshot_utils::remove_tmp_snapshot_archives(&ledger_path);
  97. let validator_log_symlink = ledger_path.join("validator.log");
  98. let logfile = if output != Output::Log {
  99. let validator_log_with_timestamp = format!(
  100. "validator-{}.log",
  101. SystemTime::now()
  102. .duration_since(UNIX_EPOCH)
  103. .unwrap()
  104. .as_millis()
  105. );
  106. let _ = fs::remove_file(&validator_log_symlink);
  107. symlink::symlink_file(&validator_log_with_timestamp, &validator_log_symlink).unwrap();
  108. Some(ledger_path.join(validator_log_with_timestamp))
  109. } else {
  110. None
  111. };
  112. agave_logger::initialize_logging(logfile);
  113. info!("{} {}", crate_name!(), solana_version::version!());
  114. info!("Starting validator with: {:#?}", std::env::args_os());
  115. solana_core::validator::report_target_features();
  116. // TODO: Ideally test-validator should *only* allow private addresses.
  117. let socket_addr_space = SocketAddrSpace::new(/*allow_private_addr=*/ true);
  118. let cli_config = if let Some(config_file) = matches.value_of("config_file") {
  119. solana_cli_config::Config::load(config_file).unwrap_or_default()
  120. } else {
  121. solana_cli_config::Config::default()
  122. };
  123. let cluster_rpc_client = value_t!(matches, "json_rpc_url", String)
  124. .map(normalize_to_url_if_moniker)
  125. .map(RpcClient::new);
  126. let (mint_address, random_mint) = pubkey_of(&matches, "mint_address")
  127. .map(|pk| (pk, false))
  128. .unwrap_or_else(|| {
  129. read_keypair_file(&cli_config.keypair_path)
  130. .map(|kp| (kp.pubkey(), false))
  131. .unwrap_or_else(|_| (Keypair::new().pubkey(), true))
  132. });
  133. let rpc_port = value_t_or_exit!(matches, "rpc_port", u16);
  134. let pub_sub_config =
  135. PubSubConfig::from_clap_arg_match(&matches).unwrap_or(PubSubConfig::default_for_tests());
  136. let faucet_port = value_t_or_exit!(matches, "faucet_port", u16);
  137. let ticks_per_slot = value_t!(matches, "ticks_per_slot", u64).ok();
  138. let slots_per_epoch = value_t!(matches, "slots_per_epoch", Slot).ok();
  139. let inflation_fixed = value_t!(matches, "inflation_fixed", f64).ok();
  140. let gossip_host = matches.value_of("gossip_host").map(|gossip_host| {
  141. warn!("--gossip-host is deprecated. Use --bind-address instead.");
  142. solana_net_utils::parse_host(gossip_host).unwrap_or_else(|err| {
  143. eprintln!("Failed to parse --gossip-host: {err}");
  144. exit(1);
  145. })
  146. });
  147. let gossip_port = value_t!(matches, "gossip_port", u16).ok();
  148. let dynamic_port_range = matches.value_of("dynamic_port_range").map(|port_range| {
  149. solana_net_utils::parse_port_range(port_range).unwrap_or_else(|| {
  150. eprintln!("Failed to parse --dynamic-port-range");
  151. exit(1);
  152. })
  153. });
  154. let bind_address = solana_net_utils::parse_host(
  155. matches
  156. .value_of("bind_address")
  157. .expect("Bind address has default value"),
  158. )
  159. .unwrap_or_else(|err| {
  160. eprintln!("Failed to parse --bind-address: {err}");
  161. exit(1);
  162. });
  163. let advertised_ip = if let Some(ip) = gossip_host {
  164. ip
  165. } else if !bind_address.is_unspecified() && !bind_address.is_loopback() {
  166. bind_address
  167. } else {
  168. IpAddr::V4(Ipv4Addr::LOCALHOST)
  169. };
  170. let compute_unit_limit = value_t!(matches, "compute_unit_limit", u64).ok();
  171. let faucet_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), faucet_port);
  172. let parse_address = |address: &str, input_type: &str| {
  173. address
  174. .parse::<Pubkey>()
  175. .or_else(|_| read_keypair_file(address).map(|keypair| keypair.pubkey()))
  176. .unwrap_or_else(|err| {
  177. println!("Error: invalid {input_type} {address}: {err}");
  178. exit(1);
  179. })
  180. };
  181. let parse_program_path = |program: &str| {
  182. let program_path = PathBuf::from(program);
  183. if !program_path.exists() {
  184. println!(
  185. "Error: program file does not exist: {}",
  186. program_path.display()
  187. );
  188. exit(1);
  189. }
  190. program_path
  191. };
  192. let mut upgradeable_programs_to_load = vec![];
  193. if let Some(values) = matches.values_of("bpf_program") {
  194. for (address, program) in values.into_iter().tuples() {
  195. let address = parse_address(address, "address");
  196. let program_path = parse_program_path(program);
  197. upgradeable_programs_to_load.push(UpgradeableProgramInfo {
  198. program_id: address,
  199. loader: solana_sdk_ids::bpf_loader_upgradeable::id(),
  200. upgrade_authority: Pubkey::default(),
  201. program_path,
  202. });
  203. }
  204. }
  205. if let Some(values) = matches.values_of("upgradeable_program") {
  206. for (address, program, upgrade_authority) in
  207. values.into_iter().tuples::<(&str, &str, &str)>()
  208. {
  209. let address = parse_address(address, "address");
  210. let program_path = parse_program_path(program);
  211. let upgrade_authority_address = if upgrade_authority == "none" {
  212. Pubkey::default()
  213. } else {
  214. upgrade_authority
  215. .parse::<Pubkey>()
  216. .or_else(|_| {
  217. read_keypair_file(upgrade_authority).map(|keypair| keypair.pubkey())
  218. })
  219. .unwrap_or_else(|err| {
  220. println!("Error: invalid upgrade_authority {upgrade_authority}: {err}");
  221. exit(1);
  222. })
  223. };
  224. upgradeable_programs_to_load.push(UpgradeableProgramInfo {
  225. program_id: address,
  226. loader: solana_sdk_ids::bpf_loader_upgradeable::id(),
  227. upgrade_authority: upgrade_authority_address,
  228. program_path,
  229. });
  230. }
  231. }
  232. let mut accounts_to_load = vec![];
  233. if let Some(values) = matches.values_of("account") {
  234. for (address, filename) in values.into_iter().tuples() {
  235. let address = if address == "-" {
  236. None
  237. } else {
  238. Some(address.parse::<Pubkey>().unwrap_or_else(|err| {
  239. println!("Error: invalid address {address}: {err}");
  240. exit(1);
  241. }))
  242. };
  243. accounts_to_load.push(AccountInfo { address, filename });
  244. }
  245. }
  246. let accounts_from_dirs: HashSet<_> = matches
  247. .values_of("account_dir")
  248. .unwrap_or_default()
  249. .collect();
  250. let accounts_to_clone: HashSet<_> = pubkeys_of(&matches, "clone_account")
  251. .map(|v| v.into_iter().collect())
  252. .unwrap_or_default();
  253. let accounts_to_maybe_clone: HashSet<_> = pubkeys_of(&matches, "maybe_clone_account")
  254. .map(|v| v.into_iter().collect())
  255. .unwrap_or_default();
  256. let upgradeable_programs_to_clone: HashSet<_> =
  257. pubkeys_of(&matches, "clone_upgradeable_program")
  258. .map(|v| v.into_iter().collect())
  259. .unwrap_or_default();
  260. let alt_accounts_to_clone: HashSet<_> = pubkeys_of(&matches, "deep_clone_address_lookup_table")
  261. .map(|v| v.into_iter().collect())
  262. .unwrap_or_default();
  263. let clone_feature_set = matches.is_present("clone_feature_set");
  264. let warp_slot = if matches.is_present("warp_slot") {
  265. Some(match matches.value_of("warp_slot") {
  266. Some(_) => value_t_or_exit!(matches, "warp_slot", Slot),
  267. None => cluster_rpc_client
  268. .as_ref()
  269. .unwrap_or_else(|_| {
  270. println!(
  271. "The --url argument must be provided if --warp-slot/-w is used without an \
  272. explicit slot"
  273. );
  274. exit(1);
  275. })
  276. .get_slot()
  277. .unwrap_or_else(|err| {
  278. println!("Unable to get current cluster slot: {err}");
  279. exit(1);
  280. }),
  281. })
  282. } else {
  283. None
  284. };
  285. let faucet_lamports = matches
  286. .value_of("faucet_sol")
  287. .and_then(sol_str_to_lamports)
  288. .unwrap();
  289. let faucet_keypair_file = ledger_path.join("faucet-keypair.json");
  290. if !faucet_keypair_file.exists() {
  291. write_keypair_file(&Keypair::new(), faucet_keypair_file.to_str().unwrap()).unwrap_or_else(
  292. |err| {
  293. println!(
  294. "Error: Failed to write {}: {}",
  295. faucet_keypair_file.display(),
  296. err
  297. );
  298. exit(1);
  299. },
  300. );
  301. }
  302. let faucet_keypair =
  303. read_keypair_file(faucet_keypair_file.to_str().unwrap()).unwrap_or_else(|err| {
  304. println!(
  305. "Error: Failed to read {}: {}",
  306. faucet_keypair_file.display(),
  307. err
  308. );
  309. exit(1);
  310. });
  311. let faucet_pubkey = faucet_keypair.pubkey();
  312. let faucet_time_slice_secs = value_t_or_exit!(matches, "faucet_time_slice_secs", u64);
  313. let faucet_per_time_cap = matches
  314. .value_of("faucet_per_time_sol_cap")
  315. .and_then(sol_str_to_lamports);
  316. let faucet_per_request_cap = matches
  317. .value_of("faucet_per_request_sol_cap")
  318. .and_then(sol_str_to_lamports);
  319. let (sender, receiver) = unbounded();
  320. thread::spawn(move || {
  321. let faucet = Arc::new(Mutex::new(Faucet::new(
  322. faucet_keypair,
  323. Some(faucet_time_slice_secs),
  324. faucet_per_time_cap,
  325. faucet_per_request_cap,
  326. )));
  327. let runtime = tokio::runtime::Builder::new_current_thread()
  328. .enable_all()
  329. .build()
  330. .unwrap();
  331. runtime.block_on(run_faucet(faucet, faucet_addr, Some(sender)));
  332. });
  333. let _ = receiver.recv().expect("run faucet").unwrap_or_else(|err| {
  334. println!("Error: failed to start faucet: {err}");
  335. exit(1);
  336. });
  337. let features_to_deactivate = pubkeys_of(&matches, "deactivate_feature").unwrap_or_default();
  338. if TestValidatorGenesis::ledger_exists(&ledger_path) {
  339. for (name, long) in &[
  340. ("bpf_program", "--bpf-program"),
  341. ("clone_account", "--clone"),
  342. ("account", "--account"),
  343. ("mint_address", "--mint"),
  344. ("ticks_per_slot", "--ticks-per-slot"),
  345. ("slots_per_epoch", "--slots-per-epoch"),
  346. ("inflation_fixed", "--inflation-fixed"),
  347. ("faucet_sol", "--faucet-sol"),
  348. ("deactivate_feature", "--deactivate-feature"),
  349. ] {
  350. if matches.is_present(name) {
  351. println!("{long} argument ignored, ledger already exists");
  352. }
  353. }
  354. } else if random_mint {
  355. println_name_value(
  356. "\nNotice!",
  357. "No wallet available. `solana airdrop` localnet SOL after creating one\n",
  358. );
  359. }
  360. let mut genesis = TestValidatorGenesis::default();
  361. genesis.max_ledger_shreds = value_of(&matches, "limit_ledger_size");
  362. genesis.max_genesis_archive_unpacked_size = Some(u64::MAX);
  363. genesis.log_messages_bytes_limit = value_t!(matches, "log_messages_bytes_limit", usize).ok();
  364. genesis.transaction_account_lock_limit =
  365. value_t!(matches, "transaction_account_lock_limit", usize).ok();
  366. genesis.enable_scheduler_bindings = matches.is_present("enable_scheduler_bindings");
  367. let tower_storage = Arc::new(FileTowerStorage::new(ledger_path.clone()));
  368. let admin_service_post_init = Arc::new(RwLock::new(None));
  369. // If geyser_plugin_config value is invalid, the validator will exit when the values are extracted below
  370. let (rpc_to_plugin_manager_sender, rpc_to_plugin_manager_receiver) =
  371. if matches.is_present("geyser_plugin_config") {
  372. let (sender, receiver) = unbounded();
  373. (Some(sender), Some(receiver))
  374. } else {
  375. (None, None)
  376. };
  377. admin_rpc_service::run(
  378. &ledger_path,
  379. admin_rpc_service::AdminRpcRequestMetadata {
  380. rpc_addr: Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), rpc_port)),
  381. start_progress: genesis.start_progress.clone(),
  382. start_time: std::time::SystemTime::now(),
  383. validator_exit: genesis.validator_exit.clone(),
  384. validator_exit_backpressure: HashMap::default(),
  385. authorized_voter_keypairs: genesis.authorized_voter_keypairs.clone(),
  386. staked_nodes_overrides: genesis.staked_nodes_overrides.clone(),
  387. post_init: admin_service_post_init,
  388. tower_storage: tower_storage.clone(),
  389. rpc_to_plugin_manager_sender,
  390. },
  391. );
  392. let dashboard = if output == Output::Dashboard {
  393. Some(Dashboard::new(
  394. &ledger_path,
  395. Some(&validator_log_symlink),
  396. Some(&mut genesis.validator_exit.write().unwrap()),
  397. ))
  398. } else {
  399. None
  400. };
  401. let rpc_bigtable_config = if matches.is_present("enable_rpc_bigtable_ledger_storage")
  402. || matches.is_present("enable_bigtable_ledger_upload")
  403. {
  404. Some(RpcBigtableConfig {
  405. enable_bigtable_ledger_upload: matches.is_present("enable_bigtable_ledger_upload"),
  406. bigtable_instance_name: value_t_or_exit!(matches, "rpc_bigtable_instance", String),
  407. bigtable_app_profile_id: value_t_or_exit!(
  408. matches,
  409. "rpc_bigtable_app_profile_id",
  410. String
  411. ),
  412. timeout: None,
  413. ..RpcBigtableConfig::default()
  414. })
  415. } else {
  416. None
  417. };
  418. genesis
  419. .ledger_path(&ledger_path)
  420. .tower_storage(tower_storage)
  421. .add_account(
  422. faucet_pubkey,
  423. AccountSharedData::new(faucet_lamports, 0, &system_program::id()),
  424. )
  425. .pubsub_config(pub_sub_config)
  426. .rpc_port(rpc_port)
  427. .add_upgradeable_programs_with_path(&upgradeable_programs_to_load)
  428. .add_accounts_from_json_files(&accounts_to_load)
  429. .unwrap_or_else(|e| {
  430. println!("Error: add_accounts_from_json_files failed: {e}");
  431. exit(1);
  432. })
  433. .add_accounts_from_directories(&accounts_from_dirs)
  434. .unwrap_or_else(|e| {
  435. println!("Error: add_accounts_from_directories failed: {e}");
  436. exit(1);
  437. })
  438. .deactivate_features(&features_to_deactivate);
  439. genesis.rpc_config(JsonRpcConfig {
  440. enable_rpc_transaction_history: true,
  441. enable_extended_tx_metadata_storage: true,
  442. rpc_bigtable_config,
  443. faucet_addr: Some(faucet_addr),
  444. account_indexes,
  445. ..JsonRpcConfig::default_for_test()
  446. });
  447. if !accounts_to_clone.is_empty() {
  448. if let Err(e) = genesis.clone_accounts(
  449. accounts_to_clone,
  450. cluster_rpc_client
  451. .as_ref()
  452. .expect("--clone-account requires --json-rpc-url argument"),
  453. false,
  454. ) {
  455. println!("Error: clone_accounts failed: {e}");
  456. exit(1);
  457. }
  458. }
  459. if !alt_accounts_to_clone.is_empty() {
  460. if let Err(e) = genesis.deep_clone_address_lookup_table_accounts(
  461. alt_accounts_to_clone,
  462. cluster_rpc_client
  463. .as_ref()
  464. .expect("--deep-clone-address-lookup-table requires --json-rpc-url argument"),
  465. ) {
  466. println!("Error: alt_accounts_to_clone failed: {e}");
  467. exit(1);
  468. }
  469. }
  470. if !accounts_to_maybe_clone.is_empty() {
  471. if let Err(e) = genesis.clone_accounts(
  472. accounts_to_maybe_clone,
  473. cluster_rpc_client
  474. .as_ref()
  475. .expect("--maybe-clone requires --json-rpc-url argument"),
  476. true,
  477. ) {
  478. println!("Error: clone_accounts failed: {e}");
  479. exit(1);
  480. }
  481. }
  482. if !upgradeable_programs_to_clone.is_empty() {
  483. if let Err(e) = genesis.clone_upgradeable_programs(
  484. upgradeable_programs_to_clone,
  485. cluster_rpc_client
  486. .as_ref()
  487. .expect("--clone-upgradeable-program requires --json-rpc-url argument"),
  488. ) {
  489. println!("Error: clone_upgradeable_programs failed: {e}");
  490. exit(1);
  491. }
  492. }
  493. if clone_feature_set {
  494. if let Err(e) = genesis.clone_feature_set(
  495. cluster_rpc_client
  496. .as_ref()
  497. .expect("--clone-feature-set requires --json-rpc-url argument"),
  498. ) {
  499. println!("Error: clone_feature_set failed: {e}");
  500. exit(1);
  501. }
  502. }
  503. if let Some(warp_slot) = warp_slot {
  504. genesis.warp_slot(warp_slot);
  505. }
  506. if let Some(ticks_per_slot) = ticks_per_slot {
  507. genesis.ticks_per_slot(ticks_per_slot);
  508. }
  509. if let Some(slots_per_epoch) = slots_per_epoch {
  510. genesis.epoch_schedule(EpochSchedule::custom(
  511. slots_per_epoch,
  512. slots_per_epoch,
  513. /* enable_warmup_epochs = */ false,
  514. ));
  515. genesis.rent = Rent::with_slots_per_epoch(slots_per_epoch);
  516. }
  517. if let Some(inflation_fixed) = inflation_fixed {
  518. genesis.inflation(Inflation::new_fixed(inflation_fixed));
  519. }
  520. genesis.gossip_host(advertised_ip);
  521. if let Some(gossip_port) = gossip_port {
  522. genesis.gossip_port(gossip_port);
  523. }
  524. if let Some(dynamic_port_range) = dynamic_port_range {
  525. genesis.port_range(dynamic_port_range);
  526. }
  527. genesis.bind_ip_addr(bind_address);
  528. if matches.is_present("geyser_plugin_config") {
  529. genesis.geyser_plugin_config_files = Some(
  530. values_t_or_exit!(matches, "geyser_plugin_config", String)
  531. .into_iter()
  532. .map(PathBuf::from)
  533. .collect(),
  534. );
  535. }
  536. if let Some(compute_unit_limit) = compute_unit_limit {
  537. genesis.compute_unit_limit(compute_unit_limit);
  538. }
  539. match genesis.start_with_mint_address_and_geyser_plugin_rpc(
  540. mint_address,
  541. socket_addr_space,
  542. rpc_to_plugin_manager_receiver,
  543. ) {
  544. Ok(test_validator) => {
  545. if let Some(dashboard) = dashboard {
  546. dashboard.run(Duration::from_millis(250));
  547. }
  548. test_validator.join();
  549. }
  550. Err(err) => {
  551. drop(dashboard);
  552. println!("Error: failed to start validator: {err}");
  553. exit(1);
  554. }
  555. }
  556. }
  557. fn remove_directory_contents(ledger_path: &Path) -> Result<(), io::Error> {
  558. for entry in fs::read_dir(ledger_path)? {
  559. let entry = entry?;
  560. if entry.metadata()?.is_dir() {
  561. fs::remove_dir_all(entry.path())?
  562. } else {
  563. fs::remove_file(entry.path())?
  564. }
  565. }
  566. Ok(())
  567. }