use std::{io, process::Stdio, time::Duration};
use futures_core::future::BoxFuture;
use tokio::{
io::{AsyncRead, AsyncWrite, BufReader},
process::{Child, Command},
task::JoinHandle,
};
use crate::protocol::{
Call, Client, Failure, Message, Notification, RequestId, Response,
stdio::{read, write},
};
pub struct StdioClient {
incoming: BufReader<Box<dyn AsyncRead + Send + Unpin>>,
outgoing: Box<dyn AsyncWrite + Send + Unpin>,
server: Option<Child>,
stderr: Option<JoinHandle<()>>,
next_id: RequestId,
}
impl StdioClient {
pub fn new(mut server: Command) -> io::Result<StdioClient> {
let mut server = server
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let outgoing = server.stdin.take().expect("a piped stdin");
let incoming = server.stdout.take().expect("a piped stdout");
let mut said = server.stderr.take().expect("a piped stderr");
let stderr = tokio::spawn(async move {
let _ = tokio::io::copy(&mut said, &mut tokio::io::stderr()).await;
});
Ok(StdioClient {
incoming: BufReader::new(Box::new(incoming)),
outgoing: Box::new(outgoing),
server: Some(server),
stderr: Some(stderr),
next_id: 0,
})
}
#[cfg(test)]
fn over(
incoming: impl AsyncRead + Send + Unpin + 'static,
outgoing: impl AsyncWrite + Send + Unpin + 'static,
) -> StdioClient {
StdioClient {
incoming: BufReader::new(Box::new(incoming)),
outgoing: Box::new(outgoing),
server: None,
stderr: None,
next_id: 0,
}
}
async fn send(&mut self, message: &Message) -> Result<(), Failure> {
write(&mut self.outgoing, message)
.await
.map_err(broke("sending a message"))
}
async fn recv(&mut self) -> Result<Option<Message>, Failure> {
read(&mut self.incoming)
.await
.map_err(broke("reading a message"))
}
async fn round_trip(&mut self, id: RequestId, call: Call) -> Result<Response, Failure> {
self.send(&Message::Request { id, call }).await?;
loop {
let Some(message) = self.recv().await? else {
return Err(Failure::broken(format!(
"the server closed the channel before answering request {id}"
)));
};
match message {
Message::Response {
id: answered,
result,
} if answered == id => {
return match result {
Response::Error(error) => Err(Failure::Refused(error)),
answer => Ok(answer),
};
}
Message::Response { id: answered, .. } => eprintln!(
"console: a response arrived for request {answered}, which nobody made"
),
other => {
return Err(Failure::broken(format!(
"the server sent {other:?}, which a client cannot answer"
)));
}
}
}
}
}
impl Client for StdioClient {
fn call(&mut self, call: Call) -> BoxFuture<'_, Result<Response, Failure>> {
Box::pin(async move {
let id = self.next_id;
self.next_id += 1;
self.round_trip(id, call).await
})
}
fn notify(&mut self, notification: Notification) -> BoxFuture<'_, Result<(), Failure>> {
Box::pin(async move { self.send(&Message::Notification(notification)).await })
}
fn quit(&mut self) -> BoxFuture<'_, Result<(), Failure>> {
Box::pin(async move {
let said = self.notify(Notification::Quit).await;
self.outgoing = Box::new(tokio::io::sink());
let Some(mut server) = self.server.take() else {
return said;
};
let status = server
.wait()
.await
.map_err(broke("waiting for the console server"))?;
if let Some(stderr) = self.stderr.take() {
let _ = tokio::time::timeout(Duration::from_secs(1), stderr).await;
}
if !status.success() {
return Err(Failure::broken(format!(
"the console server ended with {status}"
)));
}
Ok(())
})
}
}
fn broke(doing: &'static str) -> impl FnOnce(io::Error) -> Failure {
move |e| Failure::Broken(anyhow::Error::new(e).context(doing))
}
#[cfg(test)]
mod tests {
use std::{
io::Cursor,
pin::Pin,
sync::{Arc, Mutex},
task::{Context, Poll},
};
use super::*;
use crate::protocol::{Error, ExecCall, ExecResp, InitCall, InitResp, Method, ReadCall};
#[derive(Clone, Default)]
struct Sent(Arc<Mutex<Vec<u8>>>);
impl AsyncWrite for Sent {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
self.0.lock().unwrap().extend_from_slice(buf);
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
impl Sent {
async fn messages(&self) -> Vec<(Option<RequestId>, Option<Method>)> {
let bytes = self.0.lock().unwrap().clone();
let mut reader = bytes.as_slice();
let mut sent = Vec::new();
while let Some(message) = read(&mut reader).await.unwrap() {
sent.push((message.id(), message.method()));
}
sent
}
}
async fn driving(incoming: &[Message]) -> (StdioClient, Sent) {
let mut bytes = Vec::new();
for message in incoming {
write(&mut bytes, message).await.unwrap();
}
let sent = Sent::default();
(StdioClient::over(Cursor::new(bytes), sent.clone()), sent)
}
fn ran(id: RequestId, stdout: &[u8]) -> Message {
Message::Response {
id,
result: Response::Exec(ExecResp {
code: 0,
stdout: stdout.to_vec(),
..ExecResp::default()
}),
}
}
fn initialized(id: RequestId) -> Message {
Message::Response {
id,
result: Response::Init(InitResp::default()),
}
}
#[tokio::test]
async fn a_session_is_init_then_execs() {
let (mut client, sent) = driving(&[initialized(0), ran(1, b"hi\n")]).await;
client.init(InitCall::default()).await.unwrap();
client.start().await.unwrap();
let result = client
.exec(ExecCall {
cmd: vec!["sh".into(), "-c".into(), "echo hi".into()],
..ExecCall::default()
})
.await;
assert_eq!(result.unwrap().stdout, b"hi\n");
client.stop().await.unwrap();
client.quit().await.unwrap();
assert_eq!(
sent.messages().await,
[
(Some(0), Some(Method::Init)),
(None, Some(Method::Start)),
(Some(1), Some(Method::Exec)),
(None, Some(Method::Stop)),
(None, Some(Method::Quit)),
]
);
}
#[tokio::test]
async fn a_refusal_is_an_answer_and_a_closed_channel_is_not() {
let (mut client, _) = driving(&[Message::Response {
id: 0,
result: Response::Error(Error::new(Error::TIMED_OUT, "killed after 1000ms")),
}])
.await;
let failure = client.exec(ExecCall::default()).await.unwrap_err();
assert_eq!(failure.code(), Some(Error::TIMED_OUT));
let (mut client, _) = driving(&[]).await;
let failure = client.exec(ExecCall::default()).await.unwrap_err();
assert_eq!(failure.code(), None);
assert!(
failure.to_string().contains("before answering request 0"),
"{failure}"
);
}
#[tokio::test]
async fn a_client_answers_nothing() {
let (mut client, _) = driving(&[ran(99, b"who asked"), ran(0, b"mine\n")]).await;
let result = client.exec(ExecCall::default()).await.unwrap();
assert_eq!(result.stdout, b"mine\n");
let (mut client, _) = driving(&[Message::Request {
id: 1,
call: Call::Exec(ExecCall::default()),
}])
.await;
let failure = client.exec(ExecCall::default()).await.unwrap_err();
assert!(failure.to_string().contains("cannot answer"), "{failure}");
}
#[tokio::test]
async fn quitting_collects_the_server_and_says_how_it_went() {
let mut ends_badly = Command::new("sh");
ends_badly.args(["-c", "exit 3"]);
let mut client = StdioClient::new(ends_badly).unwrap();
let failure = client.quit().await.unwrap_err();
assert_eq!(failure.code(), None, "a dead server is not a refusal");
assert!(failure.to_string().contains("exit status: 3"), "{failure}");
client.quit().await.unwrap();
let mut ends_well = Command::new("sh");
ends_well.args(["-c", "exit 0"]);
StdioClient::new(ends_well).unwrap().quit().await.unwrap();
}
#[tokio::test]
async fn a_failed_call_still_spends_its_id() {
let (mut client, sent) = driving(&[]).await;
assert!(client.exec(ExecCall::default()).await.is_err());
assert!(client.read(ReadCall::default()).await.is_err());
assert_eq!(
sent.messages().await,
[(Some(0), Some(Method::Exec)), (Some(1), Some(Method::Read))]
);
}
}