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
82pub enum Addr {
84 Peer(PeerAddr<NodeId, Address>),
86 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}