Skip to main content

ckb_rpc/module/
terminal.rs

1use crate::error::RPCError;
2use async_trait::async_trait;
3use ckb_dao_utils::extract_dao_data;
4use ckb_db_schema::COLUMN_CELL;
5use ckb_jsonrpc_types::{
6    CellsInfo, Disk, DiskUsage, Global, MiningInfo, Network, NetworkInfo, Overview, PeerInfo,
7    SysInfo, TerminalPoolInfo,
8};
9use ckb_logger::error;
10use ckb_network::NetworkController;
11use ckb_shared::shared::Shared;
12use ckb_store::ChainStore;
13use ckb_types::utilities::compact_to_difficulty;
14use ckb_util::Mutex;
15use jsonrpc_core::Result;
16use jsonrpc_utils::rpc;
17use lru::LruCache;
18use std::sync::Arc;
19use std::time::{Duration, Instant};
20use sysinfo::{Disks as SysDisks, Networks as SysNetworks, System};
21
22/// Cache TTL constants for different data types
23pub mod ttl {
24    use std::time::Duration;
25
26    /// System information TTL: 5 seconds - system metrics change relatively frequently
27    pub const SYSTEM_INFO: Duration = Duration::from_secs(5);
28
29    /// Mining information TTL: 10 seconds - network difficulty and hash rate change moderately
30    pub const MINING_INFO: Duration = Duration::from_secs(10);
31
32    /// Transaction pool TTL: 2 seconds - transaction pool is highly dynamic
33    pub const TX_POOL_INFO: Duration = Duration::from_secs(2);
34
35    /// Cells information TTL: 30 seconds - blockchain cell statistics change slowly
36    pub const CELLS_INFO: Duration = Duration::from_secs(30);
37
38    /// Network latency TTL: 10 seconds - peer connections and latencies change moderately
39    pub const NETWORK_INFO: Duration = Duration::from_secs(10);
40}
41
42bitflags::bitflags! {
43    /// The bit flags used to determine what to refresh specifically on the Overview type
44    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45    pub struct RefreshKind: u32 {
46        /// Refresh nothing, use cached data when available
47        const NOTHING                      = 0b00000000;
48        /// Force refresh system information (CPU, memory, disk, network)
49        const SYSTEM_INFO                  = 0b00000001;
50        /// Force refresh mining information (difficulty, hash rate)
51        const MINING_INFO                  = 0b00000010;
52        /// Force refresh transaction pool information
53        const TX_POOL_INFO                 = 0b00000100;
54        /// Force refresh cells information
55        const CELLS_INFO                   = 0b00001000;
56        /// Force refresh network peer latency information
57        const NETWORK_INFO                 = 0b00010000;
58        /// Refresh all cached data
59        const EVERYTHING                   = 0b00011111;
60    }
61}
62
63/// Cache statistics for monitoring
64#[derive(Debug, Clone)]
65pub struct CacheStats {
66    pub sys_info_cached: usize,
67    pub mining_info_cached: usize,
68    pub tx_pool_info_cached: usize,
69    pub cells_info_cached: usize,
70    pub network_info_cached: usize,
71}
72
73/// Cache entry with timestamp for TTL
74#[derive(Clone, Debug)]
75struct CacheEntry<T> {
76    data: T,
77    timestamp: Instant,
78}
79
80impl<T> CacheEntry<T> {
81    fn new(data: T) -> Self {
82        Self {
83            data,
84            timestamp: Instant::now(),
85        }
86    }
87
88    fn is_expired(&self, ttl: Duration) -> bool {
89        self.timestamp.elapsed() > ttl
90    }
91}
92
93/// Terminal cache for storing expensive computations
94#[derive(Clone)]
95pub struct TerminalCache {
96    /// System information cache (TTL: 5 seconds)
97    sys_info: Arc<Mutex<LruCache<(), CacheEntry<SysInfo>>>>,
98    /// Mining information cache (TTL: 10 seconds)
99    mining_info: Arc<Mutex<LruCache<(), CacheEntry<MiningInfo>>>>,
100    /// Transaction pool information cache (TTL: 2 seconds)
101    tx_pool_info: Arc<Mutex<LruCache<(), CacheEntry<TerminalPoolInfo>>>>,
102    /// Cells information cache (TTL: 30 seconds)
103    cells_info: Arc<Mutex<LruCache<(), CacheEntry<CellsInfo>>>>,
104    /// Network information cache (TTL: 10 seconds)
105    network_info: Arc<Mutex<LruCache<(), CacheEntry<NetworkInfo>>>>,
106}
107
108impl TerminalCache {
109    /// Create a new terminal cache with default TTL settings
110    pub fn new() -> Self {
111        Self {
112            sys_info: Arc::new(Mutex::new(LruCache::new(1))),
113            mining_info: Arc::new(Mutex::new(LruCache::new(1))),
114            tx_pool_info: Arc::new(Mutex::new(LruCache::new(1))),
115            cells_info: Arc::new(Mutex::new(LruCache::new(1))),
116            network_info: Arc::new(Mutex::new(LruCache::new(1))),
117        }
118    }
119
120    /// Get cached system info if not expired (TTL: 5 seconds)
121    pub fn get_sys_info(&self) -> Option<SysInfo> {
122        let mut cache = self.sys_info.lock();
123        if let Some(entry) = cache.get(&())
124            && !entry.is_expired(ttl::SYSTEM_INFO)
125        {
126            return Some(entry.data.clone());
127        }
128        None
129    }
130
131    /// Cache system info
132    pub fn set_sys_info(&self, info: SysInfo) {
133        let mut cache = self.sys_info.lock();
134        cache.put((), CacheEntry::new(info));
135    }
136
137    /// Get cached mining info if not expired (TTL: 10 seconds)
138    pub fn get_mining_info(&self) -> Option<MiningInfo> {
139        let mut cache = self.mining_info.lock();
140        if let Some(entry) = cache.get(&())
141            && !entry.is_expired(ttl::MINING_INFO)
142        {
143            return Some(entry.data.clone());
144        }
145
146        None
147    }
148
149    /// Cache mining info
150    pub fn set_mining_info(&self, info: MiningInfo) {
151        let mut cache = self.mining_info.lock();
152        cache.put((), CacheEntry::new(info));
153    }
154
155    /// Get cached transaction pool info if not expired (TTL: 2 seconds)
156    pub fn get_tx_pool_info(&self) -> Option<TerminalPoolInfo> {
157        let mut cache = self.tx_pool_info.lock();
158        if let Some(entry) = cache.get(&())
159            && !entry.is_expired(ttl::TX_POOL_INFO)
160        {
161            return Some(entry.data.clone());
162        }
163        None
164    }
165
166    /// Cache transaction pool info
167    pub fn set_tx_pool_info(&self, info: TerminalPoolInfo) {
168        let mut cache = self.tx_pool_info.lock();
169        cache.put((), CacheEntry::new(info));
170    }
171
172    /// Get cached cells info if not expired (TTL: 30 seconds)
173    pub fn get_cells_info(&self) -> Option<CellsInfo> {
174        let mut cache = self.cells_info.lock();
175        if let Some(entry) = cache.get(&())
176            && !entry.is_expired(ttl::CELLS_INFO)
177        {
178            return Some(entry.data.clone());
179        }
180
181        None
182    }
183
184    /// Cache cells info
185    pub fn set_cells_info(&self, info: CellsInfo) {
186        let mut cache = self.cells_info.lock();
187        cache.put((), CacheEntry::new(info));
188    }
189
190    /// Get cached network info if not expired (TTL: 10 seconds)
191    pub fn get_network_info(&self) -> Option<NetworkInfo> {
192        let mut cache = self.network_info.lock();
193        if let Some(entry) = cache.get(&())
194            && !entry.is_expired(ttl::NETWORK_INFO)
195        {
196            return Some(entry.data.clone());
197        }
198
199        None
200    }
201
202    /// Cache network info
203    pub fn set_network_info(&self, info: NetworkInfo) {
204        let mut cache = self.network_info.lock();
205        cache.put((), CacheEntry::new(info));
206    }
207
208    /// Clear specific cache entry types based on refresh flags
209    pub fn clear_specific(&self, refresh: RefreshKind) {
210        if refresh.contains(RefreshKind::SYSTEM_INFO) {
211            self.sys_info.lock().clear();
212        }
213        if refresh.contains(RefreshKind::MINING_INFO) {
214            self.mining_info.lock().clear();
215        }
216        if refresh.contains(RefreshKind::TX_POOL_INFO) {
217            self.tx_pool_info.lock().clear();
218        }
219        if refresh.contains(RefreshKind::CELLS_INFO) {
220            self.cells_info.lock().clear();
221        }
222        if refresh.contains(RefreshKind::NETWORK_INFO) {
223            self.network_info.lock().clear();
224        }
225    }
226
227    /// Get cache statistics for monitoring
228    pub fn get_stats(&self) -> CacheStats {
229        CacheStats {
230            sys_info_cached: self.sys_info.lock().len(),
231            mining_info_cached: self.mining_info.lock().len(),
232            tx_pool_info_cached: self.tx_pool_info.lock().len(),
233            cells_info_cached: self.cells_info.lock().len(),
234            network_info_cached: self.network_info.lock().len(),
235        }
236    }
237
238    /// Clear all cached data
239    pub fn clear_all(&self) {
240        self.sys_info.lock().clear();
241        self.mining_info.lock().clear();
242        self.tx_pool_info.lock().clear();
243        self.cells_info.lock().clear();
244        self.network_info.lock().clear();
245    }
246}
247
248impl Default for TerminalCache {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254/// RPC Terminal Module, specifically designed for TUI (Terminal User Interface) applications.
255///
256/// This module provides optimized endpoints for terminal-based monitoring tools and dashboards,
257/// with intelligent caching to minimize performance impact while providing real-time insights.
258///
259/// # Intended Use Cases
260///
261/// - **TUI Monitoring Dashboards**: Real-time node status displays in terminal environments
262/// - **System Administration**: Command-line tools for node health monitoring
263/// - **Resource Monitoring**: Tracking system resource usage over time
264/// - **Network Diagnostics**: Monitoring peer connectivity and performance
265///
266/// # Performance Considerations
267///
268/// The module uses a multi-tiered caching strategy with TTLs optimized for different data
269/// change frequencies. For frequent monitoring calls, use cached data (refresh: null) to
270/// minimize system load. Force refresh only when real-time accuracy is critical.
271///
272/// # Refresh Flags Guide
273///
274/// Use RefreshKind bit flags strategically:
275/// - **Monitoring Mode**: Use `null` or `0` for cached data (recommended)
276/// - **Diagnostics Mode**: Use specific flags to refresh relevant data only
277/// - **Full Sync**: Use `EVERYTHING` (31) for complete data refresh
278#[rpc(openrpc)]
279#[async_trait]
280pub trait TerminalRpc {
281    /// Returns a comprehensive overview of CKB node status for TUI applications.
282    ///
283    /// This method aggregates system metrics, mining information, transaction pool status,
284    /// cells statistics, and network peer information into a single response, optimized
285    /// for terminal-based monitoring interfaces.
286    ///
287    /// ## Params
288    ///
289    /// * `refresh` - Optional bit flags to force refresh specific cached data types.
290    ///   Use `RefreshKind` bit flags to control which data to refresh:
291    ///   - `0x1` (SYSTEM_INFO): Force refresh system information (CPU, memory, disk, network)
292    ///   - `0x2` (MINING_INFO): Force refresh mining information (difficulty, hash rate)
293    ///   - `0x4` (TX_POOL_INFO): Force refresh transaction pool information
294    ///   - `0x8` (CELLS_INFO): Force refresh cells information
295    ///   - `0x10` (NETWORK_INFO): Force refresh network peer latency information
296    ///   - `0x1F` (EVERYTHING): Force refresh all cached data
297    ///   - `null` or `0`: Use cached data when available (recommended for frequent calls)
298    ///
299    /// ## Returns
300    ///
301    /// Returns an `Overview` structure containing:
302    /// - System information (CPU, memory, disk, network metrics)
303    /// - Mining information (network difficulty and hash rate)
304    /// - Transaction pool statistics
305    /// - Blockchain cells information
306    /// - Network peer connectivity and latency data
307    /// - CKB node version
308    ///
309    /// ## Cache Behavior
310    ///
311    /// Data is cached with different TTL values to balance freshness with performance:
312    /// - System info: 5 seconds (changes frequently)
313    /// - Mining info: 10 seconds (moderate change frequency)
314    /// - Transaction pool: 2 seconds (highly dynamic)
315    /// - Cells info: 30 seconds (relatively static)
316    /// - Network info: 10 seconds (moderate change frequency)
317    ///
318    /// ## Examples
319    ///
320    /// Get overview using cached data:
321    ///
322    /// Request
323    ///
324    /// ```json
325    /// {
326    ///   "jsonrpc": "2.0",
327    ///   "method": "get_overview",
328    ///   "params": [null],
329    ///   "id": 1
330    /// }
331    /// ```
332    ///
333    /// Response
334    ///
335    /// ```json
336    /// {
337    ///   "jsonrpc": "2.0",
338    ///   "result": {
339    ///     "sys": {
340    ///       "cpu_usage": 25.5,
341    ///       "memory": 134217728,
342    ///       "virtual_memory": 268435456,
343    ///       "disk_usage": {
344    ///         "read_bytes": 1048576,
345    ///         "total_read_bytes": 1073741824,
346    ///         "written_bytes": 524288,
347    ///         "total_written_bytes": 536870912
348    ///       },
349    ///       "global": {
350    ///         "total_memory": 8589934592,
351    ///         "used_memory": 4294967296,
352    ///         "global_cpu_usage": 150.0,
353    ///         "disks": [
354    ///           {
355    ///             "total_space": 1000000000000,
356    ///             "available_space": 500000000000,
357    ///             "is_removable": false
358    ///           }
359    ///         ],
360    ///         "networks": [
361    ///           {
362    ///             "interface_name": "eth0",
363    ///             "received": 1048576,
364    ///             "total_received": 1073741824,
365    ///             "transmitted": 524288,
366    ///             "total_transmitted": 536870912
367    ///           }
368    ///         ]
369    ///       }
370    ///     },
371    ///     "mining": {
372    ///       "difficulty": "0x1e083126",
373    ///       "hash_rate": "0x174876e800"
374    ///     },
375    ///     "pool": {
376    ///       "pending": "0x64",
377    ///       "proposed": "0x32",
378    ///       "orphan": "0x5",
379    ///       "committing": "0x1f",
380    ///       "total_recent_reject_num": "0x3",
381    ///       "total_tx_size": "0x100000",
382    ///       "total_tx_cycles": "0x2dc6c",
383    ///       "last_txs_updated_at": "0x187b3d137a1",
384    ///       "max_tx_pool_size": "0x20000000"
385    ///     },
386    ///     "cells": {
387    ///       "total_occupied_capacities": "0x15f1e59b76c000",
388    ///       "estimate_live_cells_num": "0x989680"
389    ///     },
390    ///    "network": {
391    ///    "connected_peers": "0x33",
392    ///    "outbound_peers": "0x1f",
393    ///    "inbound_peers": "0x14",
394    ///    "peers": [
395    ///        {
396    ///        "peer_id": 0,
397    ///        "is_outbound": true,
398    ///        "latency_ms": "0x98",
399    ///         "address": "/ip4/18.185.102.19/tcp/8115/p2p/QmXwUgF48ULy6hkgfqrEwEfuHW7WyWyWauueRDAYQHNDfN"
400    ///        },
401    ///        {
402    ///        "peer_id": 1,
403    ///        "is_outbound": false,
404    ///        "latency_ms": "0xa5",
405    ///         "address": "/ip4/18.185.102.19/tcp/8115/p2p/QmXwUgF48ULy6hkgfqrEwEfuHW7WyWyWauueRDAYQHNDfN"
406    ///        },
407    ///        {
408    ///        "peer_id": 2,
409    ///        "is_outbound": true,
410    ///        "latency_ms": "0x0",
411    ///         "address": "/ip4/18.185.102.19/tcp/8115/p2p/QmXwUgF48ULy6hkgfqrEwEfuHW7WyWyWauueRDAYQHNDfN"
412    ///        },
413    ///        {
414    ///        "peer_id": 3,
415    ///        "is_outbound": true,
416    ///        "latency_ms": "0x8c",
417    ///         "address": "/ip4/18.185.102.19/tcp/8115/p2p/QmXwUgF48ULy6hkgfqrEwEfuHW7WyWyWauueRDAYQHNDfN"
418    ///        }
419    ///     ]
420    ///    },
421    ///     "version": "0.100.0 (abc123def 2023-12-01)"
422    ///   },
423    ///   "id": 1
424    ///  }
425    /// ```
426    ///
427    #[rpc(name = "get_overview")]
428    fn get_overview(&self, refresh: Option<u32>) -> Result<Overview>;
429}
430
431#[derive(Clone)]
432pub(crate) struct TerminalRpcImpl {
433    pub shared: Shared,
434    pub network_controller: NetworkController,
435    pub cache: TerminalCache,
436}
437
438#[async_trait]
439impl TerminalRpc for TerminalRpcImpl {
440    fn get_overview(&self, refresh: Option<u32>) -> Result<Overview> {
441        let refresh = refresh
442            .and_then(RefreshKind::from_bits)
443            .unwrap_or(RefreshKind::NOTHING);
444
445        // If refresh everything, clear cache first
446        if refresh.contains(RefreshKind::EVERYTHING) {
447            self.cache.clear_all();
448        }
449
450        let sys = self.get_sys_info(refresh)?;
451        let mining = self.get_mining_info(refresh)?;
452        let pool = self.get_tx_pool_info(refresh)?;
453        let cells = self.get_cells_info(refresh)?;
454        let network = self.get_network_info(refresh)?;
455
456        Ok(Overview {
457            sys,
458            cells,
459            mining,
460            pool,
461            network,
462            version: self.network_controller.version().to_owned(),
463        })
464    }
465}
466
467impl TerminalRpcImpl {
468    fn get_mining_info(&self, refresh: RefreshKind) -> Result<MiningInfo> {
469        // Check cache first unless force refresh
470        if !refresh.contains(RefreshKind::MINING_INFO)
471            && let Some(cached) = self.cache.get_mining_info()
472        {
473            return Ok(cached);
474        }
475
476        // Fetch fresh data
477        let current_epoch_ext =
478            self.shared
479                .snapshot()
480                .get_current_epoch_ext()
481                .ok_or_else(|| {
482                    RPCError::custom(
483                        RPCError::CKBInternalError,
484                        "failed to get current epoch_ext",
485                    )
486                })?;
487        let difficulty = compact_to_difficulty(current_epoch_ext.compact_target());
488        let mining_info = MiningInfo {
489            difficulty,
490            // We use previous_epoch_hash_rate to approximate the full network hash power,
491            // simplifying the calculation process.
492            hash_rate: current_epoch_ext.previous_epoch_hash_rate().to_owned(),
493        };
494
495        // Cache the result
496        self.cache.set_mining_info(mining_info.clone());
497        Ok(mining_info)
498    }
499
500    fn get_sys_info(&self, refresh: RefreshKind) -> Result<SysInfo> {
501        // Check cache first unless force refresh
502        if !refresh.contains(RefreshKind::SYSTEM_INFO)
503            && let Some(cached) = self.cache.get_sys_info()
504        {
505            return Ok(cached);
506        }
507
508        // Fetch fresh system data
509        let mut sys = System::new_all();
510        sys.refresh_all();
511
512        let total_memory = sys.total_memory();
513        let used_memory = sys.used_memory();
514        let global_cpu_usage = sys.global_cpu_usage();
515        let sys_disks = SysDisks::new_with_refreshed_list();
516        let disks = sys_disks
517            .iter()
518            .map(|disk| Disk {
519                total_space: disk.total_space(),
520                available_space: disk.available_space(),
521                is_removable: disk.is_removable(),
522            })
523            .collect();
524        let sys_networks = SysNetworks::new_with_refreshed_list();
525        let networks = sys_networks
526            .iter()
527            .map(|(name, data)| Network {
528                interface_name: name.clone(),
529                received: data.received(),
530                total_received: data.total_received(),
531                transmitted: data.transmitted(),
532                total_transmitted: data.total_transmitted(),
533            })
534            .collect();
535
536        let global = Global {
537            total_memory,
538            used_memory,
539            global_cpu_usage,
540            disks,
541            networks,
542        };
543
544        let process = sys
545            .process(
546                sysinfo::get_current_pid()
547                    .map_err(|e| RPCError::custom(RPCError::CKBInternalError, e))?,
548            )
549            .ok_or_else(|| {
550                RPCError::custom(RPCError::CKBInternalError, "failed to get current process")
551            })?;
552
553        let sys_disk_usage = process.disk_usage();
554        let sys_info = SysInfo {
555            global,
556            cpu_usage: process.cpu_usage(),
557            memory: process.memory(),
558            disk_usage: DiskUsage {
559                total_written_bytes: sys_disk_usage.total_written_bytes,
560                written_bytes: sys_disk_usage.written_bytes,
561                total_read_bytes: sys_disk_usage.total_read_bytes,
562                read_bytes: sys_disk_usage.read_bytes,
563            },
564            virtual_memory: process.virtual_memory(),
565        };
566
567        // Cache the result
568        self.cache.set_sys_info(sys_info.clone());
569        Ok(sys_info)
570    }
571
572    fn get_tx_pool_info(&self, refresh: RefreshKind) -> Result<TerminalPoolInfo> {
573        // Check cache first unless force refresh
574        if !refresh.contains(RefreshKind::TX_POOL_INFO)
575            && let Some(cached) = self.cache.get_tx_pool_info()
576        {
577            return Ok(cached);
578        }
579
580        // Fetch fresh transaction pool data
581        let tx_pool = self.shared.tx_pool_controller();
582        let get_tx_pool_info = tx_pool.get_tx_pool_info();
583        if let Err(e) = get_tx_pool_info {
584            error!("Send get_tx_pool_info request error {}", e);
585            return Err(RPCError::ckb_internal_error(e));
586        };
587
588        let info = get_tx_pool_info.unwrap();
589
590        let block_template = self
591            .shared
592            .get_block_template(None, None, None)
593            .map_err(|err| {
594                error!("Send get_block_template request error {}", err);
595                RPCError::ckb_internal_error(err)
596            })?
597            .map_err(|err| {
598                error!("Get_block_template result error {}", err);
599                RPCError::from_any_error(err)
600            })?;
601
602        let total_recent_reject_num = tx_pool.get_total_recent_reject_num().map_err(|err| {
603            error!("Get_total_recent_reject_num result error {}", err);
604            RPCError::from_any_error(err)
605        })?;
606
607        let tx_pool_info = TerminalPoolInfo {
608            pending: (info.pending_size as u64).into(),
609            proposed: (info.proposed_size as u64).into(),
610            orphan: (info.orphan_size as u64).into(),
611            committing: (block_template.transactions.len() as u64).into(),
612            total_recent_reject_num: total_recent_reject_num.unwrap_or(0).into(),
613            total_tx_size: (info.total_tx_size as u64).into(),
614            total_tx_cycles: info.total_tx_cycles.into(),
615            last_txs_updated_at: info.last_txs_updated_at.into(),
616            max_tx_pool_size: info.max_tx_pool_size.into(),
617        };
618
619        // Cache the result
620        self.cache.set_tx_pool_info(tx_pool_info.clone());
621        Ok(tx_pool_info)
622    }
623
624    fn get_cells_info(&self, refresh: RefreshKind) -> Result<CellsInfo> {
625        // Check cache first unless force refresh
626        if !refresh.contains(RefreshKind::CELLS_INFO)
627            && let Some(cached) = self.cache.get_cells_info()
628        {
629            return Ok(cached);
630        }
631
632        // Fetch fresh cells data
633        let snapshot = self.shared.cloned_snapshot();
634        let tip_header = snapshot.tip_header();
635        let (_ar, _c, _s, u) = extract_dao_data(tip_header.dao());
636        let estimate_live_cells_num = self
637            .shared
638            .store()
639            .estimate_num_keys_cf(COLUMN_CELL)
640            .map_err(|err| {
641                error!("estimate_num_keys_cf error {}", err);
642                RPCError::ckb_internal_error(err)
643            })?;
644
645        let cells_info = CellsInfo {
646            total_occupied_capacities: u.into(),
647            estimate_live_cells_num: estimate_live_cells_num.unwrap_or(0).into(),
648        };
649
650        // Cache the result
651        self.cache.set_cells_info(cells_info.clone());
652        Ok(cells_info)
653    }
654
655    fn get_network_info(&self, refresh: RefreshKind) -> Result<NetworkInfo> {
656        // Check cache first unless force refresh
657        if !refresh.contains(RefreshKind::NETWORK_INFO)
658            && let Some(cached) = self.cache.get_network_info()
659        {
660            return Ok(cached);
661        }
662
663        // Fetch fresh network data
664        let peers = self.network_controller.connected_peers();
665        let total_peers = peers.len();
666        let mut outbound_peers = 0;
667        let mut inbound_peers = 0;
668        let mut peer_infos = Vec::new();
669
670        for (peer_index, peer) in peers {
671            // Count inbound vs outbound connections
672            if peer.is_outbound() {
673                outbound_peers += 1;
674            } else {
675                inbound_peers += 1;
676            }
677
678            // Extract peer ID and RTT information
679            let peer_id = peer_index.value();
680            let is_outbound = peer.is_outbound();
681            let latency_ms = if let Some(rtt) = peer.ping_rtt {
682                rtt.as_millis() as u64
683            } else {
684                0
685            };
686
687            peer_infos.push(PeerInfo {
688                peer_id,
689                is_outbound,
690                latency_ms: latency_ms.into(),
691                address: peer.connected_addr.to_string(),
692            });
693        }
694
695        let network_info = NetworkInfo {
696            connected_peers: (total_peers as u64).into(),
697            outbound_peers: (outbound_peers as u64).into(),
698            inbound_peers: (inbound_peers as u64).into(),
699            peers: peer_infos,
700        };
701
702        // Cache the result
703        self.cache.set_network_info(network_info.clone());
704        Ok(network_info)
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711    use std::time::Duration;
712
713    #[test]
714    fn test_cache_entry_expiration() {
715        let entry = CacheEntry::new("test_data");
716        assert!(!entry.is_expired(Duration::from_secs(1)));
717
718        // Even with 0ms duration, there might be a tiny delay due to execution time
719        // so we just test that it doesn't panic and returns a boolean
720        let _ = entry.is_expired(Duration::from_millis(0));
721    }
722
723    #[test]
724    fn test_refresh_kind_flags() {
725        let nothing = RefreshKind::NOTHING;
726        assert!(!nothing.contains(RefreshKind::SYSTEM_INFO));
727        assert!(!nothing.contains(RefreshKind::MINING_INFO));
728
729        let system = RefreshKind::SYSTEM_INFO;
730        assert!(system.contains(RefreshKind::SYSTEM_INFO));
731        assert!(!system.contains(RefreshKind::MINING_INFO));
732
733        let all = RefreshKind::EVERYTHING;
734        assert!(all.contains(RefreshKind::SYSTEM_INFO));
735        assert!(all.contains(RefreshKind::MINING_INFO));
736        assert!(all.contains(RefreshKind::TX_POOL_INFO));
737        assert!(all.contains(RefreshKind::CELLS_INFO));
738    }
739
740    #[test]
741    fn test_terminal_cache_basic_operations() {
742        let cache = TerminalCache::new();
743
744        // Test that cache is initially empty
745        assert!(cache.get_sys_info().is_none());
746        assert!(cache.get_mining_info().is_none());
747
748        // Test setting and getting values
749        let sys_info = SysInfo {
750            global: Global {
751                total_memory: 1000,
752                used_memory: 500,
753                global_cpu_usage: 50.0,
754                disks: vec![],
755                networks: vec![],
756            },
757            cpu_usage: 0.0,
758            memory: 0,
759            disk_usage: DiskUsage {
760                total_written_bytes: 0,
761                written_bytes: 0,
762                total_read_bytes: 0,
763                read_bytes: 0,
764            },
765            virtual_memory: 0,
766        };
767
768        cache.set_sys_info(sys_info);
769        assert!(cache.get_sys_info().is_some());
770
771        // Test clear all
772        cache.clear_all();
773        assert!(cache.get_sys_info().is_none());
774    }
775
776    #[test]
777    fn test_network_info_basic_operations() {
778        let cache = TerminalCache::new();
779
780        // Test that network cache is initially empty
781        assert!(cache.get_network_info().is_none());
782
783        // Test setting and getting network info
784        let network_info = NetworkInfo {
785            connected_peers: 5u64.into(),
786            outbound_peers: 3u64.into(),
787            inbound_peers: 2u64.into(),
788            peers: vec![
789                PeerInfo {
790                    peer_id: 0,
791                    is_outbound: true,
792                    latency_ms: 150u64.into(),
793                    address: "/ip4/192.168.1.100/tcp/8114".to_string(),
794                },
795                PeerInfo {
796                    peer_id: 1,
797                    is_outbound: true,
798                    latency_ms: 50u64.into(),
799                    address: "/ip4/192.168.1.101/tcp/8114".to_string(),
800                },
801                PeerInfo {
802                    peer_id: 2,
803                    is_outbound: false,
804                    latency_ms: 300u64.into(),
805                    address: "/ip4/192.168.1.102/tcp/8114".to_string(),
806                },
807                PeerInfo {
808                    peer_id: 3,
809                    is_outbound: false,
810                    latency_ms: 100u64.into(),
811                    address: "/ip4/192.168.1.103/tcp/8114".to_string(),
812                },
813                PeerInfo {
814                    peer_id: 4,
815                    is_outbound: true,
816                    latency_ms: 0u64.into(),
817                    address: "/ip4/192.168.1.104/tcp/8114".to_string(),
818                },
819            ],
820        };
821
822        cache.set_network_info(network_info);
823        let cached = cache
824            .get_network_info()
825            .expect("Should have cached network info");
826
827        assert_eq!(cached.connected_peers, 5u64.into());
828        assert_eq!(cached.outbound_peers, 3u64.into());
829        assert_eq!(cached.inbound_peers, 2u64.into());
830        assert_eq!(cached.peers.len(), 5);
831        assert_eq!(cached.peers[0].peer_id, 0);
832        assert!(cached.peers[0].is_outbound);
833        assert_eq!(cached.peers[0].latency_ms, 150u64.into());
834    }
835
836    #[test]
837    fn test_refresh_kind_network_flag() {
838        let nothing = RefreshKind::NOTHING;
839        assert!(!nothing.contains(RefreshKind::NETWORK_INFO));
840
841        let network = RefreshKind::NETWORK_INFO;
842        assert!(network.contains(RefreshKind::NETWORK_INFO));
843        assert!(!network.contains(RefreshKind::SYSTEM_INFO));
844
845        let all = RefreshKind::EVERYTHING;
846        assert!(all.contains(RefreshKind::NETWORK_INFO));
847        assert!(all.contains(RefreshKind::SYSTEM_INFO));
848        assert!(all.contains(RefreshKind::MINING_INFO));
849        assert!(all.contains(RefreshKind::TX_POOL_INFO));
850        assert!(all.contains(RefreshKind::CELLS_INFO));
851    }
852
853    #[test]
854    fn test_cache_stats_includes_network() {
855        let cache = TerminalCache::new();
856        let stats = cache.get_stats();
857
858        // All cache stats should be 0 initially
859        assert_eq!(stats.sys_info_cached, 0);
860        assert_eq!(stats.mining_info_cached, 0);
861        assert_eq!(stats.tx_pool_info_cached, 0);
862        assert_eq!(stats.cells_info_cached, 0);
863        assert_eq!(stats.network_info_cached, 0);
864
865        // Add network info and check stats
866        let network_info = NetworkInfo::default();
867        cache.set_network_info(network_info);
868
869        let stats = cache.get_stats();
870        assert_eq!(stats.network_info_cached, 1);
871    }
872}