pub mod actions;
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::server::DcProtocol;
use crate::state::app_state::AppState;
use actions::DC_COMMAND_RECEIVED_EVENT;
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::mpsc;
const DEFAULT_HUB_NAME: &str = "NetGetHub";
pub const MAX_COMMAND_LEN: usize = 64 * 1024;
const FIRST_BYTE_READ_TIMEOUT: Duration = Duration::from_secs(60);
const IDLE_BETWEEN_COMMANDS_TIMEOUT: Duration = Duration::from_secs(300);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"$HubIsFull|";
pub(crate) fn nmdc_field(value: &str) -> String {
value
.chars()
.filter(|c| *c != '|' && *c != '\r' && *c != '\n')
.collect()
}
fn hub_name_command(hub_name: Option<&str>, hub_topic: Option<&str>) -> Option<String> {
fn clean(value: &str) -> String {
value
.chars()
.filter(|c| *c != '|' && *c != '\r' && *c != '\n')
.collect::<String>()
.trim()
.to_string()
}
let name = hub_name.map(clean).filter(|s| !s.is_empty());
let topic = hub_topic.map(clean).filter(|s| !s.is_empty());
match (name, topic) {
(None, None) => None,
(Some(name), None) => Some(format!("$HubName {name}|")),
(name, Some(topic)) => {
let name = name.unwrap_or_else(|| DEFAULT_HUB_NAME.to_string());
Some(format!("$HubName {name} - {topic}|"))
}
}
}
pub struct DcServer;
impl DcServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
startup_params: Option<crate::protocol::StartupParams>,
) -> Result<SocketAddr> {
let hub_announcement = match &startup_params {
Some(params) => hub_name_command(
params.get_optional_string("hub_name")?.as_deref(),
params.get_optional_string("hub_topic")?.as_deref(),
),
None => None,
};
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!(
"DC server (action-based) listening on {}",
local_addr
));
let protocol = Arc::new(DcProtocol::new());
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"DC",
Some(&status_tx),
)
.await
{
Ok((stream, remote_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
let llm_clone = llm_client.clone();
let state_clone = app_state.clone();
let status_clone = status_tx.clone();
let protocol_clone = protocol.clone();
let hub_announcement = hub_announcement.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
let (read_half, write_half) = tokio::io::split(stream);
let write_half_arc = Arc::new(tokio::sync::Mutex::new(write_half));
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr,
local_addr: local_addr_conn,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
state_clone
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_clone.send("__UPDATE_UI__".to_string());
protocol_clone.add_connection(connection_id).await;
let peer_rx = crate::server::peer_support::register_peer_channel(
&state_clone,
server_id,
connection_id.as_u32(),
)
.await;
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
protocol_clone.clone(),
state_clone.clone(),
server_id,
connection_id.as_u32(),
write_half_arc.clone(),
status_clone.clone(),
);
Self::run_connection(
read_half,
&write_half_arc,
connection_id,
server_id,
&llm_clone,
&state_clone,
&status_clone,
&protocol_clone,
hub_announcement.as_deref(),
)
.await;
state_clone
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
protocol_clone.remove_connection(&connection_id).await;
state_clone
.remove_connection_from_server(server_id, connection_id)
.await;
let _ = status_clone.send("__UPDATE_UI__".to_string());
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to accept DC connection: {}", e));
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
#[allow(clippy::too_many_arguments)]
async fn run_connection<R, W>(
read_half: R,
write_half_arc: &Arc<tokio::sync::Mutex<W>>,
connection_id: ConnectionId,
server_id: crate::state::ServerId,
llm_clone: &OllamaClient,
state_clone: &Arc<AppState>,
status_clone: &mpsc::UnboundedSender<String>,
protocol_clone: &Arc<DcProtocol>,
hub_announcement: Option<&str>,
) where
R: tokio::io::AsyncRead + Unpin,
W: tokio::io::AsyncWrite + Unpin,
{
let lock_command =
format!("$Lock EXTENDEDPROTOCOLABCABCABCABCABCABC Pk={DEFAULT_HUB_NAME}|");
{
let mut writer = write_half_arc.lock().await;
if let Err(e) = writer.write_all(lock_command.as_bytes()).await {
Log::new(Some(status_clone)).error(format!("Failed to send initial Lock: {}", e));
return;
}
Log::new(Some(status_clone))
.debug(format!("DC sent {} bytes (Lock)", lock_command.len()));
state_clone
.update_connection_stats(
server_id,
connection_id,
Some(0),
Some(lock_command.len() as u64),
Some(0),
Some(1),
)
.await;
}
if let Some(announcement) = hub_announcement {
let mut writer = write_half_arc.lock().await;
if let Err(e) = writer.write_all(announcement.as_bytes()).await {
Log::new(Some(status_clone))
.error(format!("Failed to send initial HubName: {}", e));
return;
}
Log::new(Some(status_clone)).debug(format!("DC sent HubName: {}", announcement));
state_clone
.update_connection_stats(
server_id,
connection_id,
Some(0),
Some(announcement.len() as u64),
Some(0),
Some(1),
)
.await;
}
let mut read_half = tokio::io::BufReader::new(read_half);
let mut buffer = Vec::new();
let mut seen_a_command = false;
'connection: loop {
let mut byte = [0u8; 1];
let read_timeout = if seen_a_command {
IDLE_BETWEEN_COMMANDS_TIMEOUT
} else {
FIRST_BYTE_READ_TIMEOUT
};
let read =
match tokio::time::timeout(read_timeout, read_half.read_exact(&mut byte)).await {
Ok(read) => read,
Err(_) => {
Log::new(Some(status_clone)).info(format!(
"DC connection {} sent nothing for {}s; closing idle connection",
connection_id,
read_timeout.as_secs()
));
break;
}
};
match read {
Ok(_) => {
buffer.push(byte[0]);
if buffer.len() > MAX_COMMAND_LEN {
Log::new(Some(status_clone)).warn(format!(
"DC connection {} sent {} bytes with no '|' terminator (limit {}); \
closing: decision=oversize_command",
connection_id,
buffer.len(),
MAX_COMMAND_LEN
));
break;
}
if byte[0] == b'|' {
seen_a_command = true;
let command_bytes = buffer.clone();
buffer.clear();
let command_str = match String::from_utf8(command_bytes.clone()) {
Ok(s) => s,
Err(e) => {
Log::new(Some(status_clone))
.warn(format!("DC received non-UTF8 data: {}", e));
continue;
}
};
let command = command_str.trim_end_matches('|');
let preview = crate::utils::truncate_for_log(command, 100);
let log = Log::new(Some(status_clone));
log.debug(format!(
"DC received {} bytes on connection {}: {}",
command_bytes.len(),
connection_id,
preview
));
log.trace(format!("DC command: {:?}", command));
state_clone
.update_connection_stats(
server_id,
connection_id,
Some(command_bytes.len() as u64),
Some(0),
Some(1),
Some(0),
)
.await;
if let Some(rest) = command.strip_prefix("$ValidateNick ") {
if let Some(nick) = rest.split_whitespace().next() {
protocol_clone
.set_nickname(connection_id, nick.to_string())
.await;
}
} else if let Some(rest) = command.strip_prefix("$MyINFO ") {
let mut fields = rest.split_whitespace();
if fields.next() == Some("$ALL") {
if let Some(nick) = fields.next() {
protocol_clone
.set_nickname(connection_id, nick.to_string())
.await;
}
}
}
let command_type = if command.starts_with('$') {
command
.split_whitespace()
.next()
.unwrap_or("$Unknown")
.trim_start_matches('$')
} else if command.starts_with('<') {
"Chat"
} else {
"Unknown"
};
let client_nickname = protocol_clone.get_nickname(&connection_id).await;
let event = Event::new(
&DC_COMMAND_RECEIVED_EVENT,
serde_json::json!({
"command": command,
"command_type": command_type,
"client_nickname": client_nickname,
}),
);
Log::new(Some(status_clone))
.debug(format!("DC calling LLM for connection {}", connection_id));
let result = call_llm(
llm_clone,
state_clone,
server_id,
Some(connection_id),
&event,
protocol_clone.as_ref(),
)
.await;
match result {
Ok(execution_result) => {
let log = Log::new(Some(status_clone));
for message in &execution_result.messages {
log.info(format!("{}", message));
}
log.debug(format!(
"DC got {} protocol results",
execution_result.protocol_results.len()
));
if execution_result.protocol_results.is_empty() {
log.debug(format!(
"DC connection {} decision=model_no_actions",
connection_id
));
}
for protocol_result in execution_result.protocol_results {
match protocol_result {
ActionResult::Output(data) => {
let mut write = write_half_arc.lock().await;
if let Err(e) = write.write_all(&data).await {
log.error(format!(
"Failed to write DC response: {}",
e
));
break 'connection;
}
let _ = write.flush().await;
drop(write);
state_clone
.update_connection_stats(
server_id,
connection_id,
None,
Some(data.len() as u64),
None,
Some(1),
)
.await;
log.debug(format!(
"DC sent {} bytes on connection {}",
data.len(),
connection_id
));
log.trace(format!(
"DC sent data: {:?}",
String::from_utf8_lossy(&data)
));
}
ActionResult::CloseConnection => {
log.info(format!(
"DC closing connection {}: decision=model_close",
connection_id
));
break 'connection;
}
_ => {}
}
}
}
Err(e) => {
let log = Log::new(Some(status_clone));
let failure = crate::utils::WireFailure::classify(&e);
let class = if failure.is_overloaded() {
"overloaded"
} else {
"unavailable"
};
log.warn(format!(
"DC command failed on connection {}: decision=fail_closed_llm_error class={} error={}",
connection_id, class, e
));
let frame = match failure {
crate::utils::WireFailure::Overloaded => {
format!("<Hub> {}|", failure.prefixed_text())
}
crate::utils::WireFailure::Unavailable => {
format!("$Error {}|", failure.prefixed_text())
}
};
let bytes = frame.into_bytes();
let mut write = write_half_arc.lock().await;
if let Err(werr) = write.write_all(&bytes).await {
log.error(format!(
"Failed to write DC failure notice on connection {}: {}",
connection_id, werr
));
} else {
let _ = write.flush().await;
drop(write);
state_clone
.update_connection_stats(
server_id,
connection_id,
None,
Some(bytes.len() as u64),
None,
Some(1),
)
.await;
}
}
}
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
Log::new(Some(status_clone))
.debug(format!("DC connection {} closed by client", connection_id));
} else {
Log::new(Some(status_clone)).error(format!(
"DC read error on connection {}: {}",
connection_id, e
));
}
break;
}
}
}
}
}