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