use std::pin::Pin;
use futures::{SinkExt, Stream, StreamExt};
use tokio_tungstenite::tungstenite;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use crate::cli::command::{AgentArguments, CommandExecutor, CommandRequest, CommandResponse};
pub struct WebSocketExecutor {
url: String,
signature: Option<String>,
}
impl WebSocketExecutor {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
signature: None,
}
}
pub fn signature(mut self, signature: impl Into<String>) -> Self {
self.signature = Some(signature.into());
self
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[schemars(rename = "cli.command.command_executor.AuthEnvelope")]
pub struct AuthEnvelope {
pub signature: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[schemars(rename = "cli.command.command_executor.ExecuteEnvelope")]
pub struct ExecuteEnvelope {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub agent_arguments: Option<AgentArguments>,
pub request: serde_json::Value,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("connect daemon execute websocket: {0}")]
Connect(tungstenite::Error),
#[error("daemon execute websocket: {0}")]
Ws(tungstenite::Error),
#[error("decode daemon execute message: {0}")]
Json(serde_json::Error),
#[error("{0}")]
Cli(crate::cli::Error),
#[error("daemon execute stream produced no items")]
Empty,
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum Line<T> {
Err(crate::cli::Error),
Ok(T),
}
impl<T> From<Line<T>> for Result<T, Error> {
fn from(line: Line<T>) -> Self {
match line {
Line::Err(e) => Err(Error::Cli(e)),
Line::Ok(t) => Ok(t),
}
}
}
impl CommandExecutor for WebSocketExecutor {
type Error = Error;
type Stream<T>
= Pin<Box<dyn Stream<Item = Result<T, Error>> + Send>>
where
T: Send + 'static;
async fn execute<R, T>(
&self,
request: R,
agent_arguments: Option<&AgentArguments>,
) -> Result<Self::Stream<T>, Error>
where
R: CommandRequest + Send + serde::Serialize,
T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
{
let auth = AuthEnvelope {
signature: self.signature.clone(),
};
let auth = serde_json::to_string(&auth).map_err(Error::Json)?;
let envelope = ExecuteEnvelope {
agent_arguments: agent_arguments.cloned(),
request: serde_json::to_value(&request).map_err(Error::Json)?,
};
let envelope = serde_json::to_string(&envelope).map_err(Error::Json)?;
let upgrade = self
.url
.as_str()
.into_client_request()
.map_err(Error::Connect)?;
let (mut ws, _response) = tokio_tungstenite::connect_async(upgrade)
.await
.map_err(Error::Connect)?;
ws.send(tungstenite::Message::Text(auth))
.await
.map_err(Error::Ws)?;
ws.send(tungstenite::Message::Text(envelope))
.await
.map_err(Error::Ws)?;
let stream = futures::stream::unfold(Some(ws), |ws| async move {
let mut ws = ws?;
loop {
match ws.next().await {
Some(Ok(tungstenite::Message::Text(text))) => {
let item = match serde_json::from_str::<Line<T>>(&text) {
Ok(line) => line.into(),
Err(e) => Err(Error::Json(e)),
};
return Some((item, Some(ws)));
}
Some(Ok(tungstenite::Message::Close(_))) | None => return None,
Some(Ok(_)) => continue,
Some(Err(e)) => return Some((Err(Error::Ws(e)), None)),
}
}
});
Ok(Box::pin(stream))
}
async fn execute_one<R, T>(
&self,
request: R,
agent_arguments: Option<&AgentArguments>,
) -> Result<T, Error>
where
R: CommandRequest + Send + serde::Serialize,
T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
{
let mut stream = self.execute::<R, T>(request, agent_arguments).await?;
stream.next().await.ok_or(Error::Empty)?
}
}