use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use futures_lite::io::{AsyncBufReadExt, AsyncWriteExt, BufReader as AsyncBufReader};
use crate::hub::CliHub;
use crate::wire::{Inbound, Outbound, OutboundAck, OutboundError};
#[derive(Debug, Clone)]
pub enum Transport {
Stdio,
#[cfg(unix)]
Unix(PathBuf),
Manual,
}
pub(crate) fn start(transport: &Transport, hub: &CliHub) {
match transport {
Transport::Stdio => start_stdio(hub.clone()),
#[cfg(unix)]
Transport::Unix(path) => start_unix(path.clone(), hub.clone()),
Transport::Manual => {}
}
}
fn start_stdio(hub: CliHub) {
let lines = hub.subscribe();
executor_core::spawn(async move {
let stdout = std::io::stdout();
while let Ok(line) = lines.recv().await {
let mut out = stdout.lock();
let _ = writeln!(out, "{line}");
let _ = out.flush();
}
})
.detach();
std::thread::spawn(move || {
let stdin = std::io::stdin();
for line in BufReader::new(stdin.lock()).lines() {
let Ok(line) = line else { break };
if line.trim().is_empty() {
continue;
}
match serde_json::from_str::<Inbound>(&line) {
Ok(event) => {
let message_id = hub.inject(event);
hub.emit(Outbound::Ack(OutboundAck {
message_id: Some(message_id),
}));
}
Err(error) => hub.emit(Outbound::Error(OutboundError {
message: error.to_string(),
})),
}
}
});
}
#[cfg(unix)]
fn start_unix(path: PathBuf, hub: CliHub) {
use async_io::Async;
use std::os::unix::net::UnixListener;
let _ = std::fs::remove_file(&path);
let listener = match UnixListener::bind(&path).and_then(Async::new) {
Ok(listener) => listener,
Err(error) => {
tracing::error!(%error, path = %path.display(), "cli transport failed to bind");
return;
}
};
tracing::info!(path = %path.display(), "cli transport listening");
executor_core::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, _)) => {
let hub = hub.clone();
executor_core::spawn(handle_conn(stream, hub)).detach();
}
Err(error) => {
tracing::warn!(%error, "cli transport accept failed");
}
}
}
})
.detach();
}
#[cfg(unix)]
async fn handle_conn(stream: async_io::Async<std::os::unix::net::UnixStream>, hub: CliHub) {
use futures_lite::stream::StreamExt;
let (reader, mut writer) = futures_lite::io::split(stream);
let mut lines = AsyncBufReader::new(reader).lines();
while let Some(line) = lines.next().await {
let Ok(line) = line else { return };
let reply = match serde_json::from_str::<Inbound>(&line) {
Ok(Inbound::Subscribe) => {
let sink = hub.subscribe();
while let Ok(line) = sink.recv().await {
if writer.write_all(line.as_bytes()).await.is_err()
|| writer.write_all(b"\n").await.is_err()
{
return;
}
}
return;
}
Ok(event) => {
let message_id = hub.inject(event);
Outbound::Ack(OutboundAck {
message_id: Some(message_id),
})
}
Err(error) => Outbound::Error(OutboundError {
message: error.to_string(),
}),
};
if let Ok(reply) = serde_json::to_string(&reply)
&& (writer.write_all(reply.as_bytes()).await.is_err()
|| writer.write_all(b"\n").await.is_err())
{
return;
}
}
}