Client-Daemon protocol: use json instead of bincode

This permits to have a pretty PatternStatus easily
Also having a human-readable protocol format is good!

Closes #109
This commit is contained in:
ppom 2025-02-17 12:00:00 +01:00
commit 1a57481110
4 changed files with 47 additions and 50 deletions

View file

@ -3,7 +3,6 @@ use std::{
path::{Path, PathBuf},
};
use bincode::Options;
use futures::{SinkExt, StreamExt};
use tokio::net::UnixStream;
use tokio_util::{
@ -13,7 +12,7 @@ use tokio_util::{
use crate::{
cli::Format,
protocol::{bincode_options, ClientRequest, ClientStatus, DaemonResponse, Order},
protocol::{Cleanable, ClientRequest, ClientStatus, DaemonResponse, Order},
};
macro_rules! or_quit {
@ -23,14 +22,13 @@ macro_rules! or_quit {
}
async fn send_retrieve(socket: &Path, req: &ClientRequest) -> Result<DaemonResponse, String> {
let bin = bincode_options();
let conn = or_quit!(
"opening connection to daemon",
UnixStream::connect(socket).await
);
// Encode
let mut transport = Framed::new(conn, LengthDelimitedCodec::new());
let encoded_request = or_quit!("failed to encode request", bin.serialize(req));
let encoded_request = or_quit!("failed to encode request", serde_json::to_string(req));
or_quit!(
"failed to send request",
transport.send(Bytes::from(encoded_request)).await
@ -43,11 +41,12 @@ async fn send_retrieve(socket: &Path, req: &ClientRequest) -> Result<DaemonRespo
let encoded_response = or_quit!("failed to decode response", encoded_response);
Ok(or_quit!(
"failed to decode response",
bin.deserialize::<DaemonResponse>(&encoded_response)
serde_json::from_slice::<DaemonResponse>(&encoded_response)
))
}
fn print_status(cs: ClientStatus, format: Format) -> Result<(), Box<dyn Error>> {
let cs = cs.clean();
let encoded = match format {
Format::JSON => serde_json::to_string_pretty(&cs)?,
Format::YAML => serde_yaml::to_string(&cs)?,

View file

@ -6,7 +6,6 @@ use std::{
sync::Arc,
};
use bincode::Options;
use futures::{SinkExt, StreamExt};
use regex::Regex;
use tokio::net::UnixListener;
@ -18,7 +17,7 @@ use tracing::{error, warn};
use crate::{
concepts::{Config, Filter, Pattern, Stream},
protocol::{bincode_options, ClientRequest, ClientStatus, DaemonResponse},
protocol::{ClientRequest, ClientStatus, DaemonResponse},
};
use super::{filter::FilterManager, shutdown::ShutdownToken};
@ -153,7 +152,6 @@ pub async fn socket_manager(
}
};
let bin = bincode_options();
loop {
tokio::select! {
_ = shutdown.wait() => break,
@ -172,7 +170,7 @@ pub async fn socket_manager(
};
let request = or_next!(
"failed to decode request",
bin.deserialize(&encoded_request)
serde_json::from_slice(&encoded_request)
);
// Process
let response = match answer_order(config, &shared_state, request) {
@ -181,7 +179,7 @@ pub async fn socket_manager(
};
// Encode
let encoded_response =
or_next!("failed to serialize response", bin.serialize::<DaemonResponse>(&response));
or_next!("failed to serialize response", serde_json::to_string::<DaemonResponse>(&response));
or_next!(
"failed to send response:",
transport.send(Bytes::from(encoded_response)).await

View file

@ -40,34 +40,45 @@ pub type ClientStatus = BTreeMap<String, BTreeMap<String, BTreeMap<String, Patte
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct PatternStatus {
#[serde(default, skip_serializing_if = "is_zero")]
pub matches: usize,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub actions: BTreeMap<String, Vec<String>>,
}
// impl Serialize for PatternStatus {
// fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
// where
// S: serde::Serializer,
// {
// // We only skip serializing emptiness if we're on a human-readable format
// // This means we're printing for user, not exchanging it over a socket
// if serializer.is_human_readable() {
// let ser_matches = self.matches != 0;
// let ser_actions = !self.actions.is_empty();
// let mut state =
// serializer.serialize_map(Some(ser_matches as usize + ser_actions as usize))?;
// if ser_matches {
// state.serialize_entry("matches", &self.matches)?;
// }
// if ser_actions {
// state.serialize_entry("actions", &self.actions)?;
// }
// state.end()
// } else {
// let mut state = serializer.serialize_struct("PatternStatus", 2)?;
// state.serialize_field("matches", &self.matches)?;
// state.serialize_field("actions", &self.actions)?;
// state.end()
// }
// }
// }
fn is_zero(n: &usize) -> bool {
*n == 0
}
pub trait Cleanable {
/// Remove empty fields
fn clean(self) -> Self;
}
impl Cleanable for ClientStatus {
fn clean(self) -> Self {
self.into_iter()
.map(|(key, value)| {
(
key,
value
.into_iter()
.map(|(key, value)| {
(
key,
value
.into_iter()
.filter(|(_, value)| {
value.matches != 0 || !value.actions.is_empty()
})
.collect::<BTreeMap<_, _>>(),
)
})
.filter(|(_, value)| !value.is_empty())
.collect::<BTreeMap<_, _>>(),
)
})
.filter(|(_, value)| !value.is_empty())
.collect()
}
}

View file

@ -1,29 +1,19 @@
use std::marker::PhantomData;
use bincode::Options;
use serde::de::DeserializeOwned;
use thiserror::Error;
use tokio_util::codec::{Decoder, LengthDelimitedCodec};
pub type BincodeOptions = bincode::config::WithOtherIntEncoding<
bincode::config::DefaultOptions,
bincode::config::VarintEncoding,
>;
pub fn bincode_options() -> BincodeOptions {
bincode::DefaultOptions::new().with_varint_encoding()
}
#[derive(Error, Debug)]
pub enum DecodeError {
#[error("{0}")]
Io(#[from] tokio::io::Error),
#[error("{0}")]
Bincode(#[from] bincode::Error),
Json(#[from] serde_json::Error),
}
pub struct LengthPrefixedBincode<T: DeserializeOwned> {
inner: LengthDelimitedCodec,
bin: BincodeOptions,
_marker: PhantomData<T>,
}
@ -31,7 +21,6 @@ impl<T: DeserializeOwned> LengthPrefixedBincode<T> {
pub fn new() -> Self {
LengthPrefixedBincode {
inner: LengthDelimitedCodec::new(),
bin: bincode_options(),
_marker: PhantomData,
}
}
@ -52,7 +41,7 @@ impl<T: DeserializeOwned> Decoder for LengthPrefixedBincode<T> {
src: &mut tokio_util::bytes::BytesMut,
) -> Result<Option<Self::Item>, Self::Error> {
match self.inner.decode(src) {
Ok(Some(data)) => match self.bin.deserialize(&data) {
Ok(Some(data)) => match serde_json::from_slice(&data) {
Ok(thing) => Ok(Some(thing)),
Err(err) => Err(err.into()),
},