netget/client/
command_support.rs1use std::sync::Arc;
38
39use tokio::io::{AsyncWrite, AsyncWriteExt};
40use tokio::sync::{mpsc, Mutex};
41use tracing::{error, info};
42
43use crate::llm::actions::client_trait::{Client, ClientActionResult};
44use crate::state::client_handles::{
45 ClientCommand, ClientHandle, ClientSendOutcome, CLIENT_COMMAND_CAPACITY,
46};
47use crate::state::{AccessLogOwner, AppState, ClientId};
48
49pub async fn register_command_channel(
52 state: &AppState,
53 client_id: ClientId,
54) -> mpsc::Receiver<ClientCommand> {
55 let (command_tx, command_rx) = mpsc::channel(CLIENT_COMMAND_CAPACITY);
56 state
57 .register_client_handle(client_id, ClientHandle { command_tx })
58 .await;
59 command_rx
60}
61
62pub fn reply(command: ClientCommand, outcome: anyhow::Result<ClientSendOutcome>) {
64 let _ = command.reply_tx.send(outcome);
65}
66
67pub async fn handle_stream_client_command<W, P>(
74 protocol: &P,
75 write_half: &Arc<Mutex<W>>,
76 command: ClientCommand,
77 client_id: ClientId,
78 state: &AppState,
79 status_tx: &mpsc::UnboundedSender<String>,
80) -> bool
81where
82 W: AsyncWrite + Unpin,
83 P: Client + ?Sized,
84{
85 let action = command.action.clone();
86 let (outcome, should_break) = execute_command_action(protocol, write_half, &action).await;
87
88 let outcome_json = match &outcome {
91 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
92 Err(e) => serde_json::json!({"error": e.to_string()}),
93 };
94 state
95 .record_access_log(
96 AccessLogOwner::Client(client_id.as_u32()),
97 protocol.protocol_name(),
98 None,
99 "injected_action",
100 action,
101 vec![outcome_json],
102 )
103 .await;
104
105 match &outcome {
106 Ok(ClientSendOutcome::Sent { bytes_sent }) => {
107 info!(
108 "Client {} executed injected action ({} bytes sent)",
109 client_id, bytes_sent
110 );
111 }
112 Ok(ClientSendOutcome::Disconnected) => {
113 info!("Client {} disconnecting on injected action", client_id);
114 }
115 Ok(_) => {}
116 Err(e) => {
117 error!("Client {} injected action failed: {}", client_id, e);
118 let _ = status_tx.send(format!(
119 "[WARN] Client {} injected action failed: {}",
120 client_id, e
121 ));
122 }
123 }
124 let _ = status_tx.send("__UPDATE_UI__".to_string());
125
126 reply(command, outcome);
127 should_break
128}
129
130async fn execute_command_action<W, P>(
133 protocol: &P,
134 write_half: &Arc<Mutex<W>>,
135 action: &serde_json::Value,
136) -> (anyhow::Result<ClientSendOutcome>, bool)
137where
138 W: AsyncWrite + Unpin,
139 P: Client + ?Sized,
140{
141 let result = match protocol.execute_action(action.clone()) {
142 Ok(result) => result,
143 Err(e) => {
144 return (
145 Ok(ClientSendOutcome::Rejected {
146 error: e.to_string(),
147 }),
148 false,
149 )
150 }
151 };
152
153 let mut bytes_sent = 0usize;
154 let mut disconnect = false;
155 let mut details: Vec<String> = Vec::new();
156
157 let results = match result {
160 ClientActionResult::Multiple(results) => results,
161 single => vec![single],
162 };
163
164 for result in results {
165 match result {
166 ClientActionResult::SendData(bytes) => {
167 let mut guard = write_half.lock().await;
168 if let Err(e) = guard.write_all(&bytes).await {
169 return (Err(anyhow::anyhow!("write failed: {e}")), false);
170 }
171 if let Err(e) = guard.flush().await {
172 return (Err(anyhow::anyhow!("flush failed: {e}")), false);
173 }
174 bytes_sent += bytes.len();
175 }
176 ClientActionResult::Disconnect => disconnect = true,
177 ClientActionResult::WaitForMore => details.push("wait_for_more".to_string()),
178 ClientActionResult::NoAction => details.push("no_action".to_string()),
179 ClientActionResult::Custom { name, .. } => {
180 details.push(format!(
183 "custom result '{name}' not executed by generic arm"
184 ));
185 }
186 ClientActionResult::Multiple(_) => {
187 details.push("nested multiple ignored".to_string());
188 }
189 }
190 }
191
192 let outcome = if disconnect {
193 ClientSendOutcome::Disconnected
194 } else if bytes_sent > 0 {
195 ClientSendOutcome::Sent { bytes_sent }
196 } else {
197 ClientSendOutcome::Executed {
198 detail: if details.is_empty() {
199 "executed".to_string()
200 } else {
201 details.join(", ")
202 },
203 }
204 };
205 (Ok(outcome), disconnect)
206}