iroh_node_util/cli/
net.rs

1//! Define the net subcommands.
2use std::{net::SocketAddr, time::Duration};
3
4use anyhow::Result;
5use clap::Subcommand;
6use colored::Colorize;
7use comfy_table::{presets::NOTHING, Cell, Table};
8use futures_lite::{Stream, StreamExt};
9use human_time::ToHumanTimeString;
10use iroh::{
11    endpoint::{DirectAddrInfo, RemoteInfo},
12    NodeAddr, NodeId, RelayUrl,
13};
14
15/// Commands to manage the iroh network.
16#[derive(Subcommand, Debug, Clone)]
17#[allow(clippy::large_enum_variant, missing_docs)]
18pub enum NetCommands {
19    /// Get information about the different remote nodes.
20    RemoteList,
21    /// Get information about a particular remote node.
22    Remote { node_id: NodeId },
23    /// Get the node addr of this node.
24    NodeAddr,
25    /// Add this node addr to the known nodes.
26    AddNodeAddr {
27        node_id: NodeId,
28        relay: Option<RelayUrl>,
29        addresses: Vec<SocketAddr>,
30    },
31    /// Get the relay server we are connected to.
32    HomeRelay,
33}
34
35impl NetCommands {
36    /// Runs the net command given the iroh client.
37    pub async fn run(self, client: &crate::rpc::client::net::Client) -> Result<()> {
38        match self {
39            Self::RemoteList => {
40                let connections = client.remote_info_iter().await?;
41                let timestamp = time::OffsetDateTime::now_utc()
42                    .format(&time::format_description::well_known::Rfc2822)
43                    .unwrap_or_else(|_| String::from("failed to get current time"));
44
45                println!(
46                    " {}: {}\n\n{}",
47                    "current time".bold(),
48                    timestamp,
49                    fmt_remote_infos(connections).await
50                );
51            }
52            Self::Remote { node_id } => {
53                let info = client.remote_info(node_id).await?;
54                match info {
55                    Some(info) => println!("{}", fmt_info(info)),
56                    None => println!("Not Found"),
57                }
58            }
59            Self::NodeAddr => {
60                let addr = client.node_addr().await?;
61                println!("Node ID: {}", addr.node_id);
62                let relay = addr
63                    .relay_url
64                    .map(|s| s.to_string())
65                    .unwrap_or_else(|| "Not Available".to_string());
66                println!("Home Relay: {}", relay);
67                println!("Direct Addresses ({}):", addr.direct_addresses.len());
68                for da in &addr.direct_addresses {
69                    println!(" {}", da);
70                }
71            }
72            Self::AddNodeAddr {
73                node_id,
74                relay,
75                addresses,
76            } => {
77                let mut addr = NodeAddr::new(node_id).with_direct_addresses(addresses);
78                if let Some(relay) = relay {
79                    addr = addr.with_relay_url(relay);
80                }
81                client.add_node_addr(addr).await?;
82            }
83            Self::HomeRelay => {
84                let relay = client.home_relay().await?;
85                let relay = relay
86                    .map(|s| s.to_string())
87                    .unwrap_or_else(|| "Not Available".to_string());
88                println!("Home Relay: {}", relay);
89            }
90        }
91        Ok(())
92    }
93}
94
95/// Formats the remote information into a `Table`.
96async fn fmt_remote_infos(
97    mut infos: impl Stream<Item = Result<RemoteInfo, anyhow::Error>> + Unpin,
98) -> String {
99    let mut table = Table::new();
100    table.load_preset(NOTHING).set_header(
101        ["node id", "relay", "conn type", "latency", "last used"]
102            .into_iter()
103            .map(bold_cell),
104    );
105    while let Some(Ok(info)) = infos.next().await {
106        let node_id: Cell = info.node_id.to_string().into();
107        let relay_url = info
108            .relay_url
109            .map_or(String::new(), |url_info| url_info.relay_url.to_string())
110            .into();
111        let conn_type = info.conn_type.to_string().into();
112        let latency = match info.latency {
113            Some(latency) => latency.to_human_time_string(),
114            None => String::from("unknown"),
115        }
116        .into();
117        let last_used = info
118            .last_used
119            .map(fmt_how_long_ago)
120            .map(Cell::new)
121            .unwrap_or_else(never);
122        table.add_row([node_id, relay_url, conn_type, latency, last_used]);
123    }
124    table.to_string()
125}
126
127/// Formats the remote information into a `String`.
128fn fmt_info(info: RemoteInfo) -> String {
129    let RemoteInfo {
130        node_id,
131        relay_url,
132        addrs,
133        conn_type,
134        latency,
135        last_used,
136    } = info;
137    let timestamp = time::OffsetDateTime::now_utc()
138        .format(&time::format_description::well_known::Rfc2822)
139        .unwrap_or_else(|_| String::from("failed to get current time"));
140    let mut table = Table::new();
141    table.load_preset(NOTHING);
142    table.add_row([bold_cell("current time"), timestamp.into()]);
143    table.add_row([bold_cell("node id"), node_id.to_string().into()]);
144    let relay_url = relay_url
145        .map(|r| r.relay_url.to_string())
146        .unwrap_or_else(|| String::from("unknown"));
147    table.add_row([bold_cell("relay url"), relay_url.into()]);
148    table.add_row([bold_cell("connection type"), conn_type.to_string().into()]);
149    table.add_row([bold_cell("latency"), fmt_latency(latency).into()]);
150    table.add_row([
151        bold_cell("last used"),
152        last_used
153            .map(fmt_how_long_ago)
154            .map(Cell::new)
155            .unwrap_or_else(never),
156    ]);
157    table.add_row([bold_cell("known addresses"), addrs.len().into()]);
158
159    let general_info = table.to_string();
160
161    let addrs_info = fmt_addrs(addrs);
162
163    format!("{general_info}\n\n{addrs_info}")
164}
165
166/// Formats the [`DirectAddrInfo`] into a [`Table`].
167fn direct_addr_row(info: DirectAddrInfo) -> comfy_table::Row {
168    let DirectAddrInfo {
169        addr,
170        latency,
171        last_control,
172        last_payload,
173        last_alive,
174        ..
175    } = info;
176
177    let last_control = match last_control {
178        None => never(),
179        Some((how_long_ago, kind)) => {
180            format!("{kind} ( {} )", fmt_how_long_ago(how_long_ago)).into()
181        }
182    };
183    let last_payload = last_payload
184        .map(fmt_how_long_ago)
185        .map(Cell::new)
186        .unwrap_or_else(never);
187
188    let last_alive = last_alive
189        .map(fmt_how_long_ago)
190        .map(Cell::new)
191        .unwrap_or_else(never);
192
193    [
194        addr.into(),
195        fmt_latency(latency).into(),
196        last_control,
197        last_payload,
198        last_alive,
199    ]
200    .into()
201}
202
203/// Formats a collection o [`DirectAddrInfo`] into a [`Table`].
204fn fmt_addrs(addrs: Vec<DirectAddrInfo>) -> comfy_table::Table {
205    let mut table = Table::new();
206    table.load_preset(NOTHING).set_header(
207        vec!["addr", "latency", "last control", "last data", "last alive"]
208            .into_iter()
209            .map(bold_cell),
210    );
211    table.add_rows(addrs.into_iter().map(direct_addr_row));
212    table
213}
214
215/// Creates a cell with the dimmed text "never".
216fn never() -> Cell {
217    Cell::new("never").add_attribute(comfy_table::Attribute::Dim)
218}
219
220/// Formats a [`Duration`] into a human-readable `String`.
221fn fmt_how_long_ago(duration: Duration) -> String {
222    duration
223        .to_human_time_string()
224        .split_once(',')
225        .map(|(first, _rest)| first)
226        .unwrap_or("-")
227        .to_string()
228}
229
230/// Formats the latency into a human-readable `String`.
231fn fmt_latency(latency: Option<Duration>) -> String {
232    match latency {
233        Some(latency) => latency.to_human_time_string(),
234        None => String::from("unknown"),
235    }
236}
237
238/// Creates a bold cell with the given text.
239fn bold_cell(s: &str) -> Cell {
240    Cell::new(s).add_attribute(comfy_table::Attribute::Bold)
241}