radicle_cli/commands/
node.rs

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