1pub mod actions;
18
19use crate::display::{Color, DisplayCanvas, DisplayCommand};
20use crate::llm::action_helper::call_llm;
21use crate::llm::actions::protocol_trait::ActionResult;
22use crate::llm::ollama_client::OllamaClient;
23use crate::logging::emit::{Log, Sink};
24use crate::protocol::Event;
25use crate::server::connection::ConnectionId;
26use crate::state::app_state::AppState;
27use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
28use actions::{
29 VncProtocol, RESULT_CLIPBOARD, RESULT_DISPLAY, RESULT_NO_CHANGE, VNC_CLIENT_CUT_TEXT_EVENT,
30 VNC_FRAMEBUFFER_UPDATE_REQUEST_EVENT, VNC_KEY_EVENT, VNC_POINTER_EVENT,
31};
32use anyhow::{anyhow, Context, Result};
33use std::net::SocketAddr;
34use std::sync::Arc;
35use std::time::Duration;
36use tokio::io::{AsyncReadExt, AsyncWriteExt};
37use tokio::net::TcpStream;
38use tokio::sync::mpsc;
39use tracing::{debug, error, trace, warn};
40
41const FIRST_BYTE_READ_TIMEOUT: Duration = Duration::from_secs(30);
48
49const IDLE_BETWEEN_MESSAGES_TIMEOUT: Duration = Duration::from_secs(1800);
63
64const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
70
71const CONNECTION_CAP_REFUSAL: &[u8] = b"";
82
83type VncReader = crate::server::accept_bounded::IdleTimeoutReader<tokio::io::ReadHalf<TcpStream>>;
93
94pub struct VncServer;
96
97const RFB_VERSION: &[u8] = b"RFB 003.008\n";
99
100const DEFAULT_DESKTOP_NAME: &str = "NetGet VNC";
102
103pub const MAX_CUT_TEXT_LEN: u32 = 1 << 20;
113
114pub const PLACEHOLDER_BACKGROUND: Color = Color::rgb(24, 26, 34);
122
123const PLACEHOLDER_FOREGROUND: Color = Color::rgb(203, 213, 225);
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128#[repr(u8)]
129enum SecurityType {
130 None = 1,
131}
132
133#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
135pub struct VncPixelFormat {
136 pub bits_per_pixel: u8,
137 pub depth: u8,
138 pub big_endian: bool,
139 pub true_color: bool,
140 pub red_max: u16,
141 pub green_max: u16,
142 pub blue_max: u16,
143 pub red_shift: u8,
144 pub green_shift: u8,
145 pub blue_shift: u8,
146}
147
148impl VncPixelFormat {
149 pub fn default_rgb888() -> Self {
151 Self {
152 bits_per_pixel: 32,
153 depth: 24,
154 big_endian: false,
155 true_color: true,
156 red_max: 255,
157 green_max: 255,
158 blue_max: 255,
159 red_shift: 16,
160 green_shift: 8,
161 blue_shift: 0,
162 }
163 }
164}
165
166#[derive(Default)]
174struct Decision {
175 redraw: Option<Vec<DisplayCommand>>,
176 clipboard: Vec<String>,
177 no_change: bool,
178 close: bool,
179 no_answer: Option<String>,
181}
182
183struct VncConnection {
185 connection_id: ConnectionId,
186 server_id: crate::state::ServerId,
187 width: u16,
188 height: u16,
189 app_state: Arc<AppState>,
190 llm_client: OllamaClient,
191 protocol: VncProtocol,
192 status_tx: mpsc::UnboundedSender<String>,
193 write_half: Arc<tokio::sync::Mutex<tokio::io::WriteHalf<TcpStream>>>,
194 last_frame: Option<Vec<u8>>,
196 dirty: bool,
198 pending_request: bool,
200 answered_first_request: bool,
202 last_button_mask: u8,
204}
205
206impl VncServer {
207 #[allow(clippy::too_many_arguments)]
212 pub async fn spawn_with_llm_actions(
213 listen_addr: SocketAddr,
214 llm_client: OllamaClient,
215 app_state: Arc<AppState>,
216 status_tx: mpsc::UnboundedSender<String>,
217 server_id: crate::state::ServerId,
218 width: u16,
219 height: u16,
220 desktop_name: Option<String>,
221 ) -> Result<SocketAddr> {
222 let listener =
223 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
224 let local_addr = listener.local_addr()?;
225 let desktop_name =
226 Arc::new(desktop_name.unwrap_or_else(|| DEFAULT_DESKTOP_NAME.to_string()));
227
228 Log::new(Some(&status_tx)).info(format!("VNC server listening on {}", local_addr));
231 Log::new(Some(&status_tx)).debug(format!(
232 "VNC framebuffer {}x{}, desktop name '{}'",
233 width, height, desktop_name
234 ));
235
236 let task_registrar = app_state.clone();
237 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
238 let accept_handle = tokio::spawn(async move {
239 loop {
240 match crate::server::accept_bounded::accept_bounded(
241 &listener,
242 &limiter,
243 CONNECTION_CAP_REFUSAL,
244 "VNC",
245 Some(&status_tx),
246 )
247 .await
248 {
249 Ok((stream, remote_addr, permit)) => {
250 let connection_id =
251 ConnectionId::new(app_state.get_next_unified_id().await);
252 let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
253 let state_clone = app_state.clone();
254 let status_clone = status_tx.clone();
255 let llm_clone = llm_client.clone();
256 let name_clone = desktop_name.clone();
257
258 Log::new(Some(&status_clone))
259 .info(format!("VNC client connected from {}", remote_addr));
260
261 let task_owner = app_state.clone();
263 task_owner
264 .spawn_server_task(server_id, async move {
265 let _permit = permit;
269 if let Err(e) = Self::handle_connection(
270 stream,
271 connection_id,
272 remote_addr,
273 local_addr_conn,
274 server_id,
275 state_clone,
276 status_clone,
277 llm_clone,
278 width,
279 height,
280 &name_clone,
281 )
282 .await
283 {
284 error!("VNC connection error: {}", e);
285 }
286 })
287 .await;
288 }
289 Err(e) => {
290 Log::new(Some(&status_tx))
294 .error(format!("VNC accept failed, stopping loop: {}", e));
295 break;
296 }
297 }
298 }
299 });
300
301 task_registrar
302 .register_server_task(server_id, accept_handle)
303 .await;
304
305 Ok(local_addr)
306 }
307
308 #[allow(clippy::too_many_arguments)]
310 async fn handle_connection(
311 stream: TcpStream,
312 connection_id: ConnectionId,
313 remote_addr: SocketAddr,
314 local_addr: SocketAddr,
315 server_id: crate::state::ServerId,
316 app_state: Arc<AppState>,
317 status_tx: mpsc::UnboundedSender<String>,
318 llm_client: OllamaClient,
319 width: u16,
320 height: u16,
321 desktop_name: &str,
322 ) -> Result<()> {
323 let (read_half, write_half) = tokio::io::split(stream);
324 let mut read_half: VncReader = crate::server::accept_bounded::IdleTimeoutReader::with_first(
326 read_half,
327 FIRST_BYTE_READ_TIMEOUT,
328 IDLE_BETWEEN_MESSAGES_TIMEOUT,
329 );
330 let write_half = Arc::new(tokio::sync::Mutex::new(write_half));
331
332 let now = crate::utils::clock::Instant::now();
333 let conn_state = ConnectionState {
334 id: connection_id,
335 remote_addr,
336 local_addr,
337 bytes_sent: 0,
338 bytes_received: 0,
339 packets_sent: 0,
340 packets_received: 0,
341 last_activity: now,
342 status: ConnectionStatus::Active,
343 status_changed_at: now,
344 protocol_info: ProtocolConnectionInfo::empty(),
345 };
346 app_state
347 .add_connection_to_server(server_id, conn_state)
348 .await;
349 let _ = status_tx.send("__UPDATE_UI__".to_string());
350
351 let peer_protocol: Arc<dyn crate::llm::actions::protocol_trait::Server> =
362 Arc::new(VncProtocol::new());
363 let peer_rx = crate::server::peer_support::register_peer_channel(
364 &app_state,
365 server_id,
366 connection_id.as_u32(),
367 )
368 .await;
369 crate::server::peer_support::spawn_peer_command_task(
370 peer_rx,
371 peer_protocol,
372 app_state.clone(),
373 server_id,
374 connection_id.as_u32(),
375 write_half.clone(),
376 status_tx.clone(),
377 );
378
379 let result = async {
380 Self::perform_handshake(&mut read_half, &write_half, &status_tx).await?;
381
382 Log::new(Some(&status_tx)).debug(format!("VNC handshake complete for {}", remote_addr));
383 app_state
384 .update_vnc_connection_auth(server_id, connection_id, true, None)
385 .await;
386
387 Self::handle_client_init(
388 &mut read_half,
389 &write_half,
390 &status_tx,
391 width,
392 height,
393 desktop_name,
394 )
395 .await?;
396
397 let mut connection = VncConnection {
398 connection_id,
399 server_id,
400 width,
401 height,
402 app_state: app_state.clone(),
403 llm_client,
404 protocol: VncProtocol::new(),
405 status_tx: status_tx.clone(),
406 write_half: write_half.clone(),
407 last_frame: None,
408 dirty: false,
409 pending_request: false,
410 answered_first_request: false,
411 last_button_mask: 0,
412 };
413 connection.message_loop(read_half).await
414 }
415 .await;
416
417 app_state
423 .remove_peer_handle(server_id, connection_id.as_u32())
424 .await;
425 app_state
426 .remove_connection_from_server(server_id, connection_id)
427 .await;
428 let _ = status_tx.send("__UPDATE_UI__".to_string());
429
430 result
431 }
432
433 async fn perform_handshake(
435 read_half: &mut VncReader,
436 write_half: &Arc<tokio::sync::Mutex<tokio::io::WriteHalf<TcpStream>>>,
437 status_tx: &mpsc::UnboundedSender<String>,
438 ) -> Result<()> {
439 {
440 let mut writer = write_half.lock().await;
441 writer.write_all(RFB_VERSION).await?;
442 writer.flush().await?;
443 }
444 trace!(
445 "Sent RFB version: {}",
446 String::from_utf8_lossy(RFB_VERSION).trim()
447 );
448
449 let mut client_version = [0u8; 12];
450 read_half.read_exact(&mut client_version).await?;
451 trace!(
452 "Received client version: {}",
453 String::from_utf8_lossy(&client_version).trim()
454 );
455
456 {
457 let mut writer = write_half.lock().await;
458 writer.write_u8(1).await?; writer.write_u8(SecurityType::None as u8).await?;
460 writer.flush().await?;
461 }
462 trace!("Sent security types: [None]");
463
464 let chosen_security = read_half.read_u8().await?;
465 trace!("Client chose security type: {}", chosen_security);
466
467 if chosen_security == SecurityType::None as u8 {
468 let mut writer = write_half.lock().await;
469 writer.write_u32(0).await?; writer.flush().await?;
471 trace!("Sent SecurityResult: OK");
472 Log::new(Some(status_tx)).debug("VNC client authenticated");
473 Ok(())
474 } else {
475 let reason = b"only security type 1 (None) is supported";
479 {
480 let mut writer = write_half.lock().await;
481 writer.write_u32(1).await?; writer.write_u32(reason.len() as u32).await?;
483 writer.write_all(reason).await?;
484 writer.flush().await?;
485 }
486 Log::new(Some(status_tx)).warn(format!(
489 "VNC handshake refused decision=protocol_error: client requested unsupported \
490 security type {}",
491 chosen_security
492 ));
493 Err(anyhow!(
494 "client requested unsupported security type {chosen_security}"
495 ))
496 }
497 }
498
499 async fn handle_client_init(
501 read_half: &mut VncReader,
502 write_half: &Arc<tokio::sync::Mutex<tokio::io::WriteHalf<TcpStream>>>,
503 status_tx: &mpsc::UnboundedSender<String>,
504 width: u16,
505 height: u16,
506 desktop_name: &str,
507 ) -> Result<()> {
508 let shared_flag = read_half.read_u8().await?;
509 trace!("Client shared flag: {}", shared_flag);
510
511 let pixel_format = VncPixelFormat::default_rgb888();
512 let name = desktop_name.as_bytes();
513
514 let mut message = Vec::with_capacity(24 + name.len());
515 message.extend_from_slice(&width.to_be_bytes());
516 message.extend_from_slice(&height.to_be_bytes());
517 message.push(pixel_format.bits_per_pixel);
518 message.push(pixel_format.depth);
519 message.push(u8::from(pixel_format.big_endian));
520 message.push(u8::from(pixel_format.true_color));
521 message.extend_from_slice(&pixel_format.red_max.to_be_bytes());
522 message.extend_from_slice(&pixel_format.green_max.to_be_bytes());
523 message.extend_from_slice(&pixel_format.blue_max.to_be_bytes());
524 message.push(pixel_format.red_shift);
525 message.push(pixel_format.green_shift);
526 message.push(pixel_format.blue_shift);
527 message.extend_from_slice(&[0, 0, 0]); message.extend_from_slice(&(name.len() as u32).to_be_bytes());
529 message.extend_from_slice(name);
530
531 {
532 let mut writer = write_half.lock().await;
533 writer.write_all(&message).await?;
534 writer.flush().await?;
535 }
536
537 Log::new(Some(status_tx)).debug(format!(
538 "VNC initialized: {}x{} framebuffer '{}'",
539 width, height, desktop_name
540 ));
541
542 Ok(())
543 }
544}
545
546impl VncConnection {
547 async fn message_loop(&mut self, mut read_half: VncReader) -> Result<()> {
553 loop {
554 let message_type = match read_half.read_u8().await {
555 Ok(t) => t,
556 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
557 Log::new(Some(&self.status_tx)).info("VNC client disconnected");
558 break;
559 }
560 Err(e) => return Err(e.into()),
561 };
562
563 trace!("Received message type: {}", message_type);
564
565 let mut bytes_in: u64 = 1;
568
569 let keep_going = match message_type {
570 0 => {
571 let mut buf = [0u8; 19]; read_half.read_exact(&mut buf).await?;
575 bytes_in += 19;
576 trace!("SetPixelFormat received (ignored, server always sends 32bpp BGRX)");
577 true
578 }
579 2 => {
580 let _padding = read_half.read_u8().await?;
584 let num_encodings = read_half.read_u16().await?;
585 for _ in 0..num_encodings {
586 let _ = read_half.read_i32().await?;
587 }
588 bytes_in += 3 + 4 * num_encodings as u64;
589 trace!("SetEncodings received: {} encodings", num_encodings);
590 true
591 }
592 3 => {
593 let incremental = read_half.read_u8().await? != 0;
594 let x = read_half.read_u16().await?;
595 let y = read_half.read_u16().await?;
596 let width = read_half.read_u16().await?;
597 let height = read_half.read_u16().await?;
598 bytes_in += 9;
599 trace!(
600 "FramebufferUpdateRequest: incremental={}, x={}, y={}, w={}, h={}",
601 incremental,
602 x,
603 y,
604 width,
605 height
606 );
607 self.handle_update_request(incremental).await?
608 }
609 4 => {
610 let down = read_half.read_u8().await? != 0;
611 let _padding = read_half.read_u16().await?;
612 let keysym = read_half.read_u32().await?;
613 bytes_in += 7;
614 self.handle_key_event(down, keysym).await?
615 }
616 5 => {
617 let button_mask = read_half.read_u8().await?;
618 let x = read_half.read_u16().await?;
619 let y = read_half.read_u16().await?;
620 bytes_in += 5;
621 self.handle_pointer_event(button_mask, x, y).await?
622 }
623 6 => {
624 let _padding = read_half.read_u8().await?;
625 let _padding = read_half.read_u16().await?;
626 let length = read_half.read_u32().await?;
627 bytes_in += 7;
628 if length > MAX_CUT_TEXT_LEN {
629 Log::new(Some(&self.status_tx)).warn(format!(
632 "VNC ClientCutText length {} exceeds {} byte cap \
633 decision=fail_closed_cut_text_too_large, closing connection",
634 length, MAX_CUT_TEXT_LEN
635 ));
636 false
637 } else {
638 let mut text = vec![0u8; length as usize];
639 read_half.read_exact(&mut text).await?;
640 bytes_in += length as u64;
641 self.handle_cut_text(&text).await?
642 }
643 }
644 other => {
645 Log::new(Some(&self.status_tx)).warn(format!(
648 "VNC client sent unknown message type {}, closing connection",
649 other
650 ));
651 false
652 }
653 };
654
655 self.app_state
656 .update_connection_stats(
657 self.server_id,
658 self.connection_id,
659 Some(bytes_in),
660 None,
661 Some(1),
662 None,
663 )
664 .await;
665
666 if !keep_going {
667 break;
668 }
669 }
670
671 Ok(())
672 }
673
674 async fn handle_update_request(&mut self, incremental: bool) -> Result<bool> {
686 self.pending_request = true;
687
688 if incremental && self.answered_first_request {
689 if self.dirty {
690 self.send_pending_frame(false).await?;
691 } else {
692 trace!(
693 "VNC incremental request held open for {} (screen unchanged)",
694 self.connection_id
695 );
696 }
697 return Ok(true);
698 }
699
700 let event = Event::new(
701 &VNC_FRAMEBUFFER_UPDATE_REQUEST_EVENT,
702 serde_json::json!({
703 "width": self.width,
704 "height": self.height,
705 "first_request": !self.answered_first_request,
706 }),
707 );
708 let decision = self.consult(&event).await;
709 let close = self
710 .apply(decision, "the model gave no screen to draw")
711 .await?;
712
713 self.send_pending_frame(true).await?;
716 self.answered_first_request = true;
717
718 Ok(!close)
719 }
720
721 async fn handle_key_event(&mut self, down: bool, keysym: u32) -> Result<bool> {
723 let key_name = keysym_name(keysym);
724 Log::new(Some(&self.status_tx)).debug(format!(
726 "VNC KeyEvent: down={}, key={}, keysym={}",
727 down, key_name, keysym
728 ));
729
730 let event = Event::new(
731 &VNC_KEY_EVENT,
732 serde_json::json!({
733 "down": down,
734 "keysym": keysym,
735 "key": key_name,
736 }),
737 );
738 let decision = self.consult(&event).await;
739 let close = self
740 .apply(decision, "the model gave no screen to draw for this key")
741 .await?;
742 self.send_pending_frame(false).await?;
743 Ok(!close)
744 }
745
746 async fn handle_pointer_event(&mut self, button_mask: u8, x: u16, y: u16) -> Result<bool> {
753 let previous = self.last_button_mask;
754 self.last_button_mask = button_mask;
755
756 if button_mask == previous {
757 Log::new(Some(&self.status_tx)).debug_to(
760 Sink::Both,
761 format!(
762 "VNC PointerEvent: move to {},{} (buttons={:#04b})",
763 x, y, button_mask
764 ),
765 );
766 return Ok(true);
767 }
768
769 let pressed = button_mask.count_ones() > previous.count_ones();
770 Log::new(Some(&self.status_tx)).debug(format!(
773 "VNC PointerEvent: {} at {},{} (buttons={:#04b})",
774 if pressed { "press" } else { "release" },
775 x,
776 y,
777 button_mask
778 ));
779
780 let event = Event::new(
781 &VNC_POINTER_EVENT,
782 serde_json::json!({
783 "x": x,
784 "y": y,
785 "pressed": pressed,
786 "buttons": button_names(button_mask),
787 "button_mask": button_mask,
788 }),
789 );
790 let decision = self.consult(&event).await;
791 let close = self
792 .apply(decision, "the model gave no screen to draw for this click")
793 .await?;
794 self.send_pending_frame(false).await?;
795 Ok(!close)
796 }
797
798 async fn handle_cut_text(&mut self, raw: &[u8]) -> Result<bool> {
800 let text: String = raw.iter().map(|&b| b as char).collect();
803 Log::new(Some(&self.status_tx)).debug(format!(
805 "VNC ClientCutText: {} bytes from {}",
806 raw.len(),
807 self.connection_id
808 ));
809 trace!("VNC ClientCutText payload: {:?}", text);
810
811 let event = Event::new(
812 &VNC_CLIENT_CUT_TEXT_EVENT,
813 serde_json::json!({ "text": text }),
814 );
815 let decision = self.consult(&event).await;
816 let close = self.apply(decision, "").await?;
819 self.send_pending_frame(false).await?;
820 Ok(!close)
821 }
822
823 async fn consult(&self, event: &Event) -> Decision {
832 let mut decision = Decision::default();
833 let log = Log::new(Some(&self.status_tx));
834
835 let execution = match call_llm(
836 &self.llm_client,
837 &self.app_state,
838 self.server_id,
839 Some(self.connection_id),
840 event,
841 &self.protocol,
842 )
843 .await
844 {
845 Ok(execution) => execution,
846 Err(e) => {
847 let tag = match crate::utils::wire_failure::WireFailure::classify(&e) {
854 crate::utils::wire_failure::WireFailure::Overloaded => {
855 "fail_closed_llm_overloaded"
856 }
857 crate::utils::wire_failure::WireFailure::Unavailable => "fail_closed_llm_error",
858 };
859 log.error(format!(
860 "VNC {} on {} not answered decision={}: {}",
861 event.event_type.id, self.connection_id, tag, e
862 ));
863 decision.no_answer = Some(e.to_string());
864 return decision;
865 }
866 };
867
868 for message in execution.messages {
869 let _ = self.status_tx.send(message);
870 }
871
872 let mut flattened = Vec::with_capacity(execution.protocol_results.len());
874 flatten_results(execution.protocol_results, &mut flattened);
875
876 let mut saw_result = false;
877 for result in flattened {
878 match result {
879 ActionResult::Custom { name, data } if name == RESULT_DISPLAY => {
880 saw_result = true;
881 match serde_json::from_value::<Vec<DisplayCommand>>(
882 data.get("commands").cloned().unwrap_or_default(),
883 ) {
884 Ok(commands) => decision.redraw = Some(commands),
885 Err(e) => {
886 error!("VNC render result could not be decoded: {}", e);
889 decision.no_answer =
890 Some(format!("render result could not be decoded: {e}"));
891 }
892 }
893 }
894 ActionResult::Custom { name, .. } if name == RESULT_NO_CHANGE => {
895 saw_result = true;
896 decision.no_change = true;
897 }
898 ActionResult::Custom { name, data } if name == RESULT_CLIPBOARD => {
899 saw_result = true;
900 if let Some(text) = data.get("text").and_then(|v| v.as_str()) {
901 decision.clipboard.push(text.to_string());
902 }
903 }
904 ActionResult::CloseConnection => {
905 saw_result = true;
906 decision.close = true;
907 }
908 ActionResult::NoAction => {}
909 other => {
910 warn!("VNC ignoring unexpected action result: {:?}", other);
911 }
912 }
913 }
914
915 if decision.no_answer.is_some() {
919 log.error(format!(
922 "VNC {} on {} decision=fail_closed_bad_action: the render result could not be \
923 decoded",
924 event.event_type.id, self.connection_id
925 ));
926 } else if !saw_result {
927 let detail = if execution.failures.is_empty() {
928 "no protocol action was returned".to_string()
929 } else {
930 execution
931 .failures
932 .iter()
933 .map(|f| format!("{}: {}", f.action, f.error))
934 .collect::<Vec<_>>()
935 .join("; ")
936 };
937 if execution.failures.is_empty() {
940 log.warn(format!(
941 "VNC {} on {} produced no usable action decision=model_silent ({})",
942 event.event_type.id, self.connection_id, detail
943 ));
944 } else {
945 log.error(format!(
946 "VNC {} on {} produced no usable action decision=fail_closed_bad_action ({})",
947 event.event_type.id, self.connection_id, detail
948 ));
949 }
950 decision.no_answer = Some(detail);
951 } else if decision.close {
952 log.info(format!(
953 "VNC {} on {} decision=model_reject (the model closed the connection)",
954 event.event_type.id, self.connection_id
955 ));
956 } else {
957 log.info(format!(
958 "VNC {} on {} decision=model_answer",
959 event.event_type.id, self.connection_id
960 ));
961 }
962
963 decision
964 }
965
966 async fn apply(&mut self, decision: Decision, no_answer_reason: &str) -> Result<bool> {
971 for text in &decision.clipboard {
972 self.send_server_cut_text(text).await?;
973 }
974
975 if let Some(commands) = decision.redraw {
976 let pixels = render_frame(self.width, self.height, commands).await?;
977 self.store_frame(pixels);
978 } else if decision.no_change {
979 trace!(
980 "VNC screen held unchanged for {} by explicit decision",
981 self.connection_id
982 );
983 } else if decision.no_answer.is_some() && !no_answer_reason.is_empty() {
984 let pixels = render_frame(
988 self.width,
989 self.height,
990 placeholder_commands(self.height, no_answer_reason),
991 )
992 .await?;
993 self.store_frame(pixels);
994 }
995
996 Ok(decision.close)
997 }
998
999 fn store_frame(&mut self, pixels: Vec<u8>) {
1001 self.last_frame = Some(pixels);
1002 self.dirty = true;
1003 }
1004
1005 async fn send_pending_frame(&mut self, force: bool) -> Result<()> {
1011 if !self.pending_request || !(force || self.dirty) {
1012 return Ok(());
1013 }
1014
1015 if self.last_frame.is_none() {
1016 let pixels = render_frame(
1018 self.width,
1019 self.height,
1020 placeholder_commands(self.height, "no screen has been drawn yet"),
1021 )
1022 .await?;
1023 self.store_frame(pixels);
1024 }
1025
1026 let Some(pixels) = self.last_frame.as_ref() else {
1027 return Ok(());
1028 };
1029
1030 let mut frame = framebuffer_update_header(self.width, self.height);
1031 frame.extend_from_slice(pixels);
1032
1033 let frame_len = frame.len();
1034 {
1035 let mut writer = self.write_half.lock().await;
1036 writer.write_all(&frame).await?;
1037 writer.flush().await?;
1038 }
1039 self.app_state
1040 .update_connection_stats(
1041 self.server_id,
1042 self.connection_id,
1043 None,
1044 Some(frame_len as u64),
1045 None,
1046 Some(1),
1047 )
1048 .await;
1049
1050 self.pending_request = false;
1051 self.dirty = false;
1052 Log::new(Some(&self.status_tx)).debug(format!(
1055 "VNC sent {}x{} framebuffer update ({} bytes) to {}",
1056 self.width,
1057 self.height,
1058 frame.len(),
1059 self.connection_id
1060 ));
1061 Ok(())
1062 }
1063
1064 async fn send_server_cut_text(&self, text: &str) -> Result<()> {
1066 let latin1: Vec<u8> = text
1068 .chars()
1069 .map(|c| if (c as u32) <= 0xFF { c as u8 } else { b'?' })
1070 .collect();
1071
1072 let mut message = Vec::with_capacity(8 + latin1.len());
1073 message.push(3); message.extend_from_slice(&[0, 0, 0]); message.extend_from_slice(&(latin1.len() as u32).to_be_bytes());
1076 message.extend_from_slice(&latin1);
1077
1078 let message_len = message.len();
1079 {
1080 let mut writer = self.write_half.lock().await;
1081 writer.write_all(&message).await?;
1082 writer.flush().await?;
1083 }
1084 self.app_state
1085 .update_connection_stats(
1086 self.server_id,
1087 self.connection_id,
1088 None,
1089 Some(message_len as u64),
1090 None,
1091 Some(1),
1092 )
1093 .await;
1094 Log::new(Some(&self.status_tx)).debug(format!(
1097 "VNC sent {} bytes of clipboard text to {}",
1098 latin1.len(),
1099 self.connection_id
1100 ));
1101 Ok(())
1102 }
1103}
1104
1105fn flatten_results(results: Vec<ActionResult>, out: &mut Vec<ActionResult>) {
1107 for result in results {
1108 match result {
1109 ActionResult::Multiple(inner) => flatten_results(inner, out),
1110 other => out.push(other),
1111 }
1112 }
1113}
1114
1115fn framebuffer_update_header(width: u16, height: u16) -> Vec<u8> {
1121 let mut header = Vec::with_capacity(16);
1122 header.push(0); header.push(0); header.extend_from_slice(&1u16.to_be_bytes()); header.extend_from_slice(&0u16.to_be_bytes()); header.extend_from_slice(&0u16.to_be_bytes()); header.extend_from_slice(&width.to_be_bytes());
1128 header.extend_from_slice(&height.to_be_bytes());
1129 header.extend_from_slice(&0i32.to_be_bytes()); header
1131}
1132
1133async fn render_frame(width: u16, height: u16, commands: Vec<DisplayCommand>) -> Result<Vec<u8>> {
1139 let (w, h) = (width as u32, height as u32);
1140 let pixels = tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
1141 let mut canvas = DisplayCanvas::new(w, h);
1142 canvas.add_commands(commands);
1143 let image = canvas
1144 .try_render()
1145 .context("Invalid VNC display commands")?;
1146 let mut out = Vec::new();
1147 out.try_reserve_exact(w as usize * h as usize * 4)
1148 .context("Unable to allocate VNC framebuffer")?;
1149 for pixel in image.pixels() {
1150 out.extend_from_slice(&[pixel[2], pixel[1], pixel[0], 0]); }
1152 Ok(out)
1153 })
1154 .await
1155 .context("VNC framebuffer rendering failed")??;
1156
1157 debug!("Rendered VNC framebuffer: {}x{}", width, height);
1158 Ok(pixels)
1159}
1160
1161fn placeholder_commands(height: u16, reason: &str) -> Vec<DisplayCommand> {
1163 vec![
1164 DisplayCommand::SetBackground {
1165 color: PLACEHOLDER_BACKGROUND,
1166 },
1167 DisplayCommand::DrawText {
1168 x: 24,
1169 y: (height / 2) as u32,
1170 text: format!("NetGet VNC: {reason}"),
1171 font_size: 18,
1172 color: PLACEHOLDER_FOREGROUND,
1173 },
1174 ]
1175}
1176
1177fn button_names(mask: u8) -> Vec<&'static str> {
1179 const NAMES: [&str; 5] = ["left", "middle", "right", "scroll_up", "scroll_down"];
1180 NAMES
1181 .iter()
1182 .enumerate()
1183 .filter(|(bit, _)| mask & (1 << bit) != 0)
1184 .map(|(_, name)| *name)
1185 .collect()
1186}
1187
1188fn keysym_name(keysym: u32) -> String {
1190 if (0x20..=0x7E).contains(&keysym) || (0xA0..=0xFF).contains(&keysym) {
1192 if let Some(c) = char::from_u32(keysym) {
1193 return c.to_string();
1194 }
1195 }
1196 if (0x01000100..=0x0110FFFF).contains(&keysym) {
1198 if let Some(c) = char::from_u32(keysym - 0x01000000) {
1199 return c.to_string();
1200 }
1201 }
1202 if (0xFFBE..=0xFFE0).contains(&keysym) {
1204 return format!("F{}", keysym - 0xFFBD);
1205 }
1206 match keysym {
1207 0xFF08 => "BackSpace",
1208 0xFF09 => "Tab",
1209 0xFF0A => "Linefeed",
1210 0xFF0B => "Clear",
1211 0xFF0D => "Return",
1212 0xFF13 => "Pause",
1213 0xFF14 => "Scroll_Lock",
1214 0xFF15 => "Sys_Req",
1215 0xFF1B => "Escape",
1216 0xFF50 => "Home",
1217 0xFF51 => "Left",
1218 0xFF52 => "Up",
1219 0xFF53 => "Right",
1220 0xFF54 => "Down",
1221 0xFF55 => "Page_Up",
1222 0xFF56 => "Page_Down",
1223 0xFF57 => "End",
1224 0xFF58 => "Begin",
1225 0xFF63 => "Insert",
1226 0xFF7F => "Num_Lock",
1227 0xFF8D => "KP_Enter",
1228 0xFFE1 => "Shift_L",
1229 0xFFE2 => "Shift_R",
1230 0xFFE3 => "Control_L",
1231 0xFFE4 => "Control_R",
1232 0xFFE5 => "Caps_Lock",
1233 0xFFE7 => "Meta_L",
1234 0xFFE8 => "Meta_R",
1235 0xFFE9 => "Alt_L",
1236 0xFFEA => "Alt_R",
1237 0xFFEB => "Super_L",
1238 0xFFEC => "Super_R",
1239 0xFFFF => "Delete",
1240 _ => return format!("keysym_{keysym:#x}"),
1241 }
1242 .to_string()
1243}