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.
This commit is contained in:
cloudhead 2023-12-19 11:29:02 +01:00
parent 25ca4c8b92
commit fd38d88e14
No known key found for this signature in database
7 changed files with 64 additions and 57 deletions

View File

@ -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... <canceled>
✗ Error: failed to open node control socket "[..]/.radicle/node/control.sock" (entity not found)
✗ Hint: to start your node, run `rad node start`.
```

View File

@ -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);
}
}
}

View File

@ -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(())
}

View File

@ -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::<radicle::node::Error>() {
if e.is_connection_err() {
io::hint("to start your node, run `rad node start`.");
}
}
if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::<Error>() {
io::hint(hint);
}

View File

@ -245,7 +245,7 @@ impl radicle::node::Handle for Handle {
fn subscribe(
&self,
_timeout: time::Duration,
) -> Result<Box<dyn Iterator<Item = Result<Event, io::Error>>>, Error> {
) -> Result<Box<dyn Iterator<Item = Result<Event, Error>>>, Error> {
Ok(Box::new(self.events().into_iter().map(Ok)))
}

View File

@ -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<Box<dyn Iterator<Item = Result<Event, io::Error>>>, Self::Error> {
) -> Result<Box<dyn Iterator<Item = Result<Event, Self::Error>>>, Self::Error> {
Ok(Box::new(std::iter::empty()))
}

View File

@ -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<Box<dyn Iterator<Item = Result<Event, io::Error>>>, Self::Error>;
) -> Result<Box<dyn Iterator<Item = Result<Event, Self::Error>>>, Self::Error>;
}
/// Public node & device identifier.
@ -863,32 +859,27 @@ impl Node {
&self,
cmd: Command,
timeout: time::Duration,
) -> Result<impl Iterator<Item = Result<T, CallError>>, io::Error> {
let stream = UnixStream::connect(&self.socket)?;
) -> Result<impl Iterator<Item = Result<T, Error>>, 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<T> = json::from_str(&l).map_err(|e| Error::InvalidJson {
response: l.clone(),
error: e,
})?;
let result: CommandResult<T> =
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<Box<dyn Iterator<Item = Result<Event, io::Error>>>, Error> {
) -> Result<Box<dyn Iterator<Item = Result<Event, Error>>>, 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<Self::Sessions, Error> {