1pub mod actions;
9
10pub use actions::WebRtcClientProtocol;
11
12use anyhow::{Context, Result};
13use futures::{SinkExt, StreamExt};
14use std::collections::HashMap;
15use std::net::SocketAddr;
16use std::sync::Arc;
17use tokio::sync::{mpsc, Mutex};
18use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage};
19use tracing::{debug, error, info, trace, warn};
20use webrtc::api::interceptor_registry::register_default_interceptors;
21use webrtc::api::media_engine::MediaEngine;
22use webrtc::api::APIBuilder;
23use webrtc::data_channel::data_channel_message::DataChannelMessage;
24use webrtc::data_channel::RTCDataChannel;
25use webrtc::ice_transport::ice_server::RTCIceServer;
26use webrtc::interceptor::registry::Registry;
27use webrtc::peer_connection::configuration::RTCConfiguration;
28use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState;
29use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
30use webrtc::peer_connection::RTCPeerConnection;
31
32use crate::client::llm_budget::call_llm_for_client;
33use crate::client::webrtc::actions::{
34 WEBRTC_CLIENT_CHANNEL_OPENED_EVENT, WEBRTC_CLIENT_MESSAGE_RECEIVED_EVENT,
35 WEBRTC_CLIENT_SIGNALING_CONNECTED_EVENT,
36};
37use crate::llm::ollama_client::OllamaClient;
38use crate::llm::ClientLlmResult;
39use crate::protocol::Event;
40use crate::state::app_state::AppState;
41use crate::state::{ClientId, ClientStatus};
42
43#[derive(Debug, Clone, PartialEq)]
45pub enum SignalingMode {
46 Manual,
48 WebSocket { url: String, peer_id: String },
50}
51
52#[derive(Debug, Clone, PartialEq)]
58enum ConnectionState {
59 Idle,
60 Processing,
61}
62
63struct ChannelData {
65 state: ConnectionState,
66 queued_messages: std::collections::VecDeque<(String, bool)>, channel: Arc<RTCDataChannel>,
71}
72
73struct ClientData {
75 shutdown: tokio_util::sync::CancellationToken,
76 memory: String,
77 channels: HashMap<String, ChannelData>, }
79
80#[derive(Clone)]
82pub struct PeerConnectionGuard(Arc<PeerCleanup>);
83struct PeerCleanup {
84 peer: Arc<RTCPeerConnection>,
85 shutdown: tokio_util::sync::CancellationToken,
86}
87impl PeerConnectionGuard {
88 pub fn new(peer: Arc<RTCPeerConnection>) -> Self {
89 Self(Arc::new(PeerCleanup {
90 peer,
91 shutdown: tokio_util::sync::CancellationToken::new(),
92 }))
93 }
94 pub fn cancellation_token(&self) -> tokio_util::sync::CancellationToken {
95 self.0.shutdown.clone()
96 }
97}
98impl Drop for PeerCleanup {
99 fn drop(&mut self) {
100 self.shutdown.cancel();
101 let peer = self.peer.clone();
102 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
103 runtime.spawn(async move {
104 if let Err(error) = peer.close().await {
105 tracing::warn!("WebRTC close failed: {error}");
106 }
107 });
108 }
109 }
110}
111
112pub struct WebRtcClient;
114
115impl WebRtcClient {
116 pub async fn connect_with_llm_actions(
122 remote_addr: String,
123 llm_client: OllamaClient,
124 app_state: Arc<AppState>,
125 status_tx: mpsc::UnboundedSender<String>,
126 client_id: ClientId,
127 startup_params: Option<crate::protocol::StartupParams>,
128 ) -> Result<SocketAddr> {
129 info!(
130 "WebRTC client {} initializing for {}",
131 client_id, remote_addr
132 );
133
134 let signaling_mode =
136 if remote_addr.starts_with("ws://") || remote_addr.starts_with("wss://") {
137 let parts: Vec<&str> = remote_addr.rsplitn(2, '/').collect();
139 if parts.len() != 2 {
140 return Err(anyhow::anyhow!(
141 "Invalid WebSocket URL format. Expected: ws://server:port/peer_id"
142 ));
143 }
144 let peer_id = parts[0].to_string();
145 let url = parts[1].to_string();
146 SignalingMode::WebSocket {
147 url: format!("{}/", url), peer_id,
149 }
150 } else {
151 SignalingMode::Manual
152 };
153
154 info!(
155 "WebRTC client {} using signaling mode: {:?}",
156 client_id, signaling_mode
157 );
158
159 let mut m = MediaEngine::default();
161
162 let registry = Registry::new();
164 let registry = register_default_interceptors(registry, &mut m)?;
165
166 let api = APIBuilder::new()
168 .with_media_engine(m)
169 .with_interceptor_registry(registry)
170 .build();
171
172 let ice_urls: Vec<String> = match startup_params.as_ref() {
177 Some(params) => match params
178 .get_optional_array("ice_servers")
179 .map_err(|e| anyhow::anyhow!("WebRTC client parameter error: {e}"))?
180 {
181 Some(arr) => arr
182 .iter()
183 .filter_map(|v| v.as_str().map(|s| s.trim().to_string()))
184 .filter(|s| !s.is_empty())
185 .collect(),
186 None => Vec::new(),
187 },
188 None => Vec::new(),
202 };
203 let config = RTCConfiguration {
204 ice_servers: ice_urls
205 .into_iter()
206 .map(|url| RTCIceServer {
207 urls: vec![url],
208 ..Default::default()
209 })
210 .collect(),
211 ..Default::default()
212 };
213
214 let peer_connection = Arc::new(api.new_peer_connection(config).await?);
216 let lifetime = PeerConnectionGuard::new(peer_connection.clone());
217 info!("WebRTC client {} created peer connection", client_id);
218
219 let client_data = Arc::new(Mutex::new(ClientData {
221 shutdown: lifetime.cancellation_token(),
222 memory: String::new(),
223 channels: HashMap::new(),
224 }));
225
226 let data_channel = peer_connection.create_data_channel("netget", None).await?;
228 info!("WebRTC client {} created data channel 'netget'", client_id);
229
230 Self::setup_data_channel_handlers(
232 Arc::clone(&data_channel),
233 "netget".to_string(),
234 client_id,
235 Arc::clone(&client_data),
236 Arc::clone(&app_state),
237 status_tx.clone(),
238 llm_client.clone(),
239 )
240 .await;
241
242 client_data.lock().await.channels.insert(
244 "netget".to_string(),
245 ChannelData {
246 state: ConnectionState::Idle,
247 queued_messages: std::collections::VecDeque::new(),
248 channel: Arc::clone(&data_channel),
249 },
250 );
251
252 let status_tx_state = status_tx.clone();
254 let app_state_state = Arc::clone(&app_state);
255 peer_connection.on_peer_connection_state_change(Box::new(
256 move |state: RTCPeerConnectionState| {
257 let status_tx = status_tx_state.clone();
258 let app_state = Arc::clone(&app_state_state);
259 Box::pin(async move {
260 info!("WebRTC client {} connection state: {:?}", client_id, state);
261 match state {
262 RTCPeerConnectionState::Connected => {
263 app_state
264 .update_client_status(client_id, ClientStatus::Connected)
265 .await;
266 let _ = status_tx
267 .send(format!("[CLIENT] WebRTC client {} connected", client_id));
268 let _ = status_tx.send("__UPDATE_UI__".to_string());
269 }
270 RTCPeerConnectionState::Failed | RTCPeerConnectionState::Closed => {
271 app_state
272 .update_client_status(client_id, ClientStatus::Disconnected)
273 .await;
274 app_state.remove_client_handle(client_id).await;
277 let _ = status_tx
278 .send(format!("[CLIENT] WebRTC client {} disconnected", client_id));
279 let _ = status_tx.send("__UPDATE_UI__".to_string());
280 }
281 _ => {}
282 }
283 })
284 },
285 ));
286
287 app_state
290 .with_client_mut(client_id, |client| {
291 client.set_protocol_field(
292 "signaling_mode".to_string(),
293 match &signaling_mode {
294 SignalingMode::Manual => serde_json::json!("manual"),
295 SignalingMode::WebSocket { url, peer_id } => serde_json::json!({
296 "mode": "websocket",
297 "url": url,
298 "peer_id": peer_id
299 }),
300 },
301 );
302 })
303 .await;
304
305 let command_rx =
310 crate::client::command_support::register_command_channel(&app_state, client_id).await;
311 let cmd_pc = Arc::clone(&peer_connection);
312 let cmd_data = Arc::clone(&client_data);
313 let cmd_state = Arc::clone(&app_state);
314 let cmd_status_tx = status_tx.clone();
315 let cmd_llm = llm_client.clone();
316 let cmd_lifetime = lifetime.clone();
317 let cmd_task = tokio::spawn(async move {
318 let _lifetime = cmd_lifetime;
319 Self::command_loop(
320 command_rx,
321 cmd_pc,
322 cmd_data,
323 client_id,
324 cmd_state,
325 cmd_status_tx,
326 cmd_llm,
327 )
328 .await;
329 });
330 app_state.register_client_task(client_id, cmd_task).await;
331
332 let signaling_result = match signaling_mode {
333 SignalingMode::Manual => {
334 Self::manual_signaling(
336 peer_connection.clone(),
337 client_id,
338 Arc::clone(&app_state),
339 status_tx.clone(),
340 )
341 .await
342 }
343 SignalingMode::WebSocket { url, peer_id } => {
344 Self::websocket_signaling(
346 peer_connection.clone(),
347 client_id,
348 url,
349 peer_id,
350 Arc::clone(&app_state),
351 status_tx.clone(),
352 llm_client,
353 )
354 .await
355 }
356 };
357 if let Err(error) = signaling_result {
358 app_state.remove_client_handle(client_id).await;
359 let _ = peer_connection.close().await;
360 return Err(error);
361 }
362
363 let app_state_cleanup = Arc::clone(&app_state);
365 let task_registrar = app_state.clone();
368 let task_handle = tokio::spawn(async move {
369 loop {
370 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
371
372 if app_state_cleanup.get_client(client_id).await.is_none() {
374 info!("WebRTC client {} stopped", client_id);
375 app_state_cleanup.remove_client_handle(client_id).await;
377 break;
378 }
379 }
380 });
381 task_registrar
382 .register_client_task(client_id, task_handle)
383 .await;
384
385 Ok("0.0.0.0:0".parse().unwrap())
387 }
388
389 async fn manual_signaling(
391 peer_connection: Arc<RTCPeerConnection>,
392 client_id: ClientId,
393 app_state: Arc<AppState>,
394 status_tx: mpsc::UnboundedSender<String>,
395 ) -> Result<()> {
396 let offer = peer_connection.create_offer(None).await?;
398 peer_connection.set_local_description(offer).await?;
399
400 let mut gather_complete = peer_connection.gathering_complete_promise().await;
402 let _ = gather_complete.recv().await;
403
404 let local_desc = peer_connection
406 .local_description()
407 .await
408 .context("No local description available")?;
409
410 let offer_json = serde_json::to_string_pretty(&local_desc)?;
412 app_state
413 .with_client_mut(client_id, |client| {
414 client.set_protocol_field("sdp_offer".to_string(), serde_json::json!(offer_json));
415 })
416 .await;
417
418 info!(
419 "WebRTC client {} generated SDP offer (manual mode)",
420 client_id
421 );
422 let _ = status_tx.send(format!(
423 "[CLIENT] WebRTC client {} waiting for SDP answer",
424 client_id
425 ));
426 let _ = status_tx.send(format!("SDP Offer (send to peer):\n{}", offer_json));
427 let _ = status_tx.send("__UPDATE_UI__".to_string());
428
429 Ok(())
430 }
431
432 async fn websocket_signaling(
434 peer_connection: Arc<RTCPeerConnection>,
435 client_id: ClientId,
436 url: String,
437 peer_id: String,
438 app_state: Arc<AppState>,
439 status_tx: mpsc::UnboundedSender<String>,
440 llm_client: OllamaClient,
441 ) -> Result<()> {
442 info!(
443 "WebRTC client {} connecting to signaling server: {}",
444 client_id, url
445 );
446
447 let (ws_stream, _) = connect_async(&url)
449 .await
450 .context("Failed to connect to signaling server")?;
451
452 let (mut ws_tx, mut ws_rx) = ws_stream.split();
453
454 info!("WebRTC client {} connected to signaling server", client_id);
455 let _ = status_tx.send(format!(
456 "[CLIENT] WebRTC client {} connected to signaling server",
457 client_id
458 ));
459
460 let register_msg = serde_json::json!({
462 "type": "register",
463 "peer_id": peer_id,
464 });
465 ws_tx
466 .send(WsMessage::Text(register_msg.to_string()))
467 .await
468 .context("Failed to send registration")?;
469
470 debug!("WebRTC client {} registered as '{}'", client_id, peer_id);
471
472 let protocol = Arc::new(crate::client::webrtc::actions::WebRtcClientProtocol::new());
474 let event = Event::new(
475 &WEBRTC_CLIENT_SIGNALING_CONNECTED_EVENT,
476 serde_json::json!({
477 "peer_id": peer_id,
478 "server_url": url,
479 }),
480 );
481
482 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
483 match call_llm_for_client(
484 &llm_client,
485 &app_state,
486 client_id.to_string(),
487 &instruction,
488 "",
489 Some(&event),
490 protocol.as_ref(),
491 &status_tx,
492 )
493 .await
494 {
495 Ok(ClientLlmResult {
496 actions,
497 memory_updates,
498 }) => {
499 if let Some(mem) = memory_updates {
500 app_state.set_memory_for_client(client_id, mem).await;
501 }
502 if !actions.is_empty() {
507 info!(
508 "WebRTC client {}: {} action(s) answered at signaling-connected \
509 cannot run yet -- no data channel is open; they are actionable \
510 at webrtc_channel_opened",
511 client_id,
512 actions.len()
513 );
514 }
515 }
516 Err(e) => {
517 warn!(
518 "LLM error for WebRTC client {} signaling connected: {}",
519 client_id, e
520 );
521 }
522 }
523 }
524
525 let offer = peer_connection.create_offer(None).await?;
527 peer_connection.set_local_description(offer).await?;
528
529 let mut gather_complete = peer_connection.gathering_complete_promise().await;
531 let _ = gather_complete.recv().await;
532
533 let local_desc = peer_connection
534 .local_description()
535 .await
536 .context("No local description")?;
537
538 info!(
539 "WebRTC client {} generated SDP offer (WebSocket mode)",
540 client_id
541 );
542
543 let offer_json = serde_json::to_value(&local_desc)?;
548 app_state
549 .with_client_mut(client_id, |client| {
550 client.set_protocol_field("sdp_offer".to_string(), offer_json.clone());
551 })
552 .await;
553
554 let pc_for_ws = Arc::clone(&peer_connection);
556 let status_tx_ws = status_tx.clone();
557 let app_state_ws = Arc::clone(&app_state);
558
559 let task_registrar = app_state.clone();
562 let task_handle = tokio::spawn(async move {
563 while let Some(msg_result) = ws_rx.next().await {
564 match msg_result {
565 Ok(WsMessage::Text(text)) => {
566 trace!(
567 "WebRTC client {} received signaling message: {}",
568 client_id,
569 text
570 );
571
572 if let Ok(msg) = serde_json::from_str::<serde_json::Value>(&text) {
574 let msg_type = msg.get("type").and_then(|v| v.as_str());
575
576 match msg_type {
577 Some("registered") => {
578 info!(
579 "WebRTC client {} registered with signaling server",
580 client_id
581 );
582 let _ = status_tx_ws
583 .send(format!("[CLIENT] Registered as '{}'", peer_id));
584 }
585 Some("answer") => {
586 if let Some(sdp) = msg.get("sdp") {
588 info!("WebRTC client {} received SDP answer", client_id);
589
590 match serde_json::from_value::<RTCSessionDescription>(
591 sdp.clone(),
592 ) {
593 Ok(answer) => {
594 if let Err(e) =
595 pc_for_ws.set_remote_description(answer).await
596 {
597 error!(
598 "WebRTC client {} failed to set remote description: {}",
599 client_id, e
600 );
601 } else {
602 info!(
603 "WebRTC client {} connection established via signaling",
604 client_id
605 );
606 app_state_ws
607 .update_client_status(
608 client_id,
609 ClientStatus::Connected,
610 )
611 .await;
612 }
613 }
614 Err(e) => {
615 error!(
616 "WebRTC client {} failed to parse SDP answer: {}",
617 client_id, e
618 );
619 }
620 }
621 }
622 }
623 Some("ice_candidate") => {
624 trace!("WebRTC client {} received ICE candidate", client_id);
626 }
627 Some("error") => {
628 if let Some(error_msg) =
629 msg.get("message").and_then(|v| v.as_str())
630 {
631 error!(
632 "WebRTC client {} signaling error: {}",
633 client_id, error_msg
634 );
635 let _ = status_tx_ws.send(format!(
636 "[CLIENT] Signaling error: {}",
637 error_msg
638 ));
639 }
640 }
641 _ => {
642 trace!(
643 "WebRTC client {} unknown signaling message type",
644 client_id
645 );
646 }
647 }
648 }
649 }
650 Ok(WsMessage::Close(_)) => {
651 info!(
652 "WebRTC client {} signaling server closed connection",
653 client_id
654 );
655 break;
656 }
657 Err(e) => {
658 error!("WebRTC client {} signaling error: {}", client_id, e);
659 break;
660 }
661 _ => {}
662 }
663 }
664 });
665 task_registrar
666 .register_client_task(client_id, task_handle)
667 .await;
668
669 Ok(())
670 }
671
672 async fn setup_data_channel_handlers(
674 data_channel: Arc<RTCDataChannel>,
675 channel_label: String,
676 client_id: ClientId,
677 client_data: Arc<Mutex<ClientData>>,
678 app_state: Arc<AppState>,
679 status_tx: mpsc::UnboundedSender<String>,
680 llm_client: OllamaClient,
681 ) {
682 let client_data_on_open = Arc::downgrade(&client_data);
684 let app_state_on_open = Arc::clone(&app_state);
685 let status_tx_on_open = status_tx.clone();
686 let llm_on_open = llm_client.clone();
687 let label_on_open = channel_label.clone();
688 let dc_on_open = Arc::downgrade(&data_channel);
691
692 data_channel.on_open(Box::new(move || {
693 let app_state = Arc::clone(&app_state_on_open);
694 let status_tx = status_tx_on_open.clone();
695 let client_data = client_data_on_open.upgrade();
696 let llm_client = llm_on_open.clone();
697 let label = label_on_open.clone();
698 let dc = dc_on_open.upgrade();
699
700 Box::pin(async move {
701 let (Some(client_data), Some(dc)) = (client_data, dc) else {
702 return;
703 };
704 info!(
705 "WebRTC client {} data channel '{}' opened",
706 client_id, label
707 );
708 let _ = status_tx.send(format!(
709 "[CLIENT] WebRTC client {} channel '{}' opened",
710 client_id, label
711 ));
712 let _ = status_tx.send("__UPDATE_UI__".to_string());
713
714 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
716 let protocol =
717 Arc::new(crate::client::webrtc::actions::WebRtcClientProtocol::new());
718 let event = Event::new(
719 &WEBRTC_CLIENT_CHANNEL_OPENED_EVENT,
720 serde_json::json!({
721 "channel_label": label,
722 }),
723 );
724
725 let memory = client_data.lock().await.memory.clone();
726
727 let shutdown = client_data.lock().await.shutdown.clone();
728 let call = call_llm_for_client(
729 &llm_client,
730 &app_state,
731 client_id.to_string(),
732 &instruction,
733 &memory,
734 Some(&event),
735 protocol.as_ref(),
736 &status_tx,
737 );
738 let result = tokio::select! { _ = shutdown.cancelled() => return, result = call => result };
739 match result {
740 Ok(ClientLlmResult {
741 actions,
742 memory_updates,
743 }) => {
744 if let Some(mem) = memory_updates {
745 client_data.lock().await.memory = mem;
746 }
747
748 use crate::llm::actions::client_trait::{Client, ClientActionResult};
756 for action in actions {
757 match protocol.as_ref().execute_action(action) {
758 Ok(ClientActionResult::SendData(bytes)) => {
759 match dc.send(&bytes.into()).await {
760 Ok(n) => info!(
761 "WebRTC client {} sent {} bytes on '{}' \
762 when the channel opened",
763 client_id, n, label
764 ),
765 Err(e) => error!(
766 "WebRTC client {} failed to send on '{}': {}",
767 client_id, label, e
768 ),
769 }
770 }
771 Ok(ClientActionResult::Disconnect) => {
772 info!(
773 "WebRTC client {} closing channel '{}' on the \
774 model's request",
775 client_id, label
776 );
777 let _ = dc.close().await;
778 }
779 Ok(_) => {}
780 Err(e) => error!(
781 "WebRTC client {} rejected its own channel-open \
782 action: {}",
783 client_id, e
784 ),
785 }
786 }
787 }
788 Err(e) => {
789 error!(
790 "LLM error for WebRTC client {} on channel open: {}",
791 client_id, e
792 );
793 }
794 }
795 }
796 })
797 }));
798
799 let client_data_on_msg = Arc::downgrade(&client_data);
801 let app_state_on_msg = Arc::clone(&app_state);
802 let status_tx_on_msg = status_tx.clone();
803 let llm_on_msg = llm_client.clone();
804 let label_on_msg = channel_label.clone();
805 let dc_on_msg = Arc::downgrade(&data_channel);
806
807 data_channel.on_message(Box::new(move |msg: DataChannelMessage| {
808 let app_state = Arc::clone(&app_state_on_msg);
809 let status_tx = status_tx_on_msg.clone();
810 let client_data = client_data_on_msg.upgrade();
811 let llm_client = llm_on_msg.clone();
812 let dc = dc_on_msg.upgrade();
813 let label = label_on_msg.clone();
814
815 Box::pin(async move {
816 let (Some(client_data), Some(dc)) = (client_data, dc) else { return; };
817 if msg.data.len() > 1024 * 1024 {
818 warn!("WebRTC data message exceeds 1 MiB; closing channel");
819 let _ = dc.close().await;
820 return;
821 }
822 let mut is_binary = !msg.is_string;
824
825 let mut message_text = if is_binary {
826 hex::encode(&msg.data)
828 } else {
829 String::from_utf8_lossy(&msg.data).to_string()
830 };
831
832 trace!(
833 "WebRTC client {} received message on '{}': {} (binary: {})",
834 client_id,
835 label,
836 if is_binary { "hex data" } else { &message_text },
837 is_binary
838 );
839
840 let mut client_data_lock = client_data.lock().await;
842
843 if !client_data_lock.channels.contains_key(&label) {
845 warn!("WebRTC client {} received message on unknown channel '{}'", client_id, label);
846 return;
847 }
848
849 let channel_state = client_data_lock
850 .channels
851 .get(&label)
852 .map(|cd| cd.state.clone())
853 .unwrap_or(ConnectionState::Idle);
854
855 match channel_state {
856 ConnectionState::Idle => {
857 if let Some(channel_data) = client_data_lock.channels.get_mut(&label) {
859 channel_data.state = ConnectionState::Processing;
860 }
861 drop(client_data_lock);
862
863 loop {
864 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
866 let protocol = Arc::new(crate::client::webrtc::actions::WebRtcClientProtocol::new());
867 let event = Event::new(
868 &WEBRTC_CLIENT_MESSAGE_RECEIVED_EVENT,
869 serde_json::json!({
870 "channel_label": label,
871 "message": message_text,
872 "is_binary": is_binary,
873 }),
874 );
875
876 let memory = client_data.lock().await.memory.clone();
877
878 let shutdown = client_data.lock().await.shutdown.clone();
879 let call = call_llm_for_client(
880 &llm_client,
881 &app_state,
882 client_id.to_string(),
883 &instruction,
884 &memory,
885 Some(&event),
886 protocol.as_ref(),
887 &status_tx,
888 );
889 let result = tokio::select! { _ = shutdown.cancelled() => return, result = call => result };
890 match result {
891 Ok(ClientLlmResult { actions, memory_updates }) => {
892 if let Some(mem) = memory_updates {
894 client_data.lock().await.memory = mem;
895 }
896
897 for action in actions {
899 use crate::llm::actions::client_trait::Client;
900 match protocol.as_ref().execute_action(action) {
901 Ok(crate::llm::actions::client_trait::ClientActionResult::SendData(bytes)) => {
902 match dc.send(&bytes.into()).await {
904 Ok(_) => {
905 trace!("WebRTC client {} sent message on '{}'", client_id, label);
906 }
907 Err(e) => {
908 error!("WebRTC client {} failed to send on '{}': {}", client_id, label, e);
909 }
910 }
911 }
912 Ok(crate::llm::actions::client_trait::ClientActionResult::Disconnect) => {
913 info!("WebRTC client {} closing channel '{}'", client_id, label);
914 let _ = dc.close().await;
915 }
916 Ok(crate::llm::actions::client_trait::ClientActionResult::WaitForMore) => {
917 trace!("WebRTC client {} waiting for more data on '{}'", client_id, label);
918 }
919 _ => {}
920 }
921 }
922 }
923 Err(e) => {
924 error!("LLM error for WebRTC client {} on '{}': {}", client_id, label, e);
925 }
926 }
927 }
928
929 let mut client_data_lock = client_data.lock().await;
930 if let Some(channel_data) = client_data_lock.channels.get_mut(&label) {
931 if let Some((next, binary)) = channel_data.queued_messages.pop_front() {
932 message_text = next;
933 is_binary = binary;
934 drop(client_data_lock);
935 continue;
936 }
937 channel_data.state = ConnectionState::Idle;
938 }
939 break;
940 }
941 }
942 ConnectionState::Processing => {
943 if let Some(channel_data) = client_data_lock.channels.get_mut(&label) {
945 let queued_bytes: usize = channel_data.queued_messages.iter().map(|(message, _)| message.len()).sum();
946 if channel_data.queued_messages.len() >= 128 || queued_bytes.saturating_add(message_text.len()) > 8 * 1024 * 1024 {
947 drop(client_data_lock);
948 warn!("WebRTC pending message queue full; closing channel");
949 let _ = dc.close().await;
950 return;
951 }
952 channel_data.queued_messages.push_back((message_text, is_binary));
953 trace!("WebRTC client {} queued message on '{}' (already processing)", client_id, label);
954 }
955 }
956 }
957 })
958 }));
959 }
960
961 #[allow(clippy::too_many_arguments)]
985 async fn command_loop(
986 mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
987 peer_connection: Arc<RTCPeerConnection>,
988 client_data: Arc<Mutex<ClientData>>,
989 client_id: ClientId,
990 app_state: Arc<AppState>,
991 status_tx: mpsc::UnboundedSender<String>,
992 llm_client: OllamaClient,
993 ) {
994 use crate::llm::actions::client_trait::{Client, ClientActionResult};
995 use crate::llm::actions::protocol_trait::Protocol;
996 use crate::state::client_handles::ClientSendOutcome;
997 use crate::state::AccessLogOwner;
998
999 let protocol = WebRtcClientProtocol::new();
1000
1001 while let Some(command) = command_rx.recv().await {
1002 let action = command.action.clone();
1003 let label = action
1007 .get("channel")
1008 .and_then(|v| v.as_str())
1009 .unwrap_or("netget")
1010 .to_string();
1011
1012 let outcome: Result<ClientSendOutcome> = match protocol.execute_action(action.clone()) {
1013 Err(e) => Ok(ClientSendOutcome::Rejected {
1014 error: e.to_string(),
1015 }),
1016 Ok(ClientActionResult::SendData(bytes)) => {
1017 let channel = {
1018 client_data
1019 .lock()
1020 .await
1021 .channels
1022 .get(&label)
1023 .map(|c| Arc::clone(&c.channel))
1024 };
1025 match channel {
1026 None => Ok(ClientSendOutcome::Rejected {
1027 error: format!(
1028 "this client has no data channel labelled '{label}'; \
1029 open one with create_channel first"
1030 ),
1031 }),
1032 Some(channel) => match channel.send(&bytes.into()).await {
1033 Ok(bytes_sent) => Ok(ClientSendOutcome::Sent { bytes_sent }),
1034 Err(e) => Err(anyhow::anyhow!(
1035 "data channel '{}' is {} - send failed: {}",
1036 label,
1037 channel.ready_state(),
1038 e
1039 )),
1040 },
1041 }
1042 }
1043 Ok(ClientActionResult::Custom { name, data }) => match name.as_str() {
1044 "create_channel" => {
1045 let new_label = data
1046 .get("channel_label")
1047 .and_then(|v| v.as_str())
1048 .unwrap_or("netget")
1049 .to_string();
1050 match peer_connection.create_data_channel(&new_label, None).await {
1051 Ok(channel) => {
1052 Self::setup_data_channel_handlers(
1053 Arc::clone(&channel),
1054 new_label.clone(),
1055 client_id,
1056 Arc::clone(&client_data),
1057 Arc::clone(&app_state),
1058 status_tx.clone(),
1059 llm_client.clone(),
1060 )
1061 .await;
1062 client_data.lock().await.channels.insert(
1063 new_label.clone(),
1064 ChannelData {
1065 state: ConnectionState::Idle,
1066 queued_messages: std::collections::VecDeque::new(),
1067 channel,
1068 },
1069 );
1070 Ok(ClientSendOutcome::Executed {
1071 detail: format!(
1072 "data channel '{new_label}' created; it carries data \
1073 once the peer connection is established"
1074 ),
1075 })
1076 }
1077 Err(e) => Err(anyhow::anyhow!("create_data_channel failed: {e}")),
1078 }
1079 }
1080 "apply_answer" => {
1081 let answer_json = data
1082 .get("answer_json")
1083 .and_then(|v| v.as_str())
1084 .unwrap_or_default();
1085 match serde_json::from_str::<RTCSessionDescription>(answer_json) {
1086 Ok(answer) => {
1087 match peer_connection.set_remote_description(answer).await {
1088 Ok(()) => Ok(ClientSendOutcome::Executed {
1089 detail: "SDP answer applied; ICE and DTLS now proceed"
1090 .to_string(),
1091 }),
1092 Err(e) => {
1093 Err(anyhow::anyhow!("set_remote_description failed: {e}"))
1094 }
1095 }
1096 }
1097 Err(e) => Ok(ClientSendOutcome::Rejected {
1098 error: format!("answer_json is not an SDP answer: {e}"),
1099 }),
1100 }
1101 }
1102 "send_offer" => Ok(ClientSendOutcome::Rejected {
1111 error: "send_offer cannot be injected: the signaling WebSocket sink \
1112 belongs to the signaling task and is not reachable from a \
1113 command, and manual mode has no signaling connection at all"
1114 .to_string(),
1115 }),
1116 other => Ok(ClientSendOutcome::Rejected {
1117 error: format!("WebRTC client cannot apply custom result '{other}'"),
1118 }),
1119 },
1120 Ok(ClientActionResult::Disconnect) => {
1121 let channels: Vec<Arc<RTCDataChannel>> = {
1122 client_data
1123 .lock()
1124 .await
1125 .channels
1126 .values()
1127 .map(|c| Arc::clone(&c.channel))
1128 .collect()
1129 };
1130 for channel in channels {
1131 let _ = channel.close().await;
1132 }
1133 if let Err(e) = peer_connection.close().await {
1134 debug!("WebRTC client {} close: {}", client_id, e);
1135 }
1136 Ok(ClientSendOutcome::Disconnected)
1137 }
1138 Ok(ClientActionResult::WaitForMore) => Ok(ClientSendOutcome::Executed {
1139 detail: "waiting for the next message; nothing sent".to_string(),
1140 }),
1141 Ok(ClientActionResult::NoAction) => Ok(ClientSendOutcome::Executed {
1142 detail: "no_action: nothing sent".to_string(),
1143 }),
1144 Ok(ClientActionResult::Multiple(_)) => Ok(ClientSendOutcome::Rejected {
1145 error: "WebRTC client does not support Multiple action results".to_string(),
1146 }),
1147 };
1148
1149 let outcome_json = match &outcome {
1150 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
1151 Err(e) => serde_json::json!({"error": e.to_string()}),
1152 };
1153 app_state
1154 .record_access_log(
1155 AccessLogOwner::Client(client_id.as_u32()),
1156 protocol.protocol_name(),
1157 None,
1158 "injected_action",
1159 action,
1160 vec![outcome_json],
1161 )
1162 .await;
1163
1164 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
1165 if let Err(e) = &outcome {
1166 error!("WebRTC client {} injected action failed: {}", client_id, e);
1167 let _ = status_tx.send(format!(
1168 "[WARN] Client {} injected action failed: {}",
1169 client_id, e
1170 ));
1171 }
1172 let _ = status_tx.send("__UPDATE_UI__".to_string());
1173 crate::client::command_support::reply(command, outcome);
1174
1175 if disconnect {
1176 app_state.remove_client_handle(client_id).await;
1177 break;
1178 }
1179 }
1180 }
1181}