node: Add client control socket
Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
parent
e663e8c516
commit
fc30dc2668
|
|
@ -10,6 +10,9 @@ use crate::crypto::Signer;
|
||||||
use crate::protocol;
|
use crate::protocol;
|
||||||
use crate::storage::git::Storage;
|
use crate::storage::git::Storage;
|
||||||
|
|
||||||
|
pub mod handle;
|
||||||
|
pub mod socket;
|
||||||
|
|
||||||
/// Client configuration.
|
/// Client configuration.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
|
@ -101,6 +104,16 @@ impl<R: Reactor> Client<R> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new handle to communicate with the client.
|
||||||
|
pub fn handle(&self) -> handle::Handle<R> {
|
||||||
|
handle::Handle {
|
||||||
|
waker: self.reactor.waker(),
|
||||||
|
commands: self.handle.clone(),
|
||||||
|
shutdown: self.shutdown.clone(),
|
||||||
|
listening: self.listening.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Events {}
|
pub struct Events {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
use std::net;
|
||||||
|
|
||||||
|
use crossbeam_channel as chan;
|
||||||
|
use nakamoto_net::Reactor;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::identity::ProjId;
|
||||||
|
use crate::protocol;
|
||||||
|
use crate::protocol::CommandError;
|
||||||
|
|
||||||
|
/// An error resulting from a handle method.
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// The command channel is no longer connected.
|
||||||
|
#[error("command channel is not connected")]
|
||||||
|
NotConnected,
|
||||||
|
/// The command returned an error.
|
||||||
|
#[error("command failed: {0}")]
|
||||||
|
Command(#[from] CommandError),
|
||||||
|
/// The operation timed out.
|
||||||
|
#[error("the operation timed out")]
|
||||||
|
Timeout,
|
||||||
|
/// An I/O error occured.
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<chan::RecvError> for Error {
|
||||||
|
fn from(_: chan::RecvError) -> Self {
|
||||||
|
Self::NotConnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<chan::RecvTimeoutError> for Error {
|
||||||
|
fn from(err: chan::RecvTimeoutError) -> Self {
|
||||||
|
match err {
|
||||||
|
chan::RecvTimeoutError::Timeout => Self::Timeout,
|
||||||
|
chan::RecvTimeoutError::Disconnected => Self::NotConnected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<chan::SendError<T>> for Error {
|
||||||
|
fn from(_: chan::SendError<T>) -> Self {
|
||||||
|
Self::NotConnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Handle<R: Reactor> {
|
||||||
|
pub(crate) commands: chan::Sender<protocol::Command>,
|
||||||
|
pub(crate) waker: R::Waker,
|
||||||
|
pub(crate) shutdown: chan::Sender<()>,
|
||||||
|
pub(crate) listening: chan::Receiver<net::SocketAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Reactor> Handle<R> {
|
||||||
|
/// Notify the client that a project has been updated.
|
||||||
|
pub fn updated(&self, id: ProjId) -> Result<(), Error> {
|
||||||
|
self.command(protocol::Command::AnnounceInventory(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a command to the command channel, and wake up the event loop.
|
||||||
|
pub fn command(&self, cmd: protocol::Command) -> Result<(), Error> {
|
||||||
|
self.commands.send(cmd)?;
|
||||||
|
R::wake(&self.waker)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask the client to shutdown.
|
||||||
|
pub fn shutdown(self) -> Result<(), Error> {
|
||||||
|
self.shutdown.send(())?;
|
||||||
|
R::wake(&self.waker)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::os::unix::net::UnixListener;
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::{fs, io, net};
|
||||||
|
|
||||||
|
use nakamoto_net::Reactor;
|
||||||
|
|
||||||
|
use crate::client;
|
||||||
|
use crate::client::handle::Handle;
|
||||||
|
use crate::identity::ProjId;
|
||||||
|
|
||||||
|
/// Default name for control socket file.
|
||||||
|
pub const DEFAULT_NAME: &str = "radicle.sock";
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("failed to bind control socket listener: {0}")]
|
||||||
|
Bind(io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Listen for commands on the control socket, and process them.
|
||||||
|
pub fn listen<P: AsRef<Path>, R: Reactor>(path: P, handle: Handle<R>) -> Result<(), Error> {
|
||||||
|
// Remove the socket file on startup before rebinding.
|
||||||
|
fs::remove_file(&path).ok();
|
||||||
|
|
||||||
|
let listener = UnixListener::bind(path).map_err(Error::Bind)?;
|
||||||
|
for incoming in listener.incoming() {
|
||||||
|
match incoming {
|
||||||
|
Ok(mut stream) => {
|
||||||
|
if let Err(e) = drain(&stream, &handle) {
|
||||||
|
log::error!("Received {} on control socket", e);
|
||||||
|
|
||||||
|
write!(stream, "error: {}", e).ok();
|
||||||
|
|
||||||
|
stream.flush().ok();
|
||||||
|
stream.shutdown(net::Shutdown::Both).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Failed to open control socket stream: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
enum DrainError {
|
||||||
|
#[error("invalid command argument `{0}`")]
|
||||||
|
InvalidCommandArg(String),
|
||||||
|
#[error("unknown command `{0}`")]
|
||||||
|
UnknownCommand(String),
|
||||||
|
#[error("invalid command")]
|
||||||
|
InvalidCommand,
|
||||||
|
#[error("client error: {0}")]
|
||||||
|
Client(#[from] client::handle::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drain<R: Reactor>(stream: &UnixStream, handle: &Handle<R>) -> Result<(), DrainError> {
|
||||||
|
let mut reader = BufReader::new(stream);
|
||||||
|
|
||||||
|
for line in reader.by_ref().lines().flatten() {
|
||||||
|
match line.split_once(' ') {
|
||||||
|
Some(("update", arg)) => {
|
||||||
|
if let Ok(id) = ProjId::from_str(arg) {
|
||||||
|
if let Err(e) = handle.updated(id) {
|
||||||
|
return Err(DrainError::Client(e));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(DrainError::InvalidCommandArg(arg.to_owned()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((cmd, _)) => return Err(DrainError::UnknownCommand(cmd.to_owned())),
|
||||||
|
None => return Err(DrainError::InvalidCommand),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -54,8 +54,13 @@ pub enum Event {}
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Connect(net::SocketAddr),
|
Connect(net::SocketAddr),
|
||||||
Fetch(ProjId, net::SocketAddr),
|
Fetch(ProjId, net::SocketAddr),
|
||||||
|
AnnounceInventory(ProjId),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Command-related errors.
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum CommandError {}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Protocol<S, T, G> {
|
pub struct Protocol<S, T, G> {
|
||||||
/// Peers currently or recently connected.
|
/// Peers currently or recently connected.
|
||||||
|
|
@ -330,6 +335,9 @@ where
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
Command::AnnounceInventory(_proj) => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ pub fn init<S: storage::WriteStorage>(
|
||||||
git::set_upstream(
|
git::set_upstream(
|
||||||
repo,
|
repo,
|
||||||
REMOTE_NAME,
|
REMOTE_NAME,
|
||||||
|
// FIXME: Should use default branch here.
|
||||||
"master",
|
"master",
|
||||||
&format!("refs/remotes/{user_id}/heads/master"),
|
&format!("refs/remotes/{user_id}/heads/master"),
|
||||||
)?;
|
)?;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue