#[cfg(unix)]
pub mod unix;
#[cfg(target_os = "windows")]
pub mod windows;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, Result};
use kanade_shared::ipc::envelope::{RpcMessage, RpcResponse};
use kanade_shared::ipc::error::{ErrorKind, RpcError};
use kanade_shared::ipc::notifications::{Notification, NotificationAmend};
use kanade_shared::ipc::state::StateSnapshot;
use kanade_shared::wire::EffectiveConfig;
use tokio::io::{AsyncRead, AsyncWrite, ReadHalf, WriteHalf};
use tokio::sync::{broadcast, mpsc, watch};
use tracing::{debug, warn};
use crate::klp::auth::PeerIdentity;
use crate::klp::connection::ConnectionState;
use crate::klp::dispatcher::dispatch_request;
use crate::klp::framing::{read_frame, write_frame};
const PUSH_CHANNEL_CAPACITY: usize = 64;
#[derive(Clone)]
pub struct ListenerContext {
pub pc_id: Arc<str>,
pub agent_version: Arc<str>,
pub config_rx: watch::Receiver<EffectiveConfig>,
pub state_rx: watch::Receiver<StateSnapshot>,
pub log_path: PathBuf,
pub nats: async_nats::Client,
pub notif_tx: broadcast::Sender<Notification>,
pub amend_tx: broadcast::Sender<NotificationAmend>,
}
pub(crate) async fn serve_connection<S>(
stream: S,
peer: PeerIdentity,
ctx: ListenerContext,
) -> Result<()>
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let (reader, writer) = tokio::io::split(stream);
let (push_tx, push_rx) = mpsc::channel::<Vec<u8>>(PUSH_CHANNEL_CAPACITY);
let writer_log_pc = ctx.pc_id.to_string();
let writer_handle = tokio::spawn(writer_task(writer, push_rx, writer_log_pc));
let mut conn = ConnectionState::new(
peer,
ctx.pc_id.to_string(),
ctx.agent_version.to_string(),
ctx.config_rx.clone(),
ctx.state_rx.clone(),
ctx.log_path.clone(),
push_tx.clone(),
)
.with_nats(ctx.nats.clone())
.with_notifications(ctx.notif_tx.clone())
.with_amends(ctx.amend_tx.clone());
let read_loop_result = run_read_loop(reader, &mut conn, &push_tx).await;
drop(push_tx);
drop(conn);
let _ = writer_handle.await;
read_loop_result
}
async fn run_read_loop<S>(
mut reader: ReadHalf<S>,
conn: &mut ConnectionState,
push_tx: &mpsc::Sender<Vec<u8>>,
) -> Result<()>
where
S: AsyncRead + Unpin,
{
loop {
let frame = match read_frame(&mut reader).await {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
debug!(user = %conn.peer.user_display, "KLP client disconnected (EOF)");
return Ok(());
}
Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
warn!(error = %e, "KLP oversize frame; closing connection");
let _ =
push_anonymous_error(push_tx, ErrorKind::PayloadTooLarge, &e.to_string()).await;
return Ok(());
}
Err(e) => {
debug!(
error = %e,
user = %conn.peer.user_display,
"KLP connection torn down by I/O error",
);
return Ok(());
}
};
let msg: RpcMessage = match serde_json::from_slice(&frame) {
Ok(m) => m,
Err(e) => {
warn!(error = %e, "KLP JSON parse error");
let _ = push_anonymous_error(push_tx, ErrorKind::ParseError, &e.to_string()).await;
continue;
}
};
match msg {
RpcMessage::Request(req) => {
let resp = dispatch_request(conn, req).await;
let body = serde_json::to_vec(&resp).context("encode RpcResponse")?;
if push_tx.send(body).await.is_err() {
debug!(user = %conn.peer.user_display, "KLP push channel closed, exiting read loop");
return Ok(());
}
}
RpcMessage::Notification(notif) => {
debug!(method = %notif.method, "KLP notification received (no response)");
}
RpcMessage::Response(resp) => {
debug!(id = ?resp.id, "KLP unexpected client → agent response, ignoring");
}
}
}
}
async fn writer_task<S>(
mut writer: WriteHalf<S>,
mut push_rx: mpsc::Receiver<Vec<u8>>,
pc_id_for_log: String,
) where
S: AsyncWrite + Unpin,
{
while let Some(body) = push_rx.recv().await {
if let Err(e) = write_frame(&mut writer, &body).await {
warn!(
error = %e,
pc_id = %pc_id_for_log,
"KLP writer: pipe broken, exiting",
);
return;
}
}
debug!(pc_id = %pc_id_for_log, "KLP writer: push channel closed, exiting");
}
async fn push_anonymous_error(
push_tx: &mpsc::Sender<Vec<u8>>,
kind: ErrorKind,
detail: &str,
) -> Result<()> {
let err = RpcError::new(kind, detail);
let resp = RpcResponse::err_anonymous(err);
let body = serde_json::to_vec(&resp).context("encode anonymous error response")?;
push_tx
.send(body)
.await
.map_err(|_| anyhow::anyhow!("KLP push channel closed"))?;
Ok(())
}