1pub mod actions;
34pub mod keys;
35pub mod wire;
36
37use crate::display::{Color, DisplayCanvas, DisplayCommand};
38use crate::llm::action_helper::call_llm;
39use crate::llm::actions::protocol_trait::ActionResult;
40use crate::llm::ollama_client::OllamaClient;
41use crate::logging::emit::{Log, Sink};
42use crate::protocol::Event;
43use crate::server::connection::ConnectionId;
44use crate::state::app_state::AppState;
45use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
46use crate::state::ServerId;
47use actions::SpiceProtocol;
48use anyhow::{Context, Result};
49use serde_json::json;
50use std::collections::HashMap;
51use std::net::SocketAddr;
52use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
53use std::sync::Arc;
54use std::time::Duration;
55use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf};
56use tokio::net::TcpStream;
57use tokio::sync::{mpsc, watch, Mutex};
58use wire::{channel, common_cap, link_err, msg, msgc, ReadError};
59
60const FIRST_BYTE_READ_TIMEOUT: Duration = Duration::from_secs(30);
63
64const IDLE_BETWEEN_MESSAGES_TIMEOUT: Duration = Duration::from_secs(1800);
69
70const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
72
73pub const MAX_CLIENT_MESSAGE_BYTES: u32 = 64 * 1024;
77
78const CONNECTION_CAP_REFUSAL: &[u8] = &cap_refusal();
82
83const fn cap_refusal() -> [u8; wire::LINK_HEADER_BYTES + wire::LINK_REPLY_BYTES] {
84 let mut b = [0u8; wire::LINK_HEADER_BYTES + wire::LINK_REPLY_BYTES];
85 b[0] = b'R';
86 b[1] = b'E';
87 b[2] = b'D';
88 b[3] = b'Q';
89 b[4] = wire::VERSION_MAJOR as u8;
90 b[8] = wire::VERSION_MINOR as u8;
91 b[12] = wire::LINK_REPLY_BYTES as u8;
92 b[16] = link_err::ERROR as u8;
93 b[16 + 4 + wire::TICKET_PUBKEY_BYTES + 8] = wire::LINK_REPLY_BYTES as u8;
95 b
96}
97
98pub const PLACEHOLDER_BACKGROUND: Color = Color::rgb(24, 26, 34);
100const PLACEHOLDER_FOREGROUND: Color = Color::rgb(203, 213, 225);
101pub const NO_ANSWER_NOTICE: &str = "NetGet: no answer for this input; the screen is unchanged";
104
105type Reader = crate::server::accept_bounded::IdleTimeoutReader<ReadHalf<TcpStream>>;
106type Writer = Arc<Mutex<WriteHalf<TcpStream>>>;
107
108pub struct Config {
110 pub width: u32,
111 pub height: u32,
112 pub password: Option<String>,
113 pub name: String,
114}
115
116pub struct SpiceServer;
117
118struct Shared {
119 config: Config,
120 key: rsa::RsaPrivateKey,
121 pubkey: Vec<u8>,
122 sessions: std::sync::Mutex<HashMap<u32, Arc<Session>>>,
123 app_state: Arc<AppState>,
124 llm_client: OllamaClient,
125 status_tx: mpsc::UnboundedSender<String>,
126 server_id: ServerId,
127 image_ids: AtomicU64,
128}
129
130struct Session {
132 id: u32,
133 main: Mutex<Option<Arc<ChannelWriter>>>,
134 display: Mutex<DisplayState>,
135 display_attached: AtomicBool,
136 inputs_attached: AtomicBool,
137 mouse_mode: AtomicU32,
138 closed: watch::Sender<bool>,
139}
140
141#[derive(Default)]
145struct DisplayState {
146 writer: Option<Arc<ChannelWriter>>,
147 frame: Option<Arc<Vec<u8>>>,
148 marked: bool,
149}
150
151impl Session {
152 fn close(&self) {
153 let _ = self.closed.send(true);
154 }
155}
156
157struct ChannelWriter {
159 half: Writer,
160 mini: bool,
161 serial: AtomicU64,
162 app_state: Arc<AppState>,
163 server_id: ServerId,
164 connection_id: ConnectionId,
165}
166
167impl ChannelWriter {
168 async fn send(&self, msg_type: u16, body: &[u8]) -> std::io::Result<()> {
169 let len = {
170 let mut w = self.half.lock().await;
171 let serial = self.serial.fetch_add(1, Ordering::SeqCst) + 1;
172 let mut buf = wire::data_header(self.mini, serial, msg_type, body.len() as u32);
173 buf.extend_from_slice(body);
174 w.write_all(&buf).await?;
175 w.flush().await?;
176 buf.len()
177 };
178 self.app_state
179 .update_connection_stats(
180 self.server_id,
181 self.connection_id,
182 None,
183 Some(len as u64),
184 None,
185 Some(1),
186 )
187 .await;
188 Ok(())
189 }
190}
191
192#[derive(Default)]
194struct Decision {
195 accept: bool,
196 reject: bool,
197 redraw: Option<Vec<DisplayCommand>>,
198 no_change: bool,
199 notify: Vec<(u32, String)>,
200 close: bool,
201 no_answer: bool,
203}
204
205impl SpiceServer {
206 pub async fn spawn(
207 listen_addr: SocketAddr,
208 llm_client: OllamaClient,
209 app_state: Arc<AppState>,
210 status_tx: mpsc::UnboundedSender<String>,
211 server_id: ServerId,
212 config: Config,
213 ) -> Result<SocketAddr> {
214 let key = tokio::task::spawn_blocking(wire::generate_ticket_key)
217 .await
218 .context("ticket key generation")??;
219 let pubkey = wire::ticket_public_key(&key)?;
220 let listener =
221 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
222 let local_addr = listener.local_addr()?;
223 Log::new(Some(&status_tx)).info(format!("SPICE server listening on {local_addr}"));
225 Log::new(Some(&status_tx)).debug(format!(
226 "SPICE surface {}x{}, name '{}', password {}",
227 config.width,
228 config.height,
229 config.name,
230 if config.password.is_some() {
231 "required"
232 } else {
233 "not required"
234 }
235 ));
236 let shared = Arc::new(Shared {
237 config,
238 key,
239 pubkey,
240 sessions: std::sync::Mutex::new(HashMap::new()),
241 app_state: app_state.clone(),
242 llm_client,
243 status_tx: status_tx.clone(),
244 server_id,
245 image_ids: AtomicU64::new(1),
246 });
247 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
248 let registrar = app_state.clone();
249 let accept = tokio::spawn(async move {
250 loop {
251 match crate::server::accept_bounded::accept_bounded(
252 &listener,
253 &limiter,
254 CONNECTION_CAP_REFUSAL,
255 "SPICE",
256 Some(&status_tx),
257 )
258 .await
259 {
260 Ok((stream, remote, permit)) => {
261 let shared = shared.clone();
262 let local = stream.local_addr().unwrap_or(local_addr);
263 let owner = app_state.clone();
264 owner
266 .spawn_server_task(server_id, async move {
267 let _permit = permit;
268 if let Err(e) =
269 Self::handle_connection(shared.clone(), stream, remote, local)
270 .await
271 {
272 Log::new(Some(&shared.status_tx)).debug(format!(
273 "SPICE connection from {remote} ended: {e}"
274 ));
275 }
276 })
277 .await;
278 }
279 Err(e) => {
280 Log::new(Some(&status_tx))
281 .error(format!("SPICE accept failed, stopping loop: {e}"));
282 break;
283 }
284 }
285 }
286 });
287 registrar.register_server_task(server_id, accept).await;
288 Ok(local_addr)
289 }
290
291 async fn handle_connection(
292 shared: Arc<Shared>,
293 stream: TcpStream,
294 remote: SocketAddr,
295 local: SocketAddr,
296 ) -> Result<()> {
297 let app_state = shared.app_state.clone();
298 let server_id = shared.server_id;
299 let connection_id = ConnectionId::new(app_state.get_next_unified_id().await);
300 let (read_half, write_half) = tokio::io::split(stream);
301 let mut reader: Reader = crate::server::accept_bounded::IdleTimeoutReader::with_first(
302 read_half,
303 FIRST_BYTE_READ_TIMEOUT,
304 IDLE_BETWEEN_MESSAGES_TIMEOUT,
305 );
306 let writer: Writer = Arc::new(Mutex::new(write_half));
307 let now = crate::utils::clock::Instant::now();
308 app_state
309 .add_connection_to_server(
310 server_id,
311 ConnectionState {
312 id: connection_id,
313 remote_addr: remote,
314 local_addr: local,
315 bytes_sent: 0,
316 bytes_received: 0,
317 packets_sent: 0,
318 packets_received: 0,
319 last_activity: now,
320 status: ConnectionStatus::Active,
321 status_changed_at: now,
322 protocol_info: ProtocolConnectionInfo::empty(),
323 },
324 )
325 .await;
326 let _ = shared.status_tx.send("__UPDATE_UI__".to_string());
327 let peer_rx = crate::server::peer_support::register_peer_channel(
331 &app_state,
332 server_id,
333 connection_id.as_u32(),
334 )
335 .await;
336 crate::server::peer_support::spawn_peer_command_task(
337 peer_rx,
338 Arc::new(SpiceProtocol::new()),
339 app_state.clone(),
340 server_id,
341 connection_id.as_u32(),
342 writer.clone(),
343 shared.status_tx.clone(),
344 );
345
346 let result = Self::serve(&shared, &mut reader, &writer, connection_id, remote).await;
347
348 {
349 let mut w = writer.lock().await;
350 let _ = w.shutdown().await;
351 }
352 app_state
353 .remove_peer_handle(server_id, connection_id.as_u32())
354 .await;
355 app_state
356 .remove_connection_from_server(server_id, connection_id)
357 .await;
358 let _ = shared.status_tx.send("__UPDATE_UI__".to_string());
359 result
360 }
361
362 async fn serve(
364 shared: &Arc<Shared>,
365 reader: &mut Reader,
366 writer: &Writer,
367 connection_id: ConnectionId,
368 remote: SocketAddr,
369 ) -> Result<()> {
370 let log = Log::new(Some(&shared.status_tx));
371 let mut header = [0u8; wire::LINK_HEADER_BYTES];
372 reader.read_exact(&mut header).await?;
373 let size = match wire::parse_link_header(&header, wire::LINK_MESS_BYTES) {
374 Ok(size) => size,
375 Err(wire::LinkHeaderError(code)) => {
376 log.warn(format!(
377 "SPICE link from {remote} refused decision=protocol_error: {}",
378 link_err::name(code)
379 ));
380 write_raw(writer, &wire::LinkReply::error(code)).await?;
381 return Ok(());
382 }
383 };
384 let mut body = vec![0u8; size as usize];
385 reader.read_exact(&mut body).await?;
386 let link = match wire::LinkMess::decode(&body) {
387 Ok(l) => l,
388 Err(e) => {
389 log.warn(format!(
390 "SPICE link from {remote} refused decision=protocol_error: {e}"
391 ));
392 write_raw(writer, &wire::LinkReply::error(link_err::INVALID_DATA)).await?;
393 return Ok(());
394 }
395 };
396 let channel_caps = if link.channel_type == channel::MAIN {
397 vec![wire::caps_word(&[wire::main_cap::NAME_AND_UUID])]
398 } else {
399 Vec::new()
400 };
401 write_raw(
402 writer,
403 &wire::LinkReply {
404 error: link_err::OK,
405 pubkey: shared.pubkey.clone(),
406 common_caps: vec![wire::caps_word(&[
407 common_cap::AUTH_SELECTION,
408 common_cap::AUTH_SPICE,
409 common_cap::MINI_HEADER,
410 ])],
411 channel_caps,
412 }
413 .encode(),
414 )
415 .await?;
416
417 if wire::has_cap(&link.common_caps, common_cap::AUTH_SELECTION) {
418 let mechanism = reader.read_u32_le().await?;
419 if mechanism != wire::AUTH_MECHANISM_SPICE {
420 log.warn(format!(
421 "SPICE link from {remote} refused decision=protocol_error: auth mechanism \
422 {mechanism} (only SPICE tickets are offered)"
423 ));
424 write_raw(writer, &link_err::INVALID_DATA.to_le_bytes()).await?;
425 return Ok(());
426 }
427 }
428 let mut ticket = [0u8; wire::TICKET_BYTES];
429 reader.read_exact(&mut ticket).await?;
430 if let Some(password) = &shared.config.password {
431 let good = wire::decrypt_ticket(&shared.key, &ticket)
432 .is_some_and(|t| wire::secrets_equal(&t, password.as_bytes()));
433 if !good {
434 log.warn(format!(
437 "SPICE {} link from {remote} refused decision=protocol_reject_bad_ticket: \
438 the ticket does not carry the configured password",
439 channel::name(link.channel_type)
440 ));
441 write_raw(writer, &link_err::PERMISSION_DENIED.to_le_bytes()).await?;
442 return Ok(());
443 }
444 }
445 let mini = wire::has_cap(&link.common_caps, common_cap::MINI_HEADER);
446 let channel_writer = Arc::new(ChannelWriter {
447 half: writer.clone(),
448 mini,
449 serial: AtomicU64::new(0),
450 app_state: shared.app_state.clone(),
451 server_id: shared.server_id,
452 connection_id,
453 });
454
455 match link.channel_type {
456 channel::MAIN => {
457 Self::admit_main(shared, reader, channel_writer, &link, connection_id, remote).await
458 }
459 channel::DISPLAY | channel::INPUTS => {
460 let session = shared
461 .sessions
462 .lock()
463 .ok()
464 .and_then(|s| s.get(&link.connection_id).cloned());
465 let Some(session) = session else {
466 log.warn(format!(
467 "SPICE {} link from {remote} refused decision=protocol_error: no session \
468 {:#010x}",
469 channel::name(link.channel_type),
470 link.connection_id
471 ));
472 write_raw(writer, &link_err::BAD_CONNECTION_ID.to_le_bytes()).await?;
473 return Ok(());
474 };
475 let slot = if link.channel_type == channel::DISPLAY {
476 &session.display_attached
477 } else {
478 &session.inputs_attached
479 };
480 if link.channel_id != 0 || slot.swap(true, Ordering::SeqCst) {
481 log.warn(format!(
482 "SPICE {} channel {} link from {remote} refused decision=protocol_error: \
483 not available",
484 channel::name(link.channel_type),
485 link.channel_id
486 ));
487 write_raw(writer, &link_err::CHANNEL_NOT_AVAILABLE.to_le_bytes()).await?;
488 return Ok(());
489 }
490 write_raw(writer, &link_err::OK.to_le_bytes()).await?;
491 log.info(format!(
492 "SPICE {} channel linked for session {:#010x}",
493 channel::name(link.channel_type),
494 session.id
495 ));
496 let result = if link.channel_type == channel::DISPLAY {
497 Self::run_display(shared, &session, reader, channel_writer, connection_id).await
498 } else {
499 Self::run_inputs(shared, &session, reader, channel_writer, connection_id).await
500 };
501 slot.store(false, Ordering::SeqCst);
502 result
503 }
504 other => {
505 log.warn(format!(
506 "SPICE {} link from {remote} refused decision=protocol_error: channel not \
507 offered",
508 channel::name(other)
509 ));
510 write_raw(writer, &link_err::CHANNEL_NOT_AVAILABLE.to_le_bytes()).await?;
511 Ok(())
512 }
513 }
514 }
515
516 async fn admit_main(
518 shared: &Arc<Shared>,
519 reader: &mut Reader,
520 writer: Arc<ChannelWriter>,
521 link: &wire::LinkMess,
522 connection_id: ConnectionId,
523 remote: SocketAddr,
524 ) -> Result<()> {
525 let caps: Vec<&str> = [
526 (common_cap::AUTH_SELECTION, "auth_selection"),
527 (common_cap::AUTH_SPICE, "auth_spice"),
528 (common_cap::AUTH_SASL, "auth_sasl"),
529 (common_cap::MINI_HEADER, "mini_header"),
530 ]
531 .iter()
532 .filter(|(bit, _)| wire::has_cap(&link.common_caps, *bit))
533 .map(|(_, n)| *n)
534 .collect();
535 let event = Event::new(
536 &actions::CONNECTION_REQUEST_EVENT,
537 json!({
538 "client": remote.to_string(),
539 "password_configured": shared.config.password.is_some(),
540 "width": shared.config.width,
541 "height": shared.config.height,
542 "client_capabilities": caps,
543 }),
544 );
545 let decision = consult(shared, connection_id, &event).await;
546 if !decision.accept || decision.reject || decision.close {
547 write_raw(&writer.half, &link_err::PERMISSION_DENIED.to_le_bytes()).await?;
548 return Ok(());
549 }
550 let frame = match decision.redraw {
551 Some(commands) => {
552 match render_bgrx(shared.config.width, shared.config.height, commands).await {
553 Ok(f) => Some(Arc::new(f)),
554 Err(e) => {
555 Log::new(Some(&shared.status_tx))
556 .error(format!("SPICE first screen could not be rendered: {e:#}"));
557 None
558 }
559 }
560 }
561 None => None,
562 };
563 let (closed, _) = watch::channel(false);
564 let session = {
565 let Ok(mut sessions) = shared.sessions.lock() else {
566 anyhow::bail!("the session table is poisoned");
567 };
568 let mut id = rand::random::<u32>();
569 while id == 0 || sessions.contains_key(&id) {
570 id = rand::random::<u32>();
571 }
572 let session = Arc::new(Session {
573 id,
574 main: Mutex::new(None),
575 display: Mutex::new(DisplayState {
576 frame,
577 ..Default::default()
578 }),
579 display_attached: AtomicBool::new(false),
580 inputs_attached: AtomicBool::new(false),
581 mouse_mode: AtomicU32::new(wire::MOUSE_MODE_CLIENT),
582 closed,
583 });
584 sessions.insert(id, session.clone());
585 session
586 };
587 write_raw(&writer.half, &link_err::OK.to_le_bytes()).await?;
588 Log::new(Some(&shared.status_tx)).info(format!(
589 "SPICE session {:#010x} opened for {remote}",
590 session.id
591 ));
592 let result = Self::run_main(
593 shared,
594 &session,
595 reader,
596 writer,
597 link,
598 connection_id,
599 decision.notify,
600 )
601 .await;
602 session.close();
604 if let Ok(mut sessions) = shared.sessions.lock() {
605 sessions.remove(&session.id);
606 }
607 *session.main.lock().await = None;
608 Log::new(Some(&shared.status_tx))
609 .info(format!("SPICE session {:#010x} closed", session.id));
610 result
611 }
612
613 async fn run_main(
614 shared: &Arc<Shared>,
615 session: &Arc<Session>,
616 reader: &mut Reader,
617 writer: Arc<ChannelWriter>,
618 link: &wire::LinkMess,
619 connection_id: ConnectionId,
620 notices: Vec<(u32, String)>,
621 ) -> Result<()> {
622 let supported = wire::MOUSE_MODE_SERVER | wire::MOUSE_MODE_CLIENT;
623 writer
624 .send(
625 msg::MAIN_INIT,
626 &wire::main_init(
627 session.id,
628 1,
629 supported,
630 session.mouse_mode.load(Ordering::SeqCst),
631 0,
632 0,
633 multimedia_time(),
634 0,
635 ),
636 )
637 .await?;
638 if wire::has_cap(&link.channel_caps, wire::main_cap::NAME_AND_UUID) {
639 writer
640 .send(msg::MAIN_NAME, &wire::main_name(&shared.config.name))
641 .await?;
642 }
643 *session.main.lock().await = Some(writer.clone());
644 for (severity, text) in notices {
645 send_notify(session, severity, &text).await;
646 }
647 let mut closed = session.closed.subscribe();
648 loop {
649 let message = tokio::select! {
650 m = wire::read_message(reader, writer.mini, MAX_CLIENT_MESSAGE_BYTES) => m,
651 _ = closed.wait_for(|c| *c) => return Ok(()),
652 };
653 let (msg_type, body) = match message {
654 Ok(m) => m,
655 Err(e) => return end_of_channel(shared, "main", e),
656 };
657 count_in(shared, connection_id, &body).await;
658 match msg_type {
659 msgc::MAIN_ATTACH_CHANNELS => {
660 writer
661 .send(
662 msg::MAIN_CHANNELS_LIST,
663 &wire::channels_list(&[(channel::DISPLAY, 0), (channel::INPUTS, 0)]),
664 )
665 .await?;
666 }
667 msgc::MAIN_MOUSE_MODE_REQUEST => {
668 let mode = wire::Cursor::new(&body).u16("mouse mode")? as u32;
669 if mode & supported != 0 && mode.count_ones() == 1 {
670 session.mouse_mode.store(mode, Ordering::SeqCst);
671 }
672 writer
673 .send(
674 msg::MAIN_MOUSE_MODE,
675 &wire::mouse_mode(
676 supported as u16,
677 session.mouse_mode.load(Ordering::SeqCst) as u16,
678 ),
679 )
680 .await?;
681 }
682 other => tracing::trace!("SPICE main channel message {other} ignored"),
683 }
684 }
685 }
686
687 async fn run_display(
688 shared: &Arc<Shared>,
689 session: &Arc<Session>,
690 reader: &mut Reader,
691 writer: Arc<ChannelWriter>,
692 connection_id: ConnectionId,
693 ) -> Result<()> {
694 let (w, h) = (shared.config.width, shared.config.height);
695 let have_frame = {
696 let mut display = session.display.lock().await;
697 writer
698 .send(
699 msg::DISPLAY_SURFACE_CREATE,
700 &wire::surface_create(
701 0,
702 w,
703 h,
704 wire::SURFACE_FMT_32_XRGB,
705 wire::SURFACE_FLAGS_PRIMARY,
706 ),
707 )
708 .await?;
709 if let Some(frame) = display.frame.clone() {
710 send_frame(shared, &writer, &frame).await?;
711 writer.send(msg::DISPLAY_MARK, &[]).await?;
712 display.marked = true;
713 }
714 display.writer = Some(writer.clone());
715 display.frame.is_some()
716 };
717 if !have_frame {
718 let event = Event::new(
719 &actions::DISPLAY_READY_EVENT,
720 json!({"width": w, "height": h}),
721 );
722 let decision = consult(shared, connection_id, &event).await;
723 apply(shared, session, decision, true).await;
724 }
725 let mut closed = session.closed.subscribe();
726 let result = loop {
727 let message = tokio::select! {
728 m = wire::read_message(reader, writer.mini, MAX_CLIENT_MESSAGE_BYTES) => m,
729 _ = closed.wait_for(|c| *c) => break Ok(()),
730 };
731 match message {
732 Ok((msg_type, body)) => {
733 count_in(shared, connection_id, &body).await;
734 tracing::trace!("SPICE display channel message {msg_type} ignored");
735 }
736 Err(e) => break end_of_channel(shared, "display", e),
737 }
738 };
739 let mut display = session.display.lock().await;
740 display.writer = None;
741 display.marked = false;
742 result
743 }
744
745 async fn run_inputs(
746 shared: &Arc<Shared>,
747 session: &Arc<Session>,
748 reader: &mut Reader,
749 writer: Arc<ChannelWriter>,
750 connection_id: ConnectionId,
751 ) -> Result<()> {
752 let log = Log::new(Some(&shared.status_tx));
753 writer.send(msg::INPUTS_INIT, &0u16.to_le_bytes()).await?;
754 let (w, h) = (shared.config.width as i64, shared.config.height as i64);
755 let (mut x, mut y) = (w / 2, h / 2);
756 let mut decoder = keys::Decoder::default();
757 let mut held: Vec<(u8, bool)> = Vec::new();
758 let mut motions: u32 = 0;
759 let mut closed = session.closed.subscribe();
760 loop {
761 let message = tokio::select! {
762 m = wire::read_message(reader, writer.mini, MAX_CLIENT_MESSAGE_BYTES) => m,
763 _ = closed.wait_for(|c| *c) => return Ok(()),
764 };
765 let (msg_type, body) = match message {
766 Ok(m) => m,
767 Err(e) => return end_of_channel(shared, "inputs", e),
768 };
769 count_in(shared, connection_id, &body).await;
770 let mut c = wire::Cursor::new(&body);
771 let strokes = match msg_type {
772 msgc::INPUTS_KEY_DOWN | msgc::INPUTS_KEY_UP => {
773 decoder.feed_code(c.u32("key code")?)
774 }
775 msgc::INPUTS_KEY_SCANCODE => body.iter().filter_map(|b| decoder.feed(*b)).collect(),
776 msgc::INPUTS_MOUSE_MOTION | msgc::INPUTS_MOUSE_POSITION => {
777 if msg_type == msgc::INPUTS_MOUSE_MOTION {
778 let dx = c.i32("dx")? as i64;
779 let dy = c.i32("dy")? as i64;
780 x = (x + dx).clamp(0, w - 1);
781 y = (y + dy).clamp(0, h - 1);
782 } else {
783 x = (c.u32("x")? as i64).clamp(0, w - 1);
784 y = (c.u32("y")? as i64).clamp(0, h - 1);
785 }
786 log.debug_to(Sink::Both, format!("SPICE pointer at {x},{y}"));
788 motions += 1;
789 if motions.is_multiple_of(4) {
790 writer.send(msg::INPUTS_MOUSE_MOTION_ACK, &[]).await?;
793 }
794 Vec::new()
795 }
796 msgc::INPUTS_MOUSE_PRESS | msgc::INPUTS_MOUSE_RELEASE => {
797 let b = c.u8("button")?;
798 let state = c.u16("buttons state")?;
799 if msg_type == msgc::INPUTS_MOUSE_RELEASE {
800 log.debug(format!(
801 "SPICE {} button released at {x},{y}",
802 wire::button::name(b)
803 ));
804 } else {
805 let mode = if session.mouse_mode.load(Ordering::SeqCst)
806 == wire::MOUSE_MODE_CLIENT
807 {
808 "client"
809 } else {
810 "server"
811 };
812 let event = Event::new(
813 &actions::MOUSE_EVENT,
814 json!({
815 "x": x, "y": y,
816 "button": wire::button::name(b),
817 "buttons": wire::button::names_in(state | wire::button::mask(b)),
818 "mouse_mode": mode,
819 }),
820 );
821 let decision = consult(shared, connection_id, &event).await;
822 if !apply(shared, session, decision, false).await {
823 return Ok(());
824 }
825 }
826 Vec::new()
827 }
828 other => {
829 tracing::trace!("SPICE inputs channel message {other} ignored");
830 Vec::new()
831 }
832 };
833 for stroke in strokes {
834 let key = (stroke.scancode, stroke.extended);
835 if stroke.released {
836 held.retain(|k| *k != key);
837 continue;
838 }
839 if !held.contains(&key) {
840 held.push(key);
841 }
842 if keys::modifier(stroke.scancode, stroke.extended).is_some() {
843 continue;
844 }
845 let has =
846 |m: keys::Modifier| held.iter().any(|(s, e)| keys::modifier(*s, *e) == Some(m));
847 let shift = has(keys::Modifier::Shift);
848 let text = (!stroke.extended)
849 .then(|| keys::char_for(stroke.scancode, shift))
850 .flatten();
851 let event = Event::new(
852 &actions::KEY_EVENT,
853 json!({
854 "key": keys::key_name(stroke.scancode, stroke.extended, shift),
855 "text": text.map(|c| c.to_string()),
856 "scancode": stroke.scancode,
857 "extended": stroke.extended,
858 "shift": shift,
859 "ctrl": has(keys::Modifier::Ctrl),
860 "alt": has(keys::Modifier::Alt),
861 }),
862 );
863 let decision = consult(shared, connection_id, &event).await;
864 if !apply(shared, session, decision, false).await {
865 return Ok(());
866 }
867 }
868 }
869 }
870}
871
872async fn write_raw(writer: &Writer, bytes: &[u8]) -> std::io::Result<()> {
874 let mut w = writer.lock().await;
875 w.write_all(bytes).await?;
876 w.flush().await
877}
878
879async fn count_in(shared: &Shared, connection_id: ConnectionId, body: &[u8]) {
880 shared
881 .app_state
882 .update_connection_stats(
883 shared.server_id,
884 connection_id,
885 Some(body.len() as u64 + 6),
886 None,
887 Some(1),
888 None,
889 )
890 .await;
891}
892
893fn end_of_channel(shared: &Shared, name: &str, e: ReadError) -> Result<()> {
896 let log = Log::new(Some(&shared.status_tx));
897 match e {
898 ReadError::Io(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
899 log.info(format!("SPICE {name} channel closed by the client"));
900 Ok(())
901 }
902 ReadError::TooLarge { .. } | ReadError::SubList => {
903 log.warn(format!(
904 "SPICE {name} channel closed decision=fail_closed_message_too_large: {e}"
905 ));
906 Ok(())
907 }
908 ReadError::Io(e) => Err(e.into()),
909 }
910}
911
912fn multimedia_time() -> u32 {
913 crate::utils::clock::SystemTime::now()
914 .duration_since(crate::utils::clock::UNIX_EPOCH)
915 .map(|d| d.as_millis() as u32)
916 .unwrap_or(0)
917}
918
919async fn send_notify(session: &Session, severity: u32, text: &str) {
921 let main = session.main.lock().await.clone();
922 if let Some(main) = main {
923 let stamp = crate::utils::clock::SystemTime::now()
924 .duration_since(crate::utils::clock::UNIX_EPOCH)
925 .map(|d| d.as_nanos() as u64)
926 .unwrap_or(0);
927 let _ = main
928 .send(msg::NOTIFY, &wire::notify(stamp, severity, 0, text))
929 .await;
930 }
931}
932
933async fn publish_frame(shared: &Shared, session: &Session, pixels: Vec<u8>) {
935 let pixels = Arc::new(pixels);
936 let mut display = session.display.lock().await;
937 display.frame = Some(pixels.clone());
938 let Some(writer) = display.writer.clone() else {
939 return;
940 };
941 let sent = async {
942 send_frame(shared, &writer, &pixels).await?;
943 if !display.marked {
944 writer.send(msg::DISPLAY_MARK, &[]).await?;
945 }
946 std::io::Result::Ok(())
947 }
948 .await;
949 match sent {
950 Ok(()) => display.marked = true,
951 Err(e) => {
952 Log::new(Some(&shared.status_tx)).debug(format!("SPICE frame not sent: {e}"));
953 display.writer = None;
954 }
955 }
956}
957
958async fn send_frame(shared: &Shared, writer: &ChannelWriter, pixels: &[u8]) -> std::io::Result<()> {
962 let (w, h) = (shared.config.width, shared.config.height);
963 let id = shared.image_ids.fetch_add(1, Ordering::SeqCst);
964 let body = wire::draw_copy_bitmap(0, 0, 0, w, h, id, pixels);
965 writer.send(msg::DISPLAY_DRAW_COPY, &body).await
966}
967
968async fn apply(shared: &Shared, session: &Session, decision: Decision, first_screen: bool) -> bool {
972 for (severity, text) in &decision.notify {
973 send_notify(session, *severity, text).await;
974 }
975 if decision.close {
976 session.close();
977 return false;
978 }
979 let (w, h) = (shared.config.width, shared.config.height);
980 if let Some(commands) = decision.redraw {
981 match render_bgrx(w, h, commands).await {
982 Ok(pixels) => publish_frame(shared, session, pixels).await,
983 Err(e) => Log::new(Some(&shared.status_tx))
984 .error(format!("SPICE screen could not be rendered: {e:#}")),
985 }
986 } else if decision.no_answer {
987 if first_screen {
988 match render_bgrx(w, h, placeholder("the model gave no screen to draw", h)).await {
989 Ok(pixels) => publish_frame(shared, session, pixels).await,
990 Err(e) => Log::new(Some(&shared.status_tx))
991 .error(format!("SPICE placeholder could not be rendered: {e:#}")),
992 }
993 } else {
994 send_notify(session, wire::NOTIFY_SEVERITY_WARN, NO_ANSWER_NOTICE).await;
995 }
996 } else if first_screen && !decision.no_change && session.display.lock().await.frame.is_none() {
997 if let Ok(pixels) = render_bgrx(w, h, placeholder("no screen has been drawn yet", h)).await
999 {
1000 publish_frame(shared, session, pixels).await;
1001 }
1002 }
1003 true
1004}
1005
1006fn placeholder(reason: &str, height: u32) -> Vec<DisplayCommand> {
1007 vec![
1008 DisplayCommand::SetBackground {
1009 color: PLACEHOLDER_BACKGROUND,
1010 },
1011 DisplayCommand::DrawText {
1012 x: 24,
1013 y: height / 2,
1014 text: format!("NetGet SPICE: {reason}"),
1015 font_size: 18,
1016 color: PLACEHOLDER_FOREGROUND,
1017 },
1018 ]
1019}
1020
1021async fn render_bgrx(width: u32, height: u32, commands: Vec<DisplayCommand>) -> Result<Vec<u8>> {
1025 tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
1026 let mut canvas = DisplayCanvas::new(width, height);
1027 canvas.add_commands(commands);
1028 let image = canvas
1029 .try_render()
1030 .context("invalid SPICE display commands")?;
1031 let mut out = Vec::new();
1032 out.try_reserve_exact(width as usize * height as usize * 4)
1033 .context("unable to allocate the SPICE surface")?;
1034 for px in image.pixels() {
1035 out.extend_from_slice(&[px[2], px[1], px[0], 0]);
1036 }
1037 Ok(out)
1038 })
1039 .await
1040 .context("SPICE rendering failed")?
1041}
1042
1043async fn consult(shared: &Shared, connection_id: ConnectionId, event: &Event) -> Decision {
1047 let log = Log::new(Some(&shared.status_tx));
1048 let mut d = Decision::default();
1049 let id = &event.event_type.id;
1050 let execution = match call_llm(
1051 &shared.llm_client,
1052 &shared.app_state,
1053 shared.server_id,
1054 Some(connection_id),
1055 event,
1056 &SpiceProtocol::new(),
1057 )
1058 .await
1059 {
1060 Ok(x) => x,
1061 Err(e) => {
1062 let tag = match crate::utils::wire_failure::WireFailure::classify(&e) {
1063 crate::utils::wire_failure::WireFailure::Overloaded => "fail_closed_llm_overloaded",
1064 crate::utils::wire_failure::WireFailure::Unavailable => "fail_closed_llm_error",
1065 };
1066 log.error(format!(
1067 "SPICE {id} on {connection_id} not answered decision={tag}: {e}"
1068 ));
1069 d.no_answer = true;
1070 return d;
1071 }
1072 };
1073 for m in execution.messages {
1074 let _ = shared.status_tx.send(m);
1075 }
1076 let mut results = Vec::new();
1077 flatten(execution.protocol_results, &mut results);
1078 let mut usable = false;
1079 let mut undecodable = false;
1080 for r in results {
1081 match r {
1082 ActionResult::Custom { name, data } => {
1083 usable = true;
1084 match name.as_str() {
1085 actions::RESULT_ACCEPT => d.accept = true,
1086 actions::RESULT_REJECT => d.reject = true,
1087 actions::RESULT_NO_CHANGE => d.no_change = true,
1088 actions::RESULT_NOTIFY => d.notify.push((
1089 data["severity"].as_u64().unwrap_or(0) as u32,
1090 data["message"].as_str().unwrap_or_default().to_string(),
1091 )),
1092 actions::RESULT_DISPLAY => {
1093 match serde_json::from_value::<Vec<DisplayCommand>>(
1094 data["commands"].clone(),
1095 ) {
1096 Ok(c) => d.redraw = Some(c),
1097 Err(_) => undecodable = true,
1098 }
1099 }
1100 _ => usable = false,
1101 }
1102 }
1103 ActionResult::CloseConnection => {
1104 usable = true;
1105 d.close = true;
1106 }
1107 _ => {}
1108 }
1109 }
1110 let admission = event.event_type.id == actions::CONNECTION_REQUEST_EVENT.id;
1111 if undecodable {
1112 log.error(format!(
1113 "SPICE {id} on {connection_id} decision=fail_closed_bad_action: the render result \
1114 could not be decoded"
1115 ));
1116 d.no_answer = true;
1117 d.accept = false;
1118 } else if !usable {
1119 let detail = execution
1120 .failures
1121 .iter()
1122 .map(|f| format!("{}: {}", f.action, f.error))
1123 .collect::<Vec<_>>()
1124 .join("; ");
1125 if detail.is_empty() {
1126 log.warn(format!(
1127 "SPICE {id} on {connection_id} produced no usable action decision=model_silent"
1128 ));
1129 } else {
1130 log.error(format!(
1131 "SPICE {id} on {connection_id} produced no usable action \
1132 decision=fail_closed_bad_action ({detail})"
1133 ));
1134 }
1135 d.no_answer = true;
1136 } else if d.close || d.reject || (admission && !d.accept) {
1137 log.info(format!(
1138 "SPICE {id} on {connection_id} decision=model_reject"
1139 ));
1140 } else {
1141 log.info(format!(
1142 "SPICE {id} on {connection_id} decision=model_answer"
1143 ));
1144 }
1145 d
1146}
1147
1148fn flatten(results: Vec<ActionResult>, out: &mut Vec<ActionResult>) {
1149 for r in results {
1150 match r {
1151 ActionResult::Multiple(inner) => flatten(inner, out),
1152 other => out.push(other),
1153 }
1154 }
1155}