1use crate::{
2 binary::FreenetBinary,
3 docker::{DockerNatBackend, DockerNatConfig},
4 network::TestNetwork,
5 peer::{get_free_port, TestPeer},
6 process::{self, PeerProcess},
7 remote::{PeerLocation, RemoteMachine},
8 Error, Result,
9};
10use chrono::Utc;
11use std::collections::HashMap;
12use std::fs;
13use std::net::Ipv4Addr;
14use std::path::{Path, PathBuf};
15use std::process::Command;
16use std::time::{Duration, SystemTime};
17
18#[derive(Debug, Clone)]
20pub enum Backend {
21 Local,
23 DockerNat(DockerNatConfig),
25}
26
27impl Default for Backend {
28 fn default() -> Self {
29 if std::env::var("FREENET_TEST_DOCKER_NAT").is_ok() {
31 Backend::DockerNat(DockerNatConfig::default())
32 } else {
33 Backend::Local
34 }
35 }
36}
37
38struct GatewayInfo {
39 address: String,
40 public_key_path: PathBuf,
41}
42
43pub struct NetworkBuilder {
45 gateways: usize,
46 peers: usize,
47 binary: FreenetBinary,
48 min_connectivity: f64,
49 connectivity_timeout: Duration,
50 preserve_data_on_failure: bool,
51 preserve_data_on_success: bool,
52 peer_locations: HashMap<usize, PeerLocation>,
53 default_location: PeerLocation,
54 min_connections: Option<usize>,
55 max_connections: Option<usize>,
56 start_stagger: Duration,
57 backend: Backend,
58}
59
60impl Default for NetworkBuilder {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl NetworkBuilder {
67 pub fn new() -> Self {
68 Self {
69 gateways: 1,
70 peers: 3,
71 binary: FreenetBinary::default(),
72 min_connectivity: 1.0, connectivity_timeout: Duration::from_secs(30),
74 preserve_data_on_failure: false,
75 preserve_data_on_success: false,
76 peer_locations: HashMap::new(),
77 default_location: PeerLocation::Local,
78 min_connections: None,
79 max_connections: None,
80 start_stagger: Duration::from_millis(500),
81 backend: Backend::default(),
82 }
83 }
84
85 pub fn gateways(mut self, n: usize) -> Self {
87 self.gateways = n;
88 self
89 }
90
91 pub fn peers(mut self, n: usize) -> Self {
93 self.peers = n;
94 self
95 }
96
97 pub fn binary(mut self, binary: FreenetBinary) -> Self {
99 self.binary = binary;
100 self
101 }
102
103 pub fn require_connectivity(mut self, ratio: f64) -> Self {
105 self.min_connectivity = ratio;
106 self
107 }
108
109 pub fn connectivity_timeout(mut self, timeout: Duration) -> Self {
111 self.connectivity_timeout = timeout;
112 self
113 }
114
115 pub fn min_connections(mut self, min: usize) -> Self {
117 self.min_connections = Some(min);
118 self
119 }
120
121 pub fn max_connections(mut self, max: usize) -> Self {
123 self.max_connections = Some(max);
124 self
125 }
126
127 pub fn start_stagger(mut self, delay: Duration) -> Self {
129 self.start_stagger = delay;
130 self
131 }
132
133 pub fn preserve_temp_dirs_on_failure(mut self, preserve: bool) -> Self {
135 self.preserve_data_on_failure = preserve;
136 self
137 }
138
139 pub fn preserve_temp_dirs_on_success(mut self, preserve: bool) -> Self {
141 self.preserve_data_on_success = preserve;
142 self
143 }
144
145 pub fn peer_location(mut self, index: usize, location: PeerLocation) -> Self {
148 self.peer_locations.insert(index, location);
149 self
150 }
151
152 pub fn default_location(mut self, location: PeerLocation) -> Self {
154 self.default_location = location;
155 self
156 }
157
158 pub fn distribute_across_remotes(mut self, machines: Vec<RemoteMachine>) -> Self {
161 let total_peers = self.gateways + self.peers;
162 for (idx, machine) in (0..total_peers).zip(machines.iter().cycle()) {
163 self.peer_locations
164 .insert(idx, PeerLocation::Remote(machine.clone()));
165 }
166 self
167 }
168
169 pub fn backend(mut self, backend: Backend) -> Self {
171 self.backend = backend;
172 self
173 }
174
175 pub async fn build(self) -> Result<TestNetwork> {
177 match self.backend.clone() {
178 Backend::Local => self.build_local().await,
179 Backend::DockerNat(config) => self.build_docker_nat(config).await,
180 }
181 }
182
183 async fn build_local(self) -> Result<TestNetwork> {
185 let binary_path = self.binary.resolve()?;
186
187 tracing::info!(
188 "Starting test network: {} gateways, {} peers",
189 self.gateways,
190 self.peers
191 );
192
193 let base_dir = resolve_base_dir();
194 fs::create_dir_all(&base_dir)?;
195 cleanup_old_runs(&base_dir, 5)?;
196 let run_root = create_run_directory(&base_dir)?;
197
198 let mut run_status = RunStatusGuard::new(&run_root);
199
200 let mut gateways = Vec::new();
202 for i in 0..self.gateways {
203 let peer = match self.start_peer(&binary_path, i, true, &run_root).await {
204 Ok(peer) => peer,
205 Err(err) => {
206 let detail = format!("failed to start gateway {i}: {err}");
207 run_status.mark("failure", Some(&detail));
208 return Err(err);
209 }
210 };
211 gateways.push(peer);
212 }
213
214 let gateway_info: Vec<_> = gateways
216 .iter()
217 .map(|gw| GatewayInfo {
218 address: format!("{}:{}", gw.network_address, gw.network_port),
219 public_key_path: gw
220 .public_key_path
221 .clone()
222 .expect("Gateway must have public key"),
223 })
224 .collect();
225
226 let mut peers = Vec::new();
228 for i in 0..self.peers {
229 let peer = match self
230 .start_peer_with_gateways(
231 &binary_path,
232 i + self.gateways,
233 false,
234 &gateway_info,
235 &run_root,
236 )
237 .await
238 {
239 Ok(peer) => peer,
240 Err(err) => {
241 let detail = format!("failed to start peer {}: {}", i + self.gateways, err);
242 run_status.mark("failure", Some(&detail));
243 return Err(err);
244 }
245 };
246 peers.push(peer);
247 if i + 1 < self.peers && !self.start_stagger.is_zero() {
248 tokio::time::sleep(self.start_stagger).await;
249 }
250 }
251
252 let network = TestNetwork::new(gateways, peers, self.min_connectivity, run_root.clone());
253
254 match network
256 .wait_until_ready_with_timeout(self.connectivity_timeout)
257 .await
258 {
259 Ok(()) => {
260 if self.preserve_data_on_success {
261 match preserve_network_state(&network) {
262 Ok(path) => {
263 println!("Network data directories preserved at {}", path.display());
264 }
265 Err(err) => {
266 eprintln!(
267 "Failed to preserve network data directories after success: {}",
268 err
269 );
270 }
271 }
272 }
273 let detail = format!("success: gateways={}, peers={}", self.gateways, self.peers);
274 run_status.mark("success", Some(&detail));
275 Ok(network)
276 }
277 Err(err) => {
278 if let Err(log_err) = dump_recent_logs(&network) {
279 eprintln!("Failed to dump logs after connectivity error: {}", log_err);
280 }
281 if self.preserve_data_on_failure {
282 match preserve_network_state(&network) {
283 Ok(path) => {
284 eprintln!("Network data directories preserved at {}", path.display());
285 }
286 Err(copy_err) => {
287 eprintln!("Failed to preserve network data directories: {}", copy_err);
288 }
289 }
290 }
291 let detail = err.to_string();
292 run_status.mark("failure", Some(&detail));
293 Err(err)
294 }
295 }
296 }
297
298 pub fn build_sync(self) -> Result<TestNetwork> {
300 tokio::runtime::Runtime::new()?.block_on(self.build())
301 }
302
303 async fn start_peer(
304 &self,
305 binary_path: &PathBuf,
306 index: usize,
307 is_gateway: bool,
308 run_root: &Path,
309 ) -> Result<TestPeer> {
310 self.start_peer_with_gateways(binary_path, index, is_gateway, &[], run_root)
311 .await
312 }
313
314 async fn start_peer_with_gateways(
315 &self,
316 binary_path: &PathBuf,
317 index: usize,
318 is_gateway: bool,
319 gateway_info: &[GatewayInfo],
320 run_root: &Path,
321 ) -> Result<TestPeer> {
322 let location = self
324 .peer_locations
325 .get(&index)
326 .cloned()
327 .unwrap_or_else(|| self.default_location.clone());
328
329 let id = if is_gateway {
330 format!("gw{}", index)
331 } else {
332 format!("peer{}", index)
333 };
334
335 let network_address = match &location {
337 PeerLocation::Local => {
338 let addr_index = index as u32;
339 let second_octet = ((addr_index / 256) % 254 + 1) as u8;
340 let third_octet = (addr_index % 256) as u8;
341 Ipv4Addr::new(127, second_octet, third_octet, 1).to_string()
342 }
343 PeerLocation::Remote(remote) => {
344 remote.discover_public_address()?
346 }
347 };
348
349 let (ws_port, network_port) = match &location {
352 PeerLocation::Local => (get_free_port()?, get_free_port()?),
353 PeerLocation::Remote(_) => (0, 0), };
355
356 let data_dir = create_peer_dir(run_root, &id)?;
357
358 tracing::debug!(
359 "Starting {} {} - ws:{} net:{}",
360 if is_gateway { "gateway" } else { "peer" },
361 id,
362 ws_port,
363 network_port
364 );
365
366 let keypair_path = data_dir.join("keypair.pem");
368 let public_key_path = data_dir.join("public_key.pem");
369 generate_keypair(&keypair_path, &public_key_path)?;
370
371 if let PeerLocation::Remote(remote) = &location {
374 let remote_data_dir = remote.remote_work_dir().join(&id);
375
376 let mkdir_cmd = format!("mkdir -p {}", remote_data_dir.display());
378 remote.exec(&mkdir_cmd)?;
379
380 let remote_keypair = remote_data_dir.join("keypair.pem");
382 let remote_pubkey = remote_data_dir.join("public_key.pem");
383 remote.scp_upload(&keypair_path, remote_keypair.to_str().unwrap())?;
384 remote.scp_upload(&public_key_path, remote_pubkey.to_str().unwrap())?;
385
386 if !is_gateway {
388 for gw in gateway_info {
389 let gw_pubkey_name = gw.public_key_path.file_name().ok_or_else(|| {
390 Error::PeerStartupFailed("Invalid gateway pubkey path".to_string())
391 })?;
392 let remote_gw_pubkey = remote_data_dir.join(gw_pubkey_name);
393 remote.scp_upload(&gw.public_key_path, remote_gw_pubkey.to_str().unwrap())?;
394 }
395 }
396 }
397
398 let mut args = vec![
400 "network".to_string(),
401 "--data-dir".to_string(),
402 match &location {
403 PeerLocation::Local => data_dir.to_string_lossy().to_string(),
404 PeerLocation::Remote(remote) => remote
405 .remote_work_dir()
406 .join(&id)
407 .to_string_lossy()
408 .to_string(),
409 },
410 "--config-dir".to_string(),
411 match &location {
412 PeerLocation::Local => data_dir.to_string_lossy().to_string(),
413 PeerLocation::Remote(remote) => remote
414 .remote_work_dir()
415 .join(&id)
416 .to_string_lossy()
417 .to_string(),
418 },
419 "--ws-api-port".to_string(),
420 ws_port.to_string(),
421 "--network-address".to_string(),
422 network_address.clone(),
423 "--network-port".to_string(),
424 network_port.to_string(),
425 "--public-network-address".to_string(),
426 network_address.clone(),
427 "--public-network-port".to_string(),
428 network_port.to_string(),
429 "--skip-load-from-network".to_string(),
430 ];
431
432 if is_gateway {
433 args.push("--is-gateway".to_string());
434 }
435
436 args.push("--transport-keypair".to_string());
437 let keypair_arg = match &location {
438 PeerLocation::Local => data_dir.join("keypair.pem").to_string_lossy().to_string(),
439 PeerLocation::Remote(remote) => remote
440 .remote_work_dir()
441 .join(&id)
442 .join("keypair.pem")
443 .to_string_lossy()
444 .to_string(),
445 };
446 args.push(keypair_arg);
447
448 if !is_gateway && !gateway_info.is_empty() {
450 let gateways_toml = data_dir.join("gateways.toml");
451 let mut content = String::new();
452 for gw in gateway_info {
453 let gw_pubkey_path = match &location {
454 PeerLocation::Local => gw.public_key_path.clone(),
455 PeerLocation::Remote(remote) => {
456 let gw_pubkey_name = gw.public_key_path.file_name().ok_or_else(|| {
457 Error::PeerStartupFailed("Invalid gateway pubkey path".to_string())
458 })?;
459 remote.remote_work_dir().join(&id).join(gw_pubkey_name)
460 }
461 };
462 content.push_str(&format!(
463 "[[gateways]]\n\
464 address = {{ hostname = \"{}\" }}\n\
465 public_key = \"{}\"\n\n",
466 gw.address,
467 gw_pubkey_path.display()
468 ));
469 }
470 std::fs::write(&gateways_toml, content)?;
471
472 if let PeerLocation::Remote(remote) = &location {
474 let remote_gateways_toml = remote.remote_work_dir().join(&id).join("gateways.toml");
475 remote.scp_upload(&gateways_toml, remote_gateways_toml.to_str().unwrap())?;
476 }
477 }
478
479 let env_vars = vec![
481 ("NETWORK_ADDRESS".to_string(), network_address.clone()),
482 (
483 "PUBLIC_NETWORK_ADDRESS".to_string(),
484 network_address.clone(),
485 ),
486 ("PUBLIC_NETWORK_PORT".to_string(), network_port.to_string()),
487 ];
488
489 if let Some(min_conn) = self.min_connections {
490 args.push("--min-number-of-connections".to_string());
491 args.push(min_conn.to_string());
492 }
493 if let Some(max_conn) = self.max_connections {
494 args.push("--max-number-of-connections".to_string());
495 args.push(max_conn.to_string());
496 }
497
498 let process: Box<dyn PeerProcess + Send> = match &location {
500 PeerLocation::Local => Box::new(process::spawn_local_peer(
501 binary_path,
502 &args,
503 &data_dir,
504 &env_vars,
505 )?),
506 PeerLocation::Remote(remote) => {
507 let remote_data_dir = remote.remote_work_dir().join(&id);
508 let local_cache_dir = run_root.join(format!("{}-cache", id));
509 std::fs::create_dir_all(&local_cache_dir)?;
510
511 Box::new(
512 process::spawn_remote_peer(
513 binary_path,
514 &args,
515 remote,
516 &remote_data_dir,
517 &local_cache_dir,
518 &env_vars,
519 )
520 .await?,
521 )
522 }
523 };
524
525 tokio::time::sleep(Duration::from_millis(100)).await;
527
528 Ok(TestPeer {
529 id,
530 is_gateway,
531 ws_port,
532 network_port,
533 network_address,
534 data_dir,
535 process,
536 public_key_path: Some(public_key_path),
537 location,
538 })
539 }
540
541 async fn build_docker_nat(self, config: DockerNatConfig) -> Result<TestNetwork> {
543 let binary_path = self.binary.resolve()?;
544
545 tracing::info!(
546 "Starting Docker NAT test network: {} gateways, {} peers",
547 self.gateways,
548 self.peers
549 );
550
551 let base_dir = resolve_base_dir();
552 fs::create_dir_all(&base_dir)?;
553 cleanup_old_runs(&base_dir, 5)?;
554 let run_root = create_run_directory(&base_dir)?;
555
556 let mut run_status = RunStatusGuard::new(&run_root);
557
558 let mut docker_backend = DockerNatBackend::new(config).await.map_err(|e| {
560 run_status.mark("failure", Some(&format!("Docker init failed: {}", e)));
561 e
562 })?;
563
564 docker_backend.create_public_network().await.map_err(|e| {
566 run_status.mark(
567 "failure",
568 Some(&format!("Failed to create public network: {}", e)),
569 );
570 e
571 })?;
572
573 let ws_port: u16 = 9000;
575 let network_port: u16 = 31337;
576
577 let mut gateways = Vec::new();
579 for i in 0..self.gateways {
580 let data_dir = create_peer_dir(&run_root, &format!("gw{}", i))?;
581
582 let keypair_path = data_dir.join("keypair.pem");
584 let public_key_path = data_dir.join("public_key.pem");
585 generate_keypair(&keypair_path, &public_key_path)?;
586
587 let (info, process) = docker_backend
588 .create_gateway(
589 i,
590 &binary_path,
591 &keypair_path,
592 &public_key_path,
593 ws_port,
594 network_port,
595 &run_root,
596 )
597 .await
598 .map_err(|e| {
599 let detail = format!("failed to start gateway {}: {}", i, e);
600 run_status.mark("failure", Some(&detail));
601 e
602 })?;
603
604 let peer = TestPeer {
605 id: format!("gw{}", i),
606 is_gateway: true,
607 ws_port: info.host_ws_port,
608 network_port: info.network_port,
609 network_address: info.public_ip.to_string(),
610 data_dir,
611 process: Box::new(process),
612 public_key_path: Some(public_key_path),
613 location: PeerLocation::Local, };
615 gateways.push(peer);
616 }
617
618 let gateway_info: Vec<_> = gateways
620 .iter()
621 .map(|gw| GatewayInfo {
622 address: format!("{}:{}", gw.network_address, network_port),
623 public_key_path: gw
624 .public_key_path
625 .clone()
626 .expect("Gateway must have public key"),
627 })
628 .collect();
629
630 let mut peers = Vec::new();
632 for i in 0..self.peers {
633 let peer_index = i + self.gateways;
634 let data_dir = create_peer_dir(&run_root, &format!("peer{}", peer_index))?;
635
636 let keypair_path = data_dir.join("keypair.pem");
638 let public_key_path = data_dir.join("public_key.pem");
639 generate_keypair(&keypair_path, &public_key_path)?;
640
641 let gateways_toml_path = data_dir.join("gateways.toml");
643 let mut gateways_content = String::new();
644 for gw in &gateway_info {
645 gateways_content.push_str(&format!(
646 "[[gateways]]\n\
647 address = {{ hostname = \"{}\" }}\n\
648 public_key = \"/config/gw_public_key.pem\"\n\n",
649 gw.address,
650 ));
651 }
652 std::fs::write(&gateways_toml_path, &gateways_content)?;
653
654 let gateway_public_key_path = gateway_info.first().map(|gw| gw.public_key_path.clone());
656
657 let (info, process) = docker_backend
658 .create_peer(
659 peer_index,
660 &binary_path,
661 &keypair_path,
662 &public_key_path,
663 &gateways_toml_path,
664 gateway_public_key_path.as_deref(),
665 ws_port,
666 network_port,
667 &run_root,
668 )
669 .await
670 .map_err(|e| {
671 let detail = format!("failed to start peer {}: {}", peer_index, e);
672 run_status.mark("failure", Some(&detail));
673 e
674 })?;
675
676 let peer = TestPeer {
677 id: format!("peer{}", peer_index),
678 is_gateway: false,
679 ws_port: info.host_ws_port,
680 network_port: info.network_port,
681 network_address: info.private_ip.to_string(),
682 data_dir,
683 process: Box::new(process),
684 public_key_path: Some(public_key_path),
685 location: PeerLocation::Local,
686 };
687 peers.push(peer);
688
689 if i + 1 < self.peers && !self.start_stagger.is_zero() {
690 tokio::time::sleep(self.start_stagger).await;
691 }
692 }
693
694 let network = TestNetwork::new_with_docker(
696 gateways,
697 peers,
698 self.min_connectivity,
699 run_root.clone(),
700 Some(docker_backend),
701 );
702
703 match network
705 .wait_until_ready_with_timeout(self.connectivity_timeout)
706 .await
707 {
708 Ok(()) => {
709 if self.preserve_data_on_success {
710 println!(
711 "Network data directories preserved at {}",
712 run_root.display()
713 );
714 }
715 let detail = format!(
716 "success: gateways={}, peers={} (Docker NAT)",
717 self.gateways, self.peers
718 );
719 run_status.mark("success", Some(&detail));
720 Ok(network)
721 }
722 Err(err) => {
723 if let Err(log_err) = dump_recent_logs(&network) {
724 eprintln!("Failed to dump logs after connectivity error: {}", log_err);
725 }
726 if self.preserve_data_on_failure {
727 eprintln!(
728 "Network data directories preserved at {}",
729 run_root.display()
730 );
731 }
732 let detail = err.to_string();
733 run_status.mark("failure", Some(&detail));
734 Err(err)
735 }
736 }
737 }
738}
739
740fn resolve_base_dir() -> PathBuf {
741 if let Some(path) = std::env::var_os("FREENET_TEST_NETWORK_BASE_DIR") {
742 PathBuf::from(path)
743 } else if let Ok(home) = std::env::var("HOME") {
744 PathBuf::from(home).join("code/tmp/freenet-test-networks")
745 } else {
746 std::env::temp_dir().join("freenet-test-networks")
747 }
748}
749
750fn cleanup_old_runs(base_dir: &Path, max_runs: usize) -> Result<()> {
751 let mut runs: Vec<(PathBuf, SystemTime)> = fs::read_dir(base_dir)?
752 .filter_map(|entry| {
753 let entry = entry.ok()?;
754 let file_type = entry.file_type().ok()?;
755 if !file_type.is_dir() {
756 return None;
757 }
758 let metadata = entry.metadata().ok()?;
759 let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
760 Some((entry.path(), modified))
761 })
762 .collect();
763
764 if runs.len() <= max_runs {
765 return Ok(());
766 }
767
768 runs.sort_by_key(|(_, modified)| *modified);
769 let remove_count = runs.len() - max_runs;
770 for (path, _) in runs.into_iter().take(remove_count) {
771 if let Err(err) = fs::remove_dir_all(&path) {
772 tracing::warn!(
773 ?err,
774 path = %path.display(),
775 "Failed to remove old freenet test network run directory"
776 );
777 }
778 }
779
780 Ok(())
781}
782
783fn create_run_directory(base_dir: &Path) -> Result<PathBuf> {
784 let timestamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
785 for attempt in 0..100 {
786 let candidate = if attempt == 0 {
787 base_dir.join(×tamp)
788 } else {
789 base_dir.join(format!("{}-{}", ×tamp, attempt))
790 };
791 if !candidate.exists() {
792 fs::create_dir_all(&candidate)?;
793 return Ok(candidate);
794 }
795 }
796
797 Err(Error::Other(anyhow::anyhow!(
798 "Unable to allocate run directory after repeated attempts"
799 )))
800}
801
802fn create_peer_dir(run_root: &Path, id: &str) -> Result<PathBuf> {
803 let dir = run_root.join(id);
804 fs::create_dir_all(&dir)?;
805 Ok(dir)
806}
807
808struct RunStatusGuard {
809 status_path: PathBuf,
810}
811
812impl RunStatusGuard {
813 fn new(run_root: &Path) -> Self {
814 let status_path = run_root.join("run_status.txt");
815 let _ = fs::write(&status_path, b"status=initializing\n");
816 Self { status_path }
817 }
818
819 fn mark(&mut self, status: &str, detail: Option<&str>) {
820 let mut content = format!("status={}", status);
821 if let Some(detail) = detail {
822 content.push('\n');
823 content.push_str("detail=");
824 content.push_str(detail);
825 }
826 content.push('\n');
827 if let Err(err) = fs::write(&self.status_path, content) {
828 tracing::warn!(
829 ?err,
830 path = %self.status_path.display(),
831 "Failed to write run status"
832 );
833 }
834 }
835}
836
837fn generate_keypair(
838 private_key_path: &std::path::Path,
839 public_key_path: &std::path::Path,
840) -> Result<()> {
841 let output = Command::new("openssl")
843 .args([
844 "genpkey",
845 "-algorithm",
846 "RSA",
847 "-out",
848 private_key_path.to_str().unwrap(),
849 "-pkeyopt",
850 "rsa_keygen_bits:2048",
851 ])
852 .output()?;
853
854 if !output.status.success() {
855 return Err(Error::Other(anyhow::anyhow!(
856 "Failed to generate private key: {}",
857 String::from_utf8_lossy(&output.stderr)
858 )));
859 }
860
861 let output = Command::new("openssl")
863 .args([
864 "rsa",
865 "-pubout",
866 "-in",
867 private_key_path.to_str().unwrap(),
868 "-out",
869 public_key_path.to_str().unwrap(),
870 ])
871 .output()?;
872
873 if !output.status.success() {
874 return Err(Error::Other(anyhow::anyhow!(
875 "Failed to extract public key: {}",
876 String::from_utf8_lossy(&output.stderr)
877 )));
878 }
879
880 Ok(())
881}
882
883fn dump_recent_logs(network: &TestNetwork) -> Result<()> {
884 const MAX_LOG_LINES: usize = 200;
885
886 let mut logs = network.read_logs()?;
887 let total = logs.len();
888 if total > MAX_LOG_LINES {
889 logs.drain(0..(total - MAX_LOG_LINES));
890 }
891
892 eprintln!(
893 "\n--- Network connectivity check failed; showing {} of {} log entries ---",
894 logs.len(),
895 total
896 );
897
898 for entry in logs {
899 let level = entry.level.as_deref().unwrap_or("INFO");
900 let ts_display = entry
901 .timestamp_raw
902 .clone()
903 .or_else(|| entry.timestamp.map(|ts| ts.to_rfc3339()));
904
905 if let Some(ts) = ts_display {
906 eprintln!("[{}] [{}] {}: {}", entry.peer_id, ts, level, entry.message);
907 } else {
908 eprintln!("[{}] {}: {}", entry.peer_id, level, entry.message);
909 }
910 }
911
912 eprintln!("--- End of network logs ---\n");
913
914 Ok(())
915}
916
917fn preserve_network_state(network: &TestNetwork) -> Result<PathBuf> {
918 Ok(network.run_root().to_path_buf())
919}