node: Simplify control socket command parsing
Instead of using a command name plus arguments, we use a new `Command` enum that is similar to the one in `radicle_node::service`. This way, we're able to have more complex commands and parsing is simplified.
This commit is contained in:
parent
60cc0a8d01
commit
840fc496b8
|
|
@ -5,7 +5,6 @@ use std::io::LineWriter;
|
||||||
use std::os::unix::net::UnixListener;
|
use std::os::unix::net::UnixListener;
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::{io, net, time};
|
use std::{io, net, time};
|
||||||
|
|
||||||
use radicle::node::Handle;
|
use radicle::node::Handle;
|
||||||
|
|
@ -13,7 +12,7 @@ use serde_json as json;
|
||||||
|
|
||||||
use crate::identity::Id;
|
use crate::identity::Id;
|
||||||
use crate::node::NodeId;
|
use crate::node::NodeId;
|
||||||
use crate::node::{Alias, Command, CommandName, CommandResult};
|
use crate::node::{Command, CommandResult};
|
||||||
use crate::runtime;
|
use crate::runtime;
|
||||||
use crate::runtime::thread;
|
use crate::runtime::thread;
|
||||||
|
|
||||||
|
|
@ -67,11 +66,7 @@ where
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
enum CommandError {
|
enum CommandError {
|
||||||
#[error("invalid command argument `{0}`, {1}")]
|
#[error("(de)serialization failed: {0}")]
|
||||||
InvalidCommandArg(String, Box<dyn std::error::Error>),
|
|
||||||
#[error("invalid command arguments `{0:?}`")]
|
|
||||||
InvalidCommandArgs(Vec<String>),
|
|
||||||
#[error("serialization failed: {0}")]
|
|
||||||
Serialization(#[from] json::Error),
|
Serialization(#[from] json::Error),
|
||||||
#[error("runtime error: {0}")]
|
#[error("runtime error: {0}")]
|
||||||
Runtime(#[from] runtime::HandleError),
|
Runtime(#[from] runtime::HandleError),
|
||||||
|
|
@ -96,106 +91,29 @@ where
|
||||||
log::debug!(target: "control", "Received `{input}` on control socket");
|
log::debug!(target: "control", "Received `{input}` on control socket");
|
||||||
let cmd: Command = json::from_str(input)?;
|
let cmd: Command = json::from_str(input)?;
|
||||||
|
|
||||||
match cmd.name {
|
match cmd {
|
||||||
CommandName::Connect => {
|
Command::Connect { addr } => {
|
||||||
let (nid, addr) = parse::args(cmd)?;
|
let (nid, addr) = addr.into();
|
||||||
if let Err(e) = handle.connect(nid, addr) {
|
if let Err(e) = handle.connect(nid, addr) {
|
||||||
return Err(CommandError::Runtime(e));
|
return Err(CommandError::Runtime(e));
|
||||||
} else {
|
} else {
|
||||||
CommandResult::Okay { updated: true }.to_writer(writer)?;
|
CommandResult::Okay { updated: true }.to_writer(writer)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CommandName::Fetch => {
|
Command::Fetch { rid, nid } => {
|
||||||
let (rid, nid): (Id, NodeId) = parse::args(cmd)?;
|
|
||||||
fetch(rid, nid, writer, &mut handle)?;
|
fetch(rid, nid, writer, &mut handle)?;
|
||||||
}
|
}
|
||||||
CommandName::Seeds => {
|
Command::Seeds { rid } => {
|
||||||
let rid: Id = parse::arg(cmd)?;
|
|
||||||
let seeds = handle.seeds(rid)?;
|
let seeds = handle.seeds(rid)?;
|
||||||
|
|
||||||
json::to_writer(writer, &seeds)?;
|
json::to_writer(writer, &seeds)?;
|
||||||
}
|
}
|
||||||
CommandName::Sessions => {
|
Command::Sessions => {
|
||||||
let sessions = handle.sessions()?;
|
let sessions = handle.sessions()?;
|
||||||
|
|
||||||
json::to_writer(writer, &sessions)?;
|
json::to_writer(writer, &sessions)?;
|
||||||
}
|
}
|
||||||
CommandName::TrackRepo => {
|
Command::TrackRepo { rid, scope } => match handle.track_repo(rid, scope) {
|
||||||
let (rid, scope) = parse::args(cmd)?;
|
|
||||||
|
|
||||||
match handle.track_repo(rid, scope) {
|
|
||||||
Ok(updated) => {
|
|
||||||
CommandResult::Okay { updated }.to_writer(writer)?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(CommandError::Runtime(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CommandName::UntrackRepo => {
|
|
||||||
let rid: Id = parse::arg(cmd)?;
|
|
||||||
|
|
||||||
match handle.untrack_repo(rid) {
|
|
||||||
Ok(updated) => {
|
|
||||||
CommandResult::Okay { updated }.to_writer(writer)?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(CommandError::Runtime(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CommandName::TrackNode => {
|
|
||||||
let (node, alias) = match cmd.args.as_slice() {
|
|
||||||
[node] => (node.as_str(), None),
|
|
||||||
[node, alias] => (node.as_str(), Some(alias.to_owned())),
|
|
||||||
_ => return Err(CommandError::InvalidCommandArgs(cmd.args)),
|
|
||||||
};
|
|
||||||
let alias = alias
|
|
||||||
.map(|a| {
|
|
||||||
Alias::from_str(&a)
|
|
||||||
.map_err(|e| CommandError::InvalidCommandArg(node.to_owned(), Box::new(e)))
|
|
||||||
})
|
|
||||||
.transpose()?;
|
|
||||||
let nid = node
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| CommandError::InvalidCommandArg(node.to_owned(), Box::new(e)))?;
|
|
||||||
|
|
||||||
match handle.track_node(nid, alias) {
|
|
||||||
Ok(updated) => {
|
|
||||||
CommandResult::Okay { updated }.to_writer(writer)?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(CommandError::Runtime(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CommandName::UntrackNode => {
|
|
||||||
let nid: NodeId = parse::arg(cmd)?;
|
|
||||||
|
|
||||||
match handle.untrack_node(nid) {
|
|
||||||
Ok(updated) => {
|
|
||||||
CommandResult::Okay { updated }.to_writer(writer)?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(CommandError::Runtime(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CommandName::AnnounceRefs => {
|
|
||||||
let rid: Id = parse::arg(cmd)?;
|
|
||||||
|
|
||||||
if let Err(e) = handle.announce_refs(rid) {
|
|
||||||
return Err(CommandError::Runtime(e));
|
|
||||||
}
|
|
||||||
CommandResult::ok().to_writer(writer).ok();
|
|
||||||
}
|
|
||||||
CommandName::AnnounceInventory => {
|
|
||||||
if let Err(e) = handle.announce_inventory() {
|
|
||||||
return Err(CommandError::Runtime(e));
|
|
||||||
}
|
|
||||||
CommandResult::ok().to_writer(writer).ok();
|
|
||||||
}
|
|
||||||
CommandName::SyncInventory => match handle.sync_inventory() {
|
|
||||||
Ok(updated) => {
|
Ok(updated) => {
|
||||||
CommandResult::Okay { updated }.to_writer(writer)?;
|
CommandResult::Okay { updated }.to_writer(writer)?;
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +121,51 @@ where
|
||||||
return Err(CommandError::Runtime(e));
|
return Err(CommandError::Runtime(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
CommandName::Subscribe => match handle.subscribe(MAX_TIMEOUT) {
|
Command::UntrackRepo { rid } => match handle.untrack_repo(rid) {
|
||||||
|
Ok(updated) => {
|
||||||
|
CommandResult::Okay { updated }.to_writer(writer)?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(CommandError::Runtime(e));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Command::TrackNode { nid, alias } => match handle.track_node(nid, alias) {
|
||||||
|
Ok(updated) => {
|
||||||
|
CommandResult::Okay { updated }.to_writer(writer)?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(CommandError::Runtime(e));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Command::UntrackNode { nid } => match handle.untrack_node(nid) {
|
||||||
|
Ok(updated) => {
|
||||||
|
CommandResult::Okay { updated }.to_writer(writer)?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(CommandError::Runtime(e));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Command::AnnounceRefs { rid } => {
|
||||||
|
if let Err(e) = handle.announce_refs(rid) {
|
||||||
|
return Err(CommandError::Runtime(e));
|
||||||
|
}
|
||||||
|
CommandResult::ok().to_writer(writer).ok();
|
||||||
|
}
|
||||||
|
Command::AnnounceInventory => {
|
||||||
|
if let Err(e) = handle.announce_inventory() {
|
||||||
|
return Err(CommandError::Runtime(e));
|
||||||
|
}
|
||||||
|
CommandResult::ok().to_writer(writer).ok();
|
||||||
|
}
|
||||||
|
Command::SyncInventory => match handle.sync_inventory() {
|
||||||
|
Ok(updated) => {
|
||||||
|
CommandResult::Okay { updated }.to_writer(writer)?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(CommandError::Runtime(e));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Command::Subscribe => match handle.subscribe(MAX_TIMEOUT) {
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
for e in events {
|
for e in events {
|
||||||
let event = e?;
|
let event = e?;
|
||||||
|
|
@ -214,16 +176,16 @@ where
|
||||||
}
|
}
|
||||||
Err(e) => log::error!(target: "control", "Error subscribing to events: {e}"),
|
Err(e) => log::error!(target: "control", "Error subscribing to events: {e}"),
|
||||||
},
|
},
|
||||||
CommandName::Status => {
|
Command::Status => {
|
||||||
CommandResult::ok().to_writer(writer).ok();
|
CommandResult::ok().to_writer(writer).ok();
|
||||||
}
|
}
|
||||||
CommandName::NodeId => match handle.nid() {
|
Command::NodeId => match handle.nid() {
|
||||||
Ok(nid) => {
|
Ok(nid) => {
|
||||||
writeln!(writer, "{nid}")?;
|
writeln!(writer, "{nid}")?;
|
||||||
}
|
}
|
||||||
Err(e) => return Err(CommandError::Runtime(e)),
|
Err(e) => return Err(CommandError::Runtime(e)),
|
||||||
},
|
},
|
||||||
CommandName::Shutdown => {
|
Command::Shutdown => {
|
||||||
log::debug!(target: "control", "Shutdown requested..");
|
log::debug!(target: "control", "Shutdown requested..");
|
||||||
// Channel might already be disconnected if shutdown
|
// Channel might already be disconnected if shutdown
|
||||||
// came from somewhere else. Ignore errors.
|
// came from somewhere else. Ignore errors.
|
||||||
|
|
@ -251,45 +213,6 @@ fn fetch<W: Write, H: Handle<Error = runtime::HandleError>>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
mod parse {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub(super) fn arg<T: FromStr>(cmd: Command) -> Result<T, CommandError>
|
|
||||||
where
|
|
||||||
<T as FromStr>::Err: std::error::Error + 'static,
|
|
||||||
{
|
|
||||||
let [arg]: [String; 1] = cmd
|
|
||||||
.args
|
|
||||||
.clone()
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| CommandError::InvalidCommandArgs(cmd.args))?;
|
|
||||||
|
|
||||||
arg.parse()
|
|
||||||
.map_err(|e| CommandError::InvalidCommandArg(arg, Box::new(e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn args<S: FromStr, T: FromStr>(cmd: Command) -> Result<(S, T), CommandError>
|
|
||||||
where
|
|
||||||
<S as FromStr>::Err: std::error::Error + 'static,
|
|
||||||
<T as FromStr>::Err: std::error::Error + 'static,
|
|
||||||
{
|
|
||||||
let [arg1, arg2]: [String; 2] = cmd
|
|
||||||
.args
|
|
||||||
.clone()
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| CommandError::InvalidCommandArgs(cmd.args))?;
|
|
||||||
|
|
||||||
let arg1 = arg1
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| CommandError::InvalidCommandArg(arg1, Box::new(e)))?;
|
|
||||||
let arg2 = arg2
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| CommandError::InvalidCommandArg(arg2, Box::new(e)))?;
|
|
||||||
|
|
||||||
Ok((arg1, arg2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
|
|
@ -299,7 +222,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::identity::Id;
|
use crate::identity::Id;
|
||||||
use crate::node::Handle;
|
use crate::node::Handle;
|
||||||
use crate::node::{Node, NodeId};
|
use crate::node::{Alias, Node, NodeId};
|
||||||
use crate::service::tracking::Scope;
|
use crate::service::tracking::Scope;
|
||||||
use crate::test;
|
use crate::test;
|
||||||
|
|
||||||
|
|
@ -308,7 +231,7 @@ mod tests {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let handle = test::handle::Handle::default();
|
let handle = test::handle::Handle::default();
|
||||||
let socket = tmp.path().join("alice.sock");
|
let socket = tmp.path().join("alice.sock");
|
||||||
let projs = test::arbitrary::set::<Id>(1..3);
|
let rids = test::arbitrary::set::<Id>(1..3);
|
||||||
let listener = UnixListener::bind(&socket).unwrap();
|
let listener = UnixListener::bind(&socket).unwrap();
|
||||||
|
|
||||||
thread::spawn({
|
thread::spawn({
|
||||||
|
|
@ -317,7 +240,7 @@ mod tests {
|
||||||
move || listen(listener, handle)
|
move || listen(listener, handle)
|
||||||
});
|
});
|
||||||
|
|
||||||
for proj in &projs {
|
for rid in &rids {
|
||||||
let stream = loop {
|
let stream = loop {
|
||||||
if let Ok(stream) = UnixStream::connect(&socket) {
|
if let Ok(stream) = UnixStream::connect(&socket) {
|
||||||
break stream;
|
break stream;
|
||||||
|
|
@ -326,7 +249,10 @@ mod tests {
|
||||||
writeln!(
|
writeln!(
|
||||||
&stream,
|
&stream,
|
||||||
"{}",
|
"{}",
|
||||||
json::to_string(&Command::new(CommandName::AnnounceRefs, [proj])).unwrap()
|
json::to_string(&Command::AnnounceRefs {
|
||||||
|
rid: rid.to_owned()
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
@ -336,8 +262,8 @@ mod tests {
|
||||||
assert_eq!(line, json::json!({ "status": "ok" }).to_string());
|
assert_eq!(line, json::json!({ "status": "ok" }).to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
for proj in &projs {
|
for rid in &rids {
|
||||||
assert!(handle.updates.lock().unwrap().contains(proj));
|
assert!(handle.updates.lock().unwrap().contains(rid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ impl radicle::node::Handle for Handle {
|
||||||
// control thread gracefully. Since the control thread may have called this function,
|
// control thread gracefully. Since the control thread may have called this function,
|
||||||
// the control socket may already be disconnected. Ignore errors.
|
// the control socket may already be disconnected. Ignore errors.
|
||||||
UnixStream::connect(self.home.socket())
|
UnixStream::connect(self.home.socket())
|
||||||
.and_then(|sock| Command::SHUTDOWN.to_writer(sock))
|
.and_then(|sock| Command::Shutdown.to_writer(sock))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
self.controller
|
self.controller
|
||||||
|
|
|
||||||
|
|
@ -276,80 +276,65 @@ impl From<net::SocketAddr> for Address {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Command name.
|
/// Command name.
|
||||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "camelCase", tag = "type")]
|
||||||
pub enum CommandName {
|
pub enum Command {
|
||||||
/// Announce repository references for given repository to peers.
|
/// Announce repository references for given repository to peers.
|
||||||
AnnounceRefs,
|
#[serde(rename_all = "camelCase")]
|
||||||
|
AnnounceRefs { rid: Id },
|
||||||
|
|
||||||
/// Announce local repositories to peers.
|
/// Announce local repositories to peers.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
AnnounceInventory,
|
AnnounceInventory,
|
||||||
|
|
||||||
/// Sync local inventory with node.
|
/// Sync local inventory with node.
|
||||||
SyncInventory,
|
SyncInventory,
|
||||||
|
|
||||||
/// Connect to node with the given address.
|
/// Connect to node with the given address.
|
||||||
Connect,
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Connect { addr: config::ConnectAddress },
|
||||||
|
|
||||||
/// Lookup seeds for the given repository in the routing table.
|
/// Lookup seeds for the given repository in the routing table.
|
||||||
Seeds,
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Seeds { rid: Id },
|
||||||
|
|
||||||
/// Get the current peer sessions.
|
/// Get the current peer sessions.
|
||||||
Sessions,
|
Sessions,
|
||||||
|
|
||||||
/// Fetch the given repository from the network.
|
/// Fetch the given repository from the network.
|
||||||
Fetch,
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Fetch { rid: Id, nid: NodeId },
|
||||||
|
|
||||||
/// Track the given repository.
|
/// Track the given repository.
|
||||||
TrackRepo,
|
#[serde(rename_all = "camelCase")]
|
||||||
|
TrackRepo { rid: Id, scope: tracking::Scope },
|
||||||
|
|
||||||
/// Untrack the given repository.
|
/// Untrack the given repository.
|
||||||
UntrackRepo,
|
#[serde(rename_all = "camelCase")]
|
||||||
|
UntrackRepo { rid: Id },
|
||||||
|
|
||||||
/// Track the given node.
|
/// Track the given node.
|
||||||
TrackNode,
|
#[serde(rename_all = "camelCase")]
|
||||||
|
TrackNode { nid: NodeId, alias: Option<Alias> },
|
||||||
|
|
||||||
/// Untrack the given node.
|
/// Untrack the given node.
|
||||||
UntrackNode,
|
#[serde(rename_all = "camelCase")]
|
||||||
|
UntrackNode { nid: NodeId },
|
||||||
|
|
||||||
/// Get the node's status.
|
/// Get the node's status.
|
||||||
Status,
|
Status,
|
||||||
|
|
||||||
/// Get the node's NID.
|
/// Get the node's NID.
|
||||||
NodeId,
|
NodeId,
|
||||||
|
|
||||||
/// Shutdown the node.
|
/// Shutdown the node.
|
||||||
Shutdown,
|
Shutdown,
|
||||||
|
|
||||||
/// Subscribe to events.
|
/// Subscribe to events.
|
||||||
Subscribe,
|
Subscribe,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for CommandName {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
// SAFETY: The enum can always be converted to a value.
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
let val = json::to_value(self).unwrap();
|
|
||||||
// SAFETY: The value is always a string.
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
let s = val.as_str().unwrap();
|
|
||||||
|
|
||||||
write!(f, "{s}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Commands sent to the node via the control socket.
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Command {
|
|
||||||
/// Command name.
|
|
||||||
#[serde(rename = "cmd")]
|
|
||||||
pub name: CommandName,
|
|
||||||
/// Command arguments.
|
|
||||||
#[serde(rename = "args")]
|
|
||||||
pub args: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
/// Shutdown command.
|
|
||||||
pub const SHUTDOWN: Self = Self {
|
|
||||||
name: CommandName::Shutdown,
|
|
||||||
args: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Create a new command.
|
|
||||||
pub fn new<T: ToString>(name: CommandName, args: impl IntoIterator<Item = T>) -> Self {
|
|
||||||
Self {
|
|
||||||
name,
|
|
||||||
args: args.into_iter().map(|a| a.to_string()).collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write this command to a stream, including a terminating LF character.
|
/// Write this command to a stream, including a terminating LF character.
|
||||||
pub fn to_writer(&self, mut w: impl io::Write) -> io::Result<()> {
|
pub fn to_writer(&self, mut w: impl io::Write) -> io::Result<()> {
|
||||||
json::to_writer(&mut w, self).map_err(|_| io::ErrorKind::InvalidInput)?;
|
json::to_writer(&mut w, self).map_err(|_| io::ErrorKind::InvalidInput)?;
|
||||||
|
|
@ -543,8 +528,8 @@ pub enum Error {
|
||||||
Call(#[from] CallError),
|
Call(#[from] CallError),
|
||||||
#[error("node: {0}")]
|
#[error("node: {0}")]
|
||||||
Node(String),
|
Node(String),
|
||||||
#[error("received empty response for `{cmd}` command")]
|
#[error("received empty response for command")]
|
||||||
EmptyResponse { cmd: CommandName },
|
EmptyResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
|
|
@ -559,9 +544,8 @@ impl Error {
|
||||||
pub enum CallError {
|
pub enum CallError {
|
||||||
#[error("i/o: {0}")]
|
#[error("i/o: {0}")]
|
||||||
Io(#[from] io::Error),
|
Io(#[from] io::Error),
|
||||||
#[error("received invalid json in response for `{cmd}` command: '{response}': {error}")]
|
#[error("received invalid json in response to command: '{response}': {error}")]
|
||||||
InvalidJson {
|
InvalidJson {
|
||||||
cmd: CommandName,
|
|
||||||
response: String,
|
response: String,
|
||||||
error: json::Error,
|
error: json::Error,
|
||||||
},
|
},
|
||||||
|
|
@ -628,21 +612,19 @@ impl Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call a command on the node.
|
/// Call a command on the node.
|
||||||
pub fn call<A: ToString, T: DeserializeOwned>(
|
pub fn call<T: DeserializeOwned>(
|
||||||
&self,
|
&self,
|
||||||
name: CommandName,
|
cmd: Command,
|
||||||
args: impl IntoIterator<Item = A>,
|
|
||||||
timeout: time::Duration,
|
timeout: time::Duration,
|
||||||
) -> Result<impl Iterator<Item = Result<T, CallError>>, io::Error> {
|
) -> Result<impl Iterator<Item = Result<T, CallError>>, io::Error> {
|
||||||
let stream = UnixStream::connect(&self.socket)?;
|
let stream = UnixStream::connect(&self.socket)?;
|
||||||
Command::new(name, args).to_writer(&stream)?;
|
cmd.to_writer(&stream)?;
|
||||||
|
|
||||||
stream.set_read_timeout(Some(timeout))?;
|
stream.set_read_timeout(Some(timeout))?;
|
||||||
|
|
||||||
Ok(BufReader::new(stream).lines().map(move |l| {
|
Ok(BufReader::new(stream).lines().map(move |l| {
|
||||||
let l = l?;
|
let l = l?;
|
||||||
let v = json::from_str(&l).map_err(|e| CallError::InvalidJson {
|
let v = json::from_str(&l).map_err(|e| CallError::InvalidJson {
|
||||||
cmd: name,
|
|
||||||
response: l,
|
response: l,
|
||||||
error: e,
|
error: e,
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -702,16 +684,14 @@ impl Handle for Node {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn nid(&self) -> Result<NodeId, Error> {
|
fn nid(&self) -> Result<NodeId, Error> {
|
||||||
self.call::<&str, NodeId>(CommandName::NodeId, [], DEFAULT_TIMEOUT)?
|
self.call::<NodeId>(Command::NodeId, DEFAULT_TIMEOUT)?
|
||||||
.next()
|
.next()
|
||||||
.ok_or(Error::EmptyResponse {
|
.ok_or(Error::EmptyResponse)?
|
||||||
cmd: CommandName::NodeId,
|
|
||||||
})?
|
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_running(&self) -> bool {
|
fn is_running(&self) -> bool {
|
||||||
let Ok(mut lines) = self.call::<&str, CommandResult>(CommandName::Status, [], DEFAULT_TIMEOUT) else {
|
let Ok(mut lines) = self.call::<CommandResult>(Command::Status, DEFAULT_TIMEOUT) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let Some(Ok(result)) = lines.next() else {
|
let Some(Ok(result)) = lines.next() else {
|
||||||
|
|
@ -721,115 +701,81 @@ impl Handle for Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect(&mut self, nid: NodeId, addr: Address) -> Result<(), Error> {
|
fn connect(&mut self, nid: NodeId, addr: Address) -> Result<(), Error> {
|
||||||
self.call::<_, CommandResult>(
|
self.call::<CommandResult>(
|
||||||
CommandName::Connect,
|
Command::Connect {
|
||||||
[nid.to_human(), addr.to_string()],
|
addr: (nid, addr).into(),
|
||||||
|
},
|
||||||
DEFAULT_TIMEOUT,
|
DEFAULT_TIMEOUT,
|
||||||
)?
|
)?
|
||||||
.next()
|
.next()
|
||||||
.ok_or(Error::EmptyResponse {
|
.ok_or(Error::EmptyResponse)??;
|
||||||
cmd: CommandName::Connect,
|
|
||||||
})??;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seeds(&mut self, id: Id) -> Result<Seeds, Error> {
|
fn seeds(&mut self, rid: Id) -> Result<Seeds, Error> {
|
||||||
let seeds: Seeds = self
|
let seeds: Seeds = self
|
||||||
.call(CommandName::Seeds, [id.urn()], DEFAULT_TIMEOUT)?
|
.call(Command::Seeds { rid }, DEFAULT_TIMEOUT)?
|
||||||
.next()
|
.next()
|
||||||
.ok_or(Error::EmptyResponse {
|
.ok_or(Error::EmptyResponse)??;
|
||||||
cmd: CommandName::Seeds,
|
|
||||||
})??;
|
|
||||||
|
|
||||||
Ok(seeds)
|
Ok(seeds)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch(&mut self, id: Id, from: NodeId) -> Result<FetchResult, Error> {
|
fn fetch(&mut self, rid: Id, from: NodeId) -> Result<FetchResult, Error> {
|
||||||
let result = self
|
let result = self
|
||||||
.call(
|
.call(Command::Fetch { rid, nid: from }, DEFAULT_TIMEOUT)?
|
||||||
CommandName::Fetch,
|
|
||||||
[id.urn(), from.to_human()],
|
|
||||||
DEFAULT_TIMEOUT,
|
|
||||||
)?
|
|
||||||
.next()
|
.next()
|
||||||
.ok_or(Error::EmptyResponse {
|
.ok_or(Error::EmptyResponse)??;
|
||||||
cmd: CommandName::Fetch,
|
|
||||||
})??;
|
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn track_node(&mut self, id: NodeId, alias: Option<Alias>) -> Result<bool, Error> {
|
fn track_node(&mut self, nid: NodeId, alias: Option<Alias>) -> Result<bool, Error> {
|
||||||
let id = id.to_human();
|
let mut line = self.call(Command::TrackNode { nid, alias }, DEFAULT_TIMEOUT)?;
|
||||||
let args = if let Some(alias) = alias.as_deref() {
|
let response: CommandResult = line.next().ok_or(Error::EmptyResponse)??;
|
||||||
vec![id.as_str(), alias]
|
|
||||||
} else {
|
|
||||||
vec![id.as_str()]
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut line = self.call(CommandName::TrackNode, args, DEFAULT_TIMEOUT)?;
|
|
||||||
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {
|
|
||||||
cmd: CommandName::TrackNode,
|
|
||||||
})??;
|
|
||||||
|
|
||||||
response.into()
|
response.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn track_repo(&mut self, id: Id, scope: tracking::Scope) -> Result<bool, Error> {
|
fn track_repo(&mut self, rid: Id, scope: tracking::Scope) -> Result<bool, Error> {
|
||||||
let mut line = self.call(
|
let mut line = self.call(Command::TrackRepo { rid, scope }, DEFAULT_TIMEOUT)?;
|
||||||
CommandName::TrackRepo,
|
let response: CommandResult = line.next().ok_or(Error::EmptyResponse)??;
|
||||||
[id.urn(), scope.to_string()],
|
|
||||||
DEFAULT_TIMEOUT,
|
|
||||||
)?;
|
|
||||||
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {
|
|
||||||
cmd: CommandName::TrackRepo,
|
|
||||||
})??;
|
|
||||||
|
|
||||||
response.into()
|
response.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn untrack_node(&mut self, id: NodeId) -> Result<bool, Error> {
|
fn untrack_node(&mut self, nid: NodeId) -> Result<bool, Error> {
|
||||||
let mut line = self.call(CommandName::UntrackNode, [id], DEFAULT_TIMEOUT)?;
|
let mut line = self.call(Command::UntrackNode { nid }, DEFAULT_TIMEOUT)?;
|
||||||
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {
|
let response: CommandResult = line.next().ok_or(Error::EmptyResponse)??;
|
||||||
cmd: CommandName::UntrackNode,
|
|
||||||
})??;
|
|
||||||
|
|
||||||
response.into()
|
response.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn untrack_repo(&mut self, id: Id) -> Result<bool, Error> {
|
fn untrack_repo(&mut self, rid: Id) -> Result<bool, Error> {
|
||||||
let mut line = self.call(CommandName::UntrackRepo, [id.urn()], DEFAULT_TIMEOUT)?;
|
let mut line = self.call(Command::UntrackRepo { rid }, DEFAULT_TIMEOUT)?;
|
||||||
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {
|
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {})??;
|
||||||
cmd: CommandName::UntrackRepo,
|
|
||||||
})??;
|
|
||||||
|
|
||||||
response.into()
|
response.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn announce_refs(&mut self, id: Id) -> Result<(), Error> {
|
fn announce_refs(&mut self, rid: Id) -> Result<(), Error> {
|
||||||
for line in
|
for line in self.call::<CommandResult>(Command::AnnounceRefs { rid }, DEFAULT_TIMEOUT)? {
|
||||||
self.call::<_, CommandResult>(CommandName::AnnounceRefs, [id.urn()], DEFAULT_TIMEOUT)?
|
|
||||||
{
|
|
||||||
line?;
|
line?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn announce_inventory(&mut self) -> Result<(), Error> {
|
fn announce_inventory(&mut self) -> Result<(), Error> {
|
||||||
for line in
|
for line in self.call::<CommandResult>(Command::AnnounceInventory, DEFAULT_TIMEOUT)? {
|
||||||
self.call::<&str, CommandResult>(CommandName::AnnounceInventory, [], DEFAULT_TIMEOUT)?
|
|
||||||
{
|
|
||||||
line?;
|
line?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_inventory(&mut self) -> Result<bool, Error> {
|
fn sync_inventory(&mut self) -> Result<bool, Error> {
|
||||||
let mut line = self.call::<&str, _>(CommandName::SyncInventory, [], DEFAULT_TIMEOUT)?;
|
let mut line = self.call(Command::SyncInventory, DEFAULT_TIMEOUT)?;
|
||||||
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {
|
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {})??;
|
||||||
cmd: CommandName::SyncInventory,
|
|
||||||
})??;
|
|
||||||
|
|
||||||
response.into()
|
response.into()
|
||||||
}
|
}
|
||||||
|
|
@ -838,7 +784,7 @@ impl Handle for Node {
|
||||||
&self,
|
&self,
|
||||||
timeout: time::Duration,
|
timeout: time::Duration,
|
||||||
) -> Result<Box<dyn Iterator<Item = Result<Event, io::Error>>>, Error> {
|
) -> Result<Box<dyn Iterator<Item = Result<Event, io::Error>>>, Error> {
|
||||||
let events = self.call::<&str, _>(CommandName::Subscribe, [], timeout)?;
|
let events = self.call(Command::Subscribe, timeout)?;
|
||||||
|
|
||||||
Ok(Box::new(events.map(|e| {
|
Ok(Box::new(events.map(|e| {
|
||||||
e.map_err(|err| match err {
|
e.map_err(|err| match err {
|
||||||
|
|
@ -852,17 +798,15 @@ impl Handle for Node {
|
||||||
|
|
||||||
fn sessions(&self) -> Result<Self::Sessions, Error> {
|
fn sessions(&self) -> Result<Self::Sessions, Error> {
|
||||||
let sessions = self
|
let sessions = self
|
||||||
.call::<&str, Vec<Session>>(CommandName::Sessions, [], DEFAULT_TIMEOUT)?
|
.call::<Vec<Session>>(Command::Sessions, DEFAULT_TIMEOUT)?
|
||||||
.next()
|
.next()
|
||||||
.ok_or(Error::EmptyResponse {
|
.ok_or(Error::EmptyResponse {})??;
|
||||||
cmd: CommandName::Sessions,
|
|
||||||
})??;
|
|
||||||
|
|
||||||
Ok(sessions)
|
Ok(sessions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown(self) -> Result<(), Error> {
|
fn shutdown(self) -> Result<(), Error> {
|
||||||
for line in self.call::<&str, CommandResult>(CommandName::Shutdown, [], DEFAULT_TIMEOUT)? {
|
for line in self.call::<CommandResult>(Command::Shutdown, DEFAULT_TIMEOUT)? {
|
||||||
line?;
|
line?;
|
||||||
}
|
}
|
||||||
// Wait until the shutdown has completed.
|
// Wait until the shutdown has completed.
|
||||||
|
|
@ -901,11 +845,6 @@ impl AliasStore for HashMap<NodeId, Alias> {
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_command_name_display() {
|
|
||||||
assert_eq!(CommandName::TrackNode.to_string(), "track-node");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_alias() {
|
fn test_alias() {
|
||||||
assert!(Alias::from_str("cloudhead").is_ok());
|
assert!(Alias::from_str("cloudhead").is_ok());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue