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:
Alexis Sellier 2023-07-24 21:53:16 +02:00
parent 60cc0a8d01
commit 840fc496b8
No known key found for this signature in database
3 changed files with 142 additions and 277 deletions

View File

@ -5,7 +5,6 @@ use std::io::LineWriter;
use std::os::unix::net::UnixListener;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::str::FromStr;
use std::{io, net, time};
use radicle::node::Handle;
@ -13,7 +12,7 @@ use serde_json as json;
use crate::identity::Id;
use crate::node::NodeId;
use crate::node::{Alias, Command, CommandName, CommandResult};
use crate::node::{Command, CommandResult};
use crate::runtime;
use crate::runtime::thread;
@ -67,11 +66,7 @@ where
#[derive(thiserror::Error, Debug)]
enum CommandError {
#[error("invalid command argument `{0}`, {1}")]
InvalidCommandArg(String, Box<dyn std::error::Error>),
#[error("invalid command arguments `{0:?}`")]
InvalidCommandArgs(Vec<String>),
#[error("serialization failed: {0}")]
#[error("(de)serialization failed: {0}")]
Serialization(#[from] json::Error),
#[error("runtime error: {0}")]
Runtime(#[from] runtime::HandleError),
@ -96,106 +91,29 @@ where
log::debug!(target: "control", "Received `{input}` on control socket");
let cmd: Command = json::from_str(input)?;
match cmd.name {
CommandName::Connect => {
let (nid, addr) = parse::args(cmd)?;
match cmd {
Command::Connect { addr } => {
let (nid, addr) = addr.into();
if let Err(e) = handle.connect(nid, addr) {
return Err(CommandError::Runtime(e));
} else {
CommandResult::Okay { updated: true }.to_writer(writer)?;
}
}
CommandName::Fetch => {
let (rid, nid): (Id, NodeId) = parse::args(cmd)?;
Command::Fetch { rid, nid } => {
fetch(rid, nid, writer, &mut handle)?;
}
CommandName::Seeds => {
let rid: Id = parse::arg(cmd)?;
Command::Seeds { rid } => {
let seeds = handle.seeds(rid)?;
json::to_writer(writer, &seeds)?;
}
CommandName::Sessions => {
Command::Sessions => {
let sessions = handle.sessions()?;
json::to_writer(writer, &sessions)?;
}
CommandName::TrackRepo => {
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() {
Command::TrackRepo { rid, scope } => match handle.track_repo(rid, scope) {
Ok(updated) => {
CommandResult::Okay { updated }.to_writer(writer)?;
}
@ -203,7 +121,51 @@ where
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) => {
for e in events {
let event = e?;
@ -214,16 +176,16 @@ where
}
Err(e) => log::error!(target: "control", "Error subscribing to events: {e}"),
},
CommandName::Status => {
Command::Status => {
CommandResult::ok().to_writer(writer).ok();
}
CommandName::NodeId => match handle.nid() {
Command::NodeId => match handle.nid() {
Ok(nid) => {
writeln!(writer, "{nid}")?;
}
Err(e) => return Err(CommandError::Runtime(e)),
},
CommandName::Shutdown => {
Command::Shutdown => {
log::debug!(target: "control", "Shutdown requested..");
// Channel might already be disconnected if shutdown
// came from somewhere else. Ignore errors.
@ -251,45 +213,6 @@ fn fetch<W: Write, H: Handle<Error = runtime::HandleError>>(
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)]
mod tests {
use std::io::prelude::*;
@ -299,7 +222,7 @@ mod tests {
use super::*;
use crate::identity::Id;
use crate::node::Handle;
use crate::node::{Node, NodeId};
use crate::node::{Alias, Node, NodeId};
use crate::service::tracking::Scope;
use crate::test;
@ -308,7 +231,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let handle = test::handle::Handle::default();
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();
thread::spawn({
@ -317,7 +240,7 @@ mod tests {
move || listen(listener, handle)
});
for proj in &projs {
for rid in &rids {
let stream = loop {
if let Ok(stream) = UnixStream::connect(&socket) {
break stream;
@ -326,7 +249,10 @@ mod tests {
writeln!(
&stream,
"{}",
json::to_string(&Command::new(CommandName::AnnounceRefs, [proj])).unwrap()
json::to_string(&Command::AnnounceRefs {
rid: rid.to_owned()
})
.unwrap()
)
.unwrap();
@ -336,8 +262,8 @@ mod tests {
assert_eq!(line, json::json!({ "status": "ok" }).to_string());
}
for proj in &projs {
assert!(handle.updates.lock().unwrap().contains(proj));
for rid in &rids {
assert!(handle.updates.lock().unwrap().contains(rid));
}
}

View File

@ -245,7 +245,7 @@ impl radicle::node::Handle for Handle {
// control thread gracefully. Since the control thread may have called this function,
// the control socket may already be disconnected. Ignore errors.
UnixStream::connect(self.home.socket())
.and_then(|sock| Command::SHUTDOWN.to_writer(sock))
.and_then(|sock| Command::Shutdown.to_writer(sock))
.ok();
self.controller

View File

@ -276,80 +276,65 @@ impl From<net::SocketAddr> for Address {
}
/// Command name.
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CommandName {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum Command {
/// Announce repository references for given repository to peers.
AnnounceRefs,
#[serde(rename_all = "camelCase")]
AnnounceRefs { rid: Id },
/// Announce local repositories to peers.
#[serde(rename_all = "camelCase")]
AnnounceInventory,
/// Sync local inventory with node.
SyncInventory,
/// 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.
Seeds,
#[serde(rename_all = "camelCase")]
Seeds { rid: Id },
/// Get the current peer sessions.
Sessions,
/// Fetch the given repository from the network.
Fetch,
#[serde(rename_all = "camelCase")]
Fetch { rid: Id, nid: NodeId },
/// Track the given repository.
TrackRepo,
#[serde(rename_all = "camelCase")]
TrackRepo { rid: Id, scope: tracking::Scope },
/// Untrack the given repository.
UntrackRepo,
#[serde(rename_all = "camelCase")]
UntrackRepo { rid: Id },
/// Track the given node.
TrackNode,
#[serde(rename_all = "camelCase")]
TrackNode { nid: NodeId, alias: Option<Alias> },
/// Untrack the given node.
UntrackNode,
#[serde(rename_all = "camelCase")]
UntrackNode { nid: NodeId },
/// Get the node's status.
Status,
/// Get the node's NID.
NodeId,
/// Shutdown the node.
Shutdown,
/// Subscribe to events.
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 {
/// 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.
pub fn to_writer(&self, mut w: impl io::Write) -> io::Result<()> {
json::to_writer(&mut w, self).map_err(|_| io::ErrorKind::InvalidInput)?;
@ -543,8 +528,8 @@ pub enum Error {
Call(#[from] CallError),
#[error("node: {0}")]
Node(String),
#[error("received empty response for `{cmd}` command")]
EmptyResponse { cmd: CommandName },
#[error("received empty response for command")]
EmptyResponse,
}
impl Error {
@ -559,9 +544,8 @@ impl Error {
pub enum CallError {
#[error("i/o: {0}")]
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 {
cmd: CommandName,
response: String,
error: json::Error,
},
@ -628,21 +612,19 @@ impl Node {
}
/// Call a command on the node.
pub fn call<A: ToString, T: DeserializeOwned>(
pub fn call<T: DeserializeOwned>(
&self,
name: CommandName,
args: impl IntoIterator<Item = A>,
cmd: Command,
timeout: time::Duration,
) -> Result<impl Iterator<Item = Result<T, CallError>>, io::Error> {
let stream = UnixStream::connect(&self.socket)?;
Command::new(name, args).to_writer(&stream)?;
cmd.to_writer(&stream)?;
stream.set_read_timeout(Some(timeout))?;
Ok(BufReader::new(stream).lines().map(move |l| {
let l = l?;
let v = json::from_str(&l).map_err(|e| CallError::InvalidJson {
cmd: name,
response: l,
error: e,
})?;
@ -702,16 +684,14 @@ impl Handle for Node {
type Error = Error;
fn nid(&self) -> Result<NodeId, Error> {
self.call::<&str, NodeId>(CommandName::NodeId, [], DEFAULT_TIMEOUT)?
self.call::<NodeId>(Command::NodeId, DEFAULT_TIMEOUT)?
.next()
.ok_or(Error::EmptyResponse {
cmd: CommandName::NodeId,
})?
.ok_or(Error::EmptyResponse)?
.map_err(Error::from)
}
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;
};
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> {
self.call::<_, CommandResult>(
CommandName::Connect,
[nid.to_human(), addr.to_string()],
self.call::<CommandResult>(
Command::Connect {
addr: (nid, addr).into(),
},
DEFAULT_TIMEOUT,
)?
.next()
.ok_or(Error::EmptyResponse {
cmd: CommandName::Connect,
})??;
.ok_or(Error::EmptyResponse)??;
Ok(())
}
fn seeds(&mut self, id: Id) -> Result<Seeds, Error> {
fn seeds(&mut self, rid: Id) -> Result<Seeds, Error> {
let seeds: Seeds = self
.call(CommandName::Seeds, [id.urn()], DEFAULT_TIMEOUT)?
.call(Command::Seeds { rid }, DEFAULT_TIMEOUT)?
.next()
.ok_or(Error::EmptyResponse {
cmd: CommandName::Seeds,
})??;
.ok_or(Error::EmptyResponse)??;
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
.call(
CommandName::Fetch,
[id.urn(), from.to_human()],
DEFAULT_TIMEOUT,
)?
.call(Command::Fetch { rid, nid: from }, DEFAULT_TIMEOUT)?
.next()
.ok_or(Error::EmptyResponse {
cmd: CommandName::Fetch,
})??;
.ok_or(Error::EmptyResponse)??;
Ok(result)
}
fn track_node(&mut self, id: NodeId, alias: Option<Alias>) -> Result<bool, Error> {
let id = id.to_human();
let args = if let Some(alias) = alias.as_deref() {
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,
})??;
fn track_node(&mut self, nid: NodeId, alias: Option<Alias>) -> Result<bool, Error> {
let mut line = self.call(Command::TrackNode { nid, alias }, DEFAULT_TIMEOUT)?;
let response: CommandResult = line.next().ok_or(Error::EmptyResponse)??;
response.into()
}
fn track_repo(&mut self, id: Id, scope: tracking::Scope) -> Result<bool, Error> {
let mut line = self.call(
CommandName::TrackRepo,
[id.urn(), scope.to_string()],
DEFAULT_TIMEOUT,
)?;
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {
cmd: CommandName::TrackRepo,
})??;
fn track_repo(&mut self, rid: Id, scope: tracking::Scope) -> Result<bool, Error> {
let mut line = self.call(Command::TrackRepo { rid, scope }, DEFAULT_TIMEOUT)?;
let response: CommandResult = line.next().ok_or(Error::EmptyResponse)??;
response.into()
}
fn untrack_node(&mut self, id: NodeId) -> Result<bool, Error> {
let mut line = self.call(CommandName::UntrackNode, [id], DEFAULT_TIMEOUT)?;
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {
cmd: CommandName::UntrackNode,
})??;
fn untrack_node(&mut self, nid: NodeId) -> Result<bool, Error> {
let mut line = self.call(Command::UntrackNode { nid }, DEFAULT_TIMEOUT)?;
let response: CommandResult = line.next().ok_or(Error::EmptyResponse)??;
response.into()
}
fn untrack_repo(&mut self, id: Id) -> Result<bool, Error> {
let mut line = self.call(CommandName::UntrackRepo, [id.urn()], DEFAULT_TIMEOUT)?;
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {
cmd: CommandName::UntrackRepo,
})??;
fn untrack_repo(&mut self, rid: Id) -> Result<bool, Error> {
let mut line = self.call(Command::UntrackRepo { rid }, DEFAULT_TIMEOUT)?;
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {})??;
response.into()
}
fn announce_refs(&mut self, id: Id) -> Result<(), Error> {
for line in
self.call::<_, CommandResult>(CommandName::AnnounceRefs, [id.urn()], DEFAULT_TIMEOUT)?
{
fn announce_refs(&mut self, rid: Id) -> Result<(), Error> {
for line in self.call::<CommandResult>(Command::AnnounceRefs { rid }, DEFAULT_TIMEOUT)? {
line?;
}
Ok(())
}
fn announce_inventory(&mut self) -> Result<(), Error> {
for line in
self.call::<&str, CommandResult>(CommandName::AnnounceInventory, [], DEFAULT_TIMEOUT)?
{
for line in self.call::<CommandResult>(Command::AnnounceInventory, DEFAULT_TIMEOUT)? {
line?;
}
Ok(())
}
fn sync_inventory(&mut self) -> Result<bool, Error> {
let mut line = self.call::<&str, _>(CommandName::SyncInventory, [], DEFAULT_TIMEOUT)?;
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {
cmd: CommandName::SyncInventory,
})??;
let mut line = self.call(Command::SyncInventory, DEFAULT_TIMEOUT)?;
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {})??;
response.into()
}
@ -838,7 +784,7 @@ impl Handle for Node {
&self,
timeout: time::Duration,
) -> 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| {
e.map_err(|err| match err {
@ -852,17 +798,15 @@ impl Handle for Node {
fn sessions(&self) -> Result<Self::Sessions, Error> {
let sessions = self
.call::<&str, Vec<Session>>(CommandName::Sessions, [], DEFAULT_TIMEOUT)?
.call::<Vec<Session>>(Command::Sessions, DEFAULT_TIMEOUT)?
.next()
.ok_or(Error::EmptyResponse {
cmd: CommandName::Sessions,
})??;
.ok_or(Error::EmptyResponse {})??;
Ok(sessions)
}
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?;
}
// Wait until the shutdown has completed.
@ -901,11 +845,6 @@ impl AliasStore for HashMap<NodeId, Alias> {
mod test {
use super::*;
#[test]
fn test_command_name_display() {
assert_eq!(CommandName::TrackNode.to_string(), "track-node");
}
#[test]
fn test_alias() {
assert!(Alias::from_str("cloudhead").is_ok());