use std::path::Path;
#[cfg(unix)]
use interprocess::local_socket::GenericFilePath;
#[cfg(windows)]
use interprocess::local_socket::GenericNamespaced;
use interprocess::local_socket::tokio::prelude::*;
use interprocess::local_socket::{ListenerOptions, Name};
use objectiveai_sdk::cli::websocket_listener::ListenerEnd;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::sync::broadcast;
use crate::websockets::mcp_listener::socks_dir;
#[cfg(unix)]
fn socket_name(state_dir: &Path) -> std::io::Result<Name<'static>> {
socks_dir(state_dir)
.join("daemon.sock")
.to_fs_name::<GenericFilePath>()
}
#[cfg(windows)]
fn socket_name(state_dir: &Path) -> std::io::Result<Name<'static>> {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
state_dir.file_name().hash(&mut hasher);
let state = hasher.finish();
format!("objectiveai-{state:016x}-daemon.sock").to_ns_name::<GenericNamespaced>()
}
pub fn bind_socket_listener(
state_dir: &Path,
) -> std::io::Result<interprocess::local_socket::tokio::Listener> {
let _ = std::fs::create_dir_all(socks_dir(state_dir));
let name = socket_name(state_dir)?;
ListenerOptions::new()
.name(name)
.try_overwrite(true)
.create_tokio()
}
pub fn serve_socket_listener(
listener: interprocess::local_socket::tokio::Listener,
tx: broadcast::Sender<String>,
) {
tokio::spawn(async move {
loop {
let conn = match listener.accept().await {
Ok(conn) => conn,
Err(_) => continue,
};
tokio::spawn(handle_feed(conn, tx.clone()));
}
});
}
async fn handle_feed(conn: LocalSocketStream, tx: broadcast::Sender<String>) {
let (read_half, _write_half) = tokio::io::split(conn);
let mut reader = BufReader::new(read_half);
let id = uuid::Uuid::new_v4().to_string();
let mut line = String::new();
let context: serde_json::Map<String, serde_json::Value> = loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => return, Ok(_) => {}
Err(_) => return,
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
break match serde_json::from_str::<serde_json::Value>(trimmed) {
Ok(serde_json::Value::Object(map)) => map,
_ => serde_json::Map::new(),
};
};
let mut announced = false;
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => break, Ok(_) => {}
Err(_) => break,
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let value: serde_json::Value = match serde_json::from_str(trimmed) {
Ok(v) => v,
Err(_) => continue,
};
let frame = if !announced {
announced = true;
let mut envelope = context.clone();
envelope.insert("id".to_string(), serde_json::json!(id.clone()));
envelope.insert("value".to_string(), value);
serde_json::Value::Object(envelope).to_string()
} else {
serde_json::json!({
"id": id.clone(),
"value": value,
})
.to_string()
};
let _ = tx.send(frame);
}
if announced {
let end = ListenerEnd { id, end: true };
if let Ok(frame) = serde_json::to_string(&end) {
let _ = tx.send(frame);
}
}
}
#[derive(Clone)]
pub(crate) struct DaemonWsState {
pub(crate) tx: broadcast::Sender<String>,
pub(crate) ctx: crate::context::Context,
pub(crate) secret: Option<std::sync::Arc<String>>,
pub(crate) active: crate::websockets::websocket_agents::ActiveAgents,
pub(crate) conversations: crate::websockets::websocket_agent_instance::ConversationHub,
}
pub fn serve_ws(
listener: tokio::net::TcpListener,
tx: broadcast::Sender<String>,
secret: Option<std::sync::Arc<String>>,
ctx: crate::context::Context,
active: crate::websockets::websocket_agents::ActiveAgents,
conversations: crate::websockets::websocket_agent_instance::ConversationHub,
) -> tokio::task::JoinHandle<()> {
let app = axum::Router::new()
.route("/listen", axum::routing::any(listen_handler))
.route(
"/execute",
axum::routing::any(crate::websockets::daemon_execute::execute_handler),
)
.route(
"/agents/instances/list",
axum::routing::any(crate::websockets::websocket_agents::agents_handler),
)
.route(
"/agents/instances/{*aih}",
axum::routing::any(
crate::websockets::websocket_agent_instance::instance_handler,
),
)
.with_state(DaemonWsState {
tx,
ctx,
secret,
active,
conversations,
});
tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
})
}
async fn listen_handler(
axum::extract::State(state): axum::extract::State<DaemonWsState>,
ws: axum::extract::ws::WebSocketUpgrade,
) -> axum::response::Response {
ws.on_upgrade(move |mut socket| async move {
if !crate::websockets::daemon_auth::authenticate(&mut socket, state.secret.as_ref()).await
{
return;
}
pump(socket, state.tx).await;
})
}
async fn pump(mut socket: axum::extract::ws::WebSocket, tx: broadcast::Sender<String>) {
use axum::extract::ws::Message;
let mut rx = tx.subscribe();
loop {
tokio::select! {
received = rx.recv() => match received {
Ok(frame) => {
if socket.send(Message::Text(frame.into())).await.is_err() {
break;
}
}
Err(broadcast::error::RecvError::Lagged(_)) => continue,
Err(broadcast::error::RecvError::Closed) => break,
},
inbound = socket.recv() => match inbound {
None | Some(Err(_)) | Some(Ok(Message::Close(_))) => break,
Some(Ok(_)) => {}
},
}
}
}
pub async fn feed_socket(
state_dir: &Path,
context: &serde_json::Value,
request: &serde_json::Value,
responses: &[serde_json::Value],
) -> std::io::Result<()> {
let name = socket_name(state_dir)?;
let conn = LocalSocketStream::connect(name).await?;
let (_read_half, mut write_half) = tokio::io::split(conn);
write_line(&mut write_half, context).await?;
write_line(&mut write_half, request).await?;
for response in responses {
write_line(&mut write_half, response).await?;
}
write_half.flush().await?;
write_half.shutdown().await?;
Ok(())
}
async fn write_line<W: AsyncWriteExt + Unpin>(
write_half: &mut W,
value: &serde_json::Value,
) -> std::io::Result<()> {
let line = serde_json::to_string(value)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
write_half.write_all(line.as_bytes()).await?;
write_half.write_all(b"\n").await?;
Ok(())
}
pub struct FeedWriter {
write: tokio::io::WriteHalf<LocalSocketStream>,
}
impl FeedWriter {
pub async fn write(&mut self, value: &serde_json::Value) -> std::io::Result<()> {
let mut line = serde_json::to_vec(value)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
line.push(b'\n');
self.write.write_all(&line).await?;
self.write.flush().await
}
}
pub async fn connect_feed(state_dir: &Path) -> std::io::Result<FeedWriter> {
const ERROR_PIPE_BUSY: i32 = 231;
let mut attempts = 0u32;
loop {
let name = socket_name(state_dir)?;
match LocalSocketStream::connect(name).await {
Ok(conn) => {
let (_read_half, write) = tokio::io::split(conn);
return Ok(FeedWriter { write });
}
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY) && attempts < 20 => {
attempts += 1;
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
Err(e) => return Err(e),
}
}
}