use std::time::Duration;
use anyhow::{Context, Result};
use tokio::io::AsyncWriteExt;
use tracing::{debug, warn};
use super::endpoint::DaemonEndpoint;
const RECONNECT_DELAY: Duration = Duration::from_millis(500);
const RECONNECT_TIMEOUT: Duration = Duration::from_secs(10);
pub async fn forward_stdio_to_daemon(endpoint: &DaemonEndpoint) -> Result<()> {
forward_stdio_to_daemon_with_reconnect(endpoint, true).await
}
pub async fn forward_stdio_to_daemon_with_reconnect(
endpoint: &DaemonEndpoint,
allow_reconnect: bool,
) -> Result<()> {
let stream = tokio::net::UnixStream::connect(&endpoint.socket_path)
.await
.with_context(|| {
format!(
"failed to connect to daemon socket at {}",
endpoint.socket_path.display()
)
})?;
debug!(
"shim connected to daemon at {} (pid {})",
endpoint.socket_path.display(),
endpoint.pid
);
forward_stream_with_reconnect(
stream,
tokio::io::stdin(),
tokio::io::stdout(),
&endpoint.socket_path,
allow_reconnect,
)
.await
}
pub async fn forward_stream_with_reconnect<R, W>(
mut stream: tokio::net::UnixStream,
mut client_read: R,
mut client_write: W,
socket_path: &std::path::Path,
allow_reconnect: bool,
) -> Result<()>
where
R: tokio::io::AsyncRead + Unpin,
W: tokio::io::AsyncWrite + Unpin,
{
loop {
let (mut socket_read, mut socket_write) = stream.split();
let client_to_socket = async {
let result = tokio::io::copy(&mut client_read, &mut socket_write).await;
let _ = socket_write.shutdown().await;
result
};
let socket_to_client = async {
let result = tokio::io::copy(&mut socket_read, &mut client_write).await;
let _ = client_write.flush().await;
result
};
let (client_result, socket_result) = tokio::join!(client_to_socket, socket_to_client);
let client_closed = matches!(&client_result, Ok(0));
let socket_closed = matches!(&socket_result, Ok(0));
if client_closed {
debug!("shim: client stream closed, shutting down");
return Ok(());
}
if socket_closed && allow_reconnect {
debug!("shim: daemon socket closed, attempting reconnect");
stream = match try_reconnect(socket_path).await {
Some(s) => s,
None => {
warn!(
"shim: failed to reconnect to daemon within {:?}; exiting",
RECONNECT_TIMEOUT
);
anyhow::bail!("daemon connection lost and reconnect failed");
}
};
debug!("shim: reconnected to daemon, resuming forward loop");
continue;
}
if let Err(e) = client_result {
return Err(anyhow::anyhow!("shim client read error: {e}"));
}
if let Err(e) = socket_result {
return Err(anyhow::anyhow!("shim socket read error: {e}"));
}
return Ok(());
}
}
async fn try_reconnect(socket_path: &std::path::Path) -> Option<tokio::net::UnixStream> {
let deadline = tokio::time::Instant::now() + RECONNECT_TIMEOUT;
loop {
if tokio::time::Instant::now() >= deadline {
return None;
}
if let Ok(stream) = tokio::net::UnixStream::connect(socket_path).await {
return Some(stream);
}
tokio::time::sleep(RECONNECT_DELAY).await;
}
}
#[cfg(test)]
mod test {
use super::*;
use std::path::PathBuf;
#[test]
fn test_forward_fails_on_missing_socket() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let endpoint = DaemonEndpoint {
socket_path: PathBuf::from("/tmp/leindex-nonexistent-test-shim.sock"),
pid: 999_999,
pid_start_time_ms: 0,
protocol_version: 1,
leindex_version: "test".into(),
};
let result = rt.block_on(forward_stdio_to_daemon_with_reconnect(&endpoint, false));
assert!(
result.is_err(),
"forwarder must error when socket does not exist"
);
}
#[cfg(unix)]
#[tokio::test]
async fn test_shim_byte_faithful_passthrough() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let dir = tempfile::tempdir().unwrap();
let socket = dir.path().join("passthrough.sock");
let listener = tokio::net::UnixListener::bind(&socket).unwrap();
let server_task = tokio::spawn(async move {
let (mut conn, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 1024];
let n = conn.read(&mut buf).await.unwrap();
conn.write_all(&buf[..n]).await.unwrap();
conn.flush().await.unwrap();
drop(conn);
});
tokio::time::sleep(Duration::from_millis(50)).await;
let stream = tokio::net::UnixStream::connect(&socket).await.unwrap();
let (mut client_tx, client_read) = tokio::io::duplex(4096);
let (client_write, mut client_rx) = tokio::io::duplex(4096);
let forward_task = tokio::spawn(async move {
forward_stream_with_reconnect(
stream,
client_read,
client_write,
PathBuf::from("/nonexistent-for-reconnect.sock").as_path(),
false,
)
.await
});
let payload = b"{\"jsonrpc\":\"2.0\",\"id\":42,\"method\":\"test\"}\n";
client_tx.write_all(payload).await.unwrap();
client_tx.flush().await.unwrap();
let mut response = vec![0u8; payload.len()];
client_rx.read_exact(&mut response).await.unwrap();
assert_eq!(&response[..], payload, "shim must forward bytes unmodified");
drop(client_tx);
let _ = forward_task.await;
let _ = server_task.await;
}
}