From a1c4b08535f375f5b461b883568e9bcd5ae0c2b4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 14 Jan 2022 00:32:09 +0800 Subject: [PATCH 1/2] simple privacy demo Signed-off-by: fufesou --- src/platform/windows.rs | 3 +- src/server/connection.rs | 117 ++++++++++++++++++++++++++++++++------- src/ui/header.tis | 5 +- 3 files changed, 102 insertions(+), 23 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 074b3ccd1..b0c556bb5 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1013,11 +1013,10 @@ fn run_cmds(cmds: String, show: bool) -> ResultType<()> { Ok(()) } -pub fn toggle_privacy_mode(v: bool) { +pub fn toggle_blank_screen(v: bool) { let v = if v { TRUE } else { FALSE }; unsafe { blank_screen(v); - BlockInput(v); } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 75a9071f8..963c7e79b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -15,6 +15,7 @@ use hbb_common::{ tokio_util::codec::{BytesCodec, Framed}, }; use sha2::{Digest, Sha256}; +use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; @@ -84,6 +85,14 @@ const MILLI1: Duration = Duration::from_millis(1); const SEND_TIMEOUT_VIDEO: u64 = 12_000; const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; +type SyncFunc = Box; + +enum BlankChannelMsg { + On, + Off, + Exit, +} + impl Connection { pub async fn start( addr: SocketAddr, @@ -157,6 +166,12 @@ impl Connection { }, ); + let (input_sender, input_receiver) = sync_channel(1); + + let (blank_sender, blank_receiver) = sync_channel(1); + let _ = std::thread::spawn(move || Self::handle_input(input_receiver)); + let blank_handler = std::thread::spawn(move || Self::handle_blank(blank_receiver)); + loop { tokio::select! { biased; // video has higher priority @@ -232,7 +247,7 @@ impl Connection { Ok(bytes) => { last_recv_time = Instant::now(); if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { - if !conn.on_message(msg_in).await { + if !conn.on_message(msg_in, input_sender.clone(), blank_sender.clone()).await { break; } } @@ -297,6 +312,14 @@ impl Connection { } } + blank_sender.send(BlankChannelMsg::Exit).unwrap(); + if let Err(e) = blank_handler.join() { + log::error!("Failed to join blank thread, {:?}", e); + }; + + crate::platform::block_input(false); + crate::platform::toggle_blank_screen(false); + video_service::notify_video_frame_feched(id, None); super::video_service::update_test_latency(id, 0); super::video_service::update_image_quality(id, None); @@ -305,6 +328,44 @@ impl Connection { } } + // TODO: + // 1. Send "Block Input" and "Privacy Mode" when connection established. + // 2. Ctrl + Alt + Del will break "Block Input" + fn handle_input(receiver: Receiver) { + loop { + match receiver.recv() { + Ok(mut f) => { + f(); + } + _ => break, + } + } + } + + fn handle_blank(receiver: Receiver) { + let mut last_privacy = false; + loop { + match receiver.recv_timeout(std::time::Duration::from_millis(500)) { + Ok(v) => match v { + BlankChannelMsg::On => { + crate::platform::toggle_blank_screen(true); + last_privacy = true; + } + BlankChannelMsg::Off => { + crate::platform::toggle_blank_screen(false); + last_privacy = false; + } + _ => break, + }, + _ => { + if last_privacy { + crate::platform::toggle_blank_screen(true); + } + } + } + } + } + async fn try_port_forward_loop( &mut self, rx_from_cm: &mut mpsc::UnboundedReceiver, @@ -536,10 +597,15 @@ impl Connection { self.send(msg_out).await; } - async fn on_message(&mut self, msg: Message) -> bool { + async fn on_message( + &mut self, + msg: Message, + input_sender: SyncSender, + blank_sender: SyncSender, + ) -> bool { if let Some(message::Union::login_request(lr)) = msg.union { if let Some(o) = lr.option.as_ref() { - self.update_option(o); + self.update_option(o, input_sender, blank_sender); } if self.authorized { return true; @@ -654,7 +720,8 @@ impl Connection { match msg.union { Some(message::Union::mouse_event(me)) => { if self.keyboard { - handle_mouse(&me, self.inner.id()); + let conn_id = self.inner.id(); + input_sender.send(Box::new(move || handle_mouse(&me, conn_id))).unwrap(); } } Some(message::Union::key_event(mut me)) => { @@ -669,17 +736,19 @@ impl Connection { }; if is_press { if let Some(key_event::Union::unicode(_)) = me.union { - handle_key(&me); + input_sender.send(Box::new(move || handle_key(&me))).unwrap(); } else if let Some(key_event::Union::seq(_)) = me.union { - handle_key(&me); + input_sender.send(Box::new(move || handle_key(&me))).unwrap(); } else { - me.down = true; - handle_key(&me); - me.down = false; - handle_key(&me); + input_sender.send(Box::new(move || { + me.down = true; + handle_key(&me); + me.down = false; + handle_key(&me); + })).unwrap(); } } else { - handle_key(&me); + input_sender.send(Box::new(move || handle_key(&me))).unwrap(); } } } @@ -782,7 +851,7 @@ impl Connection { self.send_to_cm(ipc::Data::ChatMessage { text: c.text }); } Some(misc::Union::option(o)) => { - self.update_option(&o); + self.update_option(&o, input_sender, blank_sender); } Some(misc::Union::refresh_video(r)) => { if r { @@ -797,7 +866,12 @@ impl Connection { true } - fn update_option(&mut self, o: &OptionMessage) { + fn update_option( + &mut self, + o: &OptionMessage, + input_sender: SyncSender, + blank_sender: SyncSender, + ) { log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { self.image_quality = q.value(); @@ -857,15 +931,23 @@ impl Connection { if let Ok(q) = o.privacy_mode.enum_value() { if q != BoolOption::NotSet { self.privacy_mode = q == BoolOption::Yes; - if self.privacy_mode && self.keyboard { - crate::platform::toggle_privacy_mode(true); + if self.keyboard { + if self.privacy_mode { + input_sender.send(Box::new(move||{crate::platform::block_input(true)})).unwrap(); + blank_sender.send(BlankChannelMsg::On).unwrap(); + } else { + input_sender.send(Box::new(move||{crate::platform::block_input(false)})).unwrap(); + blank_sender.send(BlankChannelMsg::Off).unwrap(); + } } } } if self.keyboard { if let Ok(q) = o.block_input.enum_value() { if q != BoolOption::NotSet { - crate::platform::block_input(q == BoolOption::Yes); + input_sender.send(Box::new(move || { + crate::platform::block_input(q == BoolOption::Yes) + })).unwrap(); } } } @@ -880,9 +962,6 @@ impl Connection { crate::platform::lock_screen(); super::video_service::switch_to_primary(); } - if self.privacy_mode { - crate::platform::toggle_privacy_mode(false); - } self.port_forward_socket.take(); } diff --git a/src/ui/header.tis b/src/ui/header.tis index 57ef01e78..f8d72a75b 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -130,7 +130,8 @@ class Header: Reactor.Component { {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} - {false && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} + {pi.platform == "Windows" ?
  • Block user input
  • : ""} + {pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} ; } @@ -144,7 +145,7 @@ class Header: Reactor.Component { {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • {translate('Insert')} Ctrl + Alt + Del
  • : ""}
    {keyboard_enabled ?
  • {translate('Insert Lock')}
  • : ""} - {false && pi.platform == "Windows" ?
  • Block user input
  • : ""} + {pi.platform == "Windows" ?
  • Block user input
  • : ""} {handler.support_refresh() ?
  • {translate('Refresh')}
  • : ""} ; From 25492c815c9fabed5c351fb37b23e5d54cedc03c Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 15 Jan 2022 16:31:21 +0800 Subject: [PATCH 2/2] simple privacy mode Signed-off-by: fufesou --- libs/hbb_common/protos/message.proto | 15 +- src/client.rs | 2 +- src/platform/linux.rs | 5 +- src/platform/macos.rs | 5 +- src/platform/windows.rs | 6 +- src/server/connection.rs | 249 +++++++++++++++++++-------- src/ui/header.tis | 2 +- src/ui/remote.rs | 3 + 8 files changed, 197 insertions(+), 90 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 3d892877f..e6536cf38 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -158,7 +158,7 @@ enum ControlKey { Divide = 70; Equals = 71; NumpadEnter = 72; - RShift= 73; + RShift = 73; RControl = 74; RAlt = 75; CtrlAltDel = 100; @@ -173,8 +173,7 @@ message KeyEvent { uint32 chr = 4; uint32 unicode = 5; string seq = 6; - } - repeated ControlKey modifiers = 8; + } repeated ControlKey modifiers = 8; } message CursorData { @@ -348,6 +347,11 @@ message OptionMessage { BoolOption disable_clipboard = 8; } +message OptionResponse { + OptionMessage opt = 1; + string error = 2; +} + message TestDelay { int64 time = 1; bool from_client = 2; @@ -358,9 +362,7 @@ message PublicKey { bytes symmetric_value = 2; } -message SignedId { - bytes id = 1; -} +message SignedId { bytes id = 1; } message AudioFormat { uint32 sample_rate = 1; @@ -378,6 +380,7 @@ message Misc { AudioFormat audio_format = 8; string close_reason = 9; bool refresh_video = 10; + OptionResponse option_response = 11; } } diff --git a/src/client.rs b/src/client.rs index 69ab838fd..39b56dde9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -826,7 +826,7 @@ impl LoginConfigHandler { msg.lock_after_session_end = BoolOption::Yes.into(); n += 1; } - if self.get_toggle_option("privacy_mode") { + if self.get_toggle_option("privacy-mode") { msg.privacy_mode = BoolOption::Yes.into(); n += 1; } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index e6c8172be..ddc5bd2d5 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -600,12 +600,13 @@ pub fn lock_screen() { }); } -pub fn toggle_privacy_mode(_v: bool) { +pub fn toggle_blank_screen(_v: bool) { // https://unix.stackexchange.com/questions/17170/disable-keyboard-mouse-input-on-unix-under-x } -pub fn block_input(_v: bool) { +pub fn block_input(_v: bool) -> bool { // + true } pub fn is_installed() -> bool { diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 4b29bc13a..4e144fd85 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -398,12 +398,13 @@ pub fn start_os_service() { } } -pub fn toggle_privacy_mode(_v: bool) { +pub fn toggle_blank_screen(_v: bool) { // https://unix.stackexchange.com/questions/17115/disable-keyboard-mouse-temporarily } -pub fn block_input(_v: bool) { +pub fn block_input(_v: bool) -> bool { // + true } pub fn is_installed() -> bool { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index b0c556bb5..306d025ee 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1020,11 +1020,9 @@ pub fn toggle_blank_screen(v: bool) { } } -pub fn block_input(v: bool) { +pub fn block_input(v: bool) -> bool { let v = if v { TRUE } else { FALSE }; - unsafe { - BlockInput(v); - } + unsafe { BlockInput(v) == TRUE } } pub fn add_recent_document(path: &str) { diff --git a/src/server/connection.rs b/src/server/connection.rs index 963c7e79b..672f75302 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,12 +1,10 @@ -use super::input_service::*; -use super::*; -use crate::common::update_clipboard; -use crate::ipc; +use super::{input_service::*, *}; +use crate::{common::update_clipboard, ipc}; use hbb_common::{ config::Config, fs, futures::{SinkExt, StreamExt}, - sleep, timeout, + protobuf, sleep, timeout, tokio::{ net::TcpStream, sync::mpsc, @@ -30,6 +28,15 @@ pub struct ConnInner { tx_video: Option, } +enum MessageInput { + InputFunc(Box), + BlockOn, + BlockOff, + PrivacyOn, + PrivacyOff, + Exit, +} + pub struct Connection { inner: ConnInner, stream: super::Stream, @@ -51,8 +58,10 @@ pub struct Connection { show_remote_cursor: bool, // by peer privacy_mode: bool, ip: String, - disable_clipboard: bool, // by peer - disable_audio: bool, // by peer + disable_clipboard: bool, // by peer + disable_audio: bool, // by peer + tx_input: SyncSender, // handle input messages + rx_input_res: Receiver, } impl Subscriber for ConnInner { @@ -85,14 +94,6 @@ const MILLI1: Duration = Duration::from_millis(1); const SEND_TIMEOUT_VIDEO: u64 = 12_000; const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; -type SyncFunc = Box; - -enum BlankChannelMsg { - On, - Off, - Exit, -} - impl Connection { pub async fn start( addr: SocketAddr, @@ -109,6 +110,9 @@ impl Connection { let (tx_to_cm, rx_to_cm) = mpsc::unbounded_channel::(); let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); + let (tx_input, rx_input) = sync_channel(1); + let (tx_input_res, rx_input_res) = sync_channel(1); + let mut conn = Self { inner: ConnInner { id, @@ -136,6 +140,8 @@ impl Connection { ip: "".to_owned(), disable_audio: false, disable_clipboard: false, + tx_input, + rx_input_res, }; tokio::spawn(async move { if let Err(err) = start_ipc(rx_to_cm, tx_from_cm).await { @@ -166,11 +172,7 @@ impl Connection { }, ); - let (input_sender, input_receiver) = sync_channel(1); - - let (blank_sender, blank_receiver) = sync_channel(1); - let _ = std::thread::spawn(move || Self::handle_input(input_receiver)); - let blank_handler = std::thread::spawn(move || Self::handle_blank(blank_receiver)); + let handler_input = std::thread::spawn(move || Self::handle_input(rx_input, tx_input_res)); loop { tokio::select! { @@ -247,7 +249,7 @@ impl Connection { Ok(bytes) => { last_recv_time = Instant::now(); if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { - if !conn.on_message(msg_in, input_sender.clone(), blank_sender.clone()).await { + if !conn.on_message(msg_in).await { break; } } @@ -312,12 +314,14 @@ impl Connection { } } - blank_sender.send(BlankChannelMsg::Exit).unwrap(); - if let Err(e) = blank_handler.join() { - log::error!("Failed to join blank thread, {:?}", e); - }; + conn.tx_input.send(MessageInput::Exit).unwrap(); + if let Err(e) = handler_input.join() { + log::error!("Failed to join input thread, {:?}", e); + } else { + log::info!("Blank thread exited"); + } - crate::platform::block_input(false); + let _ = crate::platform::block_input(false); crate::platform::toggle_blank_screen(false); video_service::notify_video_frame_feched(id, None); @@ -328,30 +332,80 @@ impl Connection { } } - // TODO: - // 1. Send "Block Input" and "Privacy Mode" when connection established. - // 2. Ctrl + Alt + Del will break "Block Input" - fn handle_input(receiver: Receiver) { + fn handle_input(receiver: Receiver, tx_input_res: SyncSender) { + let mut block_input_mode = false; + let (tx_blank, rx_blank) = sync_channel(1); + + let handler_blank = std::thread::spawn(|| Self::handle_blank(rx_blank)); + loop { - match receiver.recv() { - Ok(mut f) => { - f(); + match receiver.recv_timeout(std::time::Duration::from_millis(500)) { + Ok(v) => match v { + MessageInput::InputFunc(mut f) => { + f(); + } + MessageInput::BlockOn => { + if crate::platform::block_input(true) { + block_input_mode = true; + tx_input_res.send(true).unwrap(); + } else { + tx_input_res.send(false).unwrap(); + } + } + MessageInput::BlockOff => { + if crate::platform::block_input(false) { + block_input_mode = false; + tx_input_res.send(true).unwrap(); + } else { + tx_input_res.send(false).unwrap(); + } + } + MessageInput::PrivacyOn => { + if crate::platform::block_input(true) { + block_input_mode = true; + tx_input_res.send(true).unwrap(); + } else { + tx_input_res.send(false).unwrap(); + } + tx_blank.send(MessageInput::PrivacyOn).unwrap(); + } + MessageInput::PrivacyOff => { + if crate::platform::block_input(false) { + block_input_mode = false; + tx_input_res.send(true).unwrap(); + } else { + tx_input_res.send(false).unwrap(); + } + tx_blank.send(MessageInput::PrivacyOff).unwrap(); + } + MessageInput::Exit => break, + }, + _ => { + if block_input_mode { + let _ = crate::platform::block_input(true); + } } - _ => break, } } + + tx_blank.send(MessageInput::Exit).unwrap(); + if let Err(_) = handler_blank.join() { + log::error!("Failed to join blank thread handler"); + } else { + log::info!("Blank thread exited"); + } } - fn handle_blank(receiver: Receiver) { + fn handle_blank(receiver: Receiver) { let mut last_privacy = false; loop { match receiver.recv_timeout(std::time::Duration::from_millis(500)) { Ok(v) => match v { - BlankChannelMsg::On => { + MessageInput::PrivacyOn => { crate::platform::toggle_blank_screen(true); last_privacy = true; } - BlankChannelMsg::Off => { + MessageInput::PrivacyOff => { crate::platform::toggle_blank_screen(false); last_privacy = false; } @@ -597,15 +651,22 @@ impl Connection { self.send(msg_out).await; } - async fn on_message( - &mut self, - msg: Message, - input_sender: SyncSender, - blank_sender: SyncSender, - ) -> bool { + async fn send_option_error(&mut self, err: T, opt: OptionMessage) { + let mut msg_out = Message::new(); + let mut res = OptionResponse::new(); + let mut misc = Misc::new(); + res.opt = protobuf::MessageField::some(opt); + res.error = err.to_string(); + + misc.set_option_response(res); + msg_out.set_misc(misc); + self.send(msg_out).await; + } + + async fn on_message(&mut self, msg: Message) -> bool { if let Some(message::Union::login_request(lr)) = msg.union { if let Some(o) = lr.option.as_ref() { - self.update_option(o, input_sender, blank_sender); + self.update_option(o).await; } if self.authorized { return true; @@ -721,7 +782,11 @@ impl Connection { Some(message::Union::mouse_event(me)) => { if self.keyboard { let conn_id = self.inner.id(); - input_sender.send(Box::new(move || handle_mouse(&me, conn_id))).unwrap(); + self.tx_input + .send(MessageInput::InputFunc(Box::new(move || { + handle_mouse(&me, conn_id) + }))) + .unwrap(); } } Some(message::Union::key_event(mut me)) => { @@ -736,19 +801,31 @@ impl Connection { }; if is_press { if let Some(key_event::Union::unicode(_)) = me.union { - input_sender.send(Box::new(move || handle_key(&me))).unwrap(); + self.tx_input + .send(MessageInput::InputFunc(Box::new(move || { + handle_key(&me) + }))) + .unwrap(); } else if let Some(key_event::Union::seq(_)) = me.union { - input_sender.send(Box::new(move || handle_key(&me))).unwrap(); + self.tx_input + .send(MessageInput::InputFunc(Box::new(move || { + handle_key(&me) + }))) + .unwrap(); } else { - input_sender.send(Box::new(move || { - me.down = true; - handle_key(&me); - me.down = false; - handle_key(&me); - })).unwrap(); + self.tx_input + .send(MessageInput::InputFunc(Box::new(move || { + me.down = true; + handle_key(&me); + me.down = false; + handle_key(&me); + }))) + .unwrap(); } } else { - input_sender.send(Box::new(move || handle_key(&me))).unwrap(); + self.tx_input + .send(MessageInput::InputFunc(Box::new(move || handle_key(&me)))) + .unwrap(); } } } @@ -851,7 +928,7 @@ impl Connection { self.send_to_cm(ipc::Data::ChatMessage { text: c.text }); } Some(misc::Union::option(o)) => { - self.update_option(&o, input_sender, blank_sender); + self.update_option(&o).await; } Some(misc::Union::refresh_video(r)) => { if r { @@ -866,12 +943,7 @@ impl Connection { true } - fn update_option( - &mut self, - o: &OptionMessage, - input_sender: SyncSender, - blank_sender: SyncSender, - ) { + async fn update_option(&mut self, o: &OptionMessage) { log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { self.image_quality = q.value(); @@ -929,25 +1001,54 @@ impl Connection { } } if let Ok(q) = o.privacy_mode.enum_value() { - if q != BoolOption::NotSet { - self.privacy_mode = q == BoolOption::Yes; - if self.keyboard { - if self.privacy_mode { - input_sender.send(Box::new(move||{crate::platform::block_input(true)})).unwrap(); - blank_sender.send(BlankChannelMsg::On).unwrap(); - } else { - input_sender.send(Box::new(move||{crate::platform::block_input(false)})).unwrap(); - blank_sender.send(BlankChannelMsg::Off).unwrap(); + if self.keyboard { + match q { + BoolOption::Yes => { + self.privacy_mode = true; + self.tx_input.send(MessageInput::PrivacyOn).unwrap(); + if self.rx_input_res.recv().unwrap() { + log::info!("Privacy mode on"); + } else { + log::error!("Failed to trun on privacy mode"); + self.send_option_error("Failed to turn on privacy mode", o.clone()) + .await; + } } + BoolOption::No => { + self.privacy_mode = false; + self.tx_input.send(MessageInput::PrivacyOff).unwrap(); + if self.rx_input_res.recv().unwrap() { + log::info!("Privacy mode off"); + } else { + log::warn!("Failed to trun off privacy mode") + } + } + _ => {} } } } if self.keyboard { if let Ok(q) = o.block_input.enum_value() { - if q != BoolOption::NotSet { - input_sender.send(Box::new(move || { - crate::platform::block_input(q == BoolOption::Yes) - })).unwrap(); + match q { + BoolOption::Yes => { + self.tx_input.send(MessageInput::BlockOn).unwrap(); + if self.rx_input_res.recv().unwrap() { + log::info!("Block input mode on"); + } else { + log::error!("Failed to trun on block input mode"); + self.send_option_error("Failed to turn on block input mode", o.clone()) + .await; + } + } + BoolOption::No => { + self.tx_input.send(MessageInput::BlockOff).unwrap(); + if self.rx_input_res.recv().unwrap() { + log::info!("Block input mode off"); + } else { + log::error!("Failed to trun off block input mode"); + } + } + _ => {} } } } diff --git a/src/ui/header.tis b/src/ui/header.tis index f8d72a75b..b7bd79393 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -145,7 +145,7 @@ class Header: Reactor.Component { {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • {translate('Insert')} Ctrl + Alt + Del
  • : ""}
    {keyboard_enabled ?
  • {translate('Insert Lock')}
  • : ""} - {pi.platform == "Windows" ?
  • Block user input
  • : ""} + {false && pi.platform == "Windows" ?
  • Block user input
  • : ""} {handler.support_refresh() ?
  • {translate('Refresh')}
  • : ""} ; diff --git a/src/ui/remote.rs b/src/ui/remote.rs index bd818feac..4797b392a 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1727,6 +1727,9 @@ impl Remote { self.handler.msgbox("error", "Connection Error", &c); return false; } + Some(misc::Union::option_response(resp)) => { + self.handler.msgbox("warn", "Option Error", &resp.error); + } _ => {} }, Some(message::Union::test_delay(t)) => {