node.rs
1 use std::ffi::OsString; 2 use std::time; 3 4 use anyhow::anyhow; 5 6 use radicle::node::{Address, Node, NodeId, PeerAddr, ROUTING_DB_FILE}; 7 use radicle::prelude::Id; 8 9 use crate::terminal as term; 10 use crate::terminal::args::{Args, Error, Help}; 11 use crate::terminal::Element as _; 12 13 #[path = "node/control.rs"] 14 mod control; 15 #[path = "node/events.rs"] 16 mod events; 17 #[path = "node/routing.rs"] 18 mod routing; 19 #[path = "node/tracking.rs"] 20 mod tracking; 21 22 pub const HELP: Help = Help { 23 name: "node", 24 description: "Control and query the Radicle Node", 25 version: env!("CARGO_PKG_VERSION"), 26 usage: r#" 27 Usage 28 29 rad node status [<option>...] 30 rad node start [--foreground] [--verbose] [<option>...] [-- <node-option>...] 31 rad node stop [<option>...] 32 rad node logs [-n <lines>] 33 rad node connect <nid>@<addr> [<option>...] 34 rad node routing [--rid <rid>] [--nid <nid>] [--json] [<option>...] 35 rad node tracking [--repos | --nodes] [<option>...] 36 rad node events [--timeout <secs>] [-n <count>] [<option>...] 37 rad node config 38 39 For `<node-option>` see `radicle-node --help`. 40 41 Start options 42 43 --foreground Start the node in the foreground 44 --verbose, -v Verbose output 45 46 Routing options 47 48 --rid <rid> Show the routing table entries for the given RID 49 --nid <nid> Show the routing table entries for the given NID 50 --json Output the routing table as json 51 52 Tracking options 53 54 --repos Show the tracked repositories table 55 --nodes Show the tracked nodes table 56 57 Events options 58 59 --timeout <secs> How long to wait to receive an event before giving up 60 --count, -n <count> Exit after <count> events 61 62 General options 63 64 --help Print help 65 "#, 66 }; 67 68 pub struct Options { 69 op: Operation, 70 } 71 72 pub enum Operation { 73 Connect { 74 addr: PeerAddr<NodeId, Address>, 75 timeout: time::Duration, 76 }, 77 Config, 78 Events { 79 timeout: time::Duration, 80 count: usize, 81 }, 82 Routing { 83 json: bool, 84 rid: Option<Id>, 85 nid: Option<NodeId>, 86 }, 87 Start { 88 foreground: bool, 89 verbose: bool, 90 options: Vec<OsString>, 91 }, 92 Logs { 93 lines: usize, 94 }, 95 Status, 96 Sessions, 97 Stop, 98 Tracking { 99 mode: TrackingMode, 100 }, 101 } 102 103 #[derive(Default)] 104 pub enum TrackingMode { 105 #[default] 106 Repos, 107 Nodes, 108 } 109 110 #[derive(Default, PartialEq, Eq)] 111 pub enum OperationName { 112 Connect, 113 Config, 114 Events, 115 Routing, 116 Logs, 117 Start, 118 #[default] 119 Status, 120 Sessions, 121 Stop, 122 Tracking, 123 } 124 125 impl Args for Options { 126 fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> { 127 use lexopt::prelude::*; 128 129 let mut foreground = false; 130 let mut options = vec![]; 131 let mut parser = lexopt::Parser::from_args(args); 132 let mut op: Option<OperationName> = None; 133 let mut tracking_mode = TrackingMode::default(); 134 let mut nid: Option<NodeId> = None; 135 let mut rid: Option<Id> = None; 136 let mut json: bool = false; 137 let mut addr: Option<PeerAddr<NodeId, Address>> = None; 138 let mut lines: usize = 60; 139 let mut count: usize = usize::MAX; 140 let mut timeout = time::Duration::MAX; 141 let mut verbose = false; 142 143 while let Some(arg) = parser.next()? { 144 match arg { 145 Long("help") | Short('h') => { 146 return Err(Error::Help.into()); 147 } 148 Value(val) if op.is_none() => match val.to_string_lossy().as_ref() { 149 "connect" => op = Some(OperationName::Connect), 150 "events" => op = Some(OperationName::Events), 151 "logs" => op = Some(OperationName::Logs), 152 "config" => op = Some(OperationName::Config), 153 "routing" => op = Some(OperationName::Routing), 154 "start" => op = Some(OperationName::Start), 155 "status" => op = Some(OperationName::Status), 156 "stop" => op = Some(OperationName::Stop), 157 "tracking" => op = Some(OperationName::Tracking), 158 "sessions" => op = Some(OperationName::Sessions), 159 160 unknown => anyhow::bail!("unknown operation '{}'", unknown), 161 }, 162 Value(val) if matches!(op, Some(OperationName::Connect)) => { 163 addr = Some(val.parse()?); 164 } 165 Long("rid") if matches!(op, Some(OperationName::Routing)) => { 166 let val = parser.value()?; 167 rid = term::args::rid(&val).ok(); 168 } 169 Long("nid") if matches!(op, Some(OperationName::Routing)) => { 170 let val = parser.value()?; 171 nid = term::args::nid(&val).ok(); 172 } 173 Long("json") if matches!(op, Some(OperationName::Routing)) => json = true, 174 Long("timeout") 175 if op == Some(OperationName::Events) || op == Some(OperationName::Connect) => 176 { 177 let val = parser.value()?; 178 timeout = term::args::seconds(&val)?; 179 } 180 Long("count") | Short('n') if matches!(op, Some(OperationName::Events)) => { 181 let val = parser.value()?; 182 count = term::args::number(&val)?; 183 } 184 Long("repos") if matches!(op, Some(OperationName::Tracking)) => { 185 tracking_mode = TrackingMode::Repos 186 } 187 Long("nodes") if matches!(op, Some(OperationName::Tracking)) => { 188 tracking_mode = TrackingMode::Nodes; 189 } 190 Long("foreground") if matches!(op, Some(OperationName::Start)) => { 191 foreground = true; 192 } 193 Long("verbose") | Short('v') if matches!(op, Some(OperationName::Start)) => { 194 verbose = true; 195 } 196 Short('n') if matches!(op, Some(OperationName::Logs)) => { 197 lines = parser.value()?.parse()?; 198 } 199 Value(val) if matches!(op, Some(OperationName::Start)) => { 200 options.push(val); 201 } 202 _ => return Err(anyhow!(arg.unexpected())), 203 } 204 } 205 206 let op = match op.unwrap_or_default() { 207 OperationName::Connect => Operation::Connect { 208 addr: addr.ok_or_else(|| { 209 anyhow!("an address of the form `<nid>@<host>:<port>` must be provided") 210 })?, 211 timeout, 212 }, 213 OperationName::Config => Operation::Config, 214 OperationName::Events => Operation::Events { timeout, count }, 215 OperationName::Routing => Operation::Routing { rid, nid, json }, 216 OperationName::Logs => Operation::Logs { lines }, 217 OperationName::Start => Operation::Start { 218 foreground, 219 verbose, 220 options, 221 }, 222 OperationName::Status => Operation::Status, 223 OperationName::Sessions => Operation::Sessions, 224 OperationName::Stop => Operation::Stop, 225 OperationName::Tracking => Operation::Tracking { 226 mode: tracking_mode, 227 }, 228 }; 229 Ok((Options { op }, vec![])) 230 } 231 } 232 233 pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { 234 let profile = ctx.profile()?; 235 let mut node = Node::new(profile.socket()); 236 237 match options.op { 238 Operation::Connect { addr, timeout } => { 239 control::connect(&mut node, addr.id, addr.addr, timeout)? 240 } 241 Operation::Config => control::config(&node)?, 242 Operation::Sessions => { 243 let sessions = control::sessions(&node)?; 244 if let Some(table) = sessions { 245 table.print(); 246 } 247 } 248 Operation::Events { timeout, count } => { 249 events::run(node, count, timeout)?; 250 } 251 Operation::Routing { rid, nid, json } => { 252 let store = 253 radicle::node::routing::Table::reader(profile.home.node().join(ROUTING_DB_FILE))?; 254 routing::run(&store, rid, nid, json)?; 255 } 256 Operation::Logs { lines } => control::logs(lines, Some(time::Duration::MAX), &profile)?, 257 Operation::Start { 258 foreground, 259 options, 260 verbose, 261 } => { 262 control::start(node, !foreground, verbose, options, &profile)?; 263 } 264 Operation::Status => { 265 control::status(&node, &profile)?; 266 } 267 Operation::Stop => { 268 control::stop(node)?; 269 } 270 Operation::Tracking { mode } => tracking::run(&profile, mode)?, 271 } 272 273 Ok(()) 274 }