1pub mod actions;
11pub mod display;
12
13use crate::client::llm_budget::call_llm_for_client;
14use crate::llm::actions::client_trait::{Client, ClientActionResult};
15use crate::logging::emit::Log;
16use crate::protocol::{ConnectContext, Event, EventType};
17use crate::server::spice::wire::{self, channel, common_cap, link_err, msg, msgc, Cursor};
18use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
19use crate::state::{AccessLogOwner, ClientStatus};
20use actions::Input;
21pub use actions::SpiceClientProtocol;
22use anyhow::{bail, ensure, Context, Result};
23use serde_json::{json, Value};
24use std::net::SocketAddr;
25use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
26use std::sync::Arc;
27use std::time::Duration;
28use tokio::io::{AsyncReadExt, AsyncWriteExt};
29use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
30use tokio::net::TcpStream;
31use tokio::sync::{mpsc, Mutex};
32use tokio::time::Instant;
33
34pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
36pub const MAX_FOLLOWUP_DEPTH: u32 = 8;
38pub const MAX_SERVER_MESSAGE_BYTES: u32 = 36 << 20;
41const MAX_SETUP_MESSAGES: usize = 256;
43const TURNS: usize = 64;
44
45pub struct Channel {
47 writer: Mutex<OwnedWriteHalf>,
48 mini: bool,
49 serial: AtomicU64,
50}
51
52impl Channel {
53 async fn send(&self, msg_type: u16, body: &[u8]) -> std::io::Result<()> {
54 let mut w = self.writer.lock().await;
55 let serial = self.serial.fetch_add(1, Ordering::SeqCst) + 1;
56 let mut buf = wire::data_header(self.mini, serial, msg_type, body.len() as u32);
57 buf.extend_from_slice(body);
58 w.write_all(&buf).await?;
59 w.flush().await
60 }
61
62 async fn shutdown(&self) {
63 let _ = self.writer.lock().await.shutdown().await;
64 }
65}
66
67pub async fn link(
70 server: SocketAddr,
71 connection_id: u32,
72 channel_type: u8,
73 password: &str,
74 channel_caps: Vec<u32>,
75) -> Result<(OwnedReadHalf, Arc<Channel>, wire::LinkReply)> {
76 let stream = TcpStream::connect(server)
77 .await
78 .with_context(|| format!("cannot reach {server}"))?;
79 let _ = stream.set_nodelay(true);
80 let (mut r, mut w) = stream.into_split();
81 let mess = wire::LinkMess {
82 connection_id,
83 channel_type,
84 channel_id: 0,
85 common_caps: vec![wire::caps_word(&[
86 common_cap::AUTH_SELECTION,
87 common_cap::AUTH_SPICE,
88 common_cap::MINI_HEADER,
89 ])],
90 channel_caps,
91 };
92 w.write_all(&mess.encode()).await?;
93 let mut header = [0u8; wire::LINK_HEADER_BYTES];
94 r.read_exact(&mut header)
95 .await
96 .context("reading the link reply")?;
97 let size = wire::parse_link_header(&header, 4).map_err(|e| {
98 anyhow::anyhow!(
99 "the {} channel's link reply is not SPICE: {}",
100 channel::name(channel_type),
101 link_err::name(e.0)
102 )
103 })?;
104 let mut body = vec![0u8; size as usize];
105 r.read_exact(&mut body).await?;
106 let reply = wire::LinkReply::decode(&body)?;
107 if reply.error != link_err::OK {
108 bail!(
109 "the server refused the {} channel link: {}",
110 channel::name(channel_type),
111 link_err::name(reply.error)
112 );
113 }
114 if wire::has_cap(&reply.common_caps, common_cap::AUTH_SELECTION) {
115 w.write_all(&wire::AUTH_MECHANISM_SPICE.to_le_bytes())
116 .await?;
117 }
118 let ticket = wire::encrypt_ticket(&reply.pubkey, password)?;
119 w.write_all(&ticket).await?;
120 let result = r.read_u32_le().await.context("reading the link result")?;
121 if result != link_err::OK {
122 bail!(
123 "the server refused the {} channel: {}",
124 channel::name(channel_type),
125 link_err::name(result)
126 );
127 }
128 let mini = wire::has_cap(&reply.common_caps, common_cap::MINI_HEADER);
129 Ok((
130 r,
131 Arc::new(Channel {
132 writer: Mutex::new(w),
133 mini,
134 serial: AtomicU64::new(0),
135 }),
136 reply,
137 ))
138}
139
140struct Setup {
142 main: (OwnedReadHalf, Arc<Channel>),
143 display: Option<(OwnedReadHalf, Arc<Channel>)>,
144 inputs: Option<(OwnedReadHalf, Arc<Channel>)>,
145 session_id: u32,
146 mouse_mode: u32,
147 agent_connected: bool,
148 name: Option<String>,
149 channels: Vec<&'static str>,
150 notices: Vec<(&'static str, String)>,
152}
153
154fn parse_notify(body: &[u8]) -> Result<(&'static str, String)> {
156 let mut c = Cursor::new(body);
157 let _stamp = c.u64("time stamp")?;
158 let severity = c.u32("severity")?;
159 let _visibility = c.u32("visibility")?;
160 let _what = c.u32("what")?;
161 let n = c.u32("message length")? as usize;
162 let text = c.bytes(n.min(c.remaining()), "message")?;
163 let text = crate::utils::sanitize::line_field(&String::from_utf8_lossy(text));
164 Ok((wire::severity_name(severity), text))
165}
166
167fn guest_name(body: &[u8]) -> Result<String> {
168 let mut c = Cursor::new(body);
169 let n = c.u32("name length")? as usize;
170 let bytes = c.bytes(n.min(c.remaining()), "name")?;
171 let end = bytes.iter().position(|b| *b == 0).unwrap_or(bytes.len());
172 Ok(crate::utils::sanitize::line_field(
173 &String::from_utf8_lossy(&bytes[..end]),
174 ))
175}
176
177async fn establish(server: SocketAddr, password: &str) -> Result<Setup> {
178 let (mut main_r, main, _) = link(
179 server,
180 0,
181 channel::MAIN,
182 password,
183 vec![wire::caps_word(&[wire::main_cap::NAME_AND_UUID])],
184 )
185 .await?;
186 let mut name = None;
187 let mut init = None;
188 let mut notices = Vec::new();
189 for _ in 0..MAX_SETUP_MESSAGES {
190 let (t, body) =
191 wire::read_message(&mut main_r, main.mini, MAX_SERVER_MESSAGE_BYTES).await?;
192 match t {
193 msg::PING => main.send(msgc::PONG, &wire::pong_for(&body)?).await?,
194 msg::MAIN_NAME => name = Some(guest_name(&body)?),
195 msg::NOTIFY => notices.push(parse_notify(&body)?),
196 msg::MAIN_INIT => {
197 let mut c = Cursor::new(&body);
198 let session_id = c.u32("session id")?;
199 let _hint = c.u32("display channels hint")?;
200 let supported = c.u32("supported mouse modes")?;
201 let current = c.u32("current mouse mode")?;
202 let agent = c.u32("agent connected")?;
203 init = Some((session_id, supported, current, agent != 0));
204 break;
205 }
206 _ => {}
207 }
208 }
209 let (session_id, supported, mut mouse_mode, agent_connected) =
210 init.context("the server sent no SPICE_MSG_MAIN_INIT")?;
211 main.send(msgc::MAIN_ATTACH_CHANNELS, &[]).await?;
212 if supported & wire::MOUSE_MODE_CLIENT != 0 && mouse_mode != wire::MOUSE_MODE_CLIENT {
213 main.send(
214 msgc::MAIN_MOUSE_MODE_REQUEST,
215 &(wire::MOUSE_MODE_CLIENT as u16).to_le_bytes(),
216 )
217 .await?;
218 }
219 let mut list = None;
220 for _ in 0..MAX_SETUP_MESSAGES {
221 let (t, body) =
222 wire::read_message(&mut main_r, main.mini, MAX_SERVER_MESSAGE_BYTES).await?;
223 match t {
224 msg::PING => main.send(msgc::PONG, &wire::pong_for(&body)?).await?,
225 msg::MAIN_NAME => name = Some(guest_name(&body)?),
226 msg::NOTIFY => notices.push(parse_notify(&body)?),
227 msg::MAIN_MOUSE_MODE => {
228 let mut c = Cursor::new(&body);
229 let _supported = c.u16("supported")?;
230 mouse_mode = u32::from(c.u16("current")?);
231 }
232 msg::MAIN_CHANNELS_LIST => {
233 let mut c = Cursor::new(&body);
234 let n = c.u32("channel count")?;
235 ensure!(
236 (n as usize) * 2 <= c.remaining(),
237 "{n} channels do not fit in the list"
238 );
239 let mut v = Vec::new();
240 for _ in 0..n {
241 v.push((c.u8("channel type")?, c.u8("channel id")?));
242 }
243 list = Some(v);
244 break;
245 }
246 _ => {}
247 }
248 }
249 let list = list.context("the server sent no SPICE_MSG_MAIN_CHANNELS_LIST")?;
250 let channels: Vec<&'static str> = list.iter().map(|(t, _)| channel::name(*t)).collect();
251 let display = if list.contains(&(channel::DISPLAY, 0)) {
252 let (r, ch, reply) = link(server, session_id, channel::DISPLAY, password, vec![]).await?;
253 let cache_pixels = (display::MAX_CACHE_BYTES / 4) as i64;
254 ch.send(
255 msgc::DISPLAY_INIT,
256 &wire::display_init(1, cache_pixels, 1, 3 << 20),
257 )
258 .await?;
259 if wire::has_cap(&reply.channel_caps, wire::display_cap::PREF_COMPRESSION) {
260 ch.send(
262 msgc::DISPLAY_PREFERRED_COMPRESSION,
263 &[wire::IMAGE_COMPRESSION_OFF],
264 )
265 .await?;
266 }
267 Some((r, ch))
268 } else {
269 None
270 };
271 let inputs = if list.contains(&(channel::INPUTS, 0)) {
272 let (r, ch, _) = link(server, session_id, channel::INPUTS, password, vec![]).await?;
273 Some((r, ch))
274 } else {
275 None
276 };
277 Ok(Setup {
278 main: (main_r, main),
279 display,
280 inputs,
281 session_id,
282 mouse_mode,
283 agent_connected,
284 name,
285 channels,
286 notices,
287 })
288}
289
290struct Shared {
292 display: std::sync::Mutex<display::DisplayModel>,
293 mouse_mode: AtomicU32,
294}
295
296fn resolve_target(remote: &str) -> Result<String> {
297 let raw = remote
298 .trim()
299 .trim_start_matches("spice://")
300 .trim_end_matches('/');
301 let has_port = raw.parse::<SocketAddr>().is_ok()
302 || raw
303 .rsplit_once(':')
304 .is_some_and(|(h, p)| !h.ends_with(':') && p.parse::<u16>().is_ok());
305 ensure!(
306 has_port,
307 "SPICE has no default port; name one, as in 127.0.0.1:5930"
308 );
309 Ok(raw.to_string())
310}
311
312pub async fn connect(ctx: ConnectContext) -> Result<SocketAddr> {
313 let password = ctx
314 .startup_params
315 .as_ref()
316 .map(|p| p.get_optional_string("password"))
317 .transpose()?
318 .flatten()
319 .unwrap_or_default();
320 ensure!(
321 password.len() <= wire::MAX_PASSWORD_LENGTH,
322 "a SPICE password is at most {} bytes",
323 wire::MAX_PASSWORD_LENGTH
324 );
325 let target = resolve_target(&ctx.remote_addr)?;
326 let server = tokio::net::lookup_host(&target)
327 .await
328 .with_context(|| format!("cannot resolve {target}"))?
329 .next()
330 .with_context(|| format!("{target} resolved to no address"))?;
331 let setup = tokio::time::timeout(CONNECT_TIMEOUT, establish(server, &password))
332 .await
333 .context("linking the SPICE channels timed out")??;
334 ctx.state
335 .update_client_status(ctx.client_id, ClientStatus::Connected)
336 .await;
337 Log::new(Some(&ctx.status_tx)).info(format!(
338 "SPICE client {} connected to {server}: session {:#010x}, channels {:?}",
339 ctx.client_id, setup.session_id, setup.channels
340 ));
341 let external =
342 crate::client::command_support::register_command_channel(&ctx.state, ctx.client_id).await;
343 let (event_tx, event_rx) = mpsc::channel::<(Event, u32)>(TURNS);
344 let (internal_tx, internal) = mpsc::channel::<(Value, u32)>(64);
345 let (end_tx, end_rx) = mpsc::channel::<String>(8);
346 let shared = Arc::new(Shared {
347 display: std::sync::Mutex::new(display::DisplayModel::default()),
348 mouse_mode: AtomicU32::new(setup.mouse_mode),
349 });
350 let _ = event_tx.try_send((
351 Event::new(
352 &actions::CONNECTED_EVENT,
353 json!({
354 "server": server.to_string(),
355 "session_id": setup.session_id,
356 "channels": setup.channels,
357 "mouse_mode": mode_name(setup.mouse_mode),
358 "name": setup.name,
359 "agent_connected": setup.agent_connected,
360 }),
361 ),
362 0,
363 ));
364 for (severity, message) in &setup.notices {
365 let _ = event_tx.try_send((
366 Event::new(
367 &actions::NOTIFY_EVENT,
368 json!({"severity": severity, "message": message}),
369 ),
370 0,
371 ));
372 }
373 let dispatcher = tokio::spawn(run_turns(ctx.clone(), event_rx, internal_tx));
374 let dispatcher_abort = dispatcher.abort_handle();
375 ctx.state
376 .register_client_task(ctx.client_id, dispatcher)
377 .await;
378
379 let main = setup.main.1.clone();
380 let display_ch = setup.display.as_ref().map(|d| d.1.clone());
381 let inputs_ch = setup.inputs.as_ref().map(|i| i.1.clone());
382 let mut readers = Vec::new();
383 for (kind, half) in [
384 ("main", Some(setup.main)),
385 ("display", setup.display),
386 ("inputs", setup.inputs),
387 ] {
388 let Some((r, ch)) = half else { continue };
389 let task = tokio::spawn(read_channel(
390 kind,
391 r,
392 ch,
393 shared.clone(),
394 event_tx.clone(),
395 end_tx.clone(),
396 ctx.status_tx.clone(),
397 ));
398 readers.push(task.abort_handle());
399 ctx.state.register_client_task(ctx.client_id, task).await;
400 }
401 drop(end_tx);
402 let session_ctx = ctx.clone();
403 let task = tokio::spawn(async move {
404 let mut session = Session {
405 shared,
406 main,
407 display: display_ch,
408 inputs: inputs_ch,
409 pointer: (0, 0),
410 buttons: 0,
411 };
412 let reason = session
413 .run(&session_ctx, external, internal, end_rx, event_tx.clone())
414 .await;
415 for r in &readers {
416 r.abort();
417 }
418 session.close().await;
419 Log::new(Some(&session_ctx.status_tx)).info(format!(
420 "SPICE client {} disconnected: {reason}",
421 session_ctx.client_id
422 ));
423 let _ = event_tx
424 .send((
425 Event::new(&actions::DISCONNECTED_EVENT, json!({"reason": reason})),
426 0,
427 ))
428 .await;
429 drop(event_tx);
430 let _ = tokio::time::timeout(Duration::from_secs(30), async {
431 while !dispatcher_abort.is_finished() {
432 tokio::time::sleep(Duration::from_millis(50)).await;
433 }
434 })
435 .await;
436 dispatcher_abort.abort();
437 session_ctx
438 .state
439 .update_client_status(session_ctx.client_id, ClientStatus::Disconnected)
440 .await;
441 session_ctx
442 .state
443 .remove_client_handle(session_ctx.client_id)
444 .await;
445 let _ = session_ctx.status_tx.send("__UPDATE_UI__".into());
446 });
447 ctx.state.register_client_task(ctx.client_id, task).await;
448 Ok(server)
449}
450
451fn mode_name(mode: u32) -> &'static str {
452 if mode == wire::MOUSE_MODE_CLIENT {
453 "client"
454 } else {
455 "server"
456 }
457}
458
459async fn read_channel(
462 kind: &'static str,
463 mut r: OwnedReadHalf,
464 ch: Arc<Channel>,
465 shared: Arc<Shared>,
466 events: mpsc::Sender<(Event, u32)>,
467 end: mpsc::Sender<String>,
468 status_tx: mpsc::UnboundedSender<String>,
469) {
470 let mut window = 0u32;
471 let mut since_ack = 0u32;
472 let emit = |event: &'static EventType, data: Value| {
473 if events.try_send((Event::new(event, data), 0)).is_err() {
474 tracing::warn!("SPICE client dropped an event: the model is {TURNS} behind decision=turn_queue_full");
475 }
476 };
477 let reason = loop {
478 let (t, body) = match wire::read_message(&mut r, ch.mini, MAX_SERVER_MESSAGE_BYTES).await {
479 Ok(m) => m,
480 Err(wire::ReadError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
481 break format!("the server closed the {kind} channel");
482 }
483 Err(e) => break format!("the {kind} channel ended: {e}"),
484 };
485 if window > 0 {
486 since_ack += 1;
487 if since_ack >= window {
488 since_ack = 0;
489 if ch.send(msgc::ACK, &[]).await.is_err() {
490 break format!("the {kind} channel could not be written");
491 }
492 }
493 }
494 let handled: Result<()> = async {
495 match t {
496 msg::SET_ACK => {
497 let mut c = Cursor::new(&body);
498 let generation = c.u32("ack generation")?;
499 window = c.u32("ack window")?;
500 since_ack = 0;
501 ch.send(msgc::ACK_SYNC, &wire::u32_body(generation)).await?;
502 }
503 msg::PING => ch.send(msgc::PONG, &wire::pong_for(&body)?).await?,
504 msg::NOTIFY => {
505 let (severity, text) = parse_notify(&body)?;
506 Log::new(Some(&status_tx))
507 .info(format!("SPICE notification ({severity}): {text}"));
508 emit(
509 &actions::NOTIFY_EVENT,
510 json!({"severity": severity, "message": text}),
511 );
512 }
513 msg::MAIN_MOUSE_MODE if kind == "main" => {
514 let mut c = Cursor::new(&body);
515 let _supported = c.u16("supported")?;
516 shared
517 .mouse_mode
518 .store(u32::from(c.u16("current")?), Ordering::SeqCst);
519 }
520 other if kind == "display" => {
521 let change = match shared.display.lock() {
522 Ok(mut d) => d.handle(other, &body)?,
523 Err(_) => bail!("the display model is poisoned"),
524 };
525 if let Some(display::Change::SurfaceCreated {
526 width,
527 height,
528 format,
529 }) = change
530 {
531 emit(
532 &actions::SURFACE_EVENT,
533 json!({"width": width, "height": height, "format": format}),
534 );
535 }
536 }
537 _ => {}
538 }
539 Ok(())
540 }
541 .await;
542 if let Err(e) = handled {
543 break format!("the {kind} channel ended: {e:#}");
544 }
545 };
546 let _ = end.send(reason).await;
547}
548
549struct PendingRead {
551 grid: u64,
552 points: Vec<(u32, u32)>,
553 settle: Duration,
554 give_up: Instant,
555 command: Option<ClientCommand>,
556 depth: u32,
557}
558
559struct Session {
560 shared: Arc<Shared>,
561 main: Arc<Channel>,
562 display: Option<Arc<Channel>>,
563 inputs: Option<Arc<Channel>>,
564 pointer: (u32, u32),
566 buttons: u16,
567}
568
569impl Session {
570 fn last_update(&self) -> Instant {
571 self.shared
572 .display
573 .lock()
574 .map(|d| d.last_update)
575 .unwrap_or_else(|_| Instant::now())
576 }
577
578 fn reading(&self, p: &PendingRead, settled: bool) -> Value {
579 let Ok(d) = self.shared.display.lock() else {
580 return json!({"error": "the display model is poisoned"});
581 };
582 let mut data = d.describe(p.grid as usize);
583 let points: Vec<Value> = p
584 .points
585 .iter()
586 .map(|(x, y)| json!({"x": x, "y": y, "color": display::pixel_hex(&d, *x, *y)}))
587 .collect();
588 data["points"] = json!(points);
589 data["settled"] = json!(settled);
590 data["mouse_mode"] = json!(mode_name(self.shared.mouse_mode.load(Ordering::SeqCst)));
591 data
592 }
593
594 async fn close(&self) {
595 self.main.shutdown().await;
596 if let Some(d) = &self.display {
597 d.shutdown().await;
598 }
599 if let Some(i) = &self.inputs {
600 i.shutdown().await;
601 }
602 }
603
604 async fn run(
605 &mut self,
606 ctx: &ConnectContext,
607 mut external: mpsc::Receiver<ClientCommand>,
608 mut internal: mpsc::Receiver<(Value, u32)>,
609 mut end: mpsc::Receiver<String>,
610 events: mpsc::Sender<(Event, u32)>,
611 ) -> String {
612 let mut pending: Option<PendingRead> = None;
613 let emit = |event: &'static EventType, data: Value, depth: u32| {
614 if events.try_send((Event::new(event, data), depth)).is_err() {
615 tracing::warn!("SPICE client {} dropped an event: the model is {TURNS} behind decision=turn_queue_full", ctx.client_id);
616 }
617 };
618 loop {
619 let wake = pending
620 .as_ref()
621 .map(|p| (self.last_update() + p.settle).min(p.give_up))
622 .unwrap_or_else(|| Instant::now() + Duration::from_secs(3600));
623 let (action, depth, command) = tokio::select! {
624 reason = end.recv() => {
625 return reason.unwrap_or_else(|| "every channel ended".into());
626 }
627 _ = tokio::time::sleep_until(wake), if pending.is_some() => {
628 let now = Instant::now();
629 let quiet = self.last_update() + pending.as_ref().map(|p| p.settle).unwrap_or_default() <= now;
630 let expired = pending.as_ref().is_some_and(|p| p.give_up <= now);
631 if !quiet && !expired {
632 continue;
633 }
634 let Some(p) = pending.take() else { continue };
635 let data = self.reading(&p, quiet);
636 if let Some(c) = p.command {
637 crate::client::command_support::reply(c, Ok(ClientSendOutcome::Executed { detail: data.to_string() }));
638 }
639 emit(&actions::SCREEN_EVENT, data, p.depth);
640 continue;
641 }
642 c = external.recv() => match c {
643 Some(c) => {
644 ctx.state.record_access_log(AccessLogOwner::Client(ctx.client_id.as_u32()), "SPICE", None,
645 "injected_action", c.action.clone(), vec![]).await;
646 (c.action.clone(), 0, Some(c))
647 }
648 None => return "the client was stopped".into(),
649 },
650 a = internal.recv() => match a {
651 Some((a, d)) => (a, d, None),
652 None => return "the client was stopped".into(),
653 },
654 };
655 let reply = |command: Option<ClientCommand>, outcome: ClientSendOutcome| {
656 if let Some(c) = command {
657 crate::client::command_support::reply(c, Ok(outcome));
658 }
659 };
660 let kind = action["type"].as_str().unwrap_or_default().to_string();
661 match SpiceClientProtocol.execute_action(action.clone()) {
662 Ok(ClientActionResult::Disconnect) => {
663 reply(command, ClientSendOutcome::Disconnected);
664 return "the client disconnected".into();
665 }
666 Ok(_) => {}
667 Err(e) => {
668 reply(
669 command,
670 ClientSendOutcome::Rejected {
671 error: e.to_string(),
672 },
673 );
674 continue;
675 }
676 }
677 let Ok(input) = actions::check(&action) else {
678 continue;
679 };
680 if let Input::ReadScreen {
681 grid,
682 settle_ms,
683 points,
684 } = input
685 {
686 if let Some(old) = pending.take() {
687 if let Some(c) = old.command {
688 crate::client::command_support::reply(
689 c,
690 Ok(ClientSendOutcome::Rejected {
691 error: "replaced by a newer spice_read_screen".into(),
692 }),
693 );
694 }
695 }
696 let settle = Duration::from_millis(settle_ms);
697 pending = Some(PendingRead {
698 grid,
699 points,
700 settle,
701 give_up: Instant::now()
702 + settle
703 + Duration::from_millis(actions::MAX_SETTLE_WAIT_MS),
704 command,
705 depth,
706 });
707 continue;
708 }
709 match self.send_input(&input).await {
710 Ok(n) => {
711 Log::new(Some(&ctx.status_tx)).info(format!(
712 "SPICE client {}: {kind} ({n} input messages)",
713 ctx.client_id
714 ));
715 reply(
716 command,
717 ClientSendOutcome::Executed {
718 detail: json!({"action": kind, "input_messages": n}).to_string(),
719 },
720 );
721 }
722 Err(e) => {
723 let message = format!("{e:#}");
724 reply(
725 command,
726 ClientSendOutcome::Executed {
727 detail: json!({"action": kind, "error": message}).to_string(),
728 },
729 );
730 emit(
731 &actions::ERROR_EVENT,
732 json!({"action": kind, "message": message}),
733 depth,
734 );
735 }
736 }
737 }
738 }
739
740 async fn send_input(&mut self, input: &Input) -> Result<usize> {
742 let inputs = self
743 .inputs
744 .clone()
745 .context("the server offered no inputs channel")?;
746 let mut sent = 0;
747 match input {
748 Input::Keys(strokes) => {
749 for ((sc, ext), released) in strokes {
750 let t = if *released {
751 msgc::INPUTS_KEY_UP
752 } else {
753 msgc::INPUTS_KEY_DOWN
754 };
755 let code = crate::server::spice::keys::key_code(*sc, *ext, *released);
756 inputs.send(t, &wire::u32_body(code)).await?;
757 sent += 1;
758 }
759 }
760 Input::Mouse {
761 x,
762 y,
763 action,
764 button,
765 } => {
766 if let Some((w, h)) = self
767 .shared
768 .display
769 .lock()
770 .ok()
771 .and_then(|d| d.primary_size())
772 {
773 ensure!(
774 *x < w && *y < h,
775 "({x}, {y}) is outside the {w}x{h} display"
776 );
777 }
778 let b = match button.as_str() {
779 "right" => wire::button::RIGHT,
780 "middle" => wire::button::MIDDLE,
781 _ => wire::button::LEFT,
782 };
783 if self.shared.mouse_mode.load(Ordering::SeqCst) == wire::MOUSE_MODE_CLIENT {
784 inputs
785 .send(
786 msgc::INPUTS_MOUSE_POSITION,
787 &wire::mouse_position(*x, *y, self.buttons, 0),
788 )
789 .await?;
790 } else {
791 let dx = *x as i32 - self.pointer.0 as i32;
792 let dy = *y as i32 - self.pointer.1 as i32;
793 inputs
794 .send(
795 msgc::INPUTS_MOUSE_MOTION,
796 &wire::mouse_motion(dx, dy, self.buttons),
797 )
798 .await?;
799 }
800 self.pointer = (*x, *y);
801 sent += 1;
802 let mut presses: Vec<(u8, bool)> = Vec::new();
803 match action.as_str() {
804 "click" => presses.extend([(b, true), (b, false)]),
805 "double_click" => {
806 presses.extend([(b, true), (b, false), (b, true), (b, false)])
807 }
808 "press" => presses.push((b, true)),
809 "release" => presses.push((b, false)),
810 "scroll_up" => {
811 presses.extend([(wire::button::UP, true), (wire::button::UP, false)])
812 }
813 "scroll_down" => {
814 presses.extend([(wire::button::DOWN, true), (wire::button::DOWN, false)])
815 }
816 _ => {}
817 }
818 for (button, down) in presses {
819 if down {
820 self.buttons |= wire::button::mask(button);
821 } else {
822 self.buttons &= !wire::button::mask(button);
823 }
824 let t = if down {
825 msgc::INPUTS_MOUSE_PRESS
826 } else {
827 msgc::INPUTS_MOUSE_RELEASE
828 };
829 inputs
830 .send(t, &wire::mouse_button(button, self.buttons))
831 .await?;
832 sent += 1;
833 }
834 }
835 Input::ReadScreen { .. } => {}
836 }
837 Ok(sent)
838 }
839}
840
841async fn run_turns(
842 ctx: ConnectContext,
843 mut events: mpsc::Receiver<(Event, u32)>,
844 internal: mpsc::Sender<(Value, u32)>,
845) {
846 let protocol = SpiceClientProtocol;
847 while let Some((event, depth)) = events.recv().await {
848 if depth >= MAX_FOLLOWUP_DEPTH {
849 tracing::warn!("SPICE client {} not asking the model about {}: {depth} turns deep decision=followup_depth", ctx.client_id, event.id());
850 continue;
851 }
852 let instruction = ctx
853 .state
854 .get_instruction_for_client(ctx.client_id)
855 .await
856 .unwrap_or_default();
857 let memory = ctx
858 .state
859 .get_memory_for_client(ctx.client_id)
860 .await
861 .unwrap_or_default();
862 match call_llm_for_client(
863 &ctx.llm_client,
864 &ctx.state,
865 ctx.client_id.to_string(),
866 &instruction,
867 &memory,
868 Some(&event),
869 &protocol,
870 &ctx.status_tx,
871 )
872 .await
873 {
874 Ok(result) => {
875 if let Some(memory) = result.memory_updates {
876 ctx.state.set_memory_for_client(ctx.client_id, memory).await;
877 }
878 for action in result.actions {
879 if internal.send((action, depth + 1)).await.is_err() {
880 return;
881 }
882 }
883 }
884 Err(e) => Log::new(Some(&ctx.status_tx)).warn(format!("SPICE client handler: {e}")),
885 }
886 }
887}