radicle_cli/commands/
node.rs

1use std::ffi::OsString;
2use std::path::PathBuf;
3use std::str::FromStr;
4use std::time;
5
6use anyhow::anyhow;
7
8use radicle::node::address::Store as AddressStore;
9use radicle::node::config::ConnectAddress;
10use radicle::node::routing::Store;
11use radicle::node::Handle as _;
12use radicle::node::{Address, Node, NodeId, PeerAddr};
13use radicle::prelude::RepoId;
14
15use crate::terminal as term;
16use crate::terminal::args::{Args, Error, Help};
17use crate::terminal::Element as _;
18
19#[path = "node/commands.rs"]
20mod commands;
21#[path = "node/control.rs"]
22pub mod control;
23#[path = "node/events.rs"]
24mod events;
25#[path = "node/routing.rs"]
26pub mod routing;
27
28pub const HELP: Help = Help {
29    name: "node",
30    description: "Control and query the Radicle Node",
31    version: env!("RADICLE_VERSION"),
32    usage: r#"
33Usage
34
35    rad node status [<option>...]
36    rad node start [--foreground] [--verbose] [<option>...] [-- <node-option>...]
37    rad node stop [<option>...]
38    rad node logs [-n <lines>]
39    rad node debug [<option>...]
40    rad node connect <nid>[@<addr>] [<option>...]
41    rad node routing [--rid <rid>] [--nid <nid>] [--json] [<option>...]
42    rad node inventory [--nid <nid>] [<option>...]
43    rad node events [--timeout <secs>] [-n <count>] [<option>...]
44    rad node config [--addresses]
45    rad node db <command> [<option>..]
46
47    For `<node-option>` see `radicle-node --help`.
48
49Start options
50
51    --foreground         Start the node in the foreground
52    --path <path>        Start node binary at path (default: radicle-node)
53    --verbose, -v        Verbose output
54
55Routing options
56
57    --rid <rid>          Show the routing table entries for the given RID
58    --nid <nid>          Show the routing table entries for the given NID
59    --json               Output the routing table as json
60
61Inventory options
62
63    --nid <nid>          List the inventory of the given NID (default: self)
64
65Events options
66
67    --timeout <secs>     How long to wait to receive an event before giving up
68    --count, -n <count>  Exit after <count> events
69
70General options
71
72    --help               Print help
73"#,
74};
75
76pub struct Options {
77    op: Operation,
78}
79
80/// Address used for the [`Operation::Connect`]
81pub enum Addr {
82    /// Fully-specified address of the form `<NID>@<Address>`
83    Peer(PeerAddr<NodeId, Address>),
84    /// Just the `NID`, to be used for address lookups.
85    Node(NodeId),
86}
87
88impl FromStr for Addr {
89    type Err = anyhow::Error;
90
91    fn from_str(s: &str) -> Result<Self, Self::Err> {
92        if s.contains("@") {
93            PeerAddr::from_str(s)
94                .map(Self::Peer)
95                .map_err(|e| anyhow!("expected <nid> or <nid>@<addr>: {e}"))
96        } else {
97            NodeId::from_str(s)
98                .map(Self::Node)
99                .map_err(|e| anyhow!("expected <nid> or <nid>@<addr>: {e}"))
100        }
101    }
102}
103
104pub enum Operation {
105    Connect {
106        addr: Addr,
107        timeout: time::Duration,
108    },
109    Config {
110        addresses: bool,
111    },
112    Db {
113        args: Vec<OsString>,
114    },
115    Events {
116        timeout: time::Duration,
117        count: usize,
118    },
119    Routing {
120        json: bool,
121        rid: Option<RepoId>,
122        nid: Option<NodeId>,
123    },
124    Start {
125        foreground: bool,
126        verbose: bool,
127        path: PathBuf,
128        options: Vec<OsString>,
129    },
130    Logs {
131        lines: usize,
132    },
133    Status,
134    Inventory {
135        nid: Option<NodeId>,
136    },
137    Debug,
138    Sessions,
139    Stop,
140}
141
142#[derive(Default, PartialEq, Eq)]
143pub enum OperationName {
144    Connect,
145    Config,
146    Db,
147    Events,
148    Routing,
149    Logs,
150    Start,
151    #[default]
152    Status,
153    Inventory,
154    Debug,
155    Sessions,
156    Stop,
157}
158
159impl Args for Options {
160    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
161        use lexopt::prelude::*;
162
163        let mut foreground = false;
164        let mut options = vec![];
165        let mut parser = lexopt::Parser::from_args(args);
166        let mut op: Option<OperationName> = None;
167        let mut nid: Option<NodeId> = None;
168        let mut rid: Option<RepoId> = None;
169        let mut json: bool = false;
170        let mut addr: Option<Addr> = None;
171        let mut lines: usize = 60;
172        let mut count: usize = usize::MAX;
173        let mut timeout = time::Duration::MAX;
174        let mut addresses = false;
175        let mut path = None;
176        let mut verbose = false;
177
178        while let Some(arg) = parser.next()? {
179            match arg {
180                Long("help") | Short('h') => {
181                    return Err(Error::Help.into());
182                }
183                Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
184                    "connect" => op = Some(OperationName::Connect),
185                    "db" => op = Some(OperationName::Db),
186                    "events" => op = Some(OperationName::Events),
187                    "logs" => op = Some(OperationName::Logs),
188                    "config" => op = Some(OperationName::Config),
189                    "routing" => op = Some(OperationName::Routing),
190                    "inventory" => op = Some(OperationName::Inventory),
191                    "start" => op = Some(OperationName::Start),
192                    "status" => op = Some(OperationName::Status),
193                    "stop" => op = Some(OperationName::Stop),
194                    "sessions" => op = Some(OperationName::Sessions),
195                    "debug" => op = Some(OperationName::Debug),
196
197                    unknown => anyhow::bail!("unknown operation '{}'", unknown),
198                },
199                Value(val) if matches!(op, Some(OperationName::Connect)) => {
200                    addr = Some(val.parse()?);
201                }
202                Long("rid") if matches!(op, Some(OperationName::Routing)) => {
203                    let val = parser.value()?;
204                    rid = term::args::rid(&val).ok();
205                }
206                Long("nid")
207                    if matches!(op, Some(OperationName::Routing))
208                        || matches!(op, Some(OperationName::Inventory)) =>
209                {
210                    let val = parser.value()?;
211                    nid = term::args::nid(&val).ok();
212                }
213                Long("json") if matches!(op, Some(OperationName::Routing)) => json = true,
214                Long("timeout")
215                    if op == Some(OperationName::Events) || op == Some(OperationName::Connect) =>
216                {
217                    let val = parser.value()?;
218                    timeout = term::args::seconds(&val)?;
219                }
220                Long("count") | Short('n') if matches!(op, Some(OperationName::Events)) => {
221                    let val = parser.value()?;
222                    count = term::args::number(&val)?;
223                }
224                Long("foreground") if matches!(op, Some(OperationName::Start)) => {
225                    foreground = true;
226                }
227                Long("addresses") if matches!(op, Some(OperationName::Config)) => {
228                    addresses = true;
229                }
230                Long("verbose") | Short('v') if matches!(op, Some(OperationName::Start)) => {
231                    verbose = true;
232                }
233                Long("path") if matches!(op, Some(OperationName::Start)) => {
234                    let val = parser.value()?;
235                    path = Some(PathBuf::from(val));
236                }
237                Short('n') if matches!(op, Some(OperationName::Logs)) => {
238                    lines = parser.value()?.parse()?;
239                }
240                Value(val) if matches!(op, Some(OperationName::Start)) => {
241                    options.push(val);
242                }
243                Value(val) if matches!(op, Some(OperationName::Db)) => {
244                    options.push(val);
245                }
246                _ => return Err(anyhow!(arg.unexpected())),
247            }
248        }
249
250        let op = match op.unwrap_or_default() {
251            OperationName::Connect => Operation::Connect {
252                addr: addr.ok_or_else(|| {
253                    anyhow!("an `<nid>` or an address of the form `<nid>@<host>:<port>` must be provided")
254                })?,
255                timeout,
256            },
257            OperationName::Config => Operation::Config { addresses },
258            OperationName::Db => Operation::Db { args: options },
259            OperationName::Events => Operation::Events { timeout, count },
260            OperationName::Routing => Operation::Routing { rid, nid, json },
261            OperationName::Logs => Operation::Logs { lines },
262            OperationName::Start => Operation::Start {
263                foreground,
264                verbose,
265                options,
266                path: path.unwrap_or(PathBuf::from("radicle-node")),
267            },
268            OperationName::Inventory => Operation::Inventory { nid },
269            OperationName::Status => Operation::Status,
270            OperationName::Debug => Operation::Debug,
271            OperationName::Sessions => Operation::Sessions,
272            OperationName::Stop => Operation::Stop,
273        };
274        Ok((Options { op }, vec![]))
275    }
276}
277
278pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
279    let profile = ctx.profile()?;
280    let mut node = Node::new(profile.socket());
281
282    match options.op {
283        Operation::Connect { addr, timeout } => match addr {
284            Addr::Peer(addr) => control::connect(&mut node, addr.id, addr.addr, timeout)?,
285            Addr::Node(nid) => {
286                let db = profile.database()?;
287                let addresses = db
288                    .addresses_of(&nid)?
289                    .into_iter()
290                    .map(|ka| ka.addr)
291                    .collect();
292                control::connect_many(&mut node, nid, addresses, timeout)?;
293            }
294        },
295        Operation::Config { addresses } => {
296            if addresses {
297                let cfg = node.config()?;
298                for addr in cfg.external_addresses {
299                    term::print(ConnectAddress::from((*profile.id(), addr)).to_string());
300                }
301            } else {
302                control::config(&node)?;
303            }
304        }
305        Operation::Db { args } => {
306            commands::db(&profile, args)?;
307        }
308        Operation::Debug => {
309            control::debug(&mut node)?;
310        }
311        Operation::Sessions => {
312            let sessions = control::sessions(&node)?;
313            if let Some(table) = sessions {
314                table.print();
315            }
316        }
317        Operation::Events { timeout, count } => {
318            events::run(node, count, timeout)?;
319        }
320        Operation::Routing { rid, nid, json } => {
321            let store = profile.database()?;
322            routing::run(&store, rid, nid, json)?;
323        }
324        Operation::Logs { lines } => control::logs(lines, Some(time::Duration::MAX), &profile)?,
325        Operation::Start {
326            foreground,
327            options,
328            path,
329            verbose,
330        } => {
331            control::start(node, !foreground, verbose, options, &path, &profile)?;
332        }
333        Operation::Inventory { nid } => {
334            let nid = nid.as_ref().unwrap_or(profile.id());
335            for rid in profile.routing()?.get_inventory(nid)? {
336                println!("{}", term::format::tertiary(rid));
337            }
338        }
339        Operation::Status => {
340            control::status(&node, &profile)?;
341        }
342        Operation::Stop => {
343            control::stop(node, &profile);
344        }
345    }
346
347    Ok(())
348}