From fd38d88e14027698497dc724b529d4a69f5abdca Mon Sep 17 00:00:00 2001 From: cloudhead Date: Tue, 19 Dec 2023 11:29:02 +0100 Subject: [PATCH] cli: Hint about how to start node In some cases, we can hint to the user about how they can start their node. We also remove the `CallError` type that was redundant. --- radicle-cli/examples/rad-node.md | 18 +++++ radicle-cli/src/commands/init.rs | 6 +- radicle-cli/src/commands/node/control.rs | 2 +- radicle-cli/src/terminal.rs | 6 ++ radicle-node/src/runtime/handle.rs | 2 +- radicle-node/src/test/handle.rs | 4 +- radicle/src/node.rs | 83 ++++++++++-------------- 7 files changed, 64 insertions(+), 57 deletions(-) diff --git a/radicle-cli/examples/rad-node.md b/radicle-cli/examples/rad-node.md index 035a9b32..d1cb7385 100644 --- a/radicle-cli/examples/rad-node.md +++ b/radicle-cli/examples/rad-node.md @@ -80,3 +80,21 @@ Running the command again gives us an error: $ rad node stop ✗ Stopping node... error: node is not running ``` + +Some commands also give us a hint if the node isn't running: + +``` (fail) +$ rad node events +✗ Error: failed to open node control socket "[..]/.radicle/node/control.sock" (entity not found) +✗ Hint: to start your node, run `rad node start`. +``` +``` (fail) +$ rad sync status +✗ Error: to sync a repository, your node must be running. To start it, run `rad node start` +``` +``` (fail) +$ rad node connect z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk@radicle.xyz:8776 +✗ Connecting to z6Mkt67…v4N1tRk@radicle.xyz:8776... +✗ Error: failed to open node control socket "[..]/.radicle/node/control.sock" (entity not found) +✗ Hint: to start your node, run `rad node start`. +``` diff --git a/radicle-cli/src/commands/init.rs b/radicle-cli/src/commands/init.rs index 813c0999..2c51506b 100644 --- a/radicle-cli/src/commands/init.rs +++ b/radicle-cli/src/commands/init.rs @@ -5,7 +5,7 @@ use std::convert::TryFrom; use std::ffi::OsString; use std::path::PathBuf; use std::str::FromStr; -use std::{env, io, time}; +use std::{env, time}; use anyhow::{anyhow, bail, Context as _}; use serde_json as json; @@ -403,12 +403,12 @@ fn sync( Ok(_) => { // Some other irrelevant event received. } - Err(e) if e.kind() == io::ErrorKind::TimedOut => { + Err(radicle::node::Error::TimedOut) => { break; } Err(e) => { spinner.error(&e); - return Err(e.into()); + return Err(e); } } } diff --git a/radicle-cli/src/commands/node/control.rs b/radicle-cli/src/commands/node/control.rs index e037367a..0b8b7d05 100644 --- a/radicle-cli/src/commands/node/control.rs +++ b/radicle-cli/src/commands/node/control.rs @@ -181,7 +181,7 @@ pub fn connect( ) { Ok(ConnectResult::Connected) => spinner.finish(), Ok(ConnectResult::Disconnected { reason }) => spinner.error(reason), - Err(err) => spinner.error(err.to_string()), + Err(err) => return Err(err.into()), } Ok(()) } diff --git a/radicle-cli/src/terminal.rs b/radicle-cli/src/terminal.rs index 0e5339d3..3d64ca68 100644 --- a/radicle-cli/src/terminal.rs +++ b/radicle-cli/src/terminal.rs @@ -139,6 +139,12 @@ pub fn fail(_name: &str, error: &anyhow::Error) { io::error(line); } + // Catch common node errors, and offer a hint. + if let Some(e) = error.downcast_ref::() { + if e.is_connection_err() { + io::hint("to start your node, run `rad node start`."); + } + } if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::() { io::hint(hint); } diff --git a/radicle-node/src/runtime/handle.rs b/radicle-node/src/runtime/handle.rs index 00e80889..234747e0 100644 --- a/radicle-node/src/runtime/handle.rs +++ b/radicle-node/src/runtime/handle.rs @@ -245,7 +245,7 @@ impl radicle::node::Handle for Handle { fn subscribe( &self, _timeout: time::Duration, - ) -> Result>>, Error> { + ) -> Result>>, Error> { Ok(Box::new(self.events().into_iter().map(Ok))) } diff --git a/radicle-node/src/test/handle.rs b/radicle-node/src/test/handle.rs index 79b7cd6e..1c06b32e 100644 --- a/radicle-node/src/test/handle.rs +++ b/radicle-node/src/test/handle.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::str::FromStr; use std::sync::{Arc, Mutex}; -use std::{io, time}; +use std::time; use radicle::git; use radicle::storage::refs::RefsAt; @@ -75,7 +75,7 @@ impl radicle::node::Handle for Handle { fn subscribe( &self, _timeout: time::Duration, - ) -> Result>>, Self::Error> { + ) -> Result>>, Self::Error> { Ok(Box::new(std::iter::empty())) } diff --git a/radicle/src/node.rs b/radicle/src/node.rs index 7629ee46..363647c1 100644 --- a/radicle/src/node.rs +++ b/radicle/src/node.rs @@ -1,3 +1,4 @@ +#![allow(clippy::type_complexity)] mod features; pub mod address; @@ -748,28 +749,14 @@ impl IntoIterator for FetchResults { /// Error returned by [`Handle`] functions. #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("failed to connect to node: {0}")] - Connect(#[from] io::Error), - #[error("failed to call node: {0}")] - Call(#[from] CallError), - #[error("node: {0}")] - Node(String), - #[error("received empty response for command")] - EmptyResponse, -} - -impl Error { - /// Check if the error is due to the not being able to connect to the local node. - pub fn is_connection_err(&self) -> bool { - matches!(self, Self::Connect(_)) - } -} - -/// Error returned by [`Node::call`] iterator. -#[derive(thiserror::Error, Debug)] -pub enum CallError { #[error("i/o: {0}")] Io(#[from] io::Error), + #[error("node: {0}")] + Node(String), + #[error("timed out reading from control socket")] + TimedOut, + #[error("failed to open node control socket {0:?} ({1})")] + Connect(PathBuf, io::ErrorKind), #[error("command error: {reason}")] Command { reason: String }, #[error("received invalid json `{response}` in response to command: {error}")] @@ -777,6 +764,15 @@ pub enum CallError { response: String, error: json::Error, }, + #[error("received empty response for command")] + EmptyResponse, +} + +impl Error { + /// Check if the error is due to the not being able to connect to the local node. + pub fn is_connection_err(&self) -> bool { + matches!(self, Self::Connect { .. }) + } } #[derive(Debug, Serialize, Deserialize)] @@ -838,7 +834,7 @@ pub trait Handle: Clone + Sync + Send { fn subscribe( &self, timeout: time::Duration, - ) -> Result>>, Self::Error>; + ) -> Result>>, Self::Error>; } /// Public node & device identifier. @@ -863,32 +859,27 @@ impl Node { &self, cmd: Command, timeout: time::Duration, - ) -> Result>, io::Error> { - let stream = UnixStream::connect(&self.socket)?; + ) -> Result>, Error> { + let stream = UnixStream::connect(&self.socket) + .map_err(|e| Error::Connect(self.socket.clone(), e.kind()))?; cmd.to_writer(&stream)?; stream.set_read_timeout(Some(timeout))?; Ok(BufReader::new(stream).lines().map(move |l| { - let l = l.map_err(|e| { - if e.kind() == io::ErrorKind::WouldBlock { - io::Error::new( - io::ErrorKind::TimedOut, - "timed out reading from node control socket", - ) - } else { - e - } + let l = l.map_err(|e| match e.kind() { + io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut => Error::TimedOut, + _ => Error::Io(e), + })?; + + let result: CommandResult = json::from_str(&l).map_err(|e| Error::InvalidJson { + response: l.clone(), + error: e, })?; - let result: CommandResult = - json::from_str(&l).map_err(|e| CallError::InvalidJson { - response: l.clone(), - error: e, - })?; match result { CommandResult::Okay(result) => Ok(result), - CommandResult::Error { reason } => Err(CallError::Command { reason }), + CommandResult::Error { reason } => Err(Error::Command { reason }), } })) } @@ -928,11 +919,11 @@ impl Node { } Ok(_) => {} - Err(e) if e.kind() == io::ErrorKind::TimedOut => { + Err(Error::TimedOut) => { timeout.extend(seeds.iter()); break; } - Err(e) => return Err(e.into()), + Err(e) => return Err(e), } if seeds.is_empty() { break; @@ -1077,18 +1068,10 @@ impl Handle for Node { fn subscribe( &self, timeout: time::Duration, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let events = self.call(Command::Subscribe, timeout)?; - Ok(Box::new(events.map(|e| { - e.map_err(|err| match err { - CallError::Io(e) => e, - CallError::InvalidJson { .. } => { - io::Error::new(io::ErrorKind::InvalidInput, err.to_string()) - } - CallError::Command { reason } => io::Error::new(io::ErrorKind::Other, reason), - }) - }))) + Ok(Box::new(events)) } fn sessions(&self) -> Result {