iroh_node_util/cli/
net.rs1use 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#[derive(Subcommand, Debug, Clone)]
17#[allow(clippy::large_enum_variant, missing_docs)]
18pub enum NetCommands {
19 RemoteList,
21 Remote { node_id: NodeId },
23 NodeAddr,
25 AddNodeAddr {
27 node_id: NodeId,
28 relay: Option<RelayUrl>,
29 addresses: Vec<SocketAddr>,
30 },
31 HomeRelay,
33}
34
35impl NetCommands {
36 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
95async 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
127fn 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
166fn 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
203fn 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
215fn never() -> Cell {
217 Cell::new("never").add_attribute(comfy_table::Attribute::Dim)
218}
219
220fn 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
230fn 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
238fn bold_cell(s: &str) -> Cell {
240 Cell::new(s).add_attribute(comfy_table::Attribute::Bold)
241}